feat: 固定操作间延迟设置并更新相关文档和测试
This commit is contained in:
@@ -517,7 +517,7 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。
|
|||||||
|
|
||||||
### 操作间延迟
|
### 操作间延迟
|
||||||
|
|
||||||
`操作间延迟` 默认开启。开启后,自动流程和手动单步在每个页面输入、选择、点击、提交、继续或授权操作完成后固定等待 2 秒;第一项页面操作不会提前等待。分格 OTP/验证码会先整组填完,然后只等待一次。
|
`操作间延迟` 固定开启。自动流程和手动单步在每个页面输入、选择、点击、提交、继续或授权操作完成后固定等待 2 秒;第一项页面操作不会提前等待。分格 OTP/验证码会先整组填完,然后只等待一次。
|
||||||
|
|
||||||
该开关不同于步间间隔,不影响邮箱轮询、短信/WhatsApp 轮询、后台 API、网络重试、后台定时器或存储持久化,也不影响 `confirm-oauth` 和 `platform-verify` 的交互节奏。
|
该开关不同于步间间隔,不影响邮箱轮询、短信/WhatsApp 轮询、后台 API、网络重试、后台定时器或存储持久化,也不影响 `confirm-oauth` 和 `platform-verify` 的交互节奏。
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -3033,7 +3033,7 @@ function normalizePersistentSettingValue(key, value) {
|
|||||||
case 'autoRunDelayEnabled':
|
case 'autoRunDelayEnabled':
|
||||||
return Boolean(value);
|
return Boolean(value);
|
||||||
case 'operationDelayEnabled':
|
case 'operationDelayEnabled':
|
||||||
return typeof value === 'boolean' ? value : true;
|
return true;
|
||||||
case 'step6CookieCleanupEnabled':
|
case 'step6CookieCleanupEnabled':
|
||||||
return Boolean(value);
|
return Boolean(value);
|
||||||
case 'stepExecutionRangeByFlow':
|
case 'stepExecutionRangeByFlow':
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
let operationDelaySettingRevision = 0;
|
let operationDelaySettingRevision = 0;
|
||||||
|
|
||||||
function normalizeOperationDelayEnabled(value) {
|
function normalizeOperationDelayEnabled(value) {
|
||||||
return typeof value === 'boolean' ? value : true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOperationDelayEnabled() {
|
function getOperationDelayEnabled() {
|
||||||
|
|||||||
+1
-1
@@ -75,6 +75,6 @@
|
|||||||
|
|
||||||
## 7. 操作间延迟
|
## 7. 操作间延迟
|
||||||
|
|
||||||
`操作间延迟` 默认开启。开启后,自动流程和手动单步在每个页面输入、选择、点击、提交、继续或授权操作完成后固定等待 2 秒;第一项页面操作不会提前等待。分格 OTP/验证码会先整组填完,然后只等待一次。
|
`操作间延迟` 固定开启。自动流程和手动单步在每个页面输入、选择、点击、提交、继续或授权操作完成后固定等待 2 秒;第一项页面操作不会提前等待。分格 OTP/验证码会先整组填完,然后只等待一次。
|
||||||
|
|
||||||
该开关不同于步间间隔,不影响邮箱轮询、短信/WhatsApp 轮询、后台 API、网络重试、后台定时器或存储持久化,也不影响 `confirm-oauth` 和 `platform-verify` 的交互节奏。
|
该开关不同于步间间隔,不影响邮箱轮询、短信/WhatsApp 轮询、后台 API、网络重试、后台定时器或存储持久化,也不影响 `confirm-oauth` 和 `platform-verify` 的交互节奏。
|
||||||
|
|||||||
+20
-35
@@ -596,26 +596,6 @@
|
|||||||
<button id="btn-fetch-email" class="btn btn-outline btn-sm data-inline-btn" type="button">获取</button>
|
<button id="btn-fetch-email" class="btn btn-outline btn-sm data-inline-btn" type="button">获取</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="data-row" id="row-step-execution-range">
|
|
||||||
<span class="data-label">执行范围</span>
|
|
||||||
<div class="data-inline setting-pair step-execution-range-setting">
|
|
||||||
<div class="setting-group setting-group-primary">
|
|
||||||
<label class="toggle-switch" for="input-step-execution-range-enabled" title="开启后,只执行指定起止步骤,范围外步骤禁用">
|
|
||||||
<input type="checkbox" id="input-step-execution-range-enabled" />
|
|
||||||
<span class="toggle-switch-track" aria-hidden="true">
|
|
||||||
<span class="toggle-switch-thumb"></span>
|
|
||||||
</span>
|
|
||||||
<span>启用</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="setting-group setting-group-secondary step-range-controls">
|
|
||||||
<span class="setting-caption">步骤</span>
|
|
||||||
<input type="number" id="input-step-execution-range-from" class="data-input auto-delay-input step-range-input" value="1" min="1" step="1" title="允许执行的起始步骤" />
|
|
||||||
<span class="data-unit">到</span>
|
|
||||||
<input type="number" id="input-step-execution-range-to" class="data-input auto-delay-input step-range-input" value="1" min="1" step="1" title="允许执行的结束步骤" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="data-row module-divider-start" id="row-auto-delay-settings">
|
<div class="data-row module-divider-start" id="row-auto-delay-settings">
|
||||||
<span class="data-label">第六步</span>
|
<span class="data-label">第六步</span>
|
||||||
<div class="data-inline setting-pair auto-delay-setting-pair">
|
<div class="data-inline setting-pair auto-delay-setting-pair">
|
||||||
@@ -644,21 +624,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="data-row" id="row-operation-delay-settings">
|
|
||||||
<span class="data-label">操作间延迟</span>
|
|
||||||
<div class="data-inline setting-pair operation-delay-setting-pair">
|
|
||||||
<div class="setting-group setting-group-primary operation-delay-setting">
|
|
||||||
<label class="toggle-switch" for="input-operation-delay-enabled" title="开启后,页面输入、选择、点击、提交、继续、授权操作完成后等待 2 秒">
|
|
||||||
<input type="checkbox" id="input-operation-delay-enabled" checked />
|
|
||||||
<span class="toggle-switch-track" aria-hidden="true">
|
|
||||||
<span class="toggle-switch-thumb"></span>
|
|
||||||
</span>
|
|
||||||
<span>启用</span>
|
|
||||||
</label>
|
|
||||||
<span class="setting-caption">页面输入、选择、点击、提交、继续、授权后等待 2 秒</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="data-row">
|
<div class="data-row">
|
||||||
<span class="data-label">自动重试</span>
|
<span class="data-label">自动重试</span>
|
||||||
<div class="data-inline setting-pair">
|
<div class="data-inline setting-pair">
|
||||||
@@ -704,6 +669,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="data-row" id="row-step-execution-range">
|
||||||
|
<span class="data-label">执行范围</span>
|
||||||
|
<div class="data-inline setting-pair step-execution-range-setting">
|
||||||
|
<div class="setting-group setting-group-primary">
|
||||||
|
<label class="toggle-switch" for="input-step-execution-range-enabled" title="开启后,只执行指定起止步骤,范围外步骤禁用">
|
||||||
|
<input type="checkbox" id="input-step-execution-range-enabled" />
|
||||||
|
<span class="toggle-switch-track" aria-hidden="true">
|
||||||
|
<span class="toggle-switch-thumb"></span>
|
||||||
|
</span>
|
||||||
|
<span>启用</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="setting-group setting-group-secondary step-range-controls">
|
||||||
|
<span class="setting-caption">步骤</span>
|
||||||
|
<input type="number" id="input-step-execution-range-from" class="data-input auto-delay-input step-range-input" value="1" min="1" step="1" title="允许执行的起始步骤" />
|
||||||
|
<span class="data-unit">到</span>
|
||||||
|
<input type="number" id="input-step-execution-range-to" class="data-input auto-delay-input step-range-input" value="1" min="1" step="1" title="允许执行的结束步骤" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="data-row module-divider-start" id="row-oauth-display">
|
<div class="data-row module-divider-start" id="row-oauth-display">
|
||||||
<span class="data-label">OAuth</span>
|
<span class="data-label">OAuth</span>
|
||||||
<span id="display-oauth-url" class="data-value mono truncate">等待中...</span>
|
<span id="display-oauth-url" class="data-value mono truncate">等待中...</span>
|
||||||
|
|||||||
+5
-11
@@ -399,7 +399,6 @@ const inputStep6CookieCleanupEnabled = document.getElementById('input-step6-cook
|
|||||||
const inputAutoDelayEnabled = document.getElementById('input-auto-delay-enabled');
|
const inputAutoDelayEnabled = document.getElementById('input-auto-delay-enabled');
|
||||||
const inputAutoDelayMinutes = document.getElementById('input-auto-delay-minutes');
|
const inputAutoDelayMinutes = document.getElementById('input-auto-delay-minutes');
|
||||||
const inputAutoStepDelaySeconds = document.getElementById('input-auto-step-delay-seconds');
|
const inputAutoStepDelaySeconds = document.getElementById('input-auto-step-delay-seconds');
|
||||||
const inputOperationDelayEnabled = document.getElementById('input-operation-delay-enabled');
|
|
||||||
const inputOAuthFlowTimeoutEnabled = document.getElementById('input-oauth-flow-timeout-enabled');
|
const inputOAuthFlowTimeoutEnabled = document.getElementById('input-oauth-flow-timeout-enabled');
|
||||||
const rowStepExecutionRange = document.getElementById('row-step-execution-range');
|
const rowStepExecutionRange = document.getElementById('row-step-execution-range');
|
||||||
const inputStepExecutionRangeEnabled = document.getElementById('input-step-execution-range-enabled');
|
const inputStepExecutionRangeEnabled = document.getElementById('input-step-execution-range-enabled');
|
||||||
@@ -554,7 +553,6 @@ let currentSignupMethod = DEFAULT_SIGNUP_METHOD;
|
|||||||
let currentPhoneSignupReloginAfterBindEmailEnabled = DEFAULT_PHONE_SIGNUP_RELOGIN_AFTER_BIND_EMAIL_ENABLED;
|
let currentPhoneSignupReloginAfterBindEmailEnabled = DEFAULT_PHONE_SIGNUP_RELOGIN_AFTER_BIND_EMAIL_ENABLED;
|
||||||
let currentStepDefinitionFlowId = DEFAULT_ACTIVE_FLOW_ID;
|
let currentStepDefinitionFlowId = DEFAULT_ACTIVE_FLOW_ID;
|
||||||
let phoneSignupReuseUiWasLocked = false;
|
let phoneSignupReuseUiWasLocked = false;
|
||||||
let lastConfirmedOperationDelayEnabled = true;
|
|
||||||
let heroSmsCountrySelectionOrder = [];
|
let heroSmsCountrySelectionOrder = [];
|
||||||
let phoneSmsProviderOrderSelection = [];
|
let phoneSmsProviderOrderSelection = [];
|
||||||
let heroSmsCountryMenuSearchKeyword = '';
|
let heroSmsCountryMenuSearchKeyword = '';
|
||||||
@@ -2634,7 +2632,7 @@ function scheduleAccountRunHistoryRefresh(delayMs = 150) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function normalizeOperationDelayEnabled(value) {
|
function normalizeOperationDelayEnabled(value) {
|
||||||
return typeof value === 'boolean' ? value : true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function appendOperationDelayLog(enabled, level = 'info', message = '') {
|
function appendOperationDelayLog(enabled, level = 'info', message = '') {
|
||||||
@@ -2642,15 +2640,13 @@ function appendOperationDelayLog(enabled, level = 'info', message = '') {
|
|||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
level,
|
level,
|
||||||
message: message || (enabled
|
message: message || (enabled
|
||||||
? '操作间延迟已开启:页面输入、选择、点击、提交、继续、授权后等待 2 秒。'
|
? '操作间延迟已固定开启。'
|
||||||
: '操作间延迟已关闭:页面操作将连续执行。'),
|
: '操作间延迟保持固定开启。'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyOperationDelayState(state = latestState, options = {}) {
|
function applyOperationDelayState(state = latestState, options = {}) {
|
||||||
const enabled = options.restoreFailed ? true : normalizeOperationDelayEnabled(state?.operationDelayEnabled);
|
const enabled = options.restoreFailed ? true : normalizeOperationDelayEnabled(state?.operationDelayEnabled);
|
||||||
lastConfirmedOperationDelayEnabled = enabled;
|
|
||||||
if (inputOperationDelayEnabled) inputOperationDelayEnabled.checked = enabled;
|
|
||||||
if (typeof syncLatestState === 'function') {
|
if (typeof syncLatestState === 'function') {
|
||||||
syncLatestState({ operationDelayEnabled: enabled });
|
syncLatestState({ operationDelayEnabled: enabled });
|
||||||
}
|
}
|
||||||
@@ -2660,6 +2656,8 @@ function applyOperationDelayState(state = latestState, options = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function persistOperationDelayToggle() {
|
async function persistOperationDelayToggle() {
|
||||||
|
applyOperationDelayState({ operationDelayEnabled: true });
|
||||||
|
return true;
|
||||||
const nextEnabled = normalizeOperationDelayEnabled(inputOperationDelayEnabled?.checked);
|
const nextEnabled = normalizeOperationDelayEnabled(inputOperationDelayEnabled?.checked);
|
||||||
try {
|
try {
|
||||||
const response = await chrome.runtime.sendMessage({
|
const response = await chrome.runtime.sendMessage({
|
||||||
@@ -13741,10 +13739,6 @@ inputPlusModeEnabled?.addEventListener('change', () => {
|
|||||||
saveSettings({ silent: true }).catch(() => { });
|
saveSettings({ silent: true }).catch(() => { });
|
||||||
});
|
});
|
||||||
|
|
||||||
inputOperationDelayEnabled?.addEventListener('change', () => {
|
|
||||||
persistOperationDelayToggle().catch(() => { });
|
|
||||||
});
|
|
||||||
|
|
||||||
selectPlusPaymentMethod?.addEventListener('change', () => {
|
selectPlusPaymentMethod?.addEventListener('change', () => {
|
||||||
selectPlusPaymentMethod.value = normalizePlusPaymentMethod(selectPlusPaymentMethod.value);
|
selectPlusPaymentMethod.value = normalizePlusPaymentMethod(selectPlusPaymentMethod.value);
|
||||||
updatePlusModeUI();
|
updatePlusModeUI();
|
||||||
|
|||||||
@@ -276,7 +276,7 @@ test('SAVE_SETTING broadcasts operation delay setting without background success
|
|||||||
addLog: async (message, level = 'info') => logs.push({ message, level }),
|
addLog: async (message, level = 'info') => logs.push({ message, level }),
|
||||||
buildLuckmailSessionSettingsPayload: () => ({}),
|
buildLuckmailSessionSettingsPayload: () => ({}),
|
||||||
buildPersistentSettingsPayload: (input = {}) => Object.prototype.hasOwnProperty.call(input, 'operationDelayEnabled')
|
buildPersistentSettingsPayload: (input = {}) => Object.prototype.hasOwnProperty.call(input, 'operationDelayEnabled')
|
||||||
? { operationDelayEnabled: input.operationDelayEnabled === false ? false : true }
|
? { operationDelayEnabled: true }
|
||||||
: {},
|
: {},
|
||||||
broadcastDataUpdate: (payload) => broadcasts.push(payload),
|
broadcastDataUpdate: (payload) => broadcasts.push(payload),
|
||||||
getState: async () => ({ ...state }),
|
getState: async () => ({ ...state }),
|
||||||
@@ -291,8 +291,8 @@ test('SAVE_SETTING broadcasts operation delay setting without background success
|
|||||||
});
|
});
|
||||||
|
|
||||||
assert.equal(response.ok, true);
|
assert.equal(response.ok, true);
|
||||||
assert.equal(state.operationDelayEnabled, false);
|
assert.equal(state.operationDelayEnabled, true);
|
||||||
assert.deepStrictEqual(broadcasts.at(-1), { operationDelayEnabled: false });
|
assert.deepStrictEqual(broadcasts.at(-1), { operationDelayEnabled: true });
|
||||||
assert.equal(logs.length, 0);
|
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', value), true);
|
||||||
}
|
}
|
||||||
assert.equal(api.normalizePersistentSettingValue('operationDelayEnabled', true), 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', () => {
|
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]) {
|
for (const value of [undefined, null, '', 0, 'false', true]) {
|
||||||
assert.equal(api.buildPersistentSettingsPayload({ operationDelayEnabled: value }, { fillDefaults: true }).operationDelayEnabled, 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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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']);
|
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();
|
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: 'confirm-oauth', kind: 'click' }), false);
|
||||||
assert.equal(api.shouldDelayOperation({ enabled: true, stepKey: 'platform-verify', kind: 'submit' }), 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);
|
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']);
|
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 = [];
|
const events = [];
|
||||||
let resolveStorage;
|
let resolveStorage;
|
||||||
const storageReady = new Promise((resolve) => { resolveStorage = resolve; });
|
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 });
|
resolveStorage({ operationDelayEnabled: false });
|
||||||
await pendingOperation;
|
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 = [];
|
const events = [];
|
||||||
let changeListener = null;
|
let changeListener = null;
|
||||||
let resolveStorage;
|
let resolveStorage;
|
||||||
@@ -88,17 +88,17 @@ test('newer disabled storage change wins over older pending restore', async () =
|
|||||||
});
|
});
|
||||||
|
|
||||||
changeListener({ operationDelayEnabled: { newValue: false } }, 'local');
|
changeListener({ operationDelayEnabled: { newValue: false } }, 'local');
|
||||||
assert.equal(api.getOperationDelayEnabled(), false);
|
assert.equal(api.getOperationDelayEnabled(), true);
|
||||||
resolveStorage({ operationDelayEnabled: true });
|
resolveStorage({ operationDelayEnabled: true });
|
||||||
await waitForTimer();
|
await waitForTimer();
|
||||||
|
|
||||||
assert.equal(api.getOperationDelayEnabled(), false);
|
assert.equal(api.getOperationDelayEnabled(), true);
|
||||||
await api.performOperationWithDelay(
|
await api.performOperationWithDelay(
|
||||||
{ stepKey: 'fill-profile', kind: 'fill' },
|
{ stepKey: 'fill-profile', kind: 'fill' },
|
||||||
async () => events.push('operation'),
|
async () => events.push('operation'),
|
||||||
{ sleep: async (ms) => events.push(`sleep:${ms}`) }
|
{ 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 () => {
|
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']);
|
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 events = [];
|
||||||
const api = loadOperationDelayApi({
|
const api = loadOperationDelayApi({
|
||||||
events,
|
events,
|
||||||
@@ -147,7 +147,7 @@ test('first operation honors async persisted disabled setting before fallback bu
|
|||||||
async () => events.push('operation'),
|
async () => events.push('operation'),
|
||||||
{ sleep: async (ms) => events.push(`sleep:${ms}`) }
|
{ 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 () => {
|
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']);
|
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 events = [];
|
||||||
const api = loadOperationDelayApi({
|
const api = loadOperationDelayApi({
|
||||||
events,
|
events,
|
||||||
@@ -181,7 +181,7 @@ test('explicit disabled metadata does not wait for hanging initial storage resto
|
|||||||
);
|
);
|
||||||
|
|
||||||
assert.equal(await resolveOrPending(operation), 'resolved');
|
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 () => {
|
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);
|
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() {} } } } });
|
const api = loadOperationDelayApi({ chrome: { storage: { local: { get: async () => ({ operationDelayEnabled: false }) }, onChanged: { addListener() {} } } } });
|
||||||
await api.refreshOperationDelaySetting();
|
await api.refreshOperationDelaySetting();
|
||||||
assert.equal(api.getOperationDelayEnabled(), false);
|
assert.equal(api.getOperationDelayEnabled(), true);
|
||||||
assert.equal(api.shouldDelayOperation({ stepKey: 'fill-profile', kind: 'fill' }), false);
|
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 = [];
|
const events = [];
|
||||||
let changeListener = null;
|
let changeListener = null;
|
||||||
const api = loadOperationDelayApi({
|
const api = loadOperationDelayApi({
|
||||||
@@ -212,14 +212,14 @@ test('storage change to disabled is honored by later delayed operations', async
|
|||||||
await api.refreshOperationDelaySetting();
|
await api.refreshOperationDelaySetting();
|
||||||
changeListener({ operationDelayEnabled: { newValue: false } }, 'local');
|
changeListener({ operationDelayEnabled: { newValue: false } }, 'local');
|
||||||
|
|
||||||
assert.equal(api.getOperationDelayEnabled(), false);
|
assert.equal(api.getOperationDelayEnabled(), true);
|
||||||
assert.equal(api.shouldDelayOperation({ stepKey: 'fill-profile', kind: 'fill' }), false);
|
assert.equal(api.shouldDelayOperation({ stepKey: 'fill-profile', kind: 'fill' }), true);
|
||||||
await api.performOperationWithDelay(
|
await api.performOperationWithDelay(
|
||||||
{ stepKey: 'fill-profile', kind: 'fill' },
|
{ stepKey: 'fill-profile', kind: 'fill' },
|
||||||
async () => events.push('operation'),
|
async () => events.push('operation'),
|
||||||
{ sleep: async (ms) => events.push(`sleep:${ms}`) }
|
{ 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 () => {
|
test('storage change to enabled is honored by later delayed operations', async () => {
|
||||||
|
|||||||
@@ -38,95 +38,43 @@ function extractFunction(name) {
|
|||||||
throw new Error(`unterminated ${name}`);
|
throw new Error(`unterminated ${name}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
test('sidepanel exposes one operation delay switch in the existing settings surface', () => {
|
test('sidepanel no longer exposes operation delay switch and places step execution range below oauth timeout', () => {
|
||||||
assert.match(html, /id="row-operation-delay-settings"/);
|
assert.doesNotMatch(html, /id="row-operation-delay-settings"/);
|
||||||
assert.match(html, /id="input-operation-delay-enabled"/);
|
assert.doesNotMatch(html, /id="input-operation-delay-enabled"/);
|
||||||
assert.match(html, /操作间延迟/);
|
|
||||||
assert.match(html, /2\s*秒/);
|
const autoDelayIndex = html.indexOf('id="row-auto-delay-settings"');
|
||||||
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}授权/);
|
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', () => {
|
test('sidepanel operation delay state is always normalized back to enabled', () => {
|
||||||
assert.match(source, /inputOperationDelayEnabled/);
|
const harness = new Function(`
|
||||||
assert.match(source, /operationDelayEnabled/);
|
let latestState = { operationDelayEnabled: false };
|
||||||
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;
|
|
||||||
function syncLatestState(nextState) {
|
function syncLatestState(nextState) {
|
||||||
latestState = { ...(latestState || {}), ...(nextState || {}) };
|
latestState = { ...(latestState || {}), ...(nextState || {}) };
|
||||||
}
|
}
|
||||||
${extractFunction('normalizeOperationDelayEnabled')}
|
${extractFunction('normalizeOperationDelayEnabled')}
|
||||||
${extractFunction('appendOperationDelayLog')}
|
|
||||||
${extractFunction('applyOperationDelayState')}
|
${extractFunction('applyOperationDelayState')}
|
||||||
${extractFunction('persistOperationDelayToggle')}
|
|
||||||
return {
|
return {
|
||||||
|
normalizeOperationDelayEnabled,
|
||||||
applyOperationDelayState,
|
applyOperationDelayState,
|
||||||
persistOperationDelayToggle,
|
|
||||||
getLatestState: () => latestState,
|
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 () => {
|
harness.applyOperationDelayState({ operationDelayEnabled: false });
|
||||||
const { chrome, harness: helpers, inputOperationDelayEnabled: input, logs, runtimeMessages: sentMessages } = loadOperationDelaySidepanelHarness();
|
assert.equal(harness.getLatestState().operationDelayEnabled, true);
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user