手机号注册模式

This commit is contained in:
QLHazyCoder
2026-05-04 12:56:35 +08:00
parent ff8b86a03b
commit c04b64c966
38 changed files with 4509 additions and 540 deletions
@@ -105,6 +105,15 @@ let currentState = {
autoRunDelayEnabled: false,
autoRunDelayMinutes: 30,
autoStepDelaySeconds: null,
signupMethod: 'phone',
resolvedSignupMethod: 'phone',
accountIdentifierType: 'phone',
accountIdentifier: '+6612345',
signupPhoneNumber: '+6612345',
signupPhoneActivation: { activationId: 'signup-activation', phoneNumber: '+6612345' },
signupPhoneCompletedActivation: { activationId: 'signup-completed', phoneNumber: '+6612345' },
signupPhoneVerificationRequestedAt: 123456,
signupPhoneVerificationPurpose: 'signup',
mailProvider: '163',
emailGenerator: 'duck',
gmailBaseEmail: 'demo@gmail.com',
@@ -162,6 +171,15 @@ async function resetState() {
autoRunDelayEnabled: prev.autoRunDelayEnabled,
autoRunDelayMinutes: prev.autoRunDelayMinutes,
autoStepDelaySeconds: prev.autoStepDelaySeconds,
signupMethod: prev.signupMethod,
resolvedSignupMethod: null,
accountIdentifierType: null,
accountIdentifier: '',
signupPhoneNumber: '',
signupPhoneActivation: null,
signupPhoneCompletedActivation: null,
signupPhoneVerificationRequestedAt: null,
signupPhoneVerificationPurpose: '',
mailProvider: prev.mailProvider,
emailGenerator: prev.emailGenerator,
gmailBaseEmail: prev.gmailBaseEmail,
@@ -335,6 +353,15 @@ return {
assert.strictEqual(snapshot.currentState.autoRunSessionId, 0, 'session id should be cleared after completion');
assert.strictEqual(snapshot.currentState.gmailBaseEmail, 'demo@gmail.com', 'gmail base email should survive fresh-attempt reset');
assert.strictEqual(snapshot.currentState.mail2925BaseEmail, 'demo@2925.com', '2925 base email should survive fresh-attempt reset');
assert.strictEqual(snapshot.currentState.signupMethod, 'phone', 'signup method setting should survive fresh-attempt reset');
assert.strictEqual(snapshot.currentState.resolvedSignupMethod, null, 'resolved signup method should be cleared before the next run freezes it again');
assert.strictEqual(snapshot.currentState.accountIdentifierType, null, 'account identifier type should be runtime-only');
assert.strictEqual(snapshot.currentState.accountIdentifier, '', 'account identifier should be runtime-only');
assert.strictEqual(snapshot.currentState.signupPhoneNumber, '', 'signup phone number should be runtime-only');
assert.strictEqual(snapshot.currentState.signupPhoneActivation, null, 'signup phone activation should be runtime-only');
assert.strictEqual(snapshot.currentState.signupPhoneCompletedActivation, null, 'completed signup phone activation should be runtime-only');
assert.strictEqual(snapshot.currentState.signupPhoneVerificationRequestedAt, null, 'signup phone request time should be runtime-only');
assert.strictEqual(snapshot.currentState.signupPhoneVerificationPurpose, '', 'signup phone purpose should be runtime-only');
assert.deepStrictEqual(
snapshot.currentState.reusablePhoneActivation,
{
@@ -66,6 +66,7 @@ test('auto-run stops step4 restart path when mail2925 ends the current thread',
const AUTO_STEP_DELAYS = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0, 10: 0 };
const LAST_STEP_ID = 10;
const FINAL_OAUTH_CHAIN_START_STEP = 7;
const SIGNUP_METHOD_PHONE = 'phone';
const chrome = {
tabs: {
update: async () => {},
@@ -107,6 +108,7 @@ async function ensureAutoEmailReady() {
}
async function broadcastAutoRunStatus() {}
async function ensureResolvedSignupMethodForRun() { return 'email'; }
async function getState() {
return currentState;
+6
View File
@@ -66,6 +66,7 @@ test('auto-run restarts from step 1 with the same email after step 4 failure', a
const AUTO_STEP_DELAYS = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0, 10: 0 };
const LAST_STEP_ID = 10;
const FINAL_OAUTH_CHAIN_START_STEP = 7;
const SIGNUP_METHOD_PHONE = 'phone';
const chrome = {
tabs: {
update: async () => {},
@@ -111,6 +112,7 @@ async function ensureAutoEmailReady() {
}
async function broadcastAutoRunStatus() {}
async function ensureResolvedSignupMethodForRun() { return 'email'; }
async function getState() {
return currentState;
@@ -215,6 +217,7 @@ test('auto-run does not restart step 4 current attempt when user_already_exists
const AUTO_STEP_DELAYS = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0, 10: 0 };
const LAST_STEP_ID = 10;
const FINAL_OAUTH_CHAIN_START_STEP = 7;
const SIGNUP_METHOD_PHONE = 'phone';
const chrome = {
tabs: {
update: async () => {},
@@ -256,6 +259,7 @@ async function ensureAutoEmailReady() {
}
async function broadcastAutoRunStatus() {}
async function ensureResolvedSignupMethodForRun() { return 'email'; }
async function getState() {
return currentState;
@@ -336,6 +340,7 @@ test('auto-run skips steps 4/5 when step 2 has already marked registration chain
const AUTO_STEP_DELAYS = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0, 10: 0 };
const LAST_STEP_ID = 10;
const FINAL_OAUTH_CHAIN_START_STEP = 7;
const SIGNUP_METHOD_PHONE = 'phone';
const chrome = {
tabs: {
update: async () => {},
@@ -376,6 +381,7 @@ async function ensureAutoEmailReady() {
}
async function broadcastAutoRunStatus() {}
async function ensureResolvedSignupMethodForRun() { return 'email'; }
async function getState() {
return currentState;
+2
View File
@@ -74,6 +74,7 @@ function createHarness(options = {}) {
const AUTO_STEP_DELAYS = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0, 10: 0 };
const LAST_STEP_ID = 10;
const FINAL_OAUTH_CHAIN_START_STEP = 7;
const SIGNUP_METHOD_PHONE = 'phone';
const LOG_PREFIX = '[test]';
const chrome = {
tabs: {
@@ -93,6 +94,7 @@ async function addLog(message, level = 'info') {
}
async function ensureAutoEmailReady() {}
async function ensureResolvedSignupMethodForRun() { return 'email'; }
async function broadcastAutoRunStatus() {}
async function getState() {
return {
@@ -54,12 +54,16 @@ test('background account history settings are normalized independently from hotm
extractFunction('normalizeHotmailLocalBaseUrl'),
extractFunction('normalizeAccountRunHistoryHelperBaseUrl'),
extractFunction('normalizeVerificationResendCount'),
extractFunction('normalizePlusPaymentMethod'),
extractFunction('normalizePhoneSmsProvider'),
extractFunction('normalizePhoneSmsProviderOrder'),
extractFunction('normalizeSignupMethod'),
extractFunction('normalizeFiveSimCountryCode'),
extractFunction('normalizeFiveSimCountryOrder'),
extractFunction('normalizeNexSmsCountryId'),
extractFunction('normalizeNexSmsCountryOrder'),
extractFunction('normalizeNexSmsServiceCode'),
extractFunction('normalizePhonePreferredActivation'),
extractFunction('normalizePhoneVerificationReplacementLimit'),
extractFunction('normalizePhoneCodeWaitSeconds'),
extractFunction('normalizePhoneCodeTimeoutWindows'),
@@ -106,6 +110,14 @@ const HERO_SMS_COUNTRY_ID = 52;
const HERO_SMS_COUNTRY_LABEL = 'Thailand';
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
const PHONE_SMS_PROVIDER_NEXSMS = 'nexsms';
const DEFAULT_PHONE_SMS_PROVIDER_ORDER = ['hero-sms', '5sim', 'nexsms'];
const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO_SMS;
const SIGNUP_METHOD_EMAIL = 'email';
const SIGNUP_METHOD_PHONE = 'phone';
const DEFAULT_SIGNUP_METHOD = SIGNUP_METHOD_EMAIL;
const DEFAULT_FIVE_SIM_PRODUCT = 'openai';
const DEFAULT_NEX_SMS_SERVICE_CODE = 'ot';
const FIVE_SIM_COUNTRY_ID = 'vietnam';
const FIVE_SIM_COUNTRY_LABEL = '越南 (Vietnam)';
const FIVE_SIM_OPERATOR = 'any';
@@ -155,9 +167,15 @@ return {
assert.equal(api.normalizePersistentSettingValue('phoneCodePollMaxRounds', '18'), 18);
assert.equal(api.normalizePersistentSettingValue('heroSmsMaxPrice', '0.123456'), '0.1235');
assert.equal(api.normalizePersistentSettingValue('heroSmsMaxPrice', '0'), '');
assert.equal(api.normalizePersistentSettingValue('heroSmsPreferredPrice', '0.051234'), '0.0512');
assert.equal(api.normalizePersistentSettingValue('signupMethod', 'phone'), 'phone');
assert.equal(api.normalizePersistentSettingValue('signupMethod', 'unknown'), 'email');
assert.equal(api.normalizePersistentSettingValue('phoneSmsProvider', '5SIM'), '5sim');
assert.equal(api.normalizePersistentSettingValue('phoneSmsProvider', 'NEXSMS'), 'nexsms');
assert.equal(api.normalizePersistentSettingValue('phoneSmsProvider', 'unknown'), 'hero-sms');
assert.deepStrictEqual(api.normalizePersistentSettingValue('phoneSmsProviderOrder', ['nexsms', '5sim', 'nexsms']), ['nexsms', '5sim']);
assert.equal(api.normalizePersistentSettingValue('fiveSimApiKey', ' demo-five '), ' demo-five ');
assert.equal(api.normalizePersistentSettingValue('fiveSimProduct', ' OpenAI! '), 'openai');
assert.equal(api.normalizePersistentSettingValue('fiveSimCountryId', ' England! '), 'vietnam');
assert.equal(api.normalizePersistentSettingValue('fiveSimCountryId', ''), 'vietnam');
assert.equal(api.normalizePersistentSettingValue('fiveSimCountryLabel', ''), '越南 (Vietnam)');
@@ -212,8 +230,36 @@ return {
api.normalizePersistentSettingValue('fiveSimCountryOrder', []),
[]
);
assert.deepStrictEqual(
api.normalizePersistentSettingValue('fiveSimCountryOrder', [' Thailand ', 'vietnam', 'thailand']),
['thailand', 'vietnam']
);
assert.equal(api.normalizePersistentSettingValue('nexSmsApiKey', ' demo-nex '), ' demo-nex ');
assert.deepStrictEqual(
api.normalizePersistentSettingValue('nexSmsCountryOrder', []),
[]
);
assert.deepStrictEqual(
api.normalizePersistentSettingValue('nexSmsCountryOrder', [1, '6', 1]),
[1, 6]
);
assert.equal(api.normalizePersistentSettingValue('nexSmsServiceCode', ' OT! '), 'ot');
assert.deepStrictEqual(
api.normalizePersistentSettingValue('phonePreferredActivation', {
provider: 'nexsms',
activationId: 'abc',
phoneNumber: '+6612345',
successfulUses: 2,
maxUses: 3,
}),
{
provider: 'nexsms',
activationId: 'abc',
phoneNumber: '+6612345',
successfulUses: 2,
maxUses: 3,
countryId: null,
countryLabel: '',
}
);
});
@@ -73,7 +73,10 @@ test('account run history helper upgrades old records, keeps stopped items and s
);
assert.deepStrictEqual(record, {
recordId: 'latest@example.com',
accountIdentifierType: 'email',
accountIdentifier: 'latest@example.com',
email: 'latest@example.com',
phoneNumber: '',
password: 'secret',
finalStatus: 'failed',
finishedAt: record.finishedAt,
@@ -140,6 +143,61 @@ test('account run history helper upgrades old records, keeps stopped items and s
assert.equal(normalizedStoppedRecord.failedStep, 7);
});
test('account run history helper accepts phone-only records without forcing email or password', () => {
const source = fs.readFileSync('background/account-run-history.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundAccountRunHistory;`)(globalScope);
const helpers = api.createAccountRunHistoryHelpers({
chrome: { storage: { local: { get: async () => ({}), set: async () => {} } } },
getState: async () => ({}),
normalizeAccountRunHistoryHelperBaseUrl: (value) => String(value || '').trim(),
});
const record = helpers.buildAccountRunHistoryRecord({
accountIdentifierType: 'phone',
accountIdentifier: '+6612345',
signupPhoneNumber: '+6612345',
password: '',
}, 'success');
assert.deepStrictEqual(record, {
recordId: 'phone:+6612345',
accountIdentifierType: 'phone',
accountIdentifier: '+6612345',
email: '',
phoneNumber: '+6612345',
password: '',
finalStatus: 'success',
finishedAt: record.finishedAt,
retryCount: 0,
failureLabel: '流程完成',
failureDetail: '',
failedStep: null,
source: 'manual',
autoRunContext: null,
plusModeEnabled: false,
contributionMode: false,
});
const normalized = helpers.normalizeAccountRunHistoryRecord({
recordId: 'phone:+6612345',
accountIdentifierType: 'phone',
accountIdentifier: '+6612345',
phoneNumber: '+6612345',
finalStatus: 'failed',
failureDetail: '步骤 8:手机号验证码超时。',
});
assert.equal(normalized.recordId, 'phone:+6612345');
assert.equal(normalized.accountIdentifierType, 'phone');
assert.equal(normalized.accountIdentifier, '+6612345');
assert.equal(normalized.email, '');
assert.equal(normalized.phoneNumber, '+6612345');
assert.equal(normalized.password, '');
assert.equal(normalized.finalStatus, 'failed');
});
test('account run history records preserve Plus and contribution mode flags', () => {
const source = fs.readFileSync('background/account-run-history.js', 'utf8');
const globalScope = {};
@@ -143,7 +143,7 @@ test('message router skips step 3 when step 2 lands on verification page', async
assert.deepStrictEqual(events.emailStates, ['user@example.com']);
assert.deepStrictEqual(events.stepStatuses, [{ step: 3, status: 'skipped' }]);
assert.equal(events.logs[0]?.message, '步骤 2:提交邮箱后页面直接进入邮箱验证码页,已自动跳过步骤 3。');
assert.equal(events.logs[0]?.message, '步骤 2:提交邮箱后页面直接进入验证码页,已自动跳过步骤 3。');
});
test('message router does not overwrite a completed step 3 when step 2 is replayed', async () => {
@@ -0,0 +1,97 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const source = fs.readFileSync('background.js', 'utf8');
function extractFunction(name) {
const markers = [`async function ${name}(`, `function ${name}(`];
const start = markers
.map((marker) => source.indexOf(marker))
.find((index) => index >= 0);
if (start < 0) {
throw new Error(`missing function ${name}`);
}
let parenDepth = 0;
let signatureEnded = false;
let braceStart = -1;
for (let i = start; i < source.length; i += 1) {
const ch = source[i];
if (ch === '(') parenDepth += 1;
if (ch === ')') {
parenDepth -= 1;
if (parenDepth === 0) signatureEnded = true;
}
if (ch === '{' && signatureEnded) {
braceStart = i;
break;
}
}
if (braceStart < 0) {
throw new Error(`missing body for function ${name}`);
}
let depth = 0;
let end = braceStart;
for (; end < source.length; end += 1) {
const ch = source[end];
if (ch === '{') depth += 1;
if (ch === '}') {
depth -= 1;
if (depth === 0) {
end += 1;
break;
}
}
}
return source.slice(start, end);
}
test('signup method resolution freezes per run and falls back when phone signup is unavailable', async () => {
const api = new Function(`
const SIGNUP_METHOD_EMAIL = 'email';
const SIGNUP_METHOD_PHONE = 'phone';
const DEFAULT_SIGNUP_METHOD = SIGNUP_METHOD_EMAIL;
const logs = [];
let state = {
signupMethod: 'phone',
phoneVerificationEnabled: true,
plusModeEnabled: false,
contributionMode: false,
resolvedSignupMethod: null,
};
async function getState() { return { ...state }; }
async function setState(updates) { state = { ...state, ...updates }; }
async function addLog(message, level = 'info') { logs.push({ message, level }); }
${extractFunction('normalizeSignupMethod')}
${extractFunction('canUsePhoneSignup')}
${extractFunction('resolveSignupMethod')}
${extractFunction('ensureResolvedSignupMethodForRun')}
return {
logs,
get state() { return state; },
setState,
resolveSignupMethod,
ensureResolvedSignupMethodForRun,
};
`)();
assert.equal(api.resolveSignupMethod({ signupMethod: 'phone', phoneVerificationEnabled: true }), 'phone');
assert.equal(api.resolveSignupMethod({ signupMethod: 'phone', phoneVerificationEnabled: false }), 'email');
assert.equal(api.resolveSignupMethod({ signupMethod: 'phone', phoneVerificationEnabled: true, plusModeEnabled: true }), 'email');
assert.equal(api.resolveSignupMethod({ signupMethod: 'email', resolvedSignupMethod: 'phone', phoneVerificationEnabled: false }), 'phone');
assert.equal(await api.ensureResolvedSignupMethodForRun(), 'phone');
assert.equal(api.state.resolvedSignupMethod, 'phone');
await api.setState({ signupMethod: 'email', phoneVerificationEnabled: false });
assert.equal(await api.ensureResolvedSignupMethodForRun(), 'phone');
assert.equal(api.state.resolvedSignupMethod, 'phone');
await api.setState({ resolvedSignupMethod: null, signupMethod: 'phone', phoneVerificationEnabled: false });
assert.equal(await api.ensureResolvedSignupMethodForRun({ force: true }), 'email');
assert.equal(api.state.resolvedSignupMethod, 'email');
assert.equal(api.logs.some((entry) => /固定为邮箱注册/.test(entry.message)), true);
});
@@ -39,6 +39,8 @@ test('step 2 completes with password step skipped when landing on email verifica
step: 2,
payload: {
email: 'user@example.com',
accountIdentifierType: 'email',
accountIdentifier: 'user@example.com',
nextSignupState: 'verification_page',
nextSignupUrl: 'https://auth.openai.com/email-verification',
skippedPasswordStep: true,
@@ -76,6 +78,8 @@ test('step 2 keeps password flow when landing on password page', async () => {
step: 2,
payload: {
email: 'user@example.com',
accountIdentifierType: 'email',
accountIdentifier: 'user@example.com',
nextSignupState: 'password_page',
nextSignupUrl: 'https://auth.openai.com/create-account/password',
skippedPasswordStep: false,
@@ -84,6 +88,80 @@ test('step 2 keeps password flow when landing on password page', async () => {
]);
});
test('step 2 uses phone activation when resolved signup method is phone', async () => {
const completedPayloads = [];
const sentPayloads = [];
const activation = {
activationId: 'signup-activation',
phoneNumber: '66959916439',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 52,
countryLabel: 'Thailand',
successfulUses: 0,
maxUses: 3,
};
const executor = step2Api.createStep2Executor({
addLog: async () => {},
chrome: { tabs: { update: async () => {} } },
completeStepFromBackground: async (step, payload) => {
completedPayloads.push({ step, payload });
},
ensureContentScriptReadyOnTab: async () => {},
ensureSignupEntryPageReady: async () => ({ tabId: 14 }),
ensureSignupPostEmailPageReadyInTab: async () => {
throw new Error('email landing helper should not be used for phone signup');
},
ensureSignupPostIdentityPageReadyInTab: async () => ({
state: 'phone_verification_page',
url: 'https://auth.openai.com/phone-verification',
}),
getTabId: async () => 14,
isTabAlive: async () => true,
phoneVerificationHelpers: {
prepareSignupPhoneActivation: async () => activation,
cancelSignupPhoneActivation: async () => {
throw new Error('activation should not be cancelled on success');
},
},
resolveSignupMethod: () => 'phone',
resolveSignupEmailForFlow: async () => {
throw new Error('email resolver should not run for phone signup');
},
sendToContentScriptResilient: async (_source, message) => {
sentPayloads.push(message.payload);
return { submitted: true };
},
SIGNUP_PAGE_INJECT_FILES: [],
});
await executor.executeStep2({ signupMethod: 'phone' });
assert.deepStrictEqual(sentPayloads, [
{
signupMethod: 'phone',
phoneNumber: '66959916439',
countryId: 52,
countryLabel: 'Thailand',
},
]);
assert.deepStrictEqual(completedPayloads, [
{
step: 2,
payload: {
accountIdentifierType: 'phone',
accountIdentifier: '66959916439',
signupPhoneNumber: '66959916439',
signupPhoneActivation: activation,
nextSignupState: 'phone_verification_page',
nextSignupUrl: 'https://auth.openai.com/phone-verification',
skippedPasswordStep: true,
},
},
]);
});
test('step 2 stops with an explicit error instead of silently skipping 3/4/5 on chatgpt home', async () => {
const completedPayloads = [];
const logs = [];
@@ -44,8 +44,81 @@ test('step 3 reuses existing generated password when rerunning the same email fl
source: 'background',
payload: {
email: 'keep@example.com',
phoneNumber: '',
accountIdentifierType: 'email',
accountIdentifier: 'keep@example.com',
password: 'Secret123!',
},
},
]);
});
test('step 3 supports phone-only signup identity when password page is present', async () => {
const events = {
passwordStates: [],
messages: [],
stateUpdates: [],
logs: [],
};
const executor = api.createStep3Executor({
addLog: async (message) => {
events.logs.push(message);
},
chrome: { tabs: { update: async () => {} } },
ensureContentScriptReadyOnTab: async () => {},
generatePassword: () => 'Generated123!',
getTabId: async () => 88,
isTabAlive: async () => true,
sendToContentScript: async (_source, message) => {
events.messages.push(message);
},
setPasswordState: async (password) => {
events.passwordStates.push(password);
},
setState: async (updates) => {
events.stateUpdates.push(updates);
},
SIGNUP_PAGE_INJECT_FILES: [],
});
await executor.executeStep3({
email: '',
signupPhoneNumber: '66959916439',
accountIdentifierType: 'phone',
accountIdentifier: '66959916439',
customPassword: 'PhoneSecret123!',
accounts: [],
});
assert.deepStrictEqual(events.passwordStates, ['PhoneSecret123!']);
assert.equal(events.logs.some((message) => /注册手机号为 66959916439/.test(message)), true);
assert.equal(events.stateUpdates.length, 1);
assert.deepStrictEqual(events.stateUpdates[0].accounts.map((account) => ({
email: account.email,
phoneNumber: account.phoneNumber,
accountIdentifierType: account.accountIdentifierType,
accountIdentifier: account.accountIdentifier,
})), [
{
email: '',
phoneNumber: '66959916439',
accountIdentifierType: 'phone',
accountIdentifier: '66959916439',
},
]);
assert.deepStrictEqual(events.messages, [
{
type: 'EXECUTE_STEP',
step: 3,
source: 'background',
payload: {
email: '',
phoneNumber: '66959916439',
accountIdentifierType: 'phone',
accountIdentifier: '66959916439',
password: 'PhoneSecret123!',
},
},
]);
});
@@ -231,6 +231,82 @@ test('step 4 forwards skipProfileStep when prepare stage already reached logged-
assert.equal(resolveCalls, 0);
});
test('step 4 phone signup branch uses SMS helper and does not poll mailbox', async () => {
const completions = [];
const phoneCalls = [];
let getMailConfigCalls = 0;
let resolveCalls = 0;
const executor = api.createStep4Executor({
addLog: async () => {},
chrome: {
tabs: {
update: async () => {},
},
},
completeStepFromBackground: async (step, payload) => {
completions.push({ step, payload });
},
confirmCustomVerificationStepBypass: async () => {},
ensureMail2925MailboxSession: async () => {},
getMailConfig: () => {
getMailConfigCalls += 1;
throw new Error('mail config should not be required for phone signup');
},
getTabId: async () => 1,
HOTMAIL_PROVIDER: 'hotmail-api',
isTabAlive: async () => true,
LUCKMAIL_PROVIDER: 'luckmail-api',
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
phoneVerificationHelpers: {
completeSignupPhoneVerificationFlow: async (tabId, options) => {
phoneCalls.push({ tabId, options });
return {
success: true,
skipProfileStep: true,
skipProfileStepReason: 'combined_verification_profile',
};
},
},
resolveSignupMethod: () => 'phone',
resolveVerificationStep: async () => {
resolveCalls += 1;
},
reuseOrCreateTab: async () => {},
sendToContentScript: async () => ({ ready: true }),
sendToContentScriptResilient: async () => ({ ready: true }),
isRetryableContentScriptTransportError: () => false,
shouldUseCustomRegistrationEmail: () => false,
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000,
throwIfStopped: () => {},
});
await executor.executeStep4({
resolvedSignupMethod: 'phone',
accountIdentifierType: 'phone',
signupPhoneNumber: '66959916439',
signupPhoneActivation: { activationId: 'signup-123', phoneNumber: '66959916439' },
});
assert.equal(getMailConfigCalls, 0);
assert.equal(resolveCalls, 0);
assert.equal(phoneCalls.length, 1);
assert.equal(phoneCalls[0].tabId, 1);
assert.equal(phoneCalls[0].options.state.accountIdentifierType, 'phone');
assert.equal(Object.prototype.hasOwnProperty.call(phoneCalls[0].options, 'signupProfile'), true);
assert.deepStrictEqual(completions, [
{
step: 4,
payload: {
phoneVerification: true,
code: '',
skipProfileStep: true,
skipProfileStepReason: 'combined_verification_profile',
},
},
]);
});
test('step 4 prepare retries transport by recovering retry page without replaying full prepare loop', async () => {
let sendToContentScriptCalls = 0;
let recoverCalls = 0;
@@ -227,6 +227,75 @@ test('step 7 forwards direct OAuth consent skip metadata when completing', async
]);
});
test('step 7 forwards phone login identity payload when account identifier is phone', async () => {
const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep7;`)(globalScope);
const events = {
payloads: [],
};
const executor = api.createStep7Executor({
addLog: async () => {},
completeStepFromBackground: async () => {},
getErrorMessage: (error) => error?.message || String(error || ''),
getLoginAuthStateLabel: (state) => state || 'unknown',
getState: async () => ({
accountIdentifierType: 'phone',
accountIdentifier: '66959916439',
signupPhoneNumber: '66959916439',
signupPhoneCompletedActivation: {
activationId: 'signup-done',
phoneNumber: '66959916439',
countryId: 52,
countryLabel: 'Thailand',
},
password: 'secret',
}),
isStep6RecoverableResult: (result) => result?.step6Outcome === 'recoverable',
isStep6SuccessResult: (result) => result?.step6Outcome === 'success',
refreshOAuthUrlBeforeStep6: async () => 'https://oauth.example/latest',
reuseOrCreateTab: async () => {},
sendToContentScriptResilient: async (_sourceName, message) => {
events.payloads.push(message.payload);
return {
step6Outcome: 'success',
state: 'phone_verification_page',
loginVerificationRequestedAt: 123456,
};
},
STEP6_MAX_ATTEMPTS: 3,
throwIfStopped: () => {},
});
await executor.executeStep7({
accountIdentifierType: 'phone',
accountIdentifier: '66959916439',
signupPhoneNumber: '66959916439',
signupPhoneCompletedActivation: {
activationId: 'signup-done',
phoneNumber: '66959916439',
countryId: 52,
countryLabel: 'Thailand',
},
password: 'secret',
});
assert.deepStrictEqual(events.payloads, [
{
email: '',
phoneNumber: '66959916439',
countryId: 52,
countryLabel: 'Thailand',
accountIdentifier: '66959916439',
loginIdentifierType: 'phone',
password: 'secret',
visibleStep: 7,
},
]);
});
test('step 7 stops immediately when management secret is missing', async () => {
const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8');
const globalScope = {};
+102
View File
@@ -90,6 +90,108 @@ test('step 8 submits login verification directly without replaying step 7', asyn
assert.equal(calls.resolveOptions.completionStep, 8);
});
test('step 8 routes phone login verification through sms helper and skips mail provider setup', async () => {
const calls = {
getMailConfigCalls: 0,
helperCalls: [],
completions: [],
};
const executor = api.createStep8Executor({
addLog: async () => {},
chrome: {
tabs: {
update: async () => {},
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async (step, payload) => {
calls.completions.push({ step, payload });
},
confirmCustomVerificationStepBypass: async () => {},
ensureStep8VerificationPageReady: async () => {
throw new Error('phone login branch should not probe email verification page readiness');
},
getOAuthFlowRemainingMs: async () => 5000,
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
getMailConfig: () => {
calls.getMailConfigCalls += 1;
return {
provider: 'qq',
label: 'QQ 邮箱',
};
},
getState: async () => ({
accountIdentifierType: 'phone',
signupPhoneCompletedActivation: {
activationId: 'signup-done',
phoneNumber: '66959916439',
},
}),
getTabId: async () => 1,
HOTMAIL_PROVIDER: 'hotmail-api',
isTabAlive: async () => true,
isVerificationMailPollingError: () => false,
LUCKMAIL_PROVIDER: 'luckmail-api',
phoneVerificationHelpers: {
completeLoginPhoneVerificationFlow: async (tabId, options) => {
calls.helperCalls.push({ tabId, visibleStep: options.visibleStep, state: options.state });
return { code: '654321' };
},
},
resolveSignupMethod: () => 'phone',
resolveVerificationStep: async () => {
throw new Error('phone login branch should not call email verification flow');
},
rerunStep7ForStep8Recovery: async () => {
throw new Error('phone login branch should not rerun step 7 in this test');
},
reuseOrCreateTab: async () => {},
setState: async () => {},
shouldUseCustomRegistrationEmail: () => false,
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000,
STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS: 8,
throwIfStopped: () => {},
});
await executor.executeStep8({
visibleStep: 8,
accountIdentifierType: 'phone',
signupPhoneCompletedActivation: {
activationId: 'signup-done',
phoneNumber: '66959916439',
},
oauthUrl: 'https://oauth.example/latest',
});
assert.equal(calls.getMailConfigCalls, 0);
assert.deepStrictEqual(calls.helperCalls, [
{
tabId: 1,
visibleStep: 8,
state: {
visibleStep: 8,
accountIdentifierType: 'phone',
signupPhoneCompletedActivation: {
activationId: 'signup-done',
phoneNumber: '66959916439',
},
oauthUrl: 'https://oauth.example/latest',
},
},
]);
assert.deepStrictEqual(calls.completions, [
{
step: 8,
payload: {
phoneVerification: true,
loginPhoneVerification: true,
code: '654321',
},
},
]);
});
test('Plus login-code step reuses step 8 verification logic but completes visible step 11', async () => {
let resolvedStep = null;
let resolvedOptions = null;
+408
View File
@@ -83,6 +83,414 @@ test('phone verification helper requests HeroSMS numbers with fixed OpenAI and T
assert.equal(requests[1].searchParams.get('api_key'), 'demo-key');
});
test('signup phone helper persists signup runtime state without touching add-phone activation', async () => {
const setStateCalls = [];
let currentState = {
heroSmsApiKey: 'demo-key',
currentPhoneActivation: {
activationId: 'add-phone-activation',
phoneNumber: '66880000000',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 52,
successfulUses: 0,
maxUses: 3,
},
};
const helpers = api.createPhoneVerificationHelpers({
addLog: async () => {},
ensureStep8SignupPageReady: async () => {},
fetchImpl: async (url) => {
const parsedUrl = new URL(url);
const action = parsedUrl.searchParams.get('action');
if (action === 'getPrices') {
return {
ok: true,
text: async () => buildHeroSmsPricesPayload(),
};
}
if (action === 'getNumber') {
return {
ok: true,
text: async () => 'ACCESS_NUMBER:signup-123:66959916439',
};
}
throw new Error(`Unexpected HeroSMS action: ${action}`);
},
getState: async () => currentState,
sendToContentScriptResilient: async () => ({}),
setState: async (updates) => {
setStateCalls.push(updates);
currentState = { ...currentState, ...updates };
},
sleepWithStop: async () => {},
throwIfStopped: () => {},
});
const activation = await helpers.prepareSignupPhoneActivation(currentState);
assert.equal(activation.activationId, 'signup-123');
assert.equal(activation.phoneNumber, '66959916439');
assert.equal(currentState.signupPhoneNumber, '66959916439');
assert.equal(currentState.signupPhoneVerificationPurpose, 'signup');
assert.deepStrictEqual(currentState.signupPhoneActivation, activation);
assert.equal(currentState.accountIdentifierType, 'phone');
assert.equal(currentState.accountIdentifier, '66959916439');
assert.equal(currentState.currentPhoneActivation.activationId, 'add-phone-activation');
assert.ok(!setStateCalls.some((updates) => Object.prototype.hasOwnProperty.call(updates, 'currentPhoneActivation')));
});
test('signup phone helper polls signup SMS code and keeps activation purpose isolated', async () => {
const setStateCalls = [];
let currentState = {
heroSmsApiKey: 'demo-key',
phoneCodeWaitSeconds: 15,
phoneCodeTimeoutWindows: 1,
phoneCodePollIntervalSeconds: 1,
phoneCodePollMaxRounds: 2,
signupPhoneActivation: {
activationId: 'signup-123',
phoneNumber: '66959916439',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 52,
successfulUses: 0,
maxUses: 3,
},
currentPhoneActivation: {
activationId: 'add-phone-activation',
phoneNumber: '66880000000',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 52,
successfulUses: 0,
maxUses: 3,
},
};
const helpers = api.createPhoneVerificationHelpers({
addLog: async () => {},
ensureStep8SignupPageReady: async () => {},
fetchImpl: async (url) => {
const parsedUrl = new URL(url);
const action = parsedUrl.searchParams.get('action');
if (action === 'getStatus') {
return {
ok: true,
text: async () => 'STATUS_OK:123456',
};
}
throw new Error(`Unexpected HeroSMS action: ${action}`);
},
getOAuthFlowStepTimeoutMs: async (fallback) => fallback,
getState: async () => currentState,
sendToContentScriptResilient: async () => ({}),
setState: async (updates) => {
setStateCalls.push(updates);
currentState = { ...currentState, ...updates };
},
sleepWithStop: async () => {},
throwIfStopped: () => {},
});
const code = await helpers.waitForSignupPhoneCode(currentState, currentState.signupPhoneActivation);
assert.equal(code, '123456');
assert.equal(currentState.currentPhoneVerificationCode, '123456');
assert.equal(currentState.signupPhoneNumber, '66959916439');
assert.equal(currentState.signupPhoneVerificationPurpose, 'signup');
assert.equal(currentState.currentPhoneActivation.activationId, 'add-phone-activation');
assert.ok(setStateCalls.some((updates) => updates.signupPhoneVerificationRequestedAt));
assert.ok(!setStateCalls.some((updates) => Object.prototype.hasOwnProperty.call(updates, 'currentPhoneActivation')));
});
test('signup phone helper finalizes or cancels signup activation without clearing add-phone activation', async () => {
const setStateCalls = [];
const statusActions = [];
let currentState = {
heroSmsApiKey: 'demo-key',
heroSmsReuseEnabled: false,
signupPhoneActivation: {
activationId: 'signup-123',
phoneNumber: '66959916439',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 52,
successfulUses: 0,
maxUses: 3,
},
currentPhoneActivation: {
activationId: 'add-phone-activation',
phoneNumber: '66880000000',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 52,
successfulUses: 0,
maxUses: 3,
},
};
const helpers = api.createPhoneVerificationHelpers({
addLog: async () => {},
ensureStep8SignupPageReady: async () => {},
fetchImpl: async (url) => {
const parsedUrl = new URL(url);
const action = parsedUrl.searchParams.get('action');
if (action === 'setStatus') {
statusActions.push(parsedUrl.searchParams.get('status'));
return {
ok: true,
text: async () => 'ACCESS_READY',
};
}
throw new Error(`Unexpected HeroSMS action: ${action}`);
},
getState: async () => currentState,
sendToContentScriptResilient: async () => ({}),
setState: async (updates) => {
setStateCalls.push(updates);
currentState = { ...currentState, ...updates };
},
sleepWithStop: async () => {},
throwIfStopped: () => {},
});
await helpers.finalizeSignupPhoneActivationAfterSuccess(currentState, currentState.signupPhoneActivation);
assert.deepStrictEqual(statusActions, ['6']);
assert.equal(currentState.signupPhoneNumber, '66959916439');
assert.equal(currentState.signupPhoneActivation, null);
assert.deepStrictEqual(currentState.signupPhoneCompletedActivation, {
activationId: 'signup-123',
phoneNumber: '66959916439',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 52,
successfulUses: 1,
maxUses: 3,
});
assert.equal(currentState.signupPhoneVerificationPurpose, '');
assert.equal(currentState.accountIdentifierType, 'phone');
assert.equal(currentState.accountIdentifier, '66959916439');
assert.equal(currentState.currentPhoneActivation.activationId, 'add-phone-activation');
assert.ok(!setStateCalls.some((updates) => Object.prototype.hasOwnProperty.call(updates, 'currentPhoneActivation')));
});
test('signup phone helper completes signup SMS verification without touching add-phone activation', async () => {
const setStateCalls = [];
const contentMessages = [];
const statusActions = [];
let currentState = {
heroSmsApiKey: 'demo-key',
heroSmsReuseEnabled: false,
phoneCodeWaitSeconds: 15,
phoneCodeTimeoutWindows: 1,
phoneCodePollIntervalSeconds: 1,
phoneCodePollMaxRounds: 2,
signupPhoneNumber: '66959916439',
signupPhoneVerificationPurpose: 'signup',
signupPhoneActivation: {
activationId: 'signup-123',
phoneNumber: '66959916439',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 52,
successfulUses: 0,
maxUses: 3,
},
currentPhoneActivation: {
activationId: 'add-phone-activation',
phoneNumber: '66880000000',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 52,
successfulUses: 0,
maxUses: 3,
},
};
const helpers = api.createPhoneVerificationHelpers({
addLog: async () => {},
ensureStep8SignupPageReady: async () => {},
fetchImpl: async (url) => {
const parsedUrl = new URL(url);
const action = parsedUrl.searchParams.get('action');
if (action === 'getStatus') {
return {
ok: true,
text: async () => 'STATUS_OK:123456',
};
}
if (action === 'setStatus') {
statusActions.push(parsedUrl.searchParams.get('status'));
return {
ok: true,
text: async () => 'ACCESS_READY',
};
}
throw new Error(`Unexpected HeroSMS action: ${action}`);
},
getOAuthFlowStepTimeoutMs: async (fallback) => fallback,
getState: async () => currentState,
sendToContentScriptResilient: async (_source, message) => {
contentMessages.push(message);
if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') {
return { success: true };
}
throw new Error(`Unexpected content-script message: ${message.type}`);
},
setState: async (updates) => {
setStateCalls.push(updates);
currentState = { ...currentState, ...updates };
},
sleepWithStop: async () => {},
throwIfStopped: () => {},
});
const result = await helpers.completeSignupPhoneVerificationFlow(77, {
state: currentState,
signupProfile: {
firstName: 'Ada',
lastName: 'Lovelace',
year: 1995,
month: 1,
day: 2,
},
});
assert.deepStrictEqual(result, { success: true });
assert.deepStrictEqual(statusActions, ['6']);
assert.deepStrictEqual(contentMessages.map((message) => ({
type: message.type,
step: message.step,
code: message.payload?.code,
purpose: message.payload?.purpose,
})), [
{
type: 'SUBMIT_PHONE_VERIFICATION_CODE',
step: 4,
code: '123456',
purpose: 'signup',
},
]);
assert.equal(currentState.signupPhoneNumber, '66959916439');
assert.equal(currentState.signupPhoneActivation, null);
assert.equal(currentState.signupPhoneVerificationPurpose, '');
assert.equal(currentState.currentPhoneVerificationCode, '');
assert.equal(currentState.currentPhoneActivation.activationId, 'add-phone-activation');
assert.ok(!setStateCalls.some((updates) => Object.prototype.hasOwnProperty.call(updates, 'currentPhoneActivation')));
});
test('signup phone helper completes login SMS verification by reusing the completed signup activation', async () => {
const setStateCalls = [];
const contentMessages = [];
const statusActions = [];
let currentState = {
heroSmsApiKey: 'demo-key',
phoneCodeWaitSeconds: 15,
phoneCodeTimeoutWindows: 1,
phoneCodePollIntervalSeconds: 1,
phoneCodePollMaxRounds: 2,
signupPhoneCompletedActivation: {
activationId: 'signup-done',
phoneNumber: '66959916439',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 52,
countryLabel: 'Thailand',
successfulUses: 1,
maxUses: 3,
},
currentPhoneActivation: {
activationId: 'add-phone-activation',
phoneNumber: '66880000000',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 52,
successfulUses: 0,
maxUses: 3,
},
};
const helpers = api.createPhoneVerificationHelpers({
addLog: async () => {},
ensureStep8SignupPageReady: async () => {},
fetchImpl: async (url) => {
const parsedUrl = new URL(url);
const action = parsedUrl.searchParams.get('action');
const id = parsedUrl.searchParams.get('id');
if (action === 'reactivate' && id === 'signup-done') {
return {
ok: true,
text: async () => 'ACCESS_READY',
};
}
if (action === 'getStatus' && id === 'signup-done') {
return {
ok: true,
text: async () => 'STATUS_OK:654321',
};
}
if (action === 'setStatus' && id === 'signup-done') {
statusActions.push(parsedUrl.searchParams.get('status'));
return {
ok: true,
text: async () => 'ACCESS_READY',
};
}
throw new Error(`Unexpected HeroSMS action: ${action}:${id}`);
},
getOAuthFlowStepTimeoutMs: async (fallback) => fallback,
getState: async () => currentState,
sendToContentScriptResilient: async (_source, message) => {
contentMessages.push(message);
if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') {
return { success: true };
}
throw new Error(`Unexpected content-script message: ${message.type}`);
},
setState: async (updates) => {
setStateCalls.push(updates);
currentState = { ...currentState, ...updates };
},
sleepWithStop: async () => {},
throwIfStopped: () => {},
});
const result = await helpers.completeLoginPhoneVerificationFlow(77, {
state: currentState,
visibleStep: 8,
});
assert.deepStrictEqual(result, { success: true });
assert.deepStrictEqual(statusActions, ['6']);
assert.deepStrictEqual(contentMessages.map((message) => ({
type: message.type,
step: message.step,
code: message.payload?.code,
purpose: message.payload?.purpose,
})), [
{
type: 'SUBMIT_PHONE_VERIFICATION_CODE',
step: 8,
code: '654321',
purpose: 'login',
},
]);
assert.equal(currentState.signupPhoneActivation, null);
assert.equal(currentState.signupPhoneVerificationPurpose, '');
assert.equal(currentState.currentPhoneVerificationCode, '');
assert.deepStrictEqual(currentState.signupPhoneCompletedActivation, {
activationId: 'signup-done',
phoneNumber: '66959916439',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 52,
countryLabel: 'Thailand',
successfulUses: 2,
maxUses: 3,
});
assert.equal(currentState.currentPhoneActivation.activationId, 'add-phone-activation');
assert.ok(setStateCalls.some((updates) => updates.signupPhoneVerificationPurpose === 'login'));
assert.ok(!setStateCalls.some((updates) => Object.prototype.hasOwnProperty.call(updates, 'currentPhoneActivation')));
});
test('phone verification helper ignores HeroSMS virtual-only stock when physicalCount is zero', async () => {
const requests = [];
const helpers = api.createPhoneVerificationHelpers({
@@ -336,7 +336,67 @@ test('account records manager supports filter chips and partial multi-select del
assert.match(list.innerHTML, /stopped@example\.com/);
assert.equal(btnDeleteSelectedAccountRecords.disabled, true);
assert.deepStrictEqual(toasts.at(-1), {
message: '已删除 1 条邮箱记录。',
message: '已删除 1 条账号记录。',
tone: 'success',
});
});
test('account records manager displays phone-only records with account identifier fallback', () => {
const source = fs.readFileSync('sidepanel/account-records-manager.js', 'utf8');
const windowObject = {};
const api = new Function('window', `${source}; return window.SidepanelAccountRecordsManager;`)(windowObject);
const list = createNode();
const meta = createNode();
const manager = api.createAccountRecordsManager({
state: {
getLatestState: () => ({
accountRunHistory: [
{
recordId: 'phone:+6612345',
accountIdentifierType: 'phone',
accountIdentifier: '+6612345',
phoneNumber: '+6612345',
email: '',
password: '',
finalStatus: 'success',
finishedAt: '2026-04-17T04:31:00.000Z',
retryCount: 0,
failureLabel: '流程完成',
},
],
}),
syncLatestState() {},
},
dom: {
accountRecordsList: list,
accountRecordsMeta: meta,
accountRecordsOverlay: createNode(),
accountRecordsPageLabel: createNode(),
accountRecordsStats: createNode(),
btnAccountRecordsNext: createNode(),
btnAccountRecordsPrev: createNode(),
btnClearAccountRecords: createNode(),
btnCloseAccountRecords: createNode(),
btnDeleteSelectedAccountRecords: createNode(),
btnOpenAccountRecords: createNode(),
btnToggleAccountRecordsSelection: createNode(),
},
helpers: {
escapeHtml: (value) => String(value || ''),
},
runtime: {
sendMessage: async () => ({}),
},
constants: {
displayTimeZone: 'Asia/Shanghai',
pageSize: 10,
},
});
manager.render();
assert.match(meta.textContent, /共 1 条/);
assert.match(list.innerHTML, /\+6612345/);
assert.doesNotMatch(list.innerHTML, /\(空邮箱\)/);
});
@@ -59,6 +59,9 @@ test('sidepanel html exposes phone verification toggle and multi-provider SMS ro
assert.match(html, /id="btn-toggle-phone-verification-section"/);
assert.match(html, /id="row-phone-verification-fold"/);
assert.match(html, /id="input-phone-verification-enabled"/);
assert.match(html, /id="row-signup-method"/);
assert.match(html, /data-signup-method="email"/);
assert.match(html, /data-signup-method="phone"/);
assert.match(html, /id="row-phone-sms-provider"/);
assert.match(html, /id="select-phone-sms-provider"/);
assert.match(html, /id="row-phone-sms-provider-order"/);
@@ -124,6 +127,7 @@ let latestState = {};
const inputPhoneVerificationEnabled = { checked: false };
const rowPhoneVerificationEnabled = { style: { display: 'none' } };
const rowPhoneVerificationFold = { style: { display: 'none' } };
const rowSignupMethod = { style: { display: 'none' } };
const rowPhoneSmsProvider = { style: { display: 'none' } };
const rowPhoneSmsProviderOrder = { style: { display: 'none' } };
const rowPhoneSmsProviderOrderActions = { style: { display: 'none' } };
@@ -169,7 +173,15 @@ const rowHeroSmsCountryFallback = { style: { display: 'none' } };
const rowHeroSmsAcquirePriority = { style: { display: 'none' } };
const rowHeroSmsApiKey = { style: { display: 'none' } };
const rowHeroSmsMaxPrice = { style: { display: 'none' } };
const rowFiveSimApiKey = { style: { display: 'none' } };
const rowFiveSimCountry = { style: { display: 'none' } };
const rowFiveSimCountryFallback = { style: { display: 'none' } };
const rowFiveSimOperator = { style: { display: 'none' } };
const rowFiveSimProduct = { style: { display: 'none' } };
const rowNexSmsApiKey = { style: { display: 'none' } };
const rowNexSmsCountry = { style: { display: 'none' } };
const rowNexSmsCountryFallback = { style: { display: 'none' } };
const rowNexSmsServiceCode = { style: { display: 'none' } };
const rowHeroSmsCurrentNumber = { style: { display: 'none' } };
const rowHeroSmsCurrentCountdown = { style: { display: 'none' } };
const rowHeroSmsPriceTiers = { style: { display: 'none' } };
@@ -183,10 +195,13 @@ const rowPhoneCodePollIntervalSeconds = { style: { display: 'none' } };
const rowPhoneCodePollMaxRounds = { style: { display: 'none' } };
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
let selectedPhoneSmsProvider = PHONE_SMS_PROVIDER_HERO_SMS;
function getSelectedPhoneSmsProvider() { return selectedPhoneSmsProvider; }
const PHONE_SMS_PROVIDER_NEXSMS = 'nexsms';
function getSelectedPhoneSmsProvider() { return selectPhoneSmsProvider.value; }
function isFiveSimProviderSelected() { return getSelectedPhoneSmsProvider() === PHONE_SMS_PROVIDER_FIVE_SIM; }
function updateHeroSmsPlatformDisplay() {}
function updateSignupMethodUI() {
rowSignupMethod.style.display = inputPhoneVerificationEnabled.checked ? '' : 'none';
}
${extractFunction('updatePhoneVerificationSettingsUI')}
@@ -194,6 +209,7 @@ return {
setLatestState: (state) => { latestState = state || {}; },
rowPhoneVerificationEnabled,
rowPhoneVerificationFold,
rowSignupMethod,
rowPhoneSmsProvider,
rowPhoneSmsProviderOrder,
rowPhoneSmsProviderOrderActions,
@@ -206,7 +222,15 @@ return {
rowHeroSmsAcquirePriority,
rowHeroSmsApiKey,
rowHeroSmsMaxPrice,
rowFiveSimApiKey,
rowFiveSimCountry,
rowFiveSimCountryFallback,
rowFiveSimOperator,
rowFiveSimProduct,
rowNexSmsApiKey,
rowNexSmsCountry,
rowNexSmsCountryFallback,
rowNexSmsServiceCode,
rowHeroSmsCurrentNumber,
rowHeroSmsCurrentCountdown,
rowHeroSmsPriceTiers,
@@ -218,7 +242,7 @@ return {
rowPhoneCodeTimeoutWindows,
rowPhoneCodePollIntervalSeconds,
rowPhoneCodePollMaxRounds,
setSelectedPhoneSmsProvider(value) { selectedPhoneSmsProvider = value; },
setSelectedPhoneSmsProvider(value) { selectPhoneSmsProvider.value = value; },
updatePhoneVerificationSettingsUI,
};
`)();
@@ -226,6 +250,7 @@ return {
api.updatePhoneVerificationSettingsUI();
assert.equal(api.rowPhoneVerificationEnabled.style.display, '');
assert.equal(api.rowPhoneVerificationFold.style.display, 'none');
assert.equal(api.rowSignupMethod.style.display, 'none');
assert.equal(api.rowPhoneSmsProvider.style.display, 'none');
assert.equal(api.rowPhoneSmsProviderOrder.style.display, 'none');
assert.equal(api.rowPhoneSmsProviderOrderActions.style.display, 'none');
@@ -262,6 +287,7 @@ return {
api.inputPhoneVerificationEnabled.checked = true;
api.updatePhoneVerificationSettingsUI();
assert.equal(api.rowPhoneVerificationFold.style.display, '');
assert.equal(api.rowSignupMethod.style.display, '');
assert.equal(api.rowPhoneSmsProvider.style.display, '');
assert.equal(api.rowPhoneSmsProviderOrder.style.display, '');
assert.equal(api.rowPhoneSmsProviderOrderActions.style.display, '');
@@ -288,7 +314,18 @@ return {
api.setSelectedPhoneSmsProvider('5sim');
api.updatePhoneVerificationSettingsUI();
assert.equal(api.rowFiveSimApiKey.style.display, '');
assert.equal(api.rowFiveSimCountry.style.display, '');
assert.equal(api.rowFiveSimCountryFallback.style.display, '');
assert.equal(api.rowFiveSimOperator.style.display, '');
assert.equal(api.rowFiveSimProduct.style.display, '');
api.setSelectedPhoneSmsProvider('nexsms');
api.updatePhoneVerificationSettingsUI();
assert.equal(api.rowNexSmsApiKey.style.display, '');
assert.equal(api.rowNexSmsCountry.style.display, '');
assert.equal(api.rowNexSmsCountryFallback.style.display, '');
assert.equal(api.rowNexSmsServiceCode.style.display, '');
});
test('collectSettingsPayload keeps local helper sync enabled while persisting sms toggle state', () => {
@@ -345,6 +382,8 @@ const inputHeroSmsApiKey = { value: 'demo-key' };
const inputFiveSimApiKey = { value: 'five-sim-key' };
const inputFiveSimOperator = { value: 'any' };
const inputFiveSimProduct = { value: 'openai' };
const inputNexSmsApiKey = { value: 'nex-key' };
const inputNexSmsServiceCode = { value: 'ot' };
const inputHeroSmsReuseEnabled = { checked: true };
const selectHeroSmsAcquirePriority = { value: 'price' };
function getSelectedPhonePreferredActivation() {
@@ -359,7 +398,7 @@ function getSelectedPhonePreferredActivation() {
};
}
const inputHeroSmsMaxPrice = { value: '0.12' };
const inputFiveSimOperator = { value: 'any' };
const inputHeroSmsPreferredPrice = { value: '0.0512' };
const inputPhoneReplacementLimit = { value: '5' };
const inputPhoneCodeWaitSeconds = { value: '75' };
const inputPhoneCodeTimeoutWindows = { value: '3' };
@@ -390,10 +429,14 @@ const DEFAULT_HERO_SMS_COUNTRY_ID = 52;
const DEFAULT_HERO_SMS_COUNTRY_LABEL = 'Thailand';
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
const PHONE_SMS_PROVIDER_NEXSMS = 'nexsms';
const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO_SMS;
const DEFAULT_FIVE_SIM_COUNTRY_ID = 'vietnam';
const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '越南 (Vietnam)';
const DEFAULT_FIVE_SIM_OPERATOR = 'any';
const DEFAULT_FIVE_SIM_PRODUCT = 'openai';
const DEFAULT_NEX_SMS_COUNTRY_ORDER = [1];
const DEFAULT_NEX_SMS_SERVICE_CODE = 'ot';
const FIVE_SIM_SUPPORTED_COUNTRY_ID_SET = new Set(['indonesia', 'thailand', 'vietnam']);
const HERO_SMS_SUPPORTED_COUNTRY_ID_SET = new Set(['6', '52', '10']);
const selectHeroSmsCountry = {
@@ -420,6 +463,13 @@ function normalizeAutoDelayMinutes(value) { return Number(value) || 30; }
function normalizeAutoStepDelaySeconds(value) { return value === '' ? null : Number(value); }
function normalizeVerificationResendCount(value, fallback) { return Number(value) || fallback; }
${extractFunction('normalizePhoneSmsProvider')}
${extractFunction('normalizePhoneSmsProviderValue')}
${extractFunction('normalizeFiveSimCountryCode')}
${extractFunction('normalizeFiveSimCountryOrderValue')}
${extractFunction('normalizeFiveSimProductValue')}
${extractFunction('normalizeNexSmsCountryIdValue')}
${extractFunction('normalizeNexSmsCountryOrderValue')}
${extractFunction('normalizeNexSmsServiceCodeValue')}
function getSelectedPhoneSmsProvider() { return normalizePhoneSmsProvider(selectPhoneSmsProvider?.value || latestState?.phoneSmsProvider); }
${extractFunction('normalizeFiveSimCountryId')}
${extractFunction('normalizeFiveSimCountryLabel')}
@@ -441,6 +491,13 @@ ${extractFunction('getSelectedHeroSmsCountryOption')}
function syncHeroSmsFallbackSelectionOrderFromSelect() {
return [{ id: 52, label: 'Thailand' }, { id: 16, label: 'United Kingdom' }];
}
function getSelectedSignupMethod() { return 'phone'; }
function getSelectedFiveSimCountries() {
return [{ id: 'thailand', code: 'thailand', label: 'Thailand' }, { id: 'vietnam', code: 'vietnam', label: 'Vietnam' }];
}
function getSelectedNexSmsCountries() {
return [{ id: 1, label: 'Country #1' }];
}
${extractFunction('collectSettingsPayload')}
return { collectSettingsPayload };
`)(normalizeIcloudTargetMailboxType, normalizeIcloudForwardMailProvider);
@@ -448,17 +505,22 @@ return { collectSettingsPayload };
const payload = api.collectSettingsPayload();
assert.equal(payload.phoneVerificationEnabled, true);
assert.equal(payload.signupMethod, 'phone');
assert.equal(payload.phoneSmsProvider, 'hero-sms');
assert.equal(payload.accountRunHistoryTextEnabled, true);
assert.equal(payload.accountRunHistoryHelperBaseUrl, 'http://127.0.0.1:17373');
assert.equal(payload.heroSmsApiKey, 'demo-key');
assert.equal(payload.fiveSimApiKey, 'five-sim-key');
assert.deepStrictEqual(payload.fiveSimCountryOrder, ['thailand', 'england']);
assert.deepStrictEqual(payload.fiveSimCountryOrder, ['thailand', 'vietnam']);
assert.equal(payload.fiveSimOperator, 'any');
assert.equal(payload.fiveSimProduct, 'openai');
assert.equal(payload.nexSmsApiKey, 'nex-key');
assert.deepStrictEqual(payload.nexSmsCountryOrder, [1]);
assert.equal(payload.nexSmsServiceCode, 'ot');
assert.equal(payload.heroSmsReuseEnabled, true);
assert.equal(payload.heroSmsAcquirePriority, 'price');
assert.equal(payload.heroSmsMaxPrice, '0.12');
assert.equal(payload.heroSmsPreferredPrice, '0.0512');
assert.deepStrictEqual(payload.phonePreferredActivation, {
provider: 'hero-sms',
activationId: 'demo-activation',
@@ -476,7 +538,7 @@ return { collectSettingsPayload };
assert.equal(payload.heroSmsCountryId, 52);
assert.equal(payload.heroSmsCountryLabel, 'Thailand');
assert.deepStrictEqual(payload.heroSmsCountryFallback, [{ id: 16, label: 'United Kingdom' }]);
assert.equal(payload.fiveSimApiKey, '');
assert.equal(payload.fiveSimApiKey, 'five-sim-key');
assert.equal(payload.fiveSimCountryId, 'vietnam');
});
@@ -612,19 +674,27 @@ const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
const DEFAULT_FIVE_SIM_COUNTRY_ID = 'vietnam';
const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '越南 (Vietnam)';
const DEFAULT_FIVE_SIM_OPERATOR = 'any';
const DEFAULT_FIVE_SIM_PRODUCT = 'openai';
const FIVE_SIM_SUPPORTED_COUNTRY_ID_SET = new Set(['indonesia', 'thailand', 'vietnam']);
const HERO_SMS_SUPPORTED_COUNTRY_ID_SET = new Set(['6', '52', '10']);
const inputHeroSmsMaxPrice = { value: '' };
const inputHeroSmsApiKey = { value: '' };
const inputFiveSimOperator = { value: 'any' };
const inputFiveSimProduct = { value: 'openai' };
const displayHeroSmsPriceTiers = { textContent: '' };
const rowHeroSmsPriceTiers = { style: { display: 'none' } };
const fetchCalls = [];
${extractFunction('normalizePhoneSmsProvider')}
${extractFunction('normalizePhoneSmsProviderValue')}
${extractFunction('normalizePhoneSmsProviderOrderValue')}
const phoneSmsProviderOrderSelection = [];
function getSelectedPhoneSmsProvider() { return '5sim'; }
function getSelectedPhoneSmsProviderOrder() { return ['5sim']; }
${extractFunction('normalizeFiveSimCountryId')}
${extractFunction('normalizeFiveSimCountryLabel')}
${extractFunction('normalizeFiveSimCountryCode')}
${extractFunction('normalizeFiveSimProductValue')}
${extractFunction('normalizeFiveSimOperator')}
${extractFunction('normalizePhoneSmsMaxPriceValue')}
${extractFunction('normalizeFiveSimMaxPriceValue')}
@@ -636,7 +706,13 @@ ${extractFunction('collectHeroSmsPriceEntriesForPreview')}
${extractFunction('formatPhoneSmsPriceEntriesSummary')}
${extractFunction('describeHeroSmsPreviewPayload')}
${extractFunction('summarizeHeroSmsPreviewError')}
${extractFunction('formatPriceTiersForPreview')}
${extractFunction('formatPriceTiersWithZeroStockForPreview')}
function normalizeHeroSmsFetchErrorMessage(error) { return error?.message || String(error); }
function getFiveSimCountryLabelByCode() { return '越南 (Vietnam)'; }
function getSelectedFiveSimCountries() {
return [{ id: 'vietnam', code: 'vietnam', label: '越南 (Vietnam)' }];
}
function syncHeroSmsFallbackSelectionOrderFromSelect() {
return [{ id: 'vietnam', label: '越南 (Vietnam)' }];
}
@@ -653,14 +729,14 @@ async function fetch(url, options = {}) {
return {
ok: true,
status: 200,
text: async () => JSON.stringify({ openai: { Category: 'activation', Qty: 4609, Price: 0.08 } }),
json: async () => ({ openai: { Category: 'activation', Qty: 4609, Price: 0.08 } }),
};
}
if (parsed.pathname === '/v1/guest/prices') {
return {
ok: true,
status: 200,
text: async () => JSON.stringify({
json: async () => ({
vietnam: {
openai: {
virtual21: { cost: 0.0769, count: 0 },
@@ -673,6 +749,7 @@ async function fetch(url, options = {}) {
throw new Error('unexpected ' + parsed.pathname);
}
${extractFunction('buildFiveSimPricePreviewLines')}
${extractFunction('previewHeroSmsPriceTiers')}
return {
@@ -685,11 +762,14 @@ return {
await api.previewHeroSmsPriceTiers();
assert.equal(api.displayHeroSmsPriceTiers.textContent, '越南 (Vietnam): 最低 0.08');
assert.equal(
api.displayHeroSmsPriceTiers.textContent,
'5sim:\n越南 (Vietnam): 最低 0.1282;档位:0.0769(x0), 0.1282(x4608)'
);
assert.equal(api.rowHeroSmsPriceTiers.style.display, '');
assert.deepStrictEqual(
api.fetchCalls.map((entry) => entry.url.pathname),
['/v1/guest/products/vietnam/any', '/v1/guest/prices']
['/v1/guest/prices']
);
});
+229
View File
@@ -136,6 +136,9 @@ ${extractConst('SIGNUP_PHONE_INPUT_SELECTOR')}
${extractConst('SIGNUP_SWITCH_TO_EMAIL_PATTERN')}
${extractConst('SIGNUP_SWITCH_ACTION_PATTERN')}
${extractConst('SIGNUP_EMAIL_ACTION_PATTERN')}
${extractConst('SIGNUP_PHONE_ACTION_PATTERN')}
${extractConst('SIGNUP_SWITCH_TO_PHONE_PATTERN')}
${extractConst('SIGNUP_MORE_OPTIONS_PATTERN')}
${extractConst('SIGNUP_WORK_EMAIL_PATTERN')}
function isVisibleElement(el) {
@@ -196,6 +199,8 @@ async function sleep(ms) {
${extractFunction('getSignupEmailInput')}
${extractFunction('getSignupPhoneInput')}
${extractFunction('findSignupUseEmailTrigger')}
${extractFunction('findSignupUsePhoneTrigger')}
${extractFunction('findSignupMoreOptionsTrigger')}
${extractFunction('getSignupEmailContinueButton')}
${extractFunction('inspectSignupEntryState')}
${extractFunction('waitForSignupEntryState')}
@@ -309,6 +314,9 @@ ${extractConst('SIGNUP_PHONE_INPUT_SELECTOR')}
${extractConst('SIGNUP_SWITCH_TO_EMAIL_PATTERN')}
${extractConst('SIGNUP_SWITCH_ACTION_PATTERN')}
${extractConst('SIGNUP_EMAIL_ACTION_PATTERN')}
${extractConst('SIGNUP_PHONE_ACTION_PATTERN')}
${extractConst('SIGNUP_SWITCH_TO_PHONE_PATTERN')}
${extractConst('SIGNUP_MORE_OPTIONS_PATTERN')}
${extractConst('SIGNUP_WORK_EMAIL_PATTERN')}
function isVisibleElement(el) {
@@ -369,6 +377,8 @@ async function sleep(ms) {
${extractFunction('getSignupEmailInput')}
${extractFunction('getSignupPhoneInput')}
${extractFunction('findSignupUseEmailTrigger')}
${extractFunction('findSignupUsePhoneTrigger')}
${extractFunction('findSignupMoreOptionsTrigger')}
${extractFunction('getSignupEmailContinueButton')}
${extractFunction('inspectSignupEntryState')}
${extractFunction('waitForSignupEntryState')}
@@ -429,3 +439,222 @@ return {
assert.equal(api.run()?.kind, 'localized-email');
});
test('submitSignupPhoneNumberAndContinue switches from email mode to phone mode and submits local number', async () => {
const api = new Function(`
const logs = [];
const clicks = [];
const filled = [];
let phase = 'email';
let now = 0;
const emailInput = {
kind: 'email',
value: '',
getAttribute(name) {
if (name === 'type') return 'email';
return '';
},
};
const phoneInput = {
kind: 'phone',
value: '',
textContent: 'Thailand (+66)',
getAttribute(name) {
if (name === 'type') return 'tel';
return '';
},
closest() {
return { textContent: 'Thailand (+66)' };
},
};
const switchButton = {
textContent: 'Continue with phone number',
value: '',
disabled: false,
getAttribute(name) {
if (name === 'type') return 'button';
return '';
},
getBoundingClientRect() {
return { width: 200, height: 48 };
},
};
const continueButton = {
textContent: 'Continue',
value: '',
disabled: false,
getAttribute(name) {
if (name === 'type') return 'submit';
if (name === 'aria-disabled') return 'false';
return '';
},
getBoundingClientRect() {
return { width: 200, height: 48 };
},
};
const document = {
querySelector(selector) {
if (selector === SIGNUP_EMAIL_INPUT_SELECTOR) {
return phase === 'email' ? emailInput : null;
}
if (selector === SIGNUP_PHONE_INPUT_SELECTOR) {
return phase === 'phone' ? phoneInput : null;
}
if (selector === 'button[type="submit"], input[type="submit"]') {
return phase === 'phone' ? continueButton : null;
}
return null;
},
querySelectorAll(selector) {
if (selector === 'button, a, [role="button"], [role="link"]') {
return phase === 'email' ? [switchButton] : [];
}
if (selector === 'a, button, [role="button"], [role="link"]') {
return [];
}
if (selector === 'button, a, [role="button"], [role="link"], input[type="button"], input[type="submit"]') {
return phase === 'phone' ? [continueButton] : [];
}
if (selector === 'input') {
return phase === 'phone' ? [phoneInput] : [emailInput];
}
return [];
},
};
const location = {
href: 'https://chatgpt.com/',
};
const window = {
setTimeout(fn) {
fn();
},
};
const Date = {
now() {
return now;
},
};
${extractConst('SIGNUP_ENTRY_TRIGGER_PATTERN')}
${extractConst('SIGNUP_EMAIL_INPUT_SELECTOR')}
${extractConst('SIGNUP_PHONE_INPUT_SELECTOR')}
${extractConst('SIGNUP_SWITCH_TO_EMAIL_PATTERN')}
${extractConst('SIGNUP_SWITCH_ACTION_PATTERN')}
${extractConst('SIGNUP_EMAIL_ACTION_PATTERN')}
${extractConst('SIGNUP_WORK_EMAIL_PATTERN')}
${extractConst('SIGNUP_PHONE_ACTION_PATTERN')}
${extractConst('SIGNUP_SWITCH_TO_PHONE_PATTERN')}
${extractConst('SIGNUP_MORE_OPTIONS_PATTERN')}
function isVisibleElement(el) {
return Boolean(el);
}
function isActionEnabled(el) {
return Boolean(el) && !el.disabled && el.getAttribute('aria-disabled') !== 'true';
}
function getActionText(el) {
return [el?.textContent, el?.value, el?.getAttribute?.('aria-label'), el?.getAttribute?.('title')]
.filter(Boolean)
.join(' ')
.replace(/\\s+/g, ' ')
.trim();
}
function getSignupPasswordInput() {
return null;
}
function isSignupPasswordPage() {
return false;
}
function getSignupPasswordSubmitButton() {
return null;
}
function findSignupEntryTrigger() {
return null;
}
function getSignupPasswordDisplayedEmail() {
return '';
}
function getPageTextSnapshot() {
return phase === 'phone' ? 'Thailand (+66)' : '';
}
function throwIfStopped() {}
function isStopError() { return false; }
function log(message, level = 'info') {
logs.push({ message, level });
}
async function humanPause() {}
function simulateClick(target) {
clicks.push(getActionText(target));
if (target === switchButton) {
phase = 'phone';
}
}
function fillInput(target, value) {
target.value = value;
filled.push({ target: target.kind, value });
}
async function sleep(ms) {
now += ms;
}
${extractFunction('getSignupEmailInput')}
${extractFunction('getSignupPhoneInput')}
${extractFunction('findSignupUseEmailTrigger')}
${extractFunction('findSignupUsePhoneTrigger')}
${extractFunction('findSignupMoreOptionsTrigger')}
${extractFunction('getSignupEmailContinueButton')}
${extractFunction('inspectSignupEntryState')}
${extractFunction('getSignupEntryStateSummary')}
function getSignupEntryDiagnostics() { return {}; }
${extractFunction('normalizePhoneDigits')}
${extractFunction('extractDialCodeFromText')}
${extractFunction('toNationalPhoneNumber')}
${extractFunction('resolveSignupPhoneDialCode')}
${extractFunction('waitForSignupPhoneEntryState')}
${extractFunction('submitSignupPhoneNumberAndContinue')}
return {
async run() {
return submitSignupPhoneNumberAndContinue({
phoneNumber: '66959916439',
countryLabel: 'Thailand',
});
},
getClicks() {
return clicks.slice();
},
getFilled() {
return filled.slice();
},
};
`)();
const result = await api.run();
assert.equal(result.submitted, true);
assert.equal(result.phoneInputValue, '959916439');
assert.deepEqual(api.getClicks(), ['Continue with phone number', 'Continue']);
assert.deepEqual(api.getFilled(), [{ target: 'phone', value: '959916439' }]);
});
+20
View File
@@ -80,6 +80,26 @@ function inspectSignupEntryState() {
return snapshot;
}
function isPhoneVerificationPageReady() {
return false;
}
function getPhoneVerificationDisplayedPhone() {
return '';
}
function getVerificationCodeTarget() {
return null;
}
function getStep4PostVerificationState() {
return null;
}
function isVerificationPageStillVisible() {
return false;
}
async function ensureSignupPasswordPageReady() {
return { ready: true };
}
+27
View File
@@ -92,6 +92,10 @@ function getLoginEmailInput() {
return ${JSON.stringify(overrides.emailInput || null)};
}
function getLoginPhoneInput() {
return ${JSON.stringify(overrides.phoneInput || null)};
}
function findOneTimeCodeLoginTrigger() {
return ${JSON.stringify(overrides.switchTrigger || null)};
}
@@ -100,6 +104,14 @@ function findLoginEntryTrigger() {
return ${JSON.stringify(overrides.loginEntryTrigger || null)};
}
function findLoginPhoneEntryTrigger() {
return ${JSON.stringify(overrides.phoneEntryTrigger || null)};
}
function findLoginMoreOptionsTrigger() {
return ${JSON.stringify(overrides.moreOptionsTrigger || null)};
}
function getLoginSubmitButton() {
return ${JSON.stringify(overrides.submitButton || null)};
}
@@ -239,9 +251,24 @@ return {
assert.strictEqual(snapshot.state, 'entry_page');
}
{
const api = createApi({
phoneInput: { id: 'phone' },
submitButton: { id: 'submit' },
});
const snapshot = api.inspectLoginAuthState();
assert.strictEqual(snapshot.state, 'phone_entry_page');
}
assert.ok(
extractFunction('inspectLoginAuthState').includes("state: 'oauth_consent_page'"),
'inspectLoginAuthState 应产出 oauth_consent_page 状态'
);
assert.ok(
extractFunction('inspectLoginAuthState').includes("state: 'phone_entry_page'"),
'inspectLoginAuthState 应产出 phone_entry_page 状态'
);
console.log('step6 login state tests passed');