diff --git a/AGENTS.md b/AGENTS.md index 7e62c06..e1207da 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -61,6 +61,14 @@ - Chrome/CDP 诊断资源必须限制在当前测量范围内:只启用本轮需要的 CDP domain,事件读取必须带过滤条件和数量上限,证据收集完成后销毁页面探针,并关闭本轮启用的所有 domain。不要在持久 `node_repl` 绑定中长期保留大批事件或完整脚本源码。 - Codex `cua_node` 进程长时间占用超过一个 CPU 核心,并不能证明 userscript 或目标页面仍在忙。应先根据父进程和工作目录确认 PID 归属,检查 JavaScript 事件循环是否真的繁忙,再做短时只读堆栈采样。2026-08-25 的故障中,REPL 事件循环占用只有约 `0.1%`,原生堆栈热点却集中在管道/文件读取、Buffer/UTF-8 转换、typed array 复制和垃圾回收;关闭 CDP domain 后没有恢复,而重置该任务的 `node_repl` 内核会终止高负载 helper,同时保留 Chrome 和 Binance 页面。内核重置会清空所有 REPL 绑定并要求重新连接 Chrome,所以只能在浏览器验证节点完成后使用;清理后必须确认热点 PID 已消失,不要把直接结束 Node 进程作为第一步。 +## Git Worktree 使用边界 + +- 默认不创建 worktree。单 Agent、串行任务、主工作区可以安全切换分支时,直接在主项目目录创建 feature branch 开发。 +- 项目或改动“看起来复杂”不是创建 worktree 的理由。必须有具体的并行、隔离或并排验证需求。 +- 只在以下情况使用 worktree:同时开发或验证多个分支;长时间测试/CDP/审查期间需要处理另一任务;多 Agent 并行且需要写入隔离;主工作区有用户未提交改动、无法安全 checkout;或必须并排比较两个版本。 +- 创建前先说明本次的具体必要性。如果不能指出上述需求,就不创建。 +- 确实需要时,手动 worktree 放在主项目同级目录,命名为 `-`;合并并授权清理后,删除 worktree、已合并本地/远程分支并 prune 元数据,不留空容器目录。 + ## Validation - `binance-orderbook-trade` 最低门槛:`npm test`、`npm run build:binance-orderbook-trade`、`npm run check:binance-orderbook-trade`。 diff --git a/docs/binance-orderbook-trade-ui-automation.md b/docs/binance-orderbook-trade-ui-automation.md index 0a7e938..f1e9fa7 100644 --- a/docs/binance-orderbook-trade-ui-automation.md +++ b/docs/binance-orderbook-trade-ui-automation.md @@ -44,6 +44,11 @@ Do not generate the Cartesian product. Maintain a mandatory risk-core matrix for business invariants, then generate pairwise or three-way covering combinations for the remaining axes. Production regressions become permanent named scenarios. +The deterministic cancel matrix currently reduces 1,536 Cartesian combinations to +19 scenarios while proving complete pairwise coverage. The generator is stable and +seedless: the same ordered axes always produce the same named vectors. A unit test +fails if a future axis/value change leaves any required pair uncovered. + ## Required Invariants Every cancel-current-symbol scenario must assert all applicable invariants: @@ -103,6 +108,11 @@ Failed L2 tests retain: - before/after control geometry; - final fixture state and event ledger. +The Playwright auto fixture writes these JSON attachments only on failure. Visual +regression uses semantic layout/style snapshots for the canonical open-expanded, +close-expanded, and collapsed panels; this avoids platform font rasterization noise +while still detecting group movement, size changes, disabled styling, and state text. + Live tests additionally retain a test-owned order ledger keyed by symbol, side, price, quantity, and creation time. Concurrent user orders are not inferred to be test orders from count changes alone. diff --git a/e2e/binance-orderbook/helpers/covering-array.js b/e2e/binance-orderbook/helpers/covering-array.js new file mode 100644 index 0000000..1a5f9a4 --- /dev/null +++ b/e2e/binance-orderbook/helpers/covering-array.js @@ -0,0 +1,93 @@ +function combinations(items, size, start = 0, prefix = [], result = []) { + if (prefix.length === size) { + result.push(prefix); + return result; + } + for (let index = start; index <= items.length - (size - prefix.length); index += 1) { + combinations(items, size, index + 1, [...prefix, items[index]], result); + } + return result; +} + +function cartesianEntries(entries, index = 0, current = {}, result = []) { + if (index === entries.length) { + result.push(current); + return result; + } + const [key, values] = entries[index]; + for (const value of values) { + cartesianEntries(entries, index + 1, { ...current, [key]: value }, result); + } + return result; +} + +function tupleKey(axisKeys, scenario) { + return JSON.stringify(axisKeys.map((key) => [key, scenario[key]])); +} + +export function enumerateCartesian(axes) { + const entries = Object.entries(axes); + if (!entries.length || entries.some(([, values]) => !Array.isArray(values) || !values.length)) { + throw new Error('Covering-array axes must contain non-empty value arrays'); + } + return cartesianEntries(entries); +} + +export function listCoveredTuples(scenario, axisKeys, strength) { + if (!Number.isInteger(strength) || strength < 1 || strength > axisKeys.length) { + throw new Error(`Invalid covering-array strength: ${strength}`); + } + return combinations(axisKeys, strength).map((keys) => tupleKey(keys, scenario)); +} + +/** + * Greedily selects the first candidate covering the most remaining tuples. + * Candidate and axis order provide a stable tie-break, so the matrix is reviewable + * and reproducible without a random seed while still avoiding the Cartesian product. + */ +export function generateCoveringArray({ axes, strength = 2, isValid = () => true }) { + const axisKeys = Object.keys(axes); + const candidates = enumerateCartesian(axes).filter(isValid); + if (!candidates.length) throw new Error('Covering-array constraints rejected every scenario'); + + const tuplesByCandidate = candidates.map((candidate) => ( + listCoveredTuples(candidate, axisKeys, strength) + )); + const uncovered = new Set(tuplesByCandidate.flat()); + const selected = []; + const available = new Set(candidates.map((_, index) => index)); + + while (uncovered.size) { + let bestIndex = -1; + let bestCoverage = -1; + for (const candidateIndex of available) { + const coverage = tuplesByCandidate[candidateIndex] + .reduce((total, tuple) => total + Number(uncovered.has(tuple)), 0); + if (coverage > bestCoverage) { + bestCoverage = coverage; + bestIndex = candidateIndex; + } + } + if (bestIndex < 0 || bestCoverage <= 0) { + throw new Error(`Unable to cover ${uncovered.size} remaining tuples`); + } + selected.push(candidates[bestIndex]); + available.delete(bestIndex); + for (const tuple of tuplesByCandidate[bestIndex]) uncovered.delete(tuple); + } + + return selected; +} + +export function findMissingCoverage({ axes, scenarios, strength = 2, isValid = () => true }) { + const axisKeys = Object.keys(axes); + const required = new Set( + enumerateCartesian(axes) + .filter(isValid) + .flatMap((scenario) => listCoveredTuples(scenario, axisKeys, strength)) + ); + for (const scenario of scenarios) { + for (const tuple of listCoveredTuples(scenario, axisKeys, strength)) required.delete(tuple); + } + return Array.from(required); +} diff --git a/e2e/binance-orderbook/helpers/userscript-page.js b/e2e/binance-orderbook/helpers/userscript-page.js index 7c239b2..171f442 100644 --- a/e2e/binance-orderbook/helpers/userscript-page.js +++ b/e2e/binance-orderbook/helpers/userscript-page.js @@ -1,16 +1,33 @@ import { fileURLToPath } from 'node:url'; import { readFile } from 'node:fs/promises'; +import { createHash } from 'node:crypto'; import { renderBinanceFuturesFixture } from '../fixtures/binance-futures.js'; const USERSCRIPT_PATH = fileURLToPath( new URL('../../../scripts/binance-orderbook-trade.user.js', import.meta.url), ); +const evidenceByPage = new WeakMap(); + +function readUserscriptVersion(source) { + const match = source.match(/^\/\/\s*@version\s+(\S+)/m); + if (!match) throw new Error('Generated userscript is missing @version metadata'); + return match[1]; +} + +export function readScenarioEvidence(page) { + return evidenceByPage.get(page) || null; +} export async function openUserscriptScenario(page, scenario) { const errors = []; page.on('pageerror', (error) => errors.push(String(error?.stack || error))); const userscriptSource = await readFile(USERSCRIPT_PATH, 'utf8'); + const userscript = { + version: readUserscriptVersion(userscriptSource), + sha256: createHash('sha256').update(userscriptSource).digest('hex'), + }; + evidenceByPage.set(page, { scenario, userscript, errors }); await page.route('https://www.binance.com/**', async (route) => { const url = new URL(route.request().url()); if (url.pathname === '/__binance_orderbook_userscript__.js') { @@ -79,11 +96,11 @@ export async function openUserscriptScenario(page, scenario) { await page.goto(`https://www.binance.com/zh-CN/futures/${scenario.currentSymbol}`); await page.locator('#jh-binance-close-qty-multiplier-panel').waitFor({ state: 'visible' }); - await page.getByRole('button', { name: '撤本币挂单' }).waitFor({ state: 'visible' }); + await page.locator('#jh-binance-ladder-toggle').waitFor({ state: 'visible' }); await page.evaluate(() => new Promise((resolve) => { requestAnimationFrame(() => requestAnimationFrame(resolve)); })); - return { errors }; + return { errors, userscript }; } export async function readFixtureState(page) { diff --git a/e2e/binance-orderbook/helpers/visual-contract.js b/e2e/binance-orderbook/helpers/visual-contract.js new file mode 100644 index 0000000..ae3b25a --- /dev/null +++ b/e2e/binance-orderbook/helpers/visual-contract.js @@ -0,0 +1,85 @@ +function round(value) { + return Math.round(value * 10) / 10; +} + +export async function readPanelVisualContract(page) { + return page.locator('#jh-binance-close-qty-multiplier-panel').evaluate((panel) => { + const rootRect = panel.getBoundingClientRect(); + const relativeRect = (element) => { + const rect = element.getBoundingClientRect(); + return { + top: Math.round((rect.top - rootRect.top) * 10) / 10, + width: Math.round(rect.width * 10) / 10, + height: Math.round(rect.height * 10) / 10, + }; + }; + const styleContract = (element) => { + const style = getComputedStyle(element); + return { + color: style.color, + backgroundColor: style.backgroundColor, + borderColor: style.borderColor, + borderRadius: style.borderRadius, + fontSize: style.fontSize, + fontWeight: style.fontWeight, + opacity: style.opacity, + }; + }; + const controlKey = (element, index) => ( + element.id + || element.getAttribute('data-ladder-action') + || element.getAttribute('data-ladder-stop') + || element.getAttribute('data-ladder-cancel-symbol') + || element.getAttribute('data-orderbook-precision-value') + || `${element.getAttribute('data-ladder-group') || 'control'}:${element.getAttribute('data-ladder-value') || index}` + ); + const controls = Array.from(panel.querySelectorAll([ + '[data-side-selector] button', + '#jh-binance-close-qty-multiplier-input', + '#jh-binance-close-qty-multiplier-dec', + '#jh-binance-close-qty-multiplier-inc', + '[data-orderbook-precision-value]', + '#jh-binance-ladder-toggle', + '[data-ladder-group]', + '[data-ladder-action]', + '[data-ladder-stop]', + '[data-ladder-cancel-symbol]', + ].join(','))).map((element, index) => ({ + key: controlKey(element, index), + text: (element.textContent || '').replace(/\s+/g, ' ').trim(), + disabled: element.matches(':disabled'), + checked: element.getAttribute('aria-checked'), + pressed: element.getAttribute('aria-pressed'), + rect: relativeRect(element), + style: styleContract(element), + })); + const groups = Object.fromEntries( + Array.from(panel.querySelectorAll('[data-panel-group]')).map((element) => [ + element.getAttribute('data-panel-group'), + { + rect: relativeRect(element), + display: getComputedStyle(element).display, + visibility: getComputedStyle(element).visibility, + }, + ]) + ); + return { + panel: { + width: Math.round(rootRect.width * 10) / 10, + height: Math.round(rootRect.height * 10) / 10, + padding: getComputedStyle(panel).padding, + borderRadius: getComputedStyle(panel).borderRadius, + }, + groups, + controls, + text: { + mode: panel.querySelector('#jh-binance-trade-mode-hint')?.textContent || '', + formula: panel.querySelector('[data-multiplier-calculation]')?.textContent || '', + precision: panel.querySelector('[data-panel-group="precision"]')?.textContent || '', + status: panel.querySelector('#jh-binance-ladder-status')?.textContent || '', + }, + }; + }).then((contract) => JSON.parse(JSON.stringify(contract), (_, value) => ( + typeof value === 'number' ? round(value) : value + ))); +} diff --git a/e2e/binance-orderbook/scenarios/cancel-covering-matrix.js b/e2e/binance-orderbook/scenarios/cancel-covering-matrix.js new file mode 100644 index 0000000..9a1f28f --- /dev/null +++ b/e2e/binance-orderbook/scenarios/cancel-covering-matrix.js @@ -0,0 +1,41 @@ +import { generateCoveringArray } from '../helpers/covering-array.js'; +import { + ORDER_SETS, + POSITION_SETS, + createCancelScenario, +} from './cancel-current-symbol.js'; + +export const CANCEL_MATRIX_AXES = Object.freeze({ + positionSet: ['none', 'current', 'other', 'both'], + orderSet: ['none', 'current', 'other', 'both'], + accountTab: ['positions', 'openOrders', 'history'], + openOrdersSubTab: ['basic', 'conditional'], + hideOtherSymbols: [false, true], + showOrders: [false, true], + dialogOutcome: ['cancel', 'confirm'], + mutationDelayMs: [0, 40], +}); + +const vectors = generateCoveringArray({ axes: CANCEL_MATRIX_AXES, strength: 2 }); + +export const CANCEL_COVERING_SCENARIOS = Object.freeze(vectors.map((vector, index) => ({ + id: `pairwise-${String(index + 1).padStart(2, '0')}`, + vector, + scenario: createCancelScenario({ + name: `cancel-${String(index + 1).padStart(2, '0')}`, + matrix: { + id: `pairwise-${String(index + 1).padStart(2, '0')}`, + strength: 2, + vector, + }, + positions: POSITION_SETS[vector.positionSet], + orders: ORDER_SETS[vector.orderSet], + ui: { + accountTab: vector.accountTab, + openOrdersSubTab: vector.openOrdersSubTab, + hideOtherSymbols: vector.hideOtherSymbols, + showOrders: vector.showOrders, + }, + host: { mutationDelayMs: vector.mutationDelayMs }, + }), +}))); diff --git a/e2e/binance-orderbook/specs/cancel-covering-matrix.pw.js b/e2e/binance-orderbook/specs/cancel-covering-matrix.pw.js new file mode 100644 index 0000000..cff2e65 --- /dev/null +++ b/e2e/binance-orderbook/specs/cancel-covering-matrix.pw.js @@ -0,0 +1,83 @@ +import { test, expect } from '../test.js'; + +import { CANCEL_COVERING_SCENARIOS } from '../scenarios/cancel-covering-matrix.js'; +import { + openUserscriptScenario, + readFixtureState, +} from '../helpers/userscript-page.js'; +import { + assertResponsiveInteraction, + assertStableGeometry, + finishInteractionProbe, + installInteractionProbe, +} from '../helpers/interaction-probe.js'; + +const CANCEL_BUTTON_SELECTOR = '[data-ladder-cancel-symbol="true"]'; + +function currentSymbolOrders(scenario) { + return scenario.orders.filter((order) => order.symbol === scenario.currentSymbol); +} + +function otherSymbolOrders(scenario) { + return scenario.orders.filter((order) => order.symbol !== scenario.currentSymbol); +} + +async function expectRestoredState(page, scenario) { + await expect.poll(async () => { + const state = await readFixtureState(page); + return { + accountTab: state.accountTab, + openOrdersSubTab: state.openOrdersSubTab, + hideOtherSymbols: state.hideOtherSymbols, + showOrders: state.showOrders, + }; + }).toEqual({ + accountTab: scenario.ui.accountTab, + openOrdersSubTab: scenario.ui.openOrdersSubTab, + hideOtherSymbols: scenario.ui.hideOtherSymbols, + showOrders: scenario.ui.showOrders, + }); +} + +for (const entry of CANCEL_COVERING_SCENARIOS) { + test(`${entry.id} preserves the cancel-current-symbol invariants`, async ({ page }) => { + const { scenario, vector } = entry; + const hasCurrentOrders = currentSymbolOrders(scenario).length > 0; + const { errors } = await openUserscriptScenario(page, scenario); + await installInteractionProbe(page, CANCEL_BUTTON_SELECTOR); + + await page.getByRole('button', { name: '撤本币挂单' }).click(); + if (!hasCurrentOrders) { + await expect(page.getByRole('button', { name: '无挂单' })).toBeVisible(); + } else { + await expect(page.getByRole('dialog')).toBeVisible(); + await expect.poll(async () => (await readFixtureState(page)).showOrders).toBe(false); + await page.getByRole('button', { + name: vector.dialogOutcome === 'confirm' ? '确认' : '取消', + }).click(); + await expect(page.getByText( + vector.dialogOutcome === 'confirm' + ? `${scenario.currentSymbol} 撤单流程结束,已恢复筛选状态` + : `${scenario.currentSymbol} 已取消撤单,已恢复页面状态`, + )).toBeVisible(); + } + + await expectRestoredState(page, scenario); + const probe = await finishInteractionProbe(page); + assertResponsiveInteraction(expect, probe); + assertStableGeometry(expect, probe.baseline, probe.current); + + const state = await readFixtureState(page); + const expectedOrders = hasCurrentOrders && vector.dialogOutcome === 'confirm' + ? otherSymbolOrders(scenario) + : scenario.orders; + expect(state.orders).toEqual(expectedOrders); + expect(state.events.filter((event) => event.type === 'dialog-opened')).toHaveLength( + hasCurrentOrders ? 1 : 0, + ); + expect(state.events.filter((event) => event.type === 'cancel-requested')).toHaveLength( + hasCurrentOrders && vector.dialogOutcome === 'confirm' ? 1 : 0, + ); + expect(errors).toEqual([]); + }); +} diff --git a/e2e/binance-orderbook/specs/cancel-current-symbol.pw.js b/e2e/binance-orderbook/specs/cancel-current-symbol.pw.js index 8e910d9..71642bf 100644 --- a/e2e/binance-orderbook/specs/cancel-current-symbol.pw.js +++ b/e2e/binance-orderbook/specs/cancel-current-symbol.pw.js @@ -1,4 +1,4 @@ -import { test, expect } from '@playwright/test'; +import { test, expect } from '../test.js'; import { OTHER_SYMBOL, diff --git a/e2e/binance-orderbook/specs/control-flows.pw.js b/e2e/binance-orderbook/specs/control-flows.pw.js index 34745d3..c4037d0 100644 --- a/e2e/binance-orderbook/specs/control-flows.pw.js +++ b/e2e/binance-orderbook/specs/control-flows.pw.js @@ -1,4 +1,4 @@ -import { test, expect } from '@playwright/test'; +import { test, expect } from '../test.js'; import { POSITION_SETS, diff --git a/e2e/binance-orderbook/specs/panel-visual-contract.pw.js b/e2e/binance-orderbook/specs/panel-visual-contract.pw.js new file mode 100644 index 0000000..4d44f03 --- /dev/null +++ b/e2e/binance-orderbook/specs/panel-visual-contract.pw.js @@ -0,0 +1,48 @@ +import { test, expect } from '../test.js'; + +import { POSITION_SETS, createCancelScenario } from '../scenarios/cancel-current-symbol.js'; +import { openUserscriptScenario } from '../helpers/userscript-page.js'; +import { readPanelVisualContract } from '../helpers/visual-contract.js'; + +const PANEL_SELECTOR = '#jh-binance-close-qty-multiplier-panel'; + +async function expectPrecisionReady(page) { + await expect(page.locator(PANEL_SELECTOR).locator('[data-orderbook-precision-value="0.01"]')) + .toBeEnabled({ timeout: 8_000 }); + await page.evaluate(() => new Promise((resolve) => { + requestAnimationFrame(() => requestAnimationFrame(resolve)); + })); +} + +async function expectVisualContract(page, name) { + const contract = await readPanelVisualContract(page); + expect(`${JSON.stringify(contract, null, 2)}\n`).toMatchSnapshot(name); +} + +test('open expanded panel matches the stable visual contract', async ({ page }) => { + await openUserscriptScenario(page, createCancelScenario({ + ui: { tradeMode: 'OPEN', ladderExpanded: true }, + })); + await expectPrecisionReady(page); + await expectVisualContract(page, 'open-expanded.visual.json'); +}); + +test('close expanded panel matches the disabled-state visual contract', async ({ page }) => { + await openUserscriptScenario(page, createCancelScenario({ + positions: POSITION_SETS.current, + ui: { tradeMode: 'CLOSE', ladderExpanded: true }, + })); + const panel = page.locator(PANEL_SELECTOR); + await expect(panel.getByRole('radio', { name: '平多' })).toBeEnabled(); + await expect(panel.getByRole('radio', { name: '平空' })).toBeDisabled(); + await expectPrecisionReady(page); + await expectVisualContract(page, 'close-expanded.visual.json'); +}); + +test('collapsed ladder keeps the compact panel visual contract', async ({ page }) => { + await openUserscriptScenario(page, createCancelScenario({ + ui: { tradeMode: 'OPEN', ladderExpanded: false }, + })); + await expectPrecisionReady(page); + await expectVisualContract(page, 'open-collapsed.visual.json'); +}); diff --git a/e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/close-expanded-visual.json b/e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/close-expanded-visual.json new file mode 100644 index 0000000..80ad890 --- /dev/null +++ b/e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/close-expanded-visual.json @@ -0,0 +1,642 @@ +{ + "panel": { + "width": 314, + "height": 467, + "padding": "8px 10px", + "borderRadius": "10px" + }, + "groups": { + "direction": { + "rect": { + "top": 9, + "width": 292, + "height": 32 + }, + "display": "flex", + "visibility": "visible" + }, + "multiplier": { + "rect": { + "top": 53, + "width": 292, + "height": 54 + }, + "display": "block", + "visibility": "visible" + }, + "precision": { + "rect": { + "top": 119, + "width": 292, + "height": 62 + }, + "display": "block", + "visibility": "visible" + }, + "ladder": { + "rect": { + "top": 193, + "width": 292, + "height": 265 + }, + "display": "block", + "visibility": "visible" + } + }, + "controls": [ + { + "key": "jh-binance-close-side-long", + "text": "平多", + "disabled": false, + "checked": "true", + "pressed": null, + "rect": { + "top": 9, + "width": 54, + "height": 30 + }, + "style": { + "color": "rgb(30, 35, 41)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "borderColor": "rgb(30, 35, 41)", + "borderRadius": "5px 0px 0px 5px", + "fontSize": "14px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "jh-binance-close-side-short", + "text": "平空", + "disabled": true, + "checked": "false", + "pressed": null, + "rect": { + "top": 9, + "width": 54, + "height": 30 + }, + "style": { + "color": "rgb(183, 189, 198)", + "backgroundColor": "rgb(245, 245, 245)", + "borderColor": "rgb(213, 217, 226)", + "borderRadius": "0px 5px 5px 0px", + "fontSize": "14px", + "fontWeight": "500", + "opacity": "0.65" + } + }, + { + "key": "jh-binance-close-qty-multiplier-input", + "text": "", + "disabled": false, + "checked": null, + "pressed": null, + "rect": { + "top": 53, + "width": 60, + "height": 32 + }, + "style": { + "color": "rgb(0, 0, 0)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "borderColor": "rgb(0, 0, 0)", + "borderRadius": "8px", + "fontSize": "15px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "jh-binance-close-qty-multiplier-dec", + "text": "-", + "disabled": true, + "checked": null, + "pressed": null, + "rect": { + "top": 53, + "width": 32, + "height": 32 + }, + "style": { + "color": "rgb(183, 189, 198)", + "backgroundColor": "rgb(245, 245, 245)", + "borderColor": "rgb(213, 217, 226)", + "borderRadius": "6px", + "fontSize": "18px", + "fontWeight": "500", + "opacity": "0.65" + } + }, + { + "key": "jh-binance-close-qty-multiplier-inc", + "text": "+", + "disabled": false, + "checked": null, + "pressed": null, + "rect": { + "top": 53, + "width": 32, + "height": 32 + }, + "style": { + "color": "rgb(94, 102, 115)", + "backgroundColor": "rgb(255, 255, 255)", + "borderColor": "rgb(213, 217, 226)", + "borderRadius": "6px", + "fontSize": "18px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "0.001", + "text": "0.001", + "disabled": false, + "checked": null, + "pressed": "false", + "rect": { + "top": 119, + "width": 49.5, + "height": 32 + }, + "style": { + "color": "rgb(94, 102, 115)", + "backgroundColor": "rgb(255, 255, 255)", + "borderColor": "rgb(213, 217, 226)", + "borderRadius": "6px", + "fontSize": "12px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "0.01", + "text": "0.01", + "disabled": false, + "checked": null, + "pressed": "false", + "rect": { + "top": 119, + "width": 49.5, + "height": 32 + }, + "style": { + "color": "rgb(94, 102, 115)", + "backgroundColor": "rgb(255, 255, 255)", + "borderColor": "rgb(213, 217, 226)", + "borderRadius": "6px", + "fontSize": "12px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "0.1", + "text": "0.1", + "disabled": false, + "checked": null, + "pressed": "true", + "rect": { + "top": 119, + "width": 49.5, + "height": 32 + }, + "style": { + "color": "rgb(0, 0, 0)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "borderColor": "rgb(0, 0, 0)", + "borderRadius": "6px", + "fontSize": "12px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "1", + "text": "1", + "disabled": false, + "checked": null, + "pressed": "false", + "rect": { + "top": 119, + "width": 49.5, + "height": 32 + }, + "style": { + "color": "rgb(94, 102, 115)", + "backgroundColor": "rgb(255, 255, 255)", + "borderColor": "rgb(213, 217, 226)", + "borderRadius": "6px", + "fontSize": "12px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "jh-binance-ladder-toggle", + "text": "Maker 阶梯 ▾", + "disabled": false, + "checked": null, + "pressed": null, + "rect": { + "top": 206, + "width": 292, + "height": 28 + }, + "style": { + "color": "rgb(0, 0, 0)", + "backgroundColor": "rgb(255, 255, 255)", + "borderColor": "rgb(213, 217, 226)", + "borderRadius": "6px", + "fontSize": "13px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "percent:0.3", + "text": "0.3%", + "disabled": false, + "checked": null, + "pressed": null, + "rect": { + "top": 243, + "width": 48.8, + "height": 28 + }, + "style": { + "color": "rgb(0, 0, 0)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "borderColor": "rgb(0, 0, 0)", + "borderRadius": "6px", + "fontSize": "13px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "percent:1", + "text": "1%", + "disabled": false, + "checked": null, + "pressed": null, + "rect": { + "top": 243, + "width": 48.8, + "height": 28 + }, + "style": { + "color": "rgb(94, 102, 115)", + "backgroundColor": "rgb(255, 255, 255)", + "borderColor": "rgb(213, 217, 226)", + "borderRadius": "6px", + "fontSize": "13px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "percent:5", + "text": "5%", + "disabled": false, + "checked": null, + "pressed": null, + "rect": { + "top": 243, + "width": 48.8, + "height": 28 + }, + "style": { + "color": "rgb(94, 102, 115)", + "backgroundColor": "rgb(255, 255, 255)", + "borderColor": "rgb(213, 217, 226)", + "borderRadius": "6px", + "fontSize": "13px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "percent:10", + "text": "10%", + "disabled": false, + "checked": null, + "pressed": null, + "rect": { + "top": 243, + "width": 48.8, + "height": 28 + }, + "style": { + "color": "rgb(94, 102, 115)", + "backgroundColor": "rgb(255, 255, 255)", + "borderColor": "rgb(213, 217, 226)", + "borderRadius": "6px", + "fontSize": "13px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "percent:30", + "text": "30%", + "disabled": false, + "checked": null, + "pressed": null, + "rect": { + "top": 243, + "width": 48.8, + "height": 28 + }, + "style": { + "color": "rgb(94, 102, 115)", + "backgroundColor": "rgb(255, 255, 255)", + "borderColor": "rgb(213, 217, 226)", + "borderRadius": "6px", + "fontSize": "13px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "levels:3", + "text": "3", + "disabled": false, + "checked": null, + "pressed": null, + "rect": { + "top": 283, + "width": 48.8, + "height": 28 + }, + "style": { + "color": "rgb(94, 102, 115)", + "backgroundColor": "rgb(255, 255, 255)", + "borderColor": "rgb(213, 217, 226)", + "borderRadius": "6px", + "fontSize": "13px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "levels:5", + "text": "5", + "disabled": false, + "checked": null, + "pressed": null, + "rect": { + "top": 283, + "width": 48.8, + "height": 28 + }, + "style": { + "color": "rgb(0, 0, 0)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "borderColor": "rgb(0, 0, 0)", + "borderRadius": "6px", + "fontSize": "13px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "levels:7", + "text": "7", + "disabled": false, + "checked": null, + "pressed": null, + "rect": { + "top": 283, + "width": 48.8, + "height": 28 + }, + "style": { + "color": "rgb(94, 102, 115)", + "backgroundColor": "rgb(255, 255, 255)", + "borderColor": "rgb(213, 217, 226)", + "borderRadius": "6px", + "fontSize": "13px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "levels:9", + "text": "9", + "disabled": false, + "checked": null, + "pressed": null, + "rect": { + "top": 283, + "width": 48.8, + "height": 28 + }, + "style": { + "color": "rgb(94, 102, 115)", + "backgroundColor": "rgb(255, 255, 255)", + "borderColor": "rgb(213, 217, 226)", + "borderRadius": "6px", + "fontSize": "13px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "step:1", + "text": "1", + "disabled": false, + "checked": null, + "pressed": null, + "rect": { + "top": 323, + "width": 48.8, + "height": 28 + }, + "style": { + "color": "rgb(94, 102, 115)", + "backgroundColor": "rgb(255, 255, 255)", + "borderColor": "rgb(213, 217, 226)", + "borderRadius": "6px", + "fontSize": "13px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "step:2", + "text": "2", + "disabled": false, + "checked": null, + "pressed": null, + "rect": { + "top": 323, + "width": 48.8, + "height": 28 + }, + "style": { + "color": "rgb(94, 102, 115)", + "backgroundColor": "rgb(255, 255, 255)", + "borderColor": "rgb(213, 217, 226)", + "borderRadius": "6px", + "fontSize": "13px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "step:3", + "text": "3", + "disabled": false, + "checked": null, + "pressed": null, + "rect": { + "top": 323, + "width": 48.8, + "height": 28 + }, + "style": { + "color": "rgb(94, 102, 115)", + "backgroundColor": "rgb(255, 255, 255)", + "borderColor": "rgb(213, 217, 226)", + "borderRadius": "6px", + "fontSize": "13px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "step:4", + "text": "4", + "disabled": false, + "checked": null, + "pressed": null, + "rect": { + "top": 323, + "width": 48.8, + "height": 28 + }, + "style": { + "color": "rgb(94, 102, 115)", + "backgroundColor": "rgb(255, 255, 255)", + "borderColor": "rgb(213, 217, 226)", + "borderRadius": "6px", + "fontSize": "13px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "step:5", + "text": "5", + "disabled": false, + "checked": null, + "pressed": null, + "rect": { + "top": 323, + "width": 48.8, + "height": 28 + }, + "style": { + "color": "rgb(0, 0, 0)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "borderColor": "rgb(0, 0, 0)", + "borderRadius": "6px", + "fontSize": "13px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "CLOSE_LONG", + "text": "阶梯平多", + "disabled": false, + "checked": null, + "pressed": null, + "rect": { + "top": 366, + "width": 144, + "height": 32 + }, + "style": { + "color": "rgb(30, 35, 41)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "borderColor": "rgb(30, 35, 41)", + "borderRadius": "6px", + "fontSize": "14px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "CLOSE_SHORT", + "text": "阶梯平空", + "disabled": true, + "checked": null, + "pressed": null, + "rect": { + "top": 366, + "width": 144, + "height": 32 + }, + "style": { + "color": "rgb(183, 189, 198)", + "backgroundColor": "rgb(245, 245, 245)", + "borderColor": "rgb(213, 217, 226)", + "borderRadius": "6px", + "fontSize": "14px", + "fontWeight": "500", + "opacity": "0.65" + } + }, + { + "key": "true", + "text": "停止阶梯挂单", + "disabled": true, + "checked": null, + "pressed": null, + "rect": { + "top": 402, + "width": 144, + "height": 32 + }, + "style": { + "color": "rgb(183, 189, 198)", + "backgroundColor": "rgb(245, 245, 245)", + "borderColor": "rgb(213, 217, 226)", + "borderRadius": "6px", + "fontSize": "14px", + "fontWeight": "500", + "opacity": "0.65" + } + }, + { + "key": "true", + "text": "撤本币挂单", + "disabled": false, + "checked": null, + "pressed": null, + "rect": { + "top": 402, + "width": 144, + "height": 32 + }, + "style": { + "color": "rgb(94, 102, 115)", + "backgroundColor": "rgb(255, 255, 255)", + "borderColor": "rgb(213, 217, 226)", + "borderRadius": "6px", + "fontSize": "14px", + "fontWeight": "500", + "opacity": "1" + } + } + ], + "text": { + "mode": "单击订单簿时", + "formula": "0.01 × 1 =0.01", + "precision": "订单簿缩放0.0010.010.11推荐 0.01更新推荐", + "status": "空闲" + } +} diff --git a/e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/open-collapsed-visual.json b/e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/open-collapsed-visual.json new file mode 100644 index 0000000..f758f67 --- /dev/null +++ b/e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/open-collapsed-visual.json @@ -0,0 +1,264 @@ +{ + "panel": { + "width": 314, + "height": 267, + "padding": "8px 10px", + "borderRadius": "10px" + }, + "groups": { + "direction": { + "rect": { + "top": 9, + "width": 292, + "height": 32 + }, + "display": "flex", + "visibility": "visible" + }, + "multiplier": { + "rect": { + "top": 53, + "width": 292, + "height": 54 + }, + "display": "block", + "visibility": "visible" + }, + "precision": { + "rect": { + "top": 119, + "width": 292, + "height": 62 + }, + "display": "block", + "visibility": "visible" + }, + "ladder": { + "rect": { + "top": 193, + "width": 292, + "height": 65 + }, + "display": "block", + "visibility": "visible" + } + }, + "controls": [ + { + "key": "jh-binance-close-side-long", + "text": "开多", + "disabled": false, + "checked": "true", + "pressed": null, + "rect": { + "top": 9, + "width": 54, + "height": 30 + }, + "style": { + "color": "rgb(30, 35, 41)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "borderColor": "rgb(30, 35, 41)", + "borderRadius": "5px 0px 0px 5px", + "fontSize": "14px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "jh-binance-close-side-short", + "text": "开空", + "disabled": false, + "checked": "false", + "pressed": null, + "rect": { + "top": 9, + "width": 54, + "height": 30 + }, + "style": { + "color": "rgb(94, 102, 115)", + "backgroundColor": "rgb(255, 255, 255)", + "borderColor": "rgb(94, 102, 115)", + "borderRadius": "0px 5px 5px 0px", + "fontSize": "14px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "jh-binance-close-qty-multiplier-input", + "text": "", + "disabled": false, + "checked": null, + "pressed": null, + "rect": { + "top": 53, + "width": 60, + "height": 32 + }, + "style": { + "color": "rgb(0, 0, 0)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "borderColor": "rgb(0, 0, 0)", + "borderRadius": "8px", + "fontSize": "15px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "jh-binance-close-qty-multiplier-dec", + "text": "-", + "disabled": true, + "checked": null, + "pressed": null, + "rect": { + "top": 53, + "width": 32, + "height": 32 + }, + "style": { + "color": "rgb(183, 189, 198)", + "backgroundColor": "rgb(245, 245, 245)", + "borderColor": "rgb(213, 217, 226)", + "borderRadius": "6px", + "fontSize": "18px", + "fontWeight": "500", + "opacity": "0.65" + } + }, + { + "key": "jh-binance-close-qty-multiplier-inc", + "text": "+", + "disabled": false, + "checked": null, + "pressed": null, + "rect": { + "top": 53, + "width": 32, + "height": 32 + }, + "style": { + "color": "rgb(94, 102, 115)", + "backgroundColor": "rgb(255, 255, 255)", + "borderColor": "rgb(213, 217, 226)", + "borderRadius": "6px", + "fontSize": "18px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "0.001", + "text": "0.001", + "disabled": false, + "checked": null, + "pressed": "false", + "rect": { + "top": 119, + "width": 49.5, + "height": 32 + }, + "style": { + "color": "rgb(94, 102, 115)", + "backgroundColor": "rgb(255, 255, 255)", + "borderColor": "rgb(213, 217, 226)", + "borderRadius": "6px", + "fontSize": "12px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "0.01", + "text": "0.01", + "disabled": false, + "checked": null, + "pressed": "false", + "rect": { + "top": 119, + "width": 49.5, + "height": 32 + }, + "style": { + "color": "rgb(94, 102, 115)", + "backgroundColor": "rgb(255, 255, 255)", + "borderColor": "rgb(213, 217, 226)", + "borderRadius": "6px", + "fontSize": "12px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "0.1", + "text": "0.1", + "disabled": false, + "checked": null, + "pressed": "true", + "rect": { + "top": 119, + "width": 49.5, + "height": 32 + }, + "style": { + "color": "rgb(0, 0, 0)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "borderColor": "rgb(0, 0, 0)", + "borderRadius": "6px", + "fontSize": "12px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "1", + "text": "1", + "disabled": false, + "checked": null, + "pressed": "false", + "rect": { + "top": 119, + "width": 49.5, + "height": 32 + }, + "style": { + "color": "rgb(94, 102, 115)", + "backgroundColor": "rgb(255, 255, 255)", + "borderColor": "rgb(213, 217, 226)", + "borderRadius": "6px", + "fontSize": "12px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "jh-binance-ladder-toggle", + "text": "Maker 阶梯 ▸", + "disabled": false, + "checked": null, + "pressed": null, + "rect": { + "top": 206, + "width": 292, + "height": 28 + }, + "style": { + "color": "rgb(0, 0, 0)", + "backgroundColor": "rgb(255, 255, 255)", + "borderColor": "rgb(213, 217, 226)", + "borderRadius": "6px", + "fontSize": "13px", + "fontWeight": "500", + "opacity": "1" + } + } + ], + "text": { + "mode": "单击订单簿时", + "formula": "0.07 × 1 =0.07≥5U @ 81", + "precision": "订单簿缩放0.0010.010.11推荐 0.01更新推荐", + "status": "空闲" + } +} diff --git a/e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/open-expanded-visual.json b/e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/open-expanded-visual.json new file mode 100644 index 0000000..7d13574 --- /dev/null +++ b/e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/open-expanded-visual.json @@ -0,0 +1,642 @@ +{ + "panel": { + "width": 314, + "height": 467, + "padding": "8px 10px", + "borderRadius": "10px" + }, + "groups": { + "direction": { + "rect": { + "top": 9, + "width": 292, + "height": 32 + }, + "display": "flex", + "visibility": "visible" + }, + "multiplier": { + "rect": { + "top": 53, + "width": 292, + "height": 54 + }, + "display": "block", + "visibility": "visible" + }, + "precision": { + "rect": { + "top": 119, + "width": 292, + "height": 62 + }, + "display": "block", + "visibility": "visible" + }, + "ladder": { + "rect": { + "top": 193, + "width": 292, + "height": 265 + }, + "display": "block", + "visibility": "visible" + } + }, + "controls": [ + { + "key": "jh-binance-close-side-long", + "text": "开多", + "disabled": false, + "checked": "true", + "pressed": null, + "rect": { + "top": 9, + "width": 54, + "height": 30 + }, + "style": { + "color": "rgb(30, 35, 41)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "borderColor": "rgb(30, 35, 41)", + "borderRadius": "5px 0px 0px 5px", + "fontSize": "14px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "jh-binance-close-side-short", + "text": "开空", + "disabled": false, + "checked": "false", + "pressed": null, + "rect": { + "top": 9, + "width": 54, + "height": 30 + }, + "style": { + "color": "rgb(94, 102, 115)", + "backgroundColor": "rgb(255, 255, 255)", + "borderColor": "rgb(94, 102, 115)", + "borderRadius": "0px 5px 5px 0px", + "fontSize": "14px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "jh-binance-close-qty-multiplier-input", + "text": "", + "disabled": false, + "checked": null, + "pressed": null, + "rect": { + "top": 53, + "width": 60, + "height": 32 + }, + "style": { + "color": "rgb(0, 0, 0)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "borderColor": "rgb(0, 0, 0)", + "borderRadius": "8px", + "fontSize": "15px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "jh-binance-close-qty-multiplier-dec", + "text": "-", + "disabled": true, + "checked": null, + "pressed": null, + "rect": { + "top": 53, + "width": 32, + "height": 32 + }, + "style": { + "color": "rgb(183, 189, 198)", + "backgroundColor": "rgb(245, 245, 245)", + "borderColor": "rgb(213, 217, 226)", + "borderRadius": "6px", + "fontSize": "18px", + "fontWeight": "500", + "opacity": "0.65" + } + }, + { + "key": "jh-binance-close-qty-multiplier-inc", + "text": "+", + "disabled": false, + "checked": null, + "pressed": null, + "rect": { + "top": 53, + "width": 32, + "height": 32 + }, + "style": { + "color": "rgb(94, 102, 115)", + "backgroundColor": "rgb(255, 255, 255)", + "borderColor": "rgb(213, 217, 226)", + "borderRadius": "6px", + "fontSize": "18px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "0.001", + "text": "0.001", + "disabled": false, + "checked": null, + "pressed": "false", + "rect": { + "top": 119, + "width": 49.5, + "height": 32 + }, + "style": { + "color": "rgb(94, 102, 115)", + "backgroundColor": "rgb(255, 255, 255)", + "borderColor": "rgb(213, 217, 226)", + "borderRadius": "6px", + "fontSize": "12px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "0.01", + "text": "0.01", + "disabled": false, + "checked": null, + "pressed": "false", + "rect": { + "top": 119, + "width": 49.5, + "height": 32 + }, + "style": { + "color": "rgb(94, 102, 115)", + "backgroundColor": "rgb(255, 255, 255)", + "borderColor": "rgb(213, 217, 226)", + "borderRadius": "6px", + "fontSize": "12px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "0.1", + "text": "0.1", + "disabled": false, + "checked": null, + "pressed": "true", + "rect": { + "top": 119, + "width": 49.5, + "height": 32 + }, + "style": { + "color": "rgb(0, 0, 0)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "borderColor": "rgb(0, 0, 0)", + "borderRadius": "6px", + "fontSize": "12px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "1", + "text": "1", + "disabled": false, + "checked": null, + "pressed": "false", + "rect": { + "top": 119, + "width": 49.5, + "height": 32 + }, + "style": { + "color": "rgb(94, 102, 115)", + "backgroundColor": "rgb(255, 255, 255)", + "borderColor": "rgb(213, 217, 226)", + "borderRadius": "6px", + "fontSize": "12px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "jh-binance-ladder-toggle", + "text": "Maker 阶梯 ▾", + "disabled": false, + "checked": null, + "pressed": null, + "rect": { + "top": 206, + "width": 292, + "height": 28 + }, + "style": { + "color": "rgb(0, 0, 0)", + "backgroundColor": "rgb(255, 255, 255)", + "borderColor": "rgb(213, 217, 226)", + "borderRadius": "6px", + "fontSize": "13px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "percent:2", + "text": "2%", + "disabled": false, + "checked": null, + "pressed": null, + "rect": { + "top": 243, + "width": 48.8, + "height": 28 + }, + "style": { + "color": "rgb(0, 0, 0)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "borderColor": "rgb(0, 0, 0)", + "borderRadius": "6px", + "fontSize": "13px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "percent:10", + "text": "10%", + "disabled": false, + "checked": null, + "pressed": null, + "rect": { + "top": 243, + "width": 48.8, + "height": 28 + }, + "style": { + "color": "rgb(94, 102, 115)", + "backgroundColor": "rgb(255, 255, 255)", + "borderColor": "rgb(213, 217, 226)", + "borderRadius": "6px", + "fontSize": "13px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "percent:30", + "text": "30%", + "disabled": false, + "checked": null, + "pressed": null, + "rect": { + "top": 243, + "width": 48.8, + "height": 28 + }, + "style": { + "color": "rgb(94, 102, 115)", + "backgroundColor": "rgb(255, 255, 255)", + "borderColor": "rgb(213, 217, 226)", + "borderRadius": "6px", + "fontSize": "13px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "percent:50", + "text": "50%", + "disabled": false, + "checked": null, + "pressed": null, + "rect": { + "top": 243, + "width": 48.8, + "height": 28 + }, + "style": { + "color": "rgb(94, 102, 115)", + "backgroundColor": "rgb(255, 255, 255)", + "borderColor": "rgb(213, 217, 226)", + "borderRadius": "6px", + "fontSize": "13px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "percent:70", + "text": "70%", + "disabled": false, + "checked": null, + "pressed": null, + "rect": { + "top": 243, + "width": 48.8, + "height": 28 + }, + "style": { + "color": "rgb(94, 102, 115)", + "backgroundColor": "rgb(255, 255, 255)", + "borderColor": "rgb(213, 217, 226)", + "borderRadius": "6px", + "fontSize": "13px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "levels:3", + "text": "3", + "disabled": false, + "checked": null, + "pressed": null, + "rect": { + "top": 283, + "width": 48.8, + "height": 28 + }, + "style": { + "color": "rgb(94, 102, 115)", + "backgroundColor": "rgb(255, 255, 255)", + "borderColor": "rgb(213, 217, 226)", + "borderRadius": "6px", + "fontSize": "13px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "levels:5", + "text": "5", + "disabled": false, + "checked": null, + "pressed": null, + "rect": { + "top": 283, + "width": 48.8, + "height": 28 + }, + "style": { + "color": "rgb(0, 0, 0)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "borderColor": "rgb(0, 0, 0)", + "borderRadius": "6px", + "fontSize": "13px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "levels:7", + "text": "7", + "disabled": false, + "checked": null, + "pressed": null, + "rect": { + "top": 283, + "width": 48.8, + "height": 28 + }, + "style": { + "color": "rgb(94, 102, 115)", + "backgroundColor": "rgb(255, 255, 255)", + "borderColor": "rgb(213, 217, 226)", + "borderRadius": "6px", + "fontSize": "13px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "levels:9", + "text": "9", + "disabled": false, + "checked": null, + "pressed": null, + "rect": { + "top": 283, + "width": 48.8, + "height": 28 + }, + "style": { + "color": "rgb(94, 102, 115)", + "backgroundColor": "rgb(255, 255, 255)", + "borderColor": "rgb(213, 217, 226)", + "borderRadius": "6px", + "fontSize": "13px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "step:1", + "text": "1", + "disabled": false, + "checked": null, + "pressed": null, + "rect": { + "top": 323, + "width": 48.8, + "height": 28 + }, + "style": { + "color": "rgb(94, 102, 115)", + "backgroundColor": "rgb(255, 255, 255)", + "borderColor": "rgb(213, 217, 226)", + "borderRadius": "6px", + "fontSize": "13px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "step:2", + "text": "2", + "disabled": false, + "checked": null, + "pressed": null, + "rect": { + "top": 323, + "width": 48.8, + "height": 28 + }, + "style": { + "color": "rgb(94, 102, 115)", + "backgroundColor": "rgb(255, 255, 255)", + "borderColor": "rgb(213, 217, 226)", + "borderRadius": "6px", + "fontSize": "13px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "step:3", + "text": "3", + "disabled": false, + "checked": null, + "pressed": null, + "rect": { + "top": 323, + "width": 48.8, + "height": 28 + }, + "style": { + "color": "rgb(94, 102, 115)", + "backgroundColor": "rgb(255, 255, 255)", + "borderColor": "rgb(213, 217, 226)", + "borderRadius": "6px", + "fontSize": "13px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "step:4", + "text": "4", + "disabled": false, + "checked": null, + "pressed": null, + "rect": { + "top": 323, + "width": 48.8, + "height": 28 + }, + "style": { + "color": "rgb(94, 102, 115)", + "backgroundColor": "rgb(255, 255, 255)", + "borderColor": "rgb(213, 217, 226)", + "borderRadius": "6px", + "fontSize": "13px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "step:5", + "text": "5", + "disabled": false, + "checked": null, + "pressed": null, + "rect": { + "top": 323, + "width": 48.8, + "height": 28 + }, + "style": { + "color": "rgb(0, 0, 0)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "borderColor": "rgb(0, 0, 0)", + "borderRadius": "6px", + "fontSize": "13px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "OPEN_LONG", + "text": "阶梯开多", + "disabled": false, + "checked": null, + "pressed": null, + "rect": { + "top": 366, + "width": 144, + "height": 32 + }, + "style": { + "color": "rgb(30, 35, 41)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "borderColor": "rgb(30, 35, 41)", + "borderRadius": "6px", + "fontSize": "14px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "OPEN_SHORT", + "text": "阶梯开空", + "disabled": false, + "checked": null, + "pressed": null, + "rect": { + "top": 366, + "width": 144, + "height": 32 + }, + "style": { + "color": "rgb(30, 35, 41)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "borderColor": "rgb(30, 35, 41)", + "borderRadius": "6px", + "fontSize": "14px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "true", + "text": "停止阶梯挂单", + "disabled": true, + "checked": null, + "pressed": null, + "rect": { + "top": 402, + "width": 144, + "height": 32 + }, + "style": { + "color": "rgb(183, 189, 198)", + "backgroundColor": "rgb(245, 245, 245)", + "borderColor": "rgb(213, 217, 226)", + "borderRadius": "6px", + "fontSize": "14px", + "fontWeight": "500", + "opacity": "0.65" + } + }, + { + "key": "true", + "text": "撤本币挂单", + "disabled": false, + "checked": null, + "pressed": null, + "rect": { + "top": 402, + "width": 144, + "height": 32 + }, + "style": { + "color": "rgb(94, 102, 115)", + "backgroundColor": "rgb(255, 255, 255)", + "borderColor": "rgb(213, 217, 226)", + "borderRadius": "6px", + "fontSize": "14px", + "fontWeight": "500", + "opacity": "1" + } + } + ], + "text": { + "mode": "单击订单簿时", + "formula": "0.07 × 1 =0.07≥5U @ 81", + "precision": "订单簿缩放0.0010.010.11推荐 0.01更新推荐", + "status": "空闲" + } +} diff --git a/e2e/binance-orderbook/test.js b/e2e/binance-orderbook/test.js new file mode 100644 index 0000000..1383c60 --- /dev/null +++ b/e2e/binance-orderbook/test.js @@ -0,0 +1,41 @@ +import { test as base, expect } from '@playwright/test'; + +import { + readFixtureState, + readScenarioEvidence, +} from './helpers/userscript-page.js'; + +function jsonAttachment(value) { + return { + body: Buffer.from(`${JSON.stringify(value, null, 2)}\n`), + contentType: 'application/json', + }; +} + +export const test = base.extend({ + // Failure evidence belongs to the test runner boundary so every future scenario + // receives the same diagnostics without duplicating cleanup code in each spec. + failureEvidence: [async ({ page }, use, testInfo) => { + await use(); + if (testInfo.status === testInfo.expectedStatus) return; + + const evidence = readScenarioEvidence(page); + if (!evidence) return; + await testInfo.attach('scenario.json', jsonAttachment(evidence.scenario)); + await testInfo.attach('userscript.json', jsonAttachment(evidence.userscript)); + await testInfo.attach('page-errors.json', jsonAttachment(evidence.errors)); + + if (!page.isClosed()) { + const fixtureState = await readFixtureState(page).catch((error) => ({ + unavailable: String(error?.message || error), + })); + await testInfo.attach('fixture-state.json', jsonAttachment(fixtureState)); + const interaction = await page.evaluate(() => ( + window.__UI_INTERACTION_PROBE__?.finish?.() || null + )).catch((error) => ({ unavailable: String(error?.message || error) })); + if (interaction) await testInfo.attach('interaction.json', jsonAttachment(interaction)); + } + }, { auto: true }], +}); + +export { expect }; diff --git a/playwright.config.js b/playwright.config.js index 8a68273..d66d57f 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -8,6 +8,7 @@ export default defineConfig({ retries: 0, workers: 1, timeout: 15_000, + snapshotPathTemplate: '{testDir}/{testFilePath}-snapshots/{arg}{ext}', expect: { timeout: 3_000, }, diff --git a/test/unit/binance-orderbook-trade/ui-covering-array.test.js b/test/unit/binance-orderbook-trade/ui-covering-array.test.js new file mode 100644 index 0000000..20501dc --- /dev/null +++ b/test/unit/binance-orderbook-trade/ui-covering-array.test.js @@ -0,0 +1,55 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import { + enumerateCartesian, + findMissingCoverage, + generateCoveringArray, +} from '../../../e2e/binance-orderbook/helpers/covering-array.js'; +import { + CANCEL_COVERING_SCENARIOS, + CANCEL_MATRIX_AXES, +} from '../../../e2e/binance-orderbook/scenarios/cancel-covering-matrix.js'; + +const AXES = Object.freeze({ + position: ['none', 'current', 'both'], + orders: ['none', 'current', 'both'], + hidden: [false, true], + outcome: ['cancel', 'confirm'], +}); + +test('covering array deterministically covers every valid pair with fewer scenarios', () => { + const first = generateCoveringArray({ axes: AXES }); + const second = generateCoveringArray({ axes: AXES }); + + assert.deepEqual(second, first); + assert.deepEqual(findMissingCoverage({ axes: AXES, scenarios: first }), []); + assert.ok(first.length < enumerateCartesian(AXES).length); +}); + +test('covering array derives requirements only from valid constrained scenarios', () => { + const isValid = (scenario) => scenario.orders !== 'none' || scenario.outcome === 'cancel'; + const scenarios = generateCoveringArray({ axes: AXES, strength: 3, isValid }); + + assert.ok(scenarios.every(isValid)); + assert.deepEqual(findMissingCoverage({ + axes: AXES, + scenarios, + strength: 3, + isValid, + }), []); +}); + +test('covering array rejects malformed axes and impossible constraints', () => { + assert.throws(() => generateCoveringArray({ axes: { empty: [] } }), /non-empty/); + assert.throws(() => generateCoveringArray({ axes: AXES, isValid: () => false }), /rejected every/); + assert.throws(() => generateCoveringArray({ axes: AXES, strength: 5 }), /Invalid/); +}); + +test('cancel UI matrix covers every declared pair without the Cartesian product', () => { + const vectors = CANCEL_COVERING_SCENARIOS.map((entry) => entry.vector); + + assert.deepEqual(findMissingCoverage({ axes: CANCEL_MATRIX_AXES, scenarios: vectors }), []); + assert.equal(vectors.length, 19); + assert.ok(vectors.length < enumerateCartesian(CANCEL_MATRIX_AXES).length); +});