Files
go_bt/tests/chatgpt_page_states.test.js

489 lines
18 KiB
JavaScript

'use strict';
const assert = require('assert');
const { _private } = require('../src/services/chatgpt_service');
const { classifyFailure } = require('../src/utils/error_classifier');
function missingLocator() {
return {
count: async () => 0,
nth() { return this; },
isVisible: async () => false,
};
}
function textPage(url, text) {
return {
url: () => url,
locator: selector => selector === 'body'
? { innerText: async () => text }
: missingLocator(),
};
}
function collectionLocator(items) {
return {
count: async () => items.length,
nth: index => items[index] || missingLocator(),
};
}
function chooserElement(text, onClick, attributes = {}) {
let visible = true;
return {
count: async () => 1,
nth() { return this; },
isVisible: async () => visible,
innerText: async () => text,
getAttribute: async name => attributes[name] || '',
scrollIntoViewIfNeeded: async () => {},
click: async () => {
const shouldHide = await onClick();
if (shouldHide !== false) visible = false;
},
};
}
function accountChooserPage({ choices, otherAccount, text, url = 'https://chatgpt.com/' }) {
return {
url: () => url,
waitForTimeout: async () => {},
locator: selector => {
if (selector === 'body') return { innerText: async () => text };
if (selector === 'button, [role="button"], a[href], [tabindex="0"]') {
return collectionLocator(choices);
}
if (selector.includes('Log in to another account')) {
return otherAccount || missingLocator();
}
return missingLocator();
},
};
}
(async () => {
const deleted = await _private.detectOpenAITerminalPage(textPage(
'https://auth.openai.com/error',
'Your account has been deactivated.',
));
assert.strictEqual(deleted.code, 'CHATGPT_ACCOUNT_DEACTIVATED');
assert.strictEqual(deleted.retryable, false);
assert.strictEqual(deleted.restartBrowser, false);
const deletedRealCopy = await _private.detectOpenAITerminalPage(textPage(
'https://auth.openai.com/error',
'Authentication Error. You do not have an account because it has been deleted or deactivated.',
));
assert.strictEqual(deletedRealCopy.code, 'CHATGPT_ACCOUNT_DEACTIVATED');
assert.strictEqual(deletedRealCopy.retryable, false);
const oops = await _private.detectOpenAITerminalPage(textPage(
'https://auth.openai.com/error',
'Oops! Something went wrong while signing you in.',
));
assert.strictEqual(oops.code, 'CHATGPT_OPENAI_OOPS');
assert.strictEqual(oops.restartBrowser, true);
assert.strictEqual(
await _private.detectOpenAITerminalPage(textPage(
'https://example.com/',
'Oops! Something went wrong while signing you in.',
)),
null,
'只能在 OpenAI 官方域名上识别终止页',
);
let terminalRaceUrl = 'https://auth.openai.com/error';
const terminalRacePage = {
url: () => terminalRaceUrl,
locator: selector => selector === 'body'
? {
innerText: async () => {
terminalRaceUrl = 'https://accounts.google.com/signin';
return 'Oops! Something went wrong while signing you in.';
},
}
: missingLocator(),
};
assert.strictEqual(
await _private.detectOpenAITerminalPage(terminalRacePage),
null,
'读取正文期间已跳到 Google 时不得使用旧 OpenAI 路由误判',
);
const welcomeCases = [
{
name: '旧版登录入口',
text: 'Welcome back. Log in or sign up',
selector: 'button:text-is("Log in or sign up")',
visible: true,
expected: true,
},
{
name: '真实新版 Google 入口',
text: 'Welcome back. Continue with Google Continue with Apple Continue with Microsoft',
selector: 'button:text-is("Continue with Google")',
visible: true,
expected: true,
},
{
name: 'Google 入口不可见',
text: 'Welcome back. Continue with Google',
selector: 'button:text-is("Continue with Google")',
visible: false,
expected: false,
},
];
for (const testCase of welcomeCases) {
const action = {
count: async () => 1,
nth() { return this; },
isVisible: async () => testCase.visible,
};
const welcomePage = {
url: () => 'https://auth.openai.com/log-in',
locator: selector => {
if (selector === 'body') return { innerText: async () => testCase.text };
if (selector === testCase.selector) return action;
return missingLocator();
},
};
assert.strictEqual(Boolean(await _private.findWelcomeAuthEntry(welcomePage)), testCase.expected, testCase.name);
}
assert.strictEqual(
await _private.findWelcomeAuthEntry(textPage('https://auth.openai.com/log-in', 'Welcome back')),
null,
'没有 Log in or sign up 操作时不应误判',
);
let targetAccountClicks = 0;
let removeAccountClicks = 0;
let targetOtherAccountClicks = 0;
const removeTarget = chooserElement(
'Target User target.user+label@googlemail.com',
async () => { removeAccountClicks++; },
{ 'aria-label': 'Remove account' },
);
const matchingTarget = chooserElement(
'Target User target.user+label@googlemail.com',
async () => { targetAccountClicks++; },
);
const targetOtherAccount = chooserElement(
'Log in to another account',
async () => { targetOtherAccountClicks++; },
);
const matchingChooser = accountChooserPage({
choices: [removeTarget, matchingTarget],
otherAccount: targetOtherAccount,
text: 'Welcome back Choose an account to continue Target User target.user+label@googlemail.com Log in to another account',
});
const matchingChooserResult = await _private.handleChatGPTAccountChooser(
matchingChooser,
'targetuser@gmail.com',
() => {},
{ transitionTimeoutMs: 0 },
);
assert.strictEqual(matchingChooserResult.action, 'target_account');
assert.strictEqual(targetAccountClicks, 1, '邮箱匹配时应点击保存的目标账号卡片');
assert.strictEqual(removeAccountClicks, 0, '不得误点账号卡片中的移除按钮');
assert.strictEqual(targetOtherAccountClicks, 0, '找到目标账号后不得切换其他账号');
let mismatchedAccountClicks = 0;
let mismatchedOtherAccountClicks = 0;
const mismatchedAccount = chooserElement(
'Another User another@example.com',
async () => { mismatchedAccountClicks++; },
);
const mismatchedOtherAccount = chooserElement(
'Log in to another account',
async () => { mismatchedOtherAccountClicks++; },
);
const mismatchedChooserResult = await _private.handleChatGPTAccountChooser(
accountChooserPage({
choices: [mismatchedAccount],
otherAccount: mismatchedOtherAccount,
text: 'Welcome back Choose an account to continue Another User another@example.com Log in to another account',
}),
'target@example.com',
() => {},
{ transitionTimeoutMs: 0 },
);
assert.strictEqual(mismatchedChooserResult.action, 'other_account');
assert.strictEqual(mismatchedAccountClicks, 0, '邮箱不匹配时不得点击已保存的其他账号');
assert.strictEqual(mismatchedOtherAccountClicks, 1, '邮箱不匹配时应点击登录其他账号');
let nonChooserClicks = 0;
const nonChooserResult = await _private.handleChatGPTAccountChooser(
accountChooserPage({
choices: [chooserElement('target@example.com', async () => { nonChooserClicks++; })],
text: 'Welcome back Continue with Google',
}),
'target@example.com',
() => {},
{ transitionTimeoutMs: 0 },
);
assert.strictEqual(nonChooserResult.found, false, '普通 Welcome back 页面不应当作账号选择弹窗');
assert.strictEqual(nonChooserClicks, 0);
let stuckChooserClicks = 0;
const stuckTarget = chooserElement(
'Target target@example.com',
async () => { stuckChooserClicks++; return false; },
);
const stuckChooserResult = await _private.waitForChatGPTResult(
accountChooserPage({
choices: [stuckTarget],
text: 'Welcome back Choose an account to continue Target target@example.com',
}),
{ email: 'target@example.com' },
23,
() => {},
{ accountChooserTransitionTimeoutMs: 0 },
);
assert.strictEqual(stuckChooserResult.code, 'CHATGPT_ACCOUNT_CHOOSER_STUCK');
assert.strictEqual(stuckChooserClicks, 3, '账号选择弹窗最多只点击三次');
let ageConfirmClicks = 0;
const confirmButton = {
count: async () => 1,
nth() { return this; },
isVisible: async () => true,
click: async () => { ageConfirmClicks++; },
};
const dialog = {
isVisible: async () => true,
innerText: async () => 'Please confirm your age is correct',
locator: selector => selector === 'button:text-is("OK")' ? confirmButton : missingLocator(),
};
const ageDialogPage = {
url: () => 'https://auth.openai.com/create-account',
waitForLoadState: async () => {},
waitForTimeout: async () => {},
locator: selector => {
if (selector === '[role="dialog"]') {
return { count: async () => 1, nth: () => dialog };
}
if (selector === 'body') return { innerText: async () => 'Please confirm your age is correct OK' };
return missingLocator();
},
};
assert.strictEqual(await _private.acceptAgeConfirmationDialog(ageDialogPage, () => {}), true);
assert.strictEqual(ageConfirmClicks, 1);
const ageDialogResult = await _private.fillAgeOnboarding(
ageDialogPage,
{ email: 'target@example.com' },
23,
() => {},
{ stabilizeDelay: 0 },
);
assert.strictEqual(ageDialogResult.ok, true);
assert.strictEqual(ageDialogResult.confirmed, true);
assert.strictEqual(ageConfirmClicks, 2);
let realAgeConfirmClicks = 0;
const realAgeConfirmButton = {
count: async () => 1,
nth() { return this; },
isVisible: async () => true,
click: async () => { realAgeConfirmClicks++; },
};
const realAgeDialogPage = {
url: () => 'https://auth.openai.com/create-account',
locator: selector => {
if (selector === 'body') {
return { innerText: async () => "You're setting your birthday to Aug 20, 2003. OK Cancel" };
}
if (selector === 'button:text-is("OK")') return realAgeConfirmButton;
return missingLocator();
},
};
assert.strictEqual(
await _private.acceptAgeConfirmationDialog(realAgeDialogPage, () => {}),
true,
'应兼容没有标准 dialog 属性的真实生日确认弹框',
);
assert.strictEqual(realAgeConfirmClicks, 1);
const blankSnapshot = {
available: true,
readyState: 'complete',
bodyTextLength: 0,
bodyHtmlLength: 0,
bodyElementCount: 0,
visibleContentCount: 0,
resourceCount: 1,
resourceTransferSize: 100,
navigationResponseEnd: 20,
};
assert.strictEqual(_private.isPureBlankSnapshot(blankSnapshot), true);
assert.strictEqual(_private.isPureBlankSnapshot({ ...blankSnapshot, readyState: 'loading' }), false);
assert.strictEqual(_private.isPureBlankSnapshot({ ...blankSnapshot, visibleContentCount: 1 }), false);
let blankState = _private.advanceStableBlankTracker(null, 'https://auth.openai.com/x', blankSnapshot, 1000);
assert.strictEqual(blankState.stable, false);
blankState = _private.advanceStableBlankTracker(blankState.tracker, 'https://auth.openai.com/x', blankSnapshot, 1000);
blankState = _private.advanceStableBlankTracker(blankState.tracker, 'https://auth.openai.com/x', blankSnapshot, 1000);
blankState = _private.advanceStableBlankTracker(blankState.tracker, 'https://auth.openai.com/x', blankSnapshot, 1000);
assert.strictEqual(blankState.stable, true, '空白页必须稳定持续达到阈值才分类');
let blankReloads = 0;
const blankPage = {
url: () => 'https://chatgpt.com/',
title: async () => '',
evaluate: async () => blankSnapshot,
waitForTimeout: async () => {},
reload: async () => { blankReloads++; },
goto: async () => { throw new Error('刷新成功后不应改用 goto'); },
locator: selector => selector === 'body'
? { innerText: async () => '' }
: missingLocator(),
};
const blankResult = await _private.waitForChatGPTResult(
blankPage,
{ email: 'target@example.com' },
23,
() => {},
);
assert.strictEqual(blankResult.code, 'CHATGPT_BLANK_PAGE');
assert.strictEqual(blankReloads, 1, '稳定空白页只允许自动刷新一次');
const cfState = { body: 'ChatGPT home', waits: 0, probeRequests: 0, probeClosed: false };
const probePage = {
goto: async () => { cfState.probeRequests++; },
locator: selector => {
assert.strictEqual(selector, 'body');
return { innerText: async () => '{}' };
},
close: async () => { cfState.probeClosed = true; },
};
const cfMainPage = {
url: () => 'https://chatgpt.com/',
context: () => ({ newPage: async () => probePage }),
waitForTimeout: async () => {
cfState.waits++;
cfState.body = 'Performing security verification Verify you are human';
},
locator: selector => selector === 'body'
? { innerText: async () => cfState.body }
: missingLocator(),
};
const interrupted = await _private.readChatGPTSessionInProbePage(cfMainPage, () => {}, {
maxAttempts: 2,
retryIntervalMs: 1000,
expectedHome: true,
});
assert.strictEqual(_private.isSessionProbeInterruption(interrupted, 'cloudflare'), true);
assert.strictEqual(cfState.probeRequests, 1, 'CF 重现后不应继续请求 Session');
assert.strictEqual(cfState.probeClosed, true);
const probeResponseCases = [
{
name: 'Cloudflare HTML',
status: 403,
contentType: 'text/html; charset=UTF-8',
body: '<!doctype html><html><body>Performing security verification Cloudflare cf-ray</body></html>',
code: 'CHATGPT_SESSION_CF_HTML',
},
{ name: 'HTTP 403', status: 403, contentType: 'application/json', body: '{}', code: 'CHATGPT_SESSION_HTTP_403' },
{ name: 'HTTP 429', status: 429, contentType: 'application/json', body: '{}', code: 'CHATGPT_SESSION_HTTP_429' },
{ name: 'HTTP 503', status: 503, contentType: 'text/plain', body: 'Service Unavailable', code: 'CHATGPT_SESSION_HTTP_5XX' },
];
for (const testCase of probeResponseCases) {
let closed = false;
const mainPage = {
context: () => ({
newPage: async () => ({
goto: async () => ({
status: () => testCase.status,
headers: async () => ({ 'content-type': testCase.contentType }),
}),
locator: () => ({ innerText: async () => testCase.body }),
close: async () => { closed = true; },
}),
}),
waitForTimeout: async () => {},
};
const result = await _private.readChatGPTSessionInProbePage(mainPage, () => {}, { maxAttempts: 1 });
assert.strictEqual(result.code, testCase.code, testCase.name);
assert.strictEqual(_private.isSessionProbeFailure(result), true, testCase.name);
assert.strictEqual(closed, true, `${testCase.name} 临时页应关闭`);
}
const routeChangeState = {
url: 'https://chatgpt.com/',
probeRequests: 0,
probeBodyReads: 0,
};
const routeChangeMainPage = {
url: () => routeChangeState.url,
context: () => ({
newPage: async () => ({
goto: async () => {
routeChangeState.probeRequests++;
routeChangeState.url = 'https://auth.openai.com/error';
},
locator: () => ({
innerText: async () => {
routeChangeState.probeBodyReads++;
return '{}';
},
}),
close: async () => {},
}),
}),
locator: selector => selector === 'body'
? { innerText: async () => "You're all set" }
: missingLocator(),
waitForTimeout: async () => {},
};
const routeChanged = await _private.readChatGPTSessionInProbePage(routeChangeMainPage, () => {}, {
maxAttempts: 2,
});
assert.strictEqual(_private.isSessionProbeInterruption(routeChanged, 'route_changed'), true);
assert.strictEqual(routeChangeState.probeRequests, 1);
assert.strictEqual(routeChangeState.probeBodyReads, 0, '主页面换路由后应立即退出,不再读取旧 Session 响应');
let oauthStarts = 0;
let oauthUrl = 'https://auth.openai.com/log-in';
const oauthPage = {
url: () => oauthUrl,
title: async () => '',
waitForTimeout: async () => {},
goto: async url => {
assert.match(url, /chatgpt\.com\/auth\/login_with/);
oauthStarts++;
oauthUrl = 'https://accounts.google.com/signin/rejected';
},
locator: selector => {
if (selector === 'body') return { innerText: async () => 'Welcome back. Continue with Google' };
if (oauthUrl.includes('auth.openai.com') && selector === 'button:text-is("Continue with Google")') {
return { count: async () => 1, nth() { return this; }, isVisible: async () => true };
}
return missingLocator();
},
};
const oauthResult = await _private.waitForChatGPTResult(
oauthPage,
{ email: 'target@example.com' },
23,
() => {},
);
assert.strictEqual(oauthStarts, 1);
assert.strictEqual(oauthResult.code, 'SIGNIN_REJECTED');
const classifiedDeleted = classifyFailure({ message: 'OpenAI账号已删除或停用' });
assert.strictEqual(classifiedDeleted.code, 'CHATGPT_ACCOUNT_DEACTIVATED');
assert.strictEqual(classifiedDeleted.restartBrowser, false);
assert.strictEqual(classifiedDeleted.label, '账号已删除/停用');
assert.strictEqual(
classifyFailure({ message: 'You do not have an account because it has been deleted or deactivated.' }).code,
'CHATGPT_ACCOUNT_DEACTIVATED',
);
assert.strictEqual(
classifyFailure({ message: 'OpenAI登录页面出现 Oops 错误' }).code,
'CHATGPT_OPENAI_OOPS',
);
assert.strictEqual(classifyFailure({ message: 'OpenAI页面持续空白' }).code, 'CHATGPT_BLANK_PAGE');
console.log('chatgpt_page_states.test.js: PASS');
})().catch(error => {
console.error(error);
process.exitCode = 1;
});