Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions e2e/binance-orderbook/specs/cancel-current-symbol.pw.js
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ test('a missing native dialog stops cleanly and restores temporary UI state', as
});

for (const dialogMode of ['extraAction', 'missingPrimary']) {
test(`an invalid ${dialogMode} dialog contract fails explicitly and preserves recovery state`, async ({ page }) => {
test(`an invalid ${dialogMode} dialog contract blocks the action and restores chart orders`, async ({ page }) => {
const scenario = createCancelScenario({
positions: POSITION_SETS.current,
orders: ORDER_SETS.current,
Expand All @@ -271,12 +271,18 @@ for (const dialogMode of ['extraAction', 'missingPrimary']) {

await page.getByRole('button', { name: '撤单' }).click();
await expect(page.getByText(
'HYPEUSDT 撤单确认弹窗结构异常,图表当前委托保持隐藏',
'HYPEUSDT 撤单确认弹窗结构异常,未执行弹窗操作',
)).toBeVisible();

const state = await readFixtureState(page);
expect(state.orders).toEqual(ORDER_SETS.current);
expect(state.showOrders).toBe(false);
expect(state.events.filter((event) => event.type === 'cancel-requested')).toEqual([]);
expect(
state.events
.filter((event) => event.type === 'show-orders')
.map((event) => event.value),
).toEqual([false, true]);
expect(state.showOrders).toBe(true);
expect(state.hideOtherSymbols).toBe(true);
expect(errors).toEqual([]);
});
Expand Down
39 changes: 19 additions & 20 deletions scripts/binance-orderbook-trade.user.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// @namespace binance.orderbook.trade
// @icon data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2214%22%20fill%3D%22%23f0b90b%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2249%22%20text-anchor%3D%22middle%22%20font-family%3D%22Arial%2C%20sans-serif%22%20font-size%3D%2242%22%20font-weight%3D%22800%22%20fill%3D%22%23111827%22%3EJ%3C%2Ftext%3E%3C%2Fsvg%3E
// @icon64 data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2214%22%20fill%3D%22%23f0b90b%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2249%22%20text-anchor%3D%22middle%22%20font-family%3D%22Arial%2C%20sans-serif%22%20font-size%3D%2242%22%20font-weight%3D%22800%22%20fill%3D%22%23111827%22%3EJ%3C%2Ftext%3E%3C%2Fsvg%3E
// @version 2.7.138
// @version 2.7.139
// @author jackhai9
// @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板
// @match https://www.binance.com/*/futures/*
Expand Down Expand Up @@ -1682,7 +1682,6 @@

// src/binance-orderbook-trade/core/tradingview-orders-recovery.js
var TRADINGVIEW_ORDERS_RECOVERY_STORAGE_KEY = "binance-orderbook-trade:tradingview-orders-recovery:v1";
var TRADINGVIEW_ORDERS_RECOVERY_MAX_AGE_MS = 10 * 60 * 1e3;
function createTradingViewOrdersRecoveryRecord(nowMs) {
if (!Number.isFinite(nowMs)) throw new Error("Chart orders recovery timestamp is invalid");
return JSON.stringify({ version: 1, originalVisible: true, createdAtMs: nowMs });
Expand All @@ -1700,9 +1699,6 @@
if (keys.join(",") !== "createdAtMs,originalVisible,version" || record.version !== 1 || record.originalVisible !== true || !Number.isFinite(record.createdAtMs) || record.createdAtMs > nowMs) {
return { status: "invalid", record: null };
}
if (nowMs - record.createdAtMs > TRADINGVIEW_ORDERS_RECOVERY_MAX_AGE_MS) {
return { status: "expired", record };
}
return { status: "valid", record };
}

Expand Down Expand Up @@ -4721,19 +4717,26 @@
dialogSignal
};
const recordAction = (eventTarget) => {
const contract = findBinanceCancelAllDialog(document, isVisibleElement);
if (!contract) return;
watcher.seenDialog = true;
const action = classifyBinanceCancelAllDialogAction(contract, eventTarget);
if (action && !watcher.action) watcher.action = action;
};
const rejectInvalidDialogAction = (event, error) => {
event.preventDefault();
event.stopImmediatePropagation();
watcher.error = error;
};
const handleClick = (event) => {
try {
const contract = findBinanceCancelAllDialog(document, isVisibleElement);
if (!contract) return;
watcher.seenDialog = true;
const action = classifyBinanceCancelAllDialogAction(contract, eventTarget);
if (action && !watcher.action) watcher.action = action;
recordAction(event.target);
} catch (error) {
watcher.error = error;
rejectInvalidDialogAction(event, error);
} finally {
dialogSignal.notify();
}
};
const handleClick = (event) => recordAction(event.target);
const handleKeydown = (event) => {
if (event.key !== "Escape" && event.key !== "Enter" && event.key !== " ") return;
try {
Expand All @@ -4747,7 +4750,7 @@
);
if (action && !watcher.action) watcher.action = action;
} catch (error) {
watcher.error = error;
rejectInvalidDialogAction(event, error);
} finally {
dialogSignal.notify();
}
Expand Down Expand Up @@ -4858,11 +4861,8 @@
tradingViewOrdersRecoveryPendingAtStartup = false;
return { status: "missing" };
}
if (recovery.status === "invalid" || recovery.status === "expired") {
emit(
"ERR",
`图表当前委托恢复记录${recovery.status === "invalid" ? "无效" : "已过期"},未修改页面状态`
);
if (recovery.status === "invalid") {
emit("ERR", "图表当前委托恢复记录无效,未修改页面状态");
clearTradingViewOrdersRecoveryRecord();
tradingViewOrdersRecoveryPendingAtStartup = false;
return { status: recovery.status };
Expand Down Expand Up @@ -5021,9 +5021,8 @@
);
} catch (error) {
restoreTemporaryUiState = false;
restoreTradingViewOrdersState = false;
emit("ERR", "币安撤单确认弹窗结构异常", error);
const message = `${symbol} 撤单确认弹窗结构异常,图表当前委托保持隐藏`;
const message = `${symbol} 撤单确认弹窗结构异常,未执行弹窗操作`;
setLadderStatus(message);
return { ok: false, status: "dialog_contract_invalid", message };
} finally {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
export const TRADINGVIEW_ORDERS_RECOVERY_STORAGE_KEY =
'binance-orderbook-trade:tradingview-orders-recovery:v1';
export const TRADINGVIEW_ORDERS_RECOVERY_MAX_AGE_MS = 10 * 60 * 1000;

export function createTradingViewOrdersRecoveryRecord(nowMs) {
if (!Number.isFinite(nowMs)) throw new Error('Chart orders recovery timestamp is invalid');
Expand All @@ -27,8 +26,5 @@ export function parseTradingViewOrdersRecoveryRecord(rawValue, nowMs) {
) {
return { status: 'invalid', record: null };
}
if (nowMs - record.createdAtMs > TRADINGVIEW_ORDERS_RECOVERY_MAX_AGE_MS) {
return { status: 'expired', record };
}
return { status: 'valid', record };
}
36 changes: 20 additions & 16 deletions src/binance-orderbook-trade/index.user.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// @namespace binance.orderbook.trade
// @icon data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2214%22%20fill%3D%22%23f0b90b%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2249%22%20text-anchor%3D%22middle%22%20font-family%3D%22Arial%2C%20sans-serif%22%20font-size%3D%2242%22%20font-weight%3D%22800%22%20fill%3D%22%23111827%22%3EJ%3C%2Ftext%3E%3C%2Fsvg%3E
// @icon64 data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2214%22%20fill%3D%22%23f0b90b%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2249%22%20text-anchor%3D%22middle%22%20font-family%3D%22Arial%2C%20sans-serif%22%20font-size%3D%2242%22%20font-weight%3D%22800%22%20fill%3D%22%23111827%22%3EJ%3C%2Ftext%3E%3C%2Fsvg%3E
// @version 2.7.138
// @version 2.7.139
// @author jackhai9
// @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板
// @match https://www.binance.com/*/futures/*
Expand Down Expand Up @@ -3473,19 +3473,27 @@ import {
};

const recordAction = (eventTarget) => {
const contract = findBinanceCancelAllDialog(document, isVisibleElement);
if (!contract) return;
watcher.seenDialog = true;
const action = classifyBinanceCancelAllDialogAction(contract, eventTarget);
if (action && !watcher.action) watcher.action = action;
};
const rejectInvalidDialogAction = (event, error) => {
// Do not let Binance execute an action that the script can no longer classify or recover from.
event.preventDefault();
event.stopImmediatePropagation();
watcher.error = error;
};
const handleClick = (event) => {
try {
const contract = findBinanceCancelAllDialog(document, isVisibleElement);
if (!contract) return;
watcher.seenDialog = true;
const action = classifyBinanceCancelAllDialogAction(contract, eventTarget);
if (action && !watcher.action) watcher.action = action;
recordAction(event.target);
} catch (error) {
watcher.error = error;
rejectInvalidDialogAction(event, error);
} finally {
dialogSignal.notify();
}
};
const handleClick = (event) => recordAction(event.target);
const handleKeydown = (event) => {
if (event.key !== 'Escape' && event.key !== 'Enter' && event.key !== ' ') return;
try {
Expand All @@ -3499,7 +3507,7 @@ import {
);
if (action && !watcher.action) watcher.action = action;
} catch (error) {
watcher.error = error;
rejectInvalidDialogAction(event, error);
} finally {
dialogSignal.notify();
}
Expand Down Expand Up @@ -3627,11 +3635,8 @@ import {
tradingViewOrdersRecoveryPendingAtStartup = false;
return { status: 'missing' };
}
if (recovery.status === 'invalid' || recovery.status === 'expired') {
emit(
'ERR',
`图表当前委托恢复记录${recovery.status === 'invalid' ? '无效' : '已过期'},未修改页面状态`,
);
if (recovery.status === 'invalid') {
emit('ERR', '图表当前委托恢复记录无效,未修改页面状态');
clearTradingViewOrdersRecoveryRecord();
tradingViewOrdersRecoveryPendingAtStartup = false;
return { status: recovery.status };
Expand Down Expand Up @@ -3809,9 +3814,8 @@ import {
);
} catch (error) {
restoreTemporaryUiState = false;
restoreTradingViewOrdersState = false;
emit('ERR', '币安撤单确认弹窗结构异常', error);
const message = `${symbol} 撤单确认弹窗结构异常,图表当前委托保持隐藏`;
const message = `${symbol} 撤单确认弹窗结构异常,未执行弹窗操作`;
setLadderStatus(message);
return { ok: false, status: 'dialog_contract_invalid', message };
} finally {
Expand Down
15 changes: 8 additions & 7 deletions test/unit/binance-orderbook-trade/chart-orders-recovery.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import test from 'node:test';
import assert from 'node:assert/strict';

import {
TRADINGVIEW_ORDERS_RECOVERY_MAX_AGE_MS,
createTradingViewOrdersRecoveryRecord,
parseTradingViewOrdersRecoveryRecord,
} from '../../../src/binance-orderbook-trade/core/tradingview-orders-recovery.js';
Expand All @@ -20,7 +19,7 @@ test('creates and parses an exact TradingView orders reload recovery record', ()
});
});

test('distinguishes missing, invalid, future, and expired recovery records', () => {
test('distinguishes missing, invalid, and future recovery records', () => {
assert.deepEqual(parseTradingViewOrdersRecoveryRecord(null, 2_000), {
status: 'missing',
record: null,
Expand All @@ -45,14 +44,16 @@ test('distinguishes missing, invalid, future, and expired recovery records', ()
status: 'invalid',
record: null,
});
});

const expiredRecord = { version: 1, originalVisible: true, createdAtMs: 1_000 };
test('valid recovery records do not expire before the hidden chart state is restored', () => {
const oldRecord = { version: 1, originalVisible: true, createdAtMs: 1_000 };
assert.deepEqual(parseTradingViewOrdersRecoveryRecord(
JSON.stringify(expiredRecord),
1_000 + TRADINGVIEW_ORDERS_RECOVERY_MAX_AGE_MS + 1,
JSON.stringify(oldRecord),
1_000 + (365 * 24 * 60 * 60 * 1000),
), {
status: 'expired',
record: expiredRecord,
status: 'valid',
record: oldRecord,
});
});

Expand Down
15 changes: 12 additions & 3 deletions test/unit/binance-orderbook-trade/source-regressions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -726,7 +726,7 @@ test('stopping a ladder aborts replacement waits before another cancel or submit
assert.match(waitOutcomeBody, /waitForPromiseOrAbort\([\s\S]*abortSignal/);
});

test('bulk cancel hides TradingView orders before opening the native dialog and restores them independently', () => {
test('bulk cancel settles hidden TradingView orders before opening the native dialog', () => {
assert.match(tradingViewOrdersSource, /applyOverrides/);
assert.match(tradingViewOrdersSource, /tradingProperties\.showOrders/);
assert.doesNotMatch(source, /coalesceTradingViewDrawingSaves/);
Expand Down Expand Up @@ -791,6 +791,8 @@ test('bulk cancel hides TradingView orders before opening the native dialog and

test('bulk cancel distinguishes native confirm from cancellation before clear polling', () => {
const watcherBody = readFunctionBody('createBinanceCancelAllDialogDecisionWatcher');
assert.match(watcherBody, /event\.preventDefault\(\)[\s\S]*event\.stopImmediatePropagation\(\)/);
assert.equal(watcherBody.match(/rejectInvalidDialogAction\(event, error\)/g)?.length, 2);
assert.match(watcherBody, /document\.addEventListener\('click', handleClick, true\)/);
assert.match(watcherBody, /document\.addEventListener\('keydown', handleKeydown, true\)/);
assert.match(watcherBody, /findBinanceCancelAllDialog\(document, isVisibleElement\)/);
Expand All @@ -813,9 +815,15 @@ test('bulk cancel distinguishes native confirm from cancellation before clear po
assert.ok(clickIndex < cancelledIndex && cancelledIndex < clearIndex);
assert.match(cancelBody, /status: 'dialog_not_found'/);
assert.match(cancelBody, /status: 'dialog_contract_invalid'/);
const invalidDialogBranch = cancelBody.match(
/catch \(error\) \{\s*restoreTemporaryUiState = false;([\s\S]*?)status: 'dialog_contract_invalid'/,
)?.[1] || '';
assert.notEqual(invalidDialogBranch, '');
assert.doesNotMatch(invalidDialogBranch, /restoreTradingViewOrdersState = false/);
assert.doesNotMatch(invalidDialogBranch, /图表当前委托保持隐藏/);
});

test('TradingView showOrders reload recovery is journaled, bounded, and retried only from startup state', () => {
test('TradingView showOrders reload recovery remains pending until restoration succeeds', () => {
assert.match(source, /tradingViewOrdersRecoveryPendingAtStartup =\s*sessionStorage\.getItem\(TRADINGVIEW_ORDERS_RECOVERY_STORAGE_KEY\) !== null/);

const hideBody = readFunctionBody('hideTradingViewOrdersForBulkCancel');
Expand All @@ -831,7 +839,8 @@ test('TradingView showOrders reload recovery is journaled, bounded, and retried
);

const recoverBody = readFunctionBody('recoverChartOrdersStateAfterReload');
assert.match(recoverBody, /recovery\.status === 'invalid' \|\| recovery\.status === 'expired'/);
assert.match(recoverBody, /recovery\.status === 'invalid'/);
assert.doesNotMatch(recoverBody, /expired/);
assert.match(recoverBody, /clearTradingViewOrdersRecoveryRecord\(\)/);
assert.match(recoverBody, /findBinanceTradingViewTargetDom\(document\)/);
assert.match(recoverBody, /state\.originalVisible = recovery\.record\.originalVisible/);
Expand Down
Loading