feat: support Cloud Mail email provider
- Merge the latest dev branch into PR #212 for a clean dev-targeted review base. - Add Cloud Mail/SkyMail provider support for email generation and verification-code polling. - Move Cloud Mail runtime logic into a dedicated background module and add docs plus regression tests.
This commit is contained in:
@@ -71,6 +71,20 @@ test('navigation utils support codex2api mode and url normalization', () => {
|
||||
assert.equal(utils.getPanelModeLabel('codex2api'), 'Codex2API');
|
||||
});
|
||||
|
||||
test('navigation utils leaves SUB2API url empty when no default is configured', () => {
|
||||
const source = fs.readFileSync('background/navigation-utils.js', 'utf8');
|
||||
const globalScope = {};
|
||||
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundNavigationUtils;`)(globalScope);
|
||||
const utils = api.createNavigationUtils({
|
||||
DEFAULT_CODEX2API_URL: 'http://localhost:8080/admin/accounts',
|
||||
DEFAULT_SUB2API_URL: '',
|
||||
normalizeLocalCpaStep9Mode: (value) => value,
|
||||
});
|
||||
|
||||
assert.equal(utils.normalizeSub2ApiUrl(''), '');
|
||||
});
|
||||
|
||||
test('navigation utils recognize the iCloud mail tab family on both supported hosts', () => {
|
||||
const source = fs.readFileSync('background/navigation-utils.js', 'utf8');
|
||||
const globalScope = {};
|
||||
|
||||
@@ -358,6 +358,106 @@ test('step 7 forwards phone login identity payload when account identifier is ph
|
||||
]);
|
||||
});
|
||||
|
||||
test('step 7 keeps Plus email login even when phone sms runtime exists', 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 () => ({
|
||||
plusModeEnabled: true,
|
||||
phoneVerificationEnabled: true,
|
||||
signupMethod: 'phone',
|
||||
email: 'plus.user@example.com',
|
||||
password: 'secret',
|
||||
signupPhoneNumber: '+441111111111',
|
||||
}),
|
||||
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: 'verification_page',
|
||||
loginVerificationRequestedAt: 123456,
|
||||
};
|
||||
},
|
||||
STEP6_MAX_ATTEMPTS: 3,
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
await executor.executeStep7({
|
||||
plusModeEnabled: true,
|
||||
phoneVerificationEnabled: true,
|
||||
signupMethod: 'phone',
|
||||
email: 'plus.user@example.com',
|
||||
password: 'secret',
|
||||
signupPhoneNumber: '+441111111111',
|
||||
visibleStep: 10,
|
||||
});
|
||||
|
||||
assert.equal(events.payloads[0].loginIdentifierType, 'email');
|
||||
assert.equal(events.payloads[0].email, 'plus.user@example.com');
|
||||
assert.equal(events.payloads[0].phoneNumber, '');
|
||||
assert.equal(events.payloads[0].accountIdentifier, 'plus.user@example.com');
|
||||
});
|
||||
|
||||
test('step 7 can infer phone login from an available phone signup configuration before step 2 finishes', 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 () => ({
|
||||
phoneVerificationEnabled: true,
|
||||
signupMethod: 'phone',
|
||||
signupPhoneNumber: '+447780579093',
|
||||
}),
|
||||
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: 987654,
|
||||
};
|
||||
},
|
||||
STEP6_MAX_ATTEMPTS: 3,
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
await executor.executeStep7({
|
||||
phoneVerificationEnabled: true,
|
||||
signupMethod: 'phone',
|
||||
signupPhoneNumber: '+447780579093',
|
||||
});
|
||||
|
||||
assert.equal(events.payloads[0].loginIdentifierType, 'phone');
|
||||
assert.equal(events.payloads[0].phoneNumber, '+447780579093');
|
||||
assert.equal(events.payloads[0].email, '');
|
||||
});
|
||||
|
||||
test('step 7 can start from a manually filled signup phone without completed step 2 or step 3 state', async () => {
|
||||
const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8');
|
||||
const globalScope = {};
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
require('../background/cloudmail-provider.js');
|
||||
|
||||
function createProviderApi(options = {}) {
|
||||
const {
|
||||
receiveMailbox = '',
|
||||
messages = [{
|
||||
id: 'mail-1',
|
||||
address: 'user@example.com',
|
||||
receivedDateTime: '2026-05-07T09:20:00.000Z',
|
||||
subject: 'OpenAI verification code',
|
||||
from: { emailAddress: { address: 'noreply@tm.openai.com' } },
|
||||
bodyPreview: 'Your verification code is 123456.',
|
||||
}],
|
||||
} = options;
|
||||
const logs = [];
|
||||
const listCalls = [];
|
||||
const fetchImpl = async (url, request = {}) => {
|
||||
const payload = request.body ? JSON.parse(request.body) : {};
|
||||
if (String(url).includes('/api/public/emailList')) {
|
||||
listCalls.push(payload.toEmail || '');
|
||||
return {
|
||||
ok: true,
|
||||
text: async () => JSON.stringify({ code: 200, data: { records: messages } }),
|
||||
};
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
text: async () => JSON.stringify({ code: 200, data: { token: 'token' } }),
|
||||
};
|
||||
};
|
||||
|
||||
const api = globalThis.MultiPageBackgroundCloudMailProvider.createCloudMailProvider({
|
||||
addLog: async (message, level) => logs.push({ message, level }),
|
||||
buildCloudMailHeaders: () => ({}),
|
||||
CLOUD_MAIL_DEFAULT_PAGE_SIZE: 20,
|
||||
CLOUD_MAIL_GENERATOR: 'cloudmail',
|
||||
CLOUD_MAIL_PROVIDER: 'cloudmail',
|
||||
fetchImpl,
|
||||
getCloudMailTokenFromResponse: () => 'token',
|
||||
getState: async () => ({}),
|
||||
joinCloudMailUrl: (baseUrl, path) => `${baseUrl}${path}`,
|
||||
normalizeCloudMailAddress: (value) => String(value || '').trim().toLowerCase(),
|
||||
normalizeCloudMailBaseUrl: (value) => String(value || '').trim(),
|
||||
normalizeCloudMailDomain: (value) => String(value || '').trim(),
|
||||
normalizeCloudMailDomains: (values) => values || [],
|
||||
normalizeCloudMailMailApiMessages: (payload) => payload?.data?.records || [],
|
||||
pickVerificationMessageWithTimeFallback: (currentMessages) => ({
|
||||
match: currentMessages[0]
|
||||
? {
|
||||
code: String(currentMessages[0].bodyPreview).match(/(\d{6})/)[1],
|
||||
receivedAt: Date.parse(currentMessages[0].receivedDateTime),
|
||||
message: currentMessages[0],
|
||||
}
|
||||
: null,
|
||||
usedRelaxedFilters: false,
|
||||
usedTimeFallback: false,
|
||||
}),
|
||||
setEmailState: async () => {},
|
||||
setPersistentSettings: async () => {},
|
||||
sleepWithStop: async () => {},
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
return {
|
||||
...api,
|
||||
snapshot() {
|
||||
return { logs, listCalls };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('pollCloudMailVerificationCode returns code for generated Cloud Mail address', async () => {
|
||||
const api = createProviderApi();
|
||||
|
||||
const result = await api.pollCloudMailVerificationCode(4, {
|
||||
email: 'user@example.com',
|
||||
cloudMailBaseUrl: 'https://mail.example.com',
|
||||
cloudMailAdminEmail: 'admin@example.com',
|
||||
cloudMailAdminPassword: 'secret',
|
||||
cloudMailToken: 'token',
|
||||
}, {
|
||||
targetEmail: 'user@example.com',
|
||||
maxAttempts: 1,
|
||||
intervalMs: 1,
|
||||
});
|
||||
|
||||
assert.equal(result.code, '123456');
|
||||
assert.deepEqual(api.snapshot().listCalls, ['user@example.com']);
|
||||
});
|
||||
|
||||
test('pollCloudMailVerificationCode prefers configured receive mailbox over registration email', async () => {
|
||||
const api = createProviderApi({
|
||||
receiveMailbox: 'forward-box@example.com',
|
||||
messages: [{
|
||||
id: 'mail-2',
|
||||
address: 'forward-box@example.com',
|
||||
receivedDateTime: '2026-05-07T10:20:00.000Z',
|
||||
subject: 'Login verification code',
|
||||
from: { emailAddress: { address: 'noreply@tm.openai.com' } },
|
||||
bodyPreview: 'Your verification code is 654321.',
|
||||
}],
|
||||
});
|
||||
|
||||
const result = await api.pollCloudMailVerificationCode(8, {
|
||||
email: 'duck-forwarded@duck.com',
|
||||
mailProvider: 'cloudmail',
|
||||
emailGenerator: 'duck',
|
||||
cloudMailBaseUrl: 'https://mail.example.com',
|
||||
cloudMailReceiveMailbox: 'forward-box@example.com',
|
||||
cloudMailAdminEmail: 'admin@example.com',
|
||||
cloudMailAdminPassword: 'secret',
|
||||
cloudMailToken: 'token',
|
||||
}, {
|
||||
targetEmail: 'duck-forwarded@duck.com',
|
||||
maxAttempts: 1,
|
||||
intervalMs: 1,
|
||||
});
|
||||
|
||||
assert.equal(result.code, '654321');
|
||||
assert.deepEqual(api.snapshot().listCalls, ['forward-box@example.com']);
|
||||
});
|
||||
|
||||
test('pollCloudMailVerificationCode ignores stale receive mailbox when generator owns the address', async () => {
|
||||
const api = createProviderApi({
|
||||
receiveMailbox: 'forward-box@example.com',
|
||||
messages: [{
|
||||
id: 'mail-3',
|
||||
address: 'generated@example.com',
|
||||
receivedDateTime: '2026-05-07T11:20:00.000Z',
|
||||
subject: 'Signup verification code',
|
||||
from: { emailAddress: { address: 'noreply@tm.openai.com' } },
|
||||
bodyPreview: 'Your verification code is 246810.',
|
||||
}],
|
||||
});
|
||||
|
||||
const result = await api.pollCloudMailVerificationCode(4, {
|
||||
email: 'generated@example.com',
|
||||
mailProvider: 'cloudmail',
|
||||
emailGenerator: 'cloudmail',
|
||||
cloudMailBaseUrl: 'https://mail.example.com',
|
||||
cloudMailReceiveMailbox: 'forward-box@example.com',
|
||||
cloudMailAdminEmail: 'admin@example.com',
|
||||
cloudMailAdminPassword: 'secret',
|
||||
cloudMailToken: 'token',
|
||||
}, {
|
||||
targetEmail: 'generated@example.com',
|
||||
maxAttempts: 1,
|
||||
intervalMs: 1,
|
||||
});
|
||||
|
||||
assert.equal(result.code, '246810');
|
||||
assert.deepEqual(api.snapshot().listCalls, ['generated@example.com']);
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const {
|
||||
buildCloudMailHeaders,
|
||||
getCloudMailTokenFromResponse,
|
||||
normalizeCloudMailBaseUrl,
|
||||
normalizeCloudMailDomain,
|
||||
normalizeCloudMailDomains,
|
||||
normalizeCloudMailMailApiMessages,
|
||||
} = require('../cloudmail-utils.js');
|
||||
|
||||
test('normalizeCloudMailBaseUrl normalizes host and preserves path', () => {
|
||||
assert.equal(
|
||||
normalizeCloudMailBaseUrl('mail.example.com/api/'),
|
||||
'https://mail.example.com/api'
|
||||
);
|
||||
assert.equal(
|
||||
normalizeCloudMailBaseUrl('http://127.0.0.1:8080'),
|
||||
'http://127.0.0.1:8080'
|
||||
);
|
||||
assert.equal(normalizeCloudMailBaseUrl('::::'), '');
|
||||
});
|
||||
|
||||
test('normalizeCloudMailDomain and domains de-duplicate valid entries', () => {
|
||||
assert.equal(normalizeCloudMailDomain('@Mail.Example.com'), 'mail.example.com');
|
||||
assert.equal(normalizeCloudMailDomain('not-a-domain'), '');
|
||||
assert.deepEqual(
|
||||
normalizeCloudMailDomains(['mail.example.com', 'MAIL.EXAMPLE.COM', 'bad-value']),
|
||||
['mail.example.com']
|
||||
);
|
||||
});
|
||||
|
||||
test('buildCloudMailHeaders includes token and content type when needed', () => {
|
||||
assert.deepEqual(
|
||||
buildCloudMailHeaders({ token: 'token-value' }, { json: true }),
|
||||
{
|
||||
Authorization: 'token-value',
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test('normalizeCloudMailMailApiMessages extracts rows from nested Cloud Mail payloads', () => {
|
||||
const messages = normalizeCloudMailMailApiMessages({
|
||||
data: {
|
||||
records: [
|
||||
{
|
||||
emailId: 'mail-1',
|
||||
toEmail: 'User@Example.com',
|
||||
sendEmail: 'noreply@tm.openai.com',
|
||||
subject: 'OpenAI verification code',
|
||||
content: '<p>Your code is <strong>654321</strong> & ready.</p>',
|
||||
createTime: '2026-05-07 10:20:00',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(messages.length, 1);
|
||||
assert.equal(messages[0].id, 'mail-1');
|
||||
assert.equal(messages[0].address, 'user@example.com');
|
||||
assert.equal(messages[0].from.emailAddress.address, 'noreply@tm.openai.com');
|
||||
assert.match(messages[0].bodyPreview, /654321/);
|
||||
assert.match(messages[0].bodyPreview, /& ready/);
|
||||
assert.equal(messages[0].receivedDateTime, '2026-05-07T10:20:00.000Z');
|
||||
});
|
||||
|
||||
test('getCloudMailTokenFromResponse supports direct and nested response shapes', () => {
|
||||
assert.equal(getCloudMailTokenFromResponse({ token: 'one' }), 'one');
|
||||
assert.equal(getCloudMailTokenFromResponse({ data: { accessToken: 'two' } }), 'two');
|
||||
});
|
||||
@@ -28,6 +28,7 @@ test('GoPay utils builds GPC queue task and balance URLs from helper endpoints',
|
||||
const api = loadGoPayUtils();
|
||||
assert.equal(api.DEFAULT_GPC_HELPER_API_URL, 'https://gpc.qlhazycoder.top');
|
||||
assert.equal(api.normalizeGpcHelperBaseUrl(''), 'https://gpc.qlhazycoder.top');
|
||||
assert.equal(api.normalizeGpcHelperBaseUrl('https://example.com/api/gp/tasks'), 'https://gpc.qlhazycoder.top');
|
||||
assert.equal(
|
||||
api.buildGpcHelperApiUrl('', '/api/checkout/start'),
|
||||
'https://gpc.qlhazycoder.top/api/checkout/start'
|
||||
|
||||
@@ -358,13 +358,14 @@ test('pickVerificationMessageWithTimeFallback can ignore afterTimestamp while ke
|
||||
|
||||
test('buildHotmailMailApiLatestUrl includes email, client id, refresh token, and mailbox', () => {
|
||||
const url = new URL(buildHotmailMailApiLatestUrl({
|
||||
apiUrl: 'https://example.com/api/mail-new',
|
||||
clientId: 'client-123',
|
||||
email: 'user@hotmail.com',
|
||||
refreshToken: 'refresh-token-xyz',
|
||||
mailbox: 'Junk',
|
||||
}));
|
||||
|
||||
assert.equal(url.origin + url.pathname, 'https://apple.882263.xyz/api/mail-new');
|
||||
assert.equal(url.origin + url.pathname, 'https://example.com/api/mail-new');
|
||||
assert.equal(url.searchParams.get('client_id'), 'client-123');
|
||||
assert.equal(url.searchParams.get('email'), 'user@hotmail.com');
|
||||
assert.equal(url.searchParams.get('refresh_token'), 'refresh-token-xyz');
|
||||
@@ -372,6 +373,13 @@ test('buildHotmailMailApiLatestUrl includes email, client id, refresh token, and
|
||||
assert.equal(url.searchParams.get('response_type'), 'json');
|
||||
});
|
||||
|
||||
test('buildHotmailMailApiLatestUrl requires an explicit api url', () => {
|
||||
assert.throws(
|
||||
() => buildHotmailMailApiLatestUrl({ email: 'user@hotmail.com' }),
|
||||
/Hotmail mail API URL is required/
|
||||
);
|
||||
});
|
||||
|
||||
test('buildHotmailMailApiLatestUrl supports custom api url and can omit response_type', () => {
|
||||
const url = new URL(buildHotmailMailApiLatestUrl({
|
||||
apiUrl: 'https://example.com/custom-mail-api',
|
||||
|
||||
@@ -135,14 +135,18 @@ ${extractFunction('getPageTextSnapshot')}
|
||||
${extractFunction('isLoginPhoneUsernameKind')}
|
||||
${extractFunction('isLoginPhoneEntryPageText')}
|
||||
${extractFunction('isInsideHiddenPhoneControl')}
|
||||
${extractFunction('getLoginInputAttributeText')}
|
||||
${extractFunction('isLoginEmailLikeInput')}
|
||||
${extractFunction('summarizePhoneInputCandidate')}
|
||||
${extractFunction('isUsablePhoneInputElement')}
|
||||
${extractFunction('collectPhoneInputCandidates')}
|
||||
${extractFunction('findUsablePhoneInput')}
|
||||
${extractFunction('getLoginEmailInput')}
|
||||
${extractFunction('getLoginPhoneInput')}
|
||||
${extractFunction('isAddPhonePageReady')}
|
||||
|
||||
return {
|
||||
getLoginEmailInput,
|
||||
getLoginPhoneInput,
|
||||
isAddPhonePageReady,
|
||||
};
|
||||
@@ -168,6 +172,62 @@ test('step 7 does not mistake email entry with a phone switch action for phone i
|
||||
assert.equal(api.isAddPhonePageReady(), false);
|
||||
});
|
||||
|
||||
test('step 7 treats unified OpenAI login page as email input despite phone option text', () => {
|
||||
const api = createPhoneLoginEntryApi({
|
||||
href: 'https://auth.openai.com/log-in-or-create-account',
|
||||
pathname: '/log-in-or-create-account',
|
||||
pageText: '\u767b\u5f55\u6216\u6ce8\u518c \u7535\u5b50\u90ae\u4ef6\u5730\u5740 \u7ee7\u7eed \u4f7f\u7528\u7535\u8bdd\u53f7\u7801\u7ee7\u7eed',
|
||||
inputAttributes: { type: 'text', placeholder: '\u7535\u5b50\u90ae\u4ef6\u5730\u5740' },
|
||||
});
|
||||
|
||||
assert.ok(api.getLoginEmailInput(), 'unified login email input should be detected');
|
||||
assert.equal(api.getLoginPhoneInput(), null, 'phone option button must not turn email input into phone input');
|
||||
});
|
||||
|
||||
test('step 7 clicks the generic other-account entry before phone entry', async () => {
|
||||
const api = new Function(`
|
||||
const clicks = [];
|
||||
const genericEntry = { id: 'generic', textContent: '\\u767b\\u5f55\\u81f3\\u53e6\\u4e00\\u4e2a\\u5e10\\u6237' };
|
||||
const phoneEntry = { id: 'phone', textContent: '\\u4f7f\\u7528\\u7535\\u8bdd\\u53f7\\u7801\\u7ee7\\u7eed' };
|
||||
|
||||
function normalizeStep6Snapshot(snapshot) { return snapshot; }
|
||||
function inspectLoginAuthState() {
|
||||
return { state: 'entry_page', loginEntryTrigger: genericEntry, phoneEntryTrigger: phoneEntry };
|
||||
}
|
||||
function findLoginEntryTrigger() { return genericEntry; }
|
||||
function findLoginPhoneEntryTrigger() { return phoneEntry; }
|
||||
function isActionEnabled() { return true; }
|
||||
function getActionText(el) { return el.textContent || ''; }
|
||||
function log() {}
|
||||
async function humanPause() {}
|
||||
function simulateClick(el) { clicks.push(el.id); }
|
||||
async function waitForLoginEntryOpenTransition() { return { state: 'phone_entry_page' }; }
|
||||
async function switchFromEmailPageToPhoneLogin() { return { routed: 'switch-phone' }; }
|
||||
async function step6LoginFromEmailPage() { return { routed: 'email' }; }
|
||||
async function step6LoginFromPasswordPage() { return { routed: 'password' }; }
|
||||
async function step6LoginFromPhonePage() { return { routed: 'phone' }; }
|
||||
async function finalizeStep6VerificationReady() { return { routed: 'verification' }; }
|
||||
function createStep6OAuthConsentSuccessResult() { return { routed: 'oauth' }; }
|
||||
function createStep6AddEmailSuccessResult() { return { routed: 'add-email' }; }
|
||||
async function createStep6LoginTimeoutRecoveryTransition() { return { action: 'recoverable', result: { routed: 'recoverable' } }; }
|
||||
function createStep6RecoverableResult(reason, snapshot, options = {}) {
|
||||
return { step6Outcome: 'recoverable', reason, state: snapshot?.state, message: options.message || '' };
|
||||
}
|
||||
|
||||
${extractFunction('step6OpenLoginEntry')}
|
||||
|
||||
return { clicks, step6OpenLoginEntry };
|
||||
`)();
|
||||
|
||||
const result = await api.step6OpenLoginEntry(
|
||||
{ loginIdentifierType: 'phone', phoneNumber: '+441111111111' },
|
||||
{ state: 'entry_page', loginEntryTrigger: { id: 'generic', textContent: '\u767b\u5f55\u81f3\u53e6\u4e00\u4e2a\u5e10\u6237' }, phoneEntryTrigger: { id: 'phone', textContent: '\u4f7f\u7528\u7535\u8bdd\u53f7\u7801\u7ee7\u7eed' } }
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(api.clicks, ['generic']);
|
||||
assert.equal(result.routed, 'phone');
|
||||
});
|
||||
|
||||
test('step 7 detects username text input when usernameKind is phone_number', () => {
|
||||
const api = createPhoneLoginEntryApi({
|
||||
phoneUsernameKind: true,
|
||||
|
||||
Reference in New Issue
Block a user