feat: add configurable operation delay

This commit is contained in:
root
2026-05-08 12:50:20 -04:00
parent 872f382815
commit c23461261b
44 changed files with 4770 additions and 193 deletions
+334 -2
View File
@@ -57,9 +57,24 @@ test('fillVerificationCode submits after split inputs are stably filled', async
const logs = [];
const clicks = [];
const filledValues = [];
const splitCodeEvents = [];
let activeOperationLabel = '';
let submitClicked = false;
const VERIFICATION_CODE_INPUT_SELECTOR = 'input[data-verification-code]';
const location = { href: 'https://auth.openai.com/email-verification' };
const window = {
CodexOperationDelay: {
async performOperationWithDelay(metadata, operation) {
splitCodeEvents.push(\`operation:\${metadata.label}:start\`);
activeOperationLabel = metadata.label;
const result = await operation();
activeOperationLabel = '';
splitCodeEvents.push(\`operation:\${metadata.label}:end\`);
splitCodeEvents.push(\`delay:\${metadata.label}:2000\`);
return result;
},
},
};
function KeyboardEvent(type, init = {}) {
this.type = type;
Object.assign(this, init);
@@ -113,8 +128,16 @@ async function handle405ResendError() {}
function fillInput(el, value) {
el.value = value;
filledValues.push(value);
const splitIndex = inputs.indexOf(el);
if (splitIndex >= 0) {
splitCodeEvents.push(\`fill-code:\${splitIndex}\`);
}
}
async function sleep(ms) {
if (activeOperationLabel) {
splitCodeEvents.push(\`sleep:\${activeOperationLabel}:\${ms}\`);
}
}
async function sleep() {}
async function waitForDocumentLoadComplete() {}
function isStep5Ready() { return false; }
function isStep8Ready() { return false; }
@@ -125,7 +148,7 @@ function isVisibleElement() { return true; }
function isActionEnabled(el) { return Boolean(el) && !el.disabled; }
function getActionText(el) { return el.textContent || ''; }
async function humanPause() {}
function simulateClick(el) { el.click(); clicks.push(el.textContent); }
function simulateClick(el) { el.click(); clicks.push(el.textContent); splitCodeEvents.push('click:submit-code'); }
async function waitForVerificationSubmitOutcome() { return { success: true }; }
${extractFunction('getVisibleSplitVerificationInputs')}
@@ -150,6 +173,7 @@ return {
logs,
clicks,
filledValues,
splitCodeEvents,
submitClicked,
currentValue: inputs.map((input) => input.value).join(''),
};
@@ -164,6 +188,22 @@ return {
assert.equal(snapshot.currentValue, '123456');
assert.equal(snapshot.submitClicked, true);
assert.deepStrictEqual(snapshot.clicks, ['Continue']);
assert.deepStrictEqual(snapshot.splitCodeEvents, [
'operation:split-code:start',
'fill-code:0',
'fill-code:1',
'fill-code:2',
'fill-code:3',
'fill-code:4',
'fill-code:5',
'operation:split-code:end',
'delay:split-code:2000',
'operation:submit-code:start',
'click:submit-code',
'operation:submit-code:end',
'delay:submit-code:2000',
]);
assert.equal(snapshot.splitCodeEvents.filter((event) => event.startsWith('delay:split-code')).length, 1);
});
test('fillVerificationCode does not short-circuit on mixed email-verification profile page before verification exits', async () => {
@@ -895,3 +935,295 @@ return {
assert.equal(result.clicks.length, 0);
assert.equal(result.logs.some(({ message }) => /检测到密码页报错/.test(message)), true);
});
test('fillSignupEmailAndContinue waits for submit operation delay before returning', async () => {
const api = new Function(`
const events = [];
let releaseSubmitDelay;
const submitDelay = new Promise((resolve) => { releaseSubmitDelay = resolve; });
const emailInput = { value: '' };
const continueButton = { textContent: 'Continue' };
const location = { href: 'https://auth.openai.com/u/signup' };
const window = {
setTimeout(callback, ms) {
events.push(\`timer:\${ms}\`);
return 1;
},
CodexOperationDelay: {
async performOperationWithDelay(metadata, operation) {
events.push(\`operation:\${metadata.label}:start\`);
const result = await operation();
events.push(\`operation:\${metadata.label}:end\`);
if (metadata.kind === 'submit') {
events.push(\`delay:\${metadata.label}:pending\`);
await submitDelay;
}
events.push(\`delay:\${metadata.label}:2000\`);
return result;
},
},
};
function getOperationDelayRunner() { return window.CodexOperationDelay.performOperationWithDelay; }
function throwIfStopped() {}
function isStopError() { return false; }
function log() {}
async function humanPause() {}
async function sleep(ms) { events.push(\`sleep:\${ms}\`); }
function fillInput(input, value) { input.value = value; events.push(\`fill:\${value}\`); }
function simulateClick(el) { events.push(\`click:\${el.textContent}\`); }
async function waitForSignupEntryState() {
return { state: 'email_entry', emailInput, continueButton, url: location.href };
}
function getSignupEmailContinueButton() { return continueButton; }
function isActionEnabled() { return true; }
${extractFunction('fillSignupEmailAndContinue')}
return {
events,
releaseSubmitDelay() { releaseSubmitDelay(); },
run() { return fillSignupEmailAndContinue('ada@example.com', 2); },
};
`)();
let settled = false;
const tracked = api.run().then((result) => {
settled = true;
api.events.push('run:resolved');
return result;
});
await new Promise((resolve) => setImmediate(resolve));
assert.equal(settled, false, 'email submit must not return before submit delay resolves');
assert.equal(api.events.includes('delay:submit-signup-email:pending'), true);
assert.equal(api.events.includes('run:resolved'), false);
api.releaseSubmitDelay();
const result = await tracked;
assert.equal(result.submitted, true);
assert.deepStrictEqual(api.events, [
'operation:signup-email:start',
'fill:ada@example.com',
'operation:signup-email:end',
'delay:signup-email:2000',
'sleep:120',
'operation:submit-signup-email:start',
'click:Continue',
'operation:submit-signup-email:end',
'delay:submit-signup-email:pending',
'delay:submit-signup-email:2000',
'run:resolved',
]);
});
test('submitSignupPhoneNumberAndContinue waits for submit operation delay before returning', async () => {
const api = new Function(`
const events = [];
let releaseSubmitDelay;
const submitDelay = new Promise((resolve) => { releaseSubmitDelay = resolve; });
const phoneInput = { value: '' };
const continueButton = { textContent: 'Continue' };
const location = { href: 'https://auth.openai.com/u/signup/phone' };
const window = {
setTimeout(callback, ms) {
events.push(\`timer:\${ms}\`);
return 1;
},
CodexOperationDelay: {
async performOperationWithDelay(metadata, operation) {
events.push(\`operation:\${metadata.label}:start\`);
const result = await operation();
events.push(\`operation:\${metadata.label}:end\`);
if (metadata.kind === 'submit') {
events.push(\`delay:\${metadata.label}:pending\`);
await submitDelay;
}
events.push(\`delay:\${metadata.label}:2000\`);
return result;
},
},
};
function getOperationDelayRunner() { return window.CodexOperationDelay.performOperationWithDelay; }
function throwIfStopped() {}
function isStopError() { return false; }
function log() {}
async function humanPause() {}
async function sleep(ms) { events.push(\`sleep:\${ms}\`); }
function fillInput(input, value) { input.value = value; events.push(\`fill:\${value}\`); }
function simulateClick(el) { events.push(\`click:\${el.textContent}\`); }
async function waitForSignupPhoneEntryState() {
return { state: 'phone_entry', phoneInput, url: location.href };
}
async function ensureSignupPhoneCountrySelected() {
return { hasCountryControl: false, matched: true, selectedOption: null };
}
function resolveSignupPhoneDialCode() { return '1'; }
function toNationalPhoneNumber() { return '5551234567'; }
function getSignupPhoneHiddenNumberInput() { return null; }
function toE164PhoneNumber() { return '+15551234567'; }
function getSignupEmailContinueButton() { return continueButton; }
function isActionEnabled() { return true; }
${extractFunction('submitSignupPhoneNumberAndContinue')}
return {
events,
releaseSubmitDelay() { releaseSubmitDelay(); },
run() { return submitSignupPhoneNumberAndContinue({ phoneNumber: '+15551234567' }); },
};
`)();
let settled = false;
const tracked = api.run().then((result) => {
settled = true;
api.events.push('run:resolved');
return result;
});
await new Promise((resolve) => setImmediate(resolve));
assert.equal(settled, false, 'phone submit must not return before submit delay resolves');
assert.equal(api.events.includes('delay:submit-signup-phone:pending'), true);
assert.equal(api.events.includes('run:resolved'), false);
api.releaseSubmitDelay();
const result = await tracked;
assert.equal(result.submitted, true);
assert.deepStrictEqual(api.events, [
'operation:signup-phone-number:start',
'fill:5551234567',
'operation:signup-phone-number:end',
'delay:signup-phone-number:2000',
'sleep:120',
'operation:submit-signup-phone:start',
'click:Continue',
'operation:submit-signup-phone:end',
'delay:submit-signup-phone:pending',
'delay:submit-signup-phone:2000',
'run:resolved',
]);
});
test('step3_fillEmailPassword reports complete before deferred submit while submit still waits for operation delay', async () => {
const api = new Function(`
const events = [];
const reports = [];
const scheduled = [];
let releaseSubmitDelay;
const submitDelay = new Promise((resolve) => { releaseSubmitDelay = resolve; });
const Date = { now: () => 12345 };
const passwordInput = { value: '' };
const submitButton = { textContent: 'Continue' };
const location = { href: 'https://auth.openai.com/u/signup/password' };
const window = {
setTimeout(callback, ms) {
events.push(\`timer:\${ms}\`);
scheduled.push(callback);
return scheduled.length;
},
CodexOperationDelay: {
async performOperationWithDelay(metadata, operation) {
events.push(\`operation:\${metadata.label}:start\`);
const result = await operation();
events.push(\`operation:\${metadata.label}:end\`);
if (metadata.kind === 'submit') {
events.push(\`delay:\${metadata.label}:pending\`);
await submitDelay;
}
events.push(\`delay:\${metadata.label}:2000\`);
return result;
},
},
};
function getOperationDelayRunner() { return window.CodexOperationDelay.performOperationWithDelay; }
function throwIfStopped() {}
function isStopError() { return false; }
function log(message) { events.push(\`log:\${message}\`); }
async function humanPause() {}
async function sleep(ms) { events.push(\`sleep:\${ms}\`); }
function fillInput(input, value) { input.value = value; events.push(\`fill-password:\${value}\`); }
function simulateClick(el) { events.push(\`click:\${el.textContent}\`); }
function inspectSignupEntryState() {
return { state: 'password_page', passwordInput, submitButton, displayedEmail: 'ada@example.com' };
}
function getSignupPasswordSubmitButton() { return submitButton; }
async function waitForElementByText() { return null; }
function logSignupPasswordDiagnostics() {}
function reportComplete(step, payload) {
reports.push({ step, payload });
events.push(\`report:\${payload.deferredSubmit}\`);
}
${extractFunction('step3_fillEmailPassword')}
return {
events,
reports,
scheduledCount() { return scheduled.length; },
async flushDeferredSubmit() {
if (!scheduled.length) throw new Error('missing deferred submit');
await scheduled[0]();
},
releaseSubmitDelay() { releaseSubmitDelay(); },
run() { return step3_fillEmailPassword({ email: 'ada@example.com', password: 'Secret123!' }); },
};
`)();
let settled = false;
const tracked = api.run().then((result) => {
settled = true;
api.events.push('run:resolved');
return result;
});
await new Promise((resolve) => setImmediate(resolve));
assert.equal(settled, true, 'step 3 must report and return before deferred submit can navigate');
const result = await tracked;
assert.equal(result.deferredSubmit, true);
assert.equal(api.reports.length, 1);
assert.equal(api.scheduledCount(), 1);
assert.equal(api.events.includes('operation:submit-signup-password:start'), false);
assert.deepStrictEqual(api.events, [
'operation:signup-password:start',
'fill-password:Secret123!',
'operation:signup-password:end',
'delay:signup-password:2000',
'log:步骤 3:密码已填写',
'report:true',
'timer:120',
'run:resolved',
]);
let flushed = false;
const flush = api.flushDeferredSubmit().then(() => {
flushed = true;
api.events.push('flush:resolved');
});
await new Promise((resolve) => setImmediate(resolve));
assert.equal(api.events.includes('operation:submit-signup-password:start'), true);
assert.equal(api.events.includes('delay:submit-signup-password:pending'), true);
assert.equal(flushed, false, 'deferred submit callback must wait for submit delay before resolving');
api.releaseSubmitDelay();
await flush;
assert.deepStrictEqual(api.events, [
'operation:signup-password:start',
'fill-password:Secret123!',
'operation:signup-password:end',
'delay:signup-password:2000',
'log:步骤 3:密码已填写',
'report:true',
'timer:120',
'run:resolved',
'sleep:500',
'operation:submit-signup-password:start',
'click:Continue',
'operation:submit-signup-password:end',
'delay:submit-signup-password:pending',
'delay:submit-signup-password:2000',
'log:步骤 3:表单已提交',
'flush:resolved',
]);
});