Files
EasyCLIProxyAPI/tests/agent-native-oauth-ui.cjs
T

229 lines
14 KiB
JavaScript

const { chromium } = require(process.env.PLAYWRIGHT_MODULE || 'playwright');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const base = 'http://127.0.0.1:1421';
(async () => {
const browser = await chromium.launch({ channel: 'msedge', headless: true, args: ['--no-proxy-server'] });
try {
const page = await browser.newPage();
page.setDefaultTimeout(10000);
page.setDefaultNavigationTimeout(30000);
await page.route('**/*', route => route.request().url().startsWith(base + '/') ? route.continue() : route.abort());
const errors = [];
page.on('pageerror', error => errors.push(String(error)));
const restore = () => page.getByRole('button', { name: '重置登录', exact: true });
const update = () => page.locator('.agent-save-actions .primary-button');
const core = () => page.getByRole('tab', { name: '基础配置', exact: true }).click();
const manage = () => page.getByRole('tab', { name: '配置管理', exact: true }).click();
const restoreOfficial = async () => {
await manage();
assert.equal(await page.locator('.agent-management-clear-integration .agent-clear-integration').count(), 1);
await restore().click();
await page.waitForFunction(() => !document.querySelector('.agent-clear-integration').disabled);
assert.equal(await page.getByRole('tab', { name: '配置管理', exact: true }).getAttribute('aria-selected'), 'true');
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth), false);
await core();
};
const open = async query => {
await page.goto(`${base}/tests/fixtures/agent-backups.html?reset-selections&client=codex&${query}`, { waitUntil: 'domcontentloaded' });
await update().waitFor();
await page.waitForFunction(() => {
const button = document.querySelector('.agents-page .management-header button');
return window.fixtureCalls.some(call => call.cmd === 'get_agent_models') && button && !button.disabled;
});
};
const waitMode = async enabled => {
await page.waitForFunction(value => {
const button = document.querySelector('.agent-save-actions .primary-button');
const status = document.querySelector('.agent-client-list button.active small');
return button && (status?.textContent === '未接入 CPA') === value;
}, enabled);
};
const assertButtonOrder = async (applied = true) => {
const positions = await page.locator('.agent-save-actions button').evaluateAll(nodes => nodes.map(node => {
const rect = node.getBoundingClientRect();
return { x: rect.x, y: rect.y, width: rect.width };
}));
assert.equal(positions.length, applied ? 2 : 1);
if (applied) {
const [closePosition, updatePosition] = positions;
assert.ok(closePosition.x + closePosition.width <= updatePosition.x + 1, 'Close is left of Update');
assert.ok(Math.abs(closePosition.y - updatePosition.y) < 1, 'Close and Update share a row');
}
assert.equal(await restore().count(), 0, 'Official sign-in is only available in configuration management');
assert.equal(await page.locator('.agent-native-oauth, [role="switch"]').count(), 0, 'The old mode switch is gone');
};
fs.mkdirSync('misc', { recursive: true });
for (const width of [1280, 360]) for (const embedded of [false, true]) {
await page.setViewportSize({ width, height: 900 });
await open(embedded ? 'embedded' : '');
await assertButtonOrder();
await restoreOfficial();
await waitMode(true);
assert.equal(await page.locator('.agent-close-configuration').count(), 0);
assert.equal(await page.locator('.agent-model-trigger').count(), 1, 'CPA models remain selectable for reconnect');
await assertButtonOrder(false);
assert.match(await page.locator('.agent-save-feedback').innerText(), /更新配置将重新接入 CPA/);
const calls = await page.evaluate(() => window.fixtureCalls);
assert.equal(calls.filter(call => ['check_codex_oauth_login', 'update_agent_config'].includes(call.cmd)).length, 0);
assert.equal(calls.filter(call => call.cmd === 'restore_codex_official_config').length, 1);
await page.evaluate(() => window.fixtureRemount());
await waitMode(true);
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth), false);
await manage();
await restore().waitFor();
await page.evaluate(() => window.fixtureRemount());
await restore().waitFor();
assert.equal(await page.getByRole('tab', { name: '配置管理', exact: true }).getAttribute('aria-selected'), 'true');
if (width === 1280) await page.screenshot({ animations: 'disabled', path: `misc/restore-official-${embedded ? 'embedded' : 'full'}.png`, fullPage: true });
await core();
await page.locator('.agent-model-trigger').click();
await page.getByRole('option', { name: 'gpt-two' }).click();
await update().click();
await waitMode(false);
const updated = (await page.evaluate(() => window.fixtureCalls)).findLast(call => call.cmd === 'update_agent_config');
assert.equal(updated.args.model, 'gpt-two');
assert.equal(updated.args.oauthConfiguration, false);
if (!embedded) {
await restoreOfficial();
await waitMode(true);
await page.getByRole('radio', { name: 'OAuth 登录方式', exact: true }).click();
await update().click();
await waitMode(false);
assert.equal((await page.evaluate(() => window.fixtureCalls)).findLast(call => call.cmd === 'update_agent_config').args.oauthConfiguration, true);
}
}
await open('fresh');
assert.equal(await update().innerText(), '应用配置修改');
assert.equal(await page.locator('.agent-close-configuration').count(), 0);
await restoreOfficial();
await waitMode(true);
await update().click();
await waitMode(false);
await open('no-core');
await restoreOfficial();
await waitMode(true);
assert.equal(await update().isDisabled(), true, 'An offline core prevents reconnect, but not Restore');
await open('fail-native-oauth');
await restoreOfficial();
await page.getByRole('alert').filter({ hasText: /模拟切换失败/ }).waitFor();
await waitMode(false);
await restoreOfficial();
await waitMode(true);
await open('fail-apply');
await restoreOfficial();
await waitMode(true);
await update().click();
await page.getByRole('alert').filter({ hasText: /模拟配置写入失败/ }).waitFor();
await waitMode(true);
await update().click();
await waitMode(false);
const close = () => page.getByRole('button', { name: '关闭配置修改', exact: true });
const apply = () => page.getByRole('button', { name: '应用配置修改', exact: true });
for (const embedded of [false, true]) {
await open(embedded ? 'embedded' : '');
await close().waitFor();
assert.equal(await update().innerText(), '更新配置');
await update().click();
await page.waitForFunction(() => document.documentElement.dataset.fixtureApplied === '1');
assert.equal((await page.evaluate(() => window.fixtureCalls)).filter(call => call.cmd === 'close_codex_config_modification').length, 0, 'Update never closes an unchanged configuration');
await page.locator('.agent-model-trigger').click();
await page.getByRole('option', { name: 'gpt-two' }).click();
assert.equal(await close().isEnabled(), true, 'A model draft does not hide or disable Close');
await close().click();
await apply().waitFor();
assert.equal(await close().count(), 0, 'Close is hidden after disconnect');
assert.equal((await page.evaluate(() => window.fixtureCalls)).filter(call => call.cmd === 'close_codex_config_modification').length, 1);
await page.evaluate(() => window.fixtureRemount());
await apply().waitFor();
await apply().click();
await page.waitForFunction(() => !document.querySelector('.agent-close-configuration').disabled);
if (!embedded) {
await page.getByRole('radio', { name: 'OAuth 登录方式', exact: true }).click();
await page.getByRole('button', { name: '更新配置', exact: true }).click();
await page.waitForFunction(() => !document.querySelector('.agent-close-configuration').disabled);
assert.equal((await page.evaluate(() => window.fixtureCalls)).findLast(call => call.cmd === 'update_agent_config').args.oauthConfiguration, true);
await page.getByRole('radio', { name: 'API 密钥', exact: true }).click();
assert.equal(await close().isEnabled(), true, 'An authentication draft keeps Close available');
await page.getByRole('button', { name: '更新配置', exact: true }).click();
await page.waitForFunction(() => !document.querySelector('.agent-close-configuration').disabled);
assert.equal((await page.evaluate(() => window.fixtureCalls)).findLast(call => call.cmd === 'update_agent_config').args.oauthConfiguration, false);
}
}
for (const width of [1280, 360]) for (const embedded of [false, true]) {
await page.setViewportSize({ width, height: 900 });
await open(`fresh&fail-apply${embedded ? '&embedded' : ''}`);
assert.equal(await close().count(), 0, 'An unapplied configuration has no Close button');
await assertButtonOrder(false);
await apply().click();
await page.getByRole('alert').filter({ hasText: /模拟配置写入失败/ }).waitFor();
assert.equal(await close().count(), 0, 'A failed application does not reveal Close');
await apply().click();
await close().waitFor();
await assertButtonOrder();
await page.evaluate(() => window.fixtureRemount());
await close().waitFor();
await close().click();
await apply().waitFor();
assert.equal(await close().count(), 0, 'Closing hides the action again');
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth), false);
}
await open('no-core');
await close().waitFor();
assert.equal(await close().isDisabled(), false, 'Closing is available while CPA is offline');
await close().click();
await apply().waitFor();
await open('fail-close');
await close().click();
await page.getByRole('alert').filter({ hasText: /模拟关闭失败/ }).waitFor();
await close().waitFor();
await close().click();
await apply().waitFor();
for (const width of [1280, 360]) {
await page.setViewportSize({ width, height: 900 });
await open('no-oauth-login');
const oauth = () => page.getByRole('radio', { name: 'OAuth 登录方式', exact: true });
await oauth().click();
const dialog = page.getByRole('alertdialog');
await dialog.waitFor();
assert.match(await dialog.innerText(), /配置管理.*重置登录/);
assert.equal((await dialog.innerText()).includes('清空配置'), false);
assert.equal(await page.getByRole('radio', { name: 'API 密钥', exact: true }).getAttribute('aria-checked'), 'true');
assert.equal((await page.evaluate(() => window.fixtureCalls)).filter(call => ['restore_codex_official_config', 'clear_codex_config', 'update_agent_config'].includes(call.cmd)).length, 0);
await dialog.getByRole('button', { name: '前往配置管理', exact: true }).click();
await dialog.waitFor({ state: 'detached' });
await restore().waitFor();
await page.waitForFunction(() => document.activeElement?.id === 'agent-clear-integration');
assert.equal((await page.evaluate(() => window.fixtureCalls)).filter(call => call.cmd === 'restore_codex_official_config').length, 0, 'Guidance navigates without clearing configuration');
await restore().click();
await page.getByRole('status').filter({ hasText: /已清除 CPA 接入/ }).waitFor();
assert.equal((await page.evaluate(() => window.fixtureCalls)).filter(call => call.cmd === 'clear_codex_config').length, 0, 'Clear integration never calls Clear configuration');
await core();
await oauth().click();
await dialog.waitFor();
await dialog.getByRole('button', { name: '我知道了', exact: true }).click();
assert.equal(await page.getByRole('tab', { name: '基础配置', exact: true }).getAttribute('aria-selected'), 'true');
await page.evaluate(() => { window.fixtureOauthLoggedIn = true; });
await oauth().click();
await page.waitForFunction(() => document.querySelector('[data-value="oauth"]')?.getAttribute('aria-checked') === 'true');
assert.equal(await dialog.count(), 0, 'An existing login can select OAuth directly');
await update().click();
await close().waitFor();
assert.equal((await page.evaluate(() => window.fixtureCalls)).findLast(call => call.cmd === 'update_agent_config').args.oauthConfiguration, true);
}
for (const client of ['opencode', 'zcode']) {
await page.goto(`${base}/tests/fixtures/agent-backups.html?reset-selections&client=${client}`, { waitUntil: 'domcontentloaded' });
await page.getByRole('button', { name: '更新配置', exact: true }).waitFor();
assert.equal(await close().count(), 1, 'Other managed clients can also close configuration');
assert.equal(await restore().count(), 0);
await manage();
assert.equal(await page.getByRole('button', { name: '清除接入', exact: true }).count(), 1, 'Other clients can clear CPA integration in management');
assert.equal(await page.locator('#agent-clear-integration').count(), 0, 'The official Codex login action remains Codex-only');
}
assert.deepEqual(errors, []);
console.log('PASS: Reset sign-in in management, missing-login guidance and navigation, login then OAuth reconnect, independent Close/Update, failures, and both layouts at 1280/360px.');
} finally { await browser.close(); }
})().catch(error => { console.error(error); process.exitCode = 1; });