Files
EasyCLIProxyAPI/tests/agent-native-oauth-ui.cjs
lzt404 6dc8650e68 style(ui): unify subpage tabs and tighten usage layout
Move agent detection into the client list, simplify usage filters and pagination, and route interface text through the shared type scale. Subpage navigation now uses underlined tabs instead of segmented controls.
2026-10-04 17:31:52 +08:00

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('.agent-client-list-heading 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; });