feat: 固定操作间延迟设置并更新相关文档和测试

This commit is contained in:
QLHazyCoder
2026-05-18 01:29:24 +08:00
parent 7564481868
commit e5dc675369
10 changed files with 79 additions and 152 deletions
@@ -276,7 +276,7 @@ test('SAVE_SETTING broadcasts operation delay setting without background success
addLog: async (message, level = 'info') => logs.push({ message, level }),
buildLuckmailSessionSettingsPayload: () => ({}),
buildPersistentSettingsPayload: (input = {}) => Object.prototype.hasOwnProperty.call(input, 'operationDelayEnabled')
? { operationDelayEnabled: input.operationDelayEnabled === false ? false : true }
? { operationDelayEnabled: true }
: {},
broadcastDataUpdate: (payload) => broadcasts.push(payload),
getState: async () => ({ ...state }),
@@ -291,8 +291,8 @@ test('SAVE_SETTING broadcasts operation delay setting without background success
});
assert.equal(response.ok, true);
assert.equal(state.operationDelayEnabled, false);
assert.deepStrictEqual(broadcasts.at(-1), { operationDelayEnabled: false });
assert.equal(state.operationDelayEnabled, true);
assert.deepStrictEqual(broadcasts.at(-1), { operationDelayEnabled: true });
assert.equal(logs.length, 0);
});
@@ -46,7 +46,7 @@ test('operationDelayEnabled defaults to enabled and strictly normalizes values',
assert.equal(api.normalizePersistentSettingValue('operationDelayEnabled', value), true);
}
assert.equal(api.normalizePersistentSettingValue('operationDelayEnabled', true), true);
assert.equal(api.normalizePersistentSettingValue('operationDelayEnabled', false), false);
assert.equal(api.normalizePersistentSettingValue('operationDelayEnabled', false), true);
});
test('operationDelayEnabled is normalized through the background settings payload path', () => {
@@ -63,5 +63,5 @@ test('operationDelayEnabled is normalized through the background settings payloa
for (const value of [undefined, null, '', 0, 'false', true]) {
assert.equal(api.buildPersistentSettingsPayload({ operationDelayEnabled: value }, { fillDefaults: true }).operationDelayEnabled, true);
}
assert.equal(api.buildPersistentSettingsPayload({ operationDelayEnabled: false }).operationDelayEnabled, false);
assert.equal(api.buildPersistentSettingsPayload({ operationDelayEnabled: false }).operationDelayEnabled, true);
});
+19 -19
View File
@@ -34,9 +34,9 @@ test('operation delay runs action first and waits once after completion', async
assert.deepStrictEqual(events, ['operation:start', 'operation:end', 'sleep:2000']);
});
test('operation delay skips disabled mode and excluded step keys', async () => {
test('operation delay ignores disabled flags but still skips excluded step keys', async () => {
const api = loadOperationDelayApi();
assert.equal(api.shouldDelayOperation({ enabled: false, stepKey: 'fill-profile', kind: 'click' }), false);
assert.equal(api.shouldDelayOperation({ enabled: false, stepKey: 'fill-profile', kind: 'click' }), true);
assert.equal(api.shouldDelayOperation({ enabled: true, stepKey: 'confirm-oauth', kind: 'click' }), false);
assert.equal(api.shouldDelayOperation({ enabled: true, stepKey: 'platform-verify', kind: 'submit' }), false);
assert.equal(api.shouldDelayOperation({ enabled: true, stepKey: 'fill-profile', kind: 'fill' }), true);
@@ -49,7 +49,7 @@ test('operation delay defaults unresolved settings to enabled', async () => {
assert.deepStrictEqual(events, ['operation', 'sleep:2000']);
});
test('persisted disabled setting prevents first operation delay even before initial restore resolves', async () => {
test('persisted disabled setting is normalized back to enabled before first operation delay resolves', async () => {
const events = [];
let resolveStorage;
const storageReady = new Promise((resolve) => { resolveStorage = resolve; });
@@ -69,10 +69,10 @@ test('persisted disabled setting prevents first operation delay even before init
resolveStorage({ operationDelayEnabled: false });
await pendingOperation;
assert.deepStrictEqual(events, ['operation']);
assert.deepStrictEqual(events, ['operation', 'sleep:2000']);
});
test('newer disabled storage change wins over older pending restore', async () => {
test('disabled storage changes are normalized back to enabled even when they arrive before restore', async () => {
const events = [];
let changeListener = null;
let resolveStorage;
@@ -88,17 +88,17 @@ test('newer disabled storage change wins over older pending restore', async () =
});
changeListener({ operationDelayEnabled: { newValue: false } }, 'local');
assert.equal(api.getOperationDelayEnabled(), false);
assert.equal(api.getOperationDelayEnabled(), true);
resolveStorage({ operationDelayEnabled: true });
await waitForTimer();
assert.equal(api.getOperationDelayEnabled(), false);
assert.equal(api.getOperationDelayEnabled(), true);
await api.performOperationWithDelay(
{ stepKey: 'fill-profile', kind: 'fill' },
async () => events.push('operation'),
{ sleep: async (ms) => events.push(`sleep:${ms}`) }
);
assert.deepStrictEqual(events, ['operation']);
assert.deepStrictEqual(events, ['operation', 'sleep:2000']);
});
test('newer enabled storage change wins over older pending restore', async () => {
@@ -130,7 +130,7 @@ test('newer enabled storage change wins over older pending restore', async () =>
assert.deepStrictEqual(events, ['operation', 'sleep:2000']);
});
test('first operation honors async persisted disabled setting before fallback budget expires', async () => {
test('first operation keeps the delay even when async persisted state says disabled', async () => {
const events = [];
const api = loadOperationDelayApi({
events,
@@ -147,7 +147,7 @@ test('first operation honors async persisted disabled setting before fallback bu
async () => events.push('operation'),
{ sleep: async (ms) => events.push(`sleep:${ms}`) }
);
assert.deepStrictEqual(events, ['operation']);
assert.deepStrictEqual(events, ['operation', 'sleep:2000']);
});
test('hanging initial storage restore falls back to enabled delay and returns', async () => {
@@ -167,7 +167,7 @@ test('hanging initial storage restore falls back to enabled delay and returns',
assert.deepStrictEqual(events, ['operation', 'sleep:2000']);
});
test('explicit disabled metadata does not wait for hanging initial storage restore', async () => {
test('explicit disabled metadata is normalized back to enabled during hanging initial restore', async () => {
const events = [];
const api = loadOperationDelayApi({
events,
@@ -181,7 +181,7 @@ test('explicit disabled metadata does not wait for hanging initial storage resto
);
assert.equal(await resolveOrPending(operation), 'resolved');
assert.deepStrictEqual(events, ['operation']);
assert.deepStrictEqual(events, ['operation', 'sleep:2000']);
});
test('invalid persisted values still resolve to enabled', async () => {
@@ -190,14 +190,14 @@ test('invalid persisted values still resolve to enabled', async () => {
assert.equal(api.getOperationDelayEnabled(), true);
});
test('shouldDelayOperation uses cached disabled setting by default', async () => {
test('shouldDelayOperation normalizes cached disabled setting back to enabled by default', async () => {
const api = loadOperationDelayApi({ chrome: { storage: { local: { get: async () => ({ operationDelayEnabled: false }) }, onChanged: { addListener() {} } } } });
await api.refreshOperationDelaySetting();
assert.equal(api.getOperationDelayEnabled(), false);
assert.equal(api.shouldDelayOperation({ stepKey: 'fill-profile', kind: 'fill' }), false);
assert.equal(api.getOperationDelayEnabled(), true);
assert.equal(api.shouldDelayOperation({ stepKey: 'fill-profile', kind: 'fill' }), true);
});
test('storage change to disabled is honored by later delayed operations', async () => {
test('storage change to disabled is normalized back to enabled for later delayed operations', async () => {
const events = [];
let changeListener = null;
const api = loadOperationDelayApi({
@@ -212,14 +212,14 @@ test('storage change to disabled is honored by later delayed operations', async
await api.refreshOperationDelaySetting();
changeListener({ operationDelayEnabled: { newValue: false } }, 'local');
assert.equal(api.getOperationDelayEnabled(), false);
assert.equal(api.shouldDelayOperation({ stepKey: 'fill-profile', kind: 'fill' }), false);
assert.equal(api.getOperationDelayEnabled(), true);
assert.equal(api.shouldDelayOperation({ stepKey: 'fill-profile', kind: 'fill' }), true);
await api.performOperationWithDelay(
{ stepKey: 'fill-profile', kind: 'fill' },
async () => events.push('operation'),
{ sleep: async (ms) => events.push(`sleep:${ms}`) }
);
assert.deepStrictEqual(events, ['operation']);
assert.deepStrictEqual(events, ['operation', 'sleep:2000']);
});
test('storage change to enabled is honored by later delayed operations', async () => {
+26 -78
View File
@@ -38,95 +38,43 @@ function extractFunction(name) {
throw new Error(`unterminated ${name}`);
}
test('sidepanel exposes one operation delay switch in the existing settings surface', () => {
assert.match(html, /id="row-operation-delay-settings"/);
assert.match(html, /id="input-operation-delay-enabled"/);
assert.match(html, /操作间延迟/);
assert.match(html, /2\s*秒/);
assert.match(html, /id="row-operation-delay-settings"[\s\S]{0,900}输入[\s\S]{0,900}选择[\s\S]{0,900}点击[\s\S]{0,900}提交[\s\S]{0,900}继续[\s\S]{0,900}授权/);
test('sidepanel no longer exposes operation delay switch and places step execution range below oauth timeout', () => {
assert.doesNotMatch(html, /id="row-operation-delay-settings"/);
assert.doesNotMatch(html, /id="input-operation-delay-enabled"/);
const autoDelayIndex = html.indexOf('id="row-auto-delay-settings"');
const oauthTimeoutIndex = html.indexOf('id="row-oauth-flow-timeout"');
const stepRangeIndex = html.indexOf('id="row-step-execution-range"');
const oauthDisplayIndex = html.indexOf('id="row-oauth-display"');
assert.notEqual(autoDelayIndex, -1);
assert.notEqual(oauthTimeoutIndex, -1);
assert.notEqual(stepRangeIndex, -1);
assert.notEqual(oauthDisplayIndex, -1);
assert.ok(stepRangeIndex > autoDelayIndex, 'step execution range should still remain below the step6 toggle row');
assert.ok(stepRangeIndex > oauthTimeoutIndex, 'step execution range should render below oauth timeout');
assert.ok(stepRangeIndex < oauthDisplayIndex, 'step execution range should stay above oauth runtime display');
});
test('sidepanel owns operation delay toggle feedback exactly once', () => {
assert.match(source, /inputOperationDelayEnabled/);
assert.match(source, /operationDelayEnabled/);
assert.match(source, /appendLog\(\{[\s\S]*操作间延迟/);
assert.doesNotMatch(source, /operationDelayEnabled[\s\S]{0,240}addLog\(/);
});
test('sidepanel operation delay restore and save failure contracts are explicit', () => {
assert.match(source, /normalizeOperationDelayEnabled/);
assert.match(source, /lastConfirmedOperationDelayEnabled/);
assert.match(source, /操作间延迟设置读取失败/);
assert.match(source, /操作间延迟设置保存失败/);
assert.match(source, /inputOperationDelayEnabled\.checked\s*=\s*lastConfirmedOperationDelayEnabled/);
assert.match(source, /typeof\s+applyOperationDelayState\s*===\s*['"]function['"]/);
});
test('sidepanel syncs operation delay DATA_UPDATED without becoming the success-log owner', () => {
assert.match(source, /message\.payload\.operationDelayEnabled\s*!==\s*undefined[\s\S]{0,240}applyOperationDelayState\(message\.payload\)/);
});
function loadOperationDelaySidepanelHarness() {
const runtimeMessages = [];
const logs = [];
const inputOperationDelayEnabled = { checked: true, disabled: false };
const chrome = {
runtime: {
sendMessage: async (message) => {
runtimeMessages.push(message);
return { ok: true, state: { operationDelayEnabled: message.payload.operationDelayEnabled } };
},
},
};
const harness = new Function('chrome', 'appendLog', 'inputOperationDelayEnabled', 'initialLatestState', `
let latestState = initialLatestState;
let lastConfirmedOperationDelayEnabled = true;
test('sidepanel operation delay state is always normalized back to enabled', () => {
const harness = new Function(`
let latestState = { operationDelayEnabled: false };
function syncLatestState(nextState) {
latestState = { ...(latestState || {}), ...(nextState || {}) };
}
${extractFunction('normalizeOperationDelayEnabled')}
${extractFunction('appendOperationDelayLog')}
${extractFunction('applyOperationDelayState')}
${extractFunction('persistOperationDelayToggle')}
return {
normalizeOperationDelayEnabled,
applyOperationDelayState,
persistOperationDelayToggle,
getLatestState: () => latestState,
getLastConfirmedOperationDelayEnabled: () => lastConfirmedOperationDelayEnabled,
setLastConfirmedOperationDelayEnabled: (value) => { lastConfirmedOperationDelayEnabled = value; },
};
`)(chrome, (entry) => logs.push(entry), inputOperationDelayEnabled, { operationDelayEnabled: true });
`)();
return { chrome, harness, inputOperationDelayEnabled, logs, runtimeMessages };
}
assert.equal(harness.normalizeOperationDelayEnabled(undefined), true);
assert.equal(harness.normalizeOperationDelayEnabled(false), true);
assert.equal(harness.normalizeOperationDelayEnabled(true), true);
test('operation delay switch logs once, defaults restore failures to enabled, and rolls back save failures', async () => {
const { chrome, harness: helpers, inputOperationDelayEnabled: input, logs, runtimeMessages: sentMessages } = loadOperationDelaySidepanelHarness();
helpers.applyOperationDelayState({ operationDelayEnabled: false });
assert.equal(input.checked, false);
assert.equal(helpers.getLatestState().operationDelayEnabled, false);
helpers.applyOperationDelayState({ operationDelayEnabled: undefined }, { restoreFailed: true });
assert.equal(input.checked, true);
assert.equal(helpers.getLatestState().operationDelayEnabled, true);
assert.equal(logs.filter((entry) => entry.level === 'warn').length, 1);
logs.length = 0;
input.checked = false;
await helpers.persistOperationDelayToggle();
assert.equal(sentMessages.at(-1).payload.operationDelayEnabled, false);
assert.equal(logs.length, 1);
assert.match(logs[0].message, /关闭|off/i);
sentMessages.length = 0;
chrome.runtime.sendMessage = async () => { throw new Error('network down'); };
logs.length = 0;
input.checked = true;
helpers.setLastConfirmedOperationDelayEnabled(false);
await assert.rejects(() => helpers.persistOperationDelayToggle(), /network down/);
assert.equal(input.checked, false);
assert.equal(logs.filter((entry) => entry.level === 'error').length, 1);
harness.applyOperationDelayState({ operationDelayEnabled: false });
assert.equal(harness.getLatestState().operationDelayEnabled, true);
});