- Add plugin store, version selection, configuration, and OAuth login - Support Muse (Meta) OAuth device authorization - Redesign home dashboard and add batch model health checks - Support per-credential provider editing, removal, and reordering - Improve usage charts and track actual response models - Preserve installed plugins and runtime data during portable rebuilds - Update translations, documentation, and related tests
173 lines
12 KiB
JavaScript
173 lines
12 KiB
JavaScript
const { chromium } = require(process.env.PLAYWRIGHT_MODULE || 'playwright');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const base = process.env.META_OAUTH_TEST_BASE_URL || process.env.DEVIN_TEST_BASE_URL || '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({ viewport: { width: 1280, height: 900 } });
|
|
page.setDefaultTimeout(10000);
|
|
const errors = [];
|
|
page.on('pageerror', (error) => errors.push(String(error)));
|
|
await page.route('**/*', route => route.request().url().startsWith(base + '/') ? route.continue() : route.abort());
|
|
|
|
const open = async (query = '') => {
|
|
await page.goto(base + '/tests/fixtures/meta-oauth.html?' + query, { waitUntil: 'domcontentloaded' });
|
|
await page.locator('.oauth-card, .simple-mode-provider-card').first().waitFor();
|
|
};
|
|
const managementCard = () => page.locator('.oauth-card').filter({ hasText: /Muse \(Meta\) OAuth|Muse OAuth|Meta OAuth/ });
|
|
const easyCard = () => page.locator('.simple-mode-provider-card').filter({ hasText: /Muse \(Meta\) OAuth|Muse OAuth|Meta OAuth/ });
|
|
const calls = () => page.evaluate(() => window.metaFixture.calls);
|
|
const fixture = () => page.evaluate(() => window.metaFixture);
|
|
const installClipboard = () => page.evaluate(() => {
|
|
Object.defineProperty(navigator, 'clipboard', {
|
|
configurable: true,
|
|
value: { writeText: async (value) => window.metaFixture.copied.push(String(value)) },
|
|
});
|
|
});
|
|
const waitForStatusCall = () => page.waitForFunction(() => window.metaFixture.statusCalls > 0);
|
|
const startManagement = async () => {
|
|
const card = managementCard();
|
|
await card.getByRole('button', { name: /Start Sign-In|开始登录/, exact: true }).click();
|
|
await card.getByText('META-CODE-1', { exact: true }).waitFor();
|
|
};
|
|
|
|
// Management view: Meta is a seventh OAuth provider, with device code and no callback input.
|
|
await open('locale=en');
|
|
const card = managementCard();
|
|
assert.equal(await page.locator('.oauth-card').count(), 7, 'Meta should add a seventh OAuth card');
|
|
assert.equal(await card.count(), 1, 'the Muse (Meta) card should be unique');
|
|
assert.equal(await card.getByRole('textbox').count(), 0, 'Meta must not render a callback textbox');
|
|
await installClipboard();
|
|
await startManagement();
|
|
assert.ok((await calls()).some(call => call.cmd === 'start_oauth_login' && call.args.provider === 'meta' && call.args.browser === 'none'));
|
|
assert.equal(await card.getByText('Device Code', { exact: true }).count(), 1);
|
|
assert.equal(await card.getByRole('button', { name: 'Copy Device Code', exact: true }).count(), 1);
|
|
await card.getByRole('button', { name: 'Copy Device Code', exact: true }).click();
|
|
await page.waitForFunction(() => window.metaFixture.copied.includes('META-CODE-1'));
|
|
await card.getByRole('button', { name: 'Open Link', exact: true }).click();
|
|
assert.ok((await calls()).some(call => call.cmd === 'open_oauth_url' && String(call.args.url).includes('meta-attempt-1')));
|
|
|
|
// Complete the flow and confirm the newly written credential is assigned priority zero.
|
|
await waitForStatusCall();
|
|
await page.evaluate(() => { window.metaFixture.status = 'ok'; });
|
|
await card.getByText('Completed', { exact: true }).waitFor();
|
|
assert.ok((await fixture()).priorityPatches.some(patch => patch.name === 'meta-oauth-1.json' && patch.priority === 0));
|
|
assert.equal(await card.getByText('META-CODE-1', { exact: true }).count(), 0, 'success clears the device code');
|
|
|
|
// A refresh cancels the old session and stale completion cannot win over attempt two.
|
|
await open('locale=en');
|
|
const refreshed = managementCard();
|
|
await page.evaluate(() => { window.metaFixture.holdStatus = true; });
|
|
await refreshed.getByRole('button', { name: /Start Sign-In/, exact: true }).click();
|
|
await refreshed.getByText('META-CODE-1', { exact: true }).waitFor();
|
|
await page.waitForFunction(() => window.metaFixture.releaseStatus !== null);
|
|
await refreshed.getByRole('button', { name: 'Refresh Link', exact: true }).click();
|
|
await page.waitForFunction(() => window.metaFixture.attempts === 2);
|
|
await refreshed.getByText('META-CODE-2', { exact: true }).waitFor();
|
|
const refreshedFixture = await fixture();
|
|
assert.ok(refreshedFixture.cancelledStates.includes('meta-attempt-1'));
|
|
assert.equal(await refreshed.getByText('META-CODE-1', { exact: true }).count(), 0);
|
|
await page.evaluate(() => { if (window.metaFixture.releaseStatus) window.metaFixture.releaseStatus(); });
|
|
await page.waitForTimeout(100);
|
|
assert.equal(await refreshed.getByText('Completed', { exact: true }).count(), 0, 'stale attempt must not complete refreshed login');
|
|
assert.ok(await refreshed.getByText('META-CODE-2', { exact: true }).isVisible());
|
|
|
|
// Error state can be retried and a later success still clears its device code.
|
|
await open('locale=en');
|
|
const retry = managementCard();
|
|
await retry.getByRole('button', { name: /Start Sign-In/, exact: true }).click();
|
|
await retry.getByText('META-CODE-1', { exact: true }).waitFor();
|
|
await waitForStatusCall();
|
|
await page.evaluate(() => { window.metaFixture.status = 'error'; });
|
|
await page.getByText(/sign-in failed/i).waitFor();
|
|
await retry.getByRole('button', { name: /Start Sign-In/, exact: true }).click();
|
|
await page.waitForFunction(() => window.metaFixture.attempts === 2);
|
|
await retry.getByText('META-CODE-2', { exact: true }).waitFor();
|
|
await page.waitForFunction(() => window.metaFixture.statusCalls > 1);
|
|
await page.evaluate(() => { window.metaFixture.status = 'ok'; });
|
|
await retry.getByText('Completed', { exact: true }).waitFor();
|
|
|
|
// Easy mode uses the same Meta flow and keeps the browser selection explicit.
|
|
await open('view=easy&locale=en&openError=1');
|
|
const easy = easyCard();
|
|
assert.equal(await page.locator('.simple-mode-provider-card').count(), 7, 'Easy mode should expose Meta OAuth');
|
|
await installClipboard();
|
|
await easy.getByRole('button', { name: 'Start Sign-In', exact: true }).click();
|
|
await easy.getByText('META-CODE-1', { exact: true }).waitFor();
|
|
assert.equal(await easy.getByText('Device Code', { exact: true }).count(), 1);
|
|
assert.equal(await easy.getByRole('textbox').count(), 0);
|
|
await easy.getByRole('button', { name: 'Copy Device Code', exact: true }).click();
|
|
await page.waitForFunction(() => window.metaFixture.copied.includes('META-CODE-1'));
|
|
await easy.getByRole('button', { name: 'Copy Link', exact: true }).click();
|
|
await page.waitForFunction(() => window.metaFixture.copied.includes('https://auth.meta.ai/device?state=meta-attempt-1'));
|
|
await easy.getByRole('button', { name: 'Open Link', exact: true }).click();
|
|
assert.ok((await calls()).some(call => call.cmd === 'open_oauth_url'
|
|
&& call.args.url === 'https://auth.meta.ai/device?state=meta-attempt-1'
|
|
&& call.args.browser === 'default'), 'Easy mode retains a usable fallback link when automatic browser launch fails');
|
|
await page.waitForFunction(() => window.metaFixture.calls.some(call => call.cmd === 'start_oauth_login' && call.args.provider === 'meta'));
|
|
const easyStart = (await calls()).find(call => call.cmd === 'start_oauth_login' && call.args.provider === 'meta');
|
|
assert.equal(easyStart.args.browser, 'default');
|
|
await waitForStatusCall();
|
|
await page.evaluate(() => { window.metaFixture.status = 'ok'; });
|
|
await easy.locator('.state-pill.success').waitFor();
|
|
assert.equal(await easy.getByText('META-CODE-1', { exact: true }).count(), 0, 'Easy mode success clears the device code');
|
|
assert.equal(await easy.locator('.simple-mode-oauth-authorization').count(), 0, 'Easy mode success clears its authorization link');
|
|
|
|
// Meta's 900-second expiry outlives the old ten-minute fallback, then clears stale controls.
|
|
await open('view=easy&locale=en');
|
|
const expiring = easyCard();
|
|
await expiring.getByRole('button', { name: 'Start Sign-In', exact: true }).click();
|
|
await expiring.getByText('META-CODE-1', { exact: true }).waitFor();
|
|
const pollsBeforeClockAdvance = await page.evaluate(() => {
|
|
window.metaFixture.clockOffsetMs = 601_000;
|
|
return window.metaFixture.statusCalls;
|
|
});
|
|
await page.waitForFunction((previous) => window.metaFixture.statusCalls > previous, pollsBeforeClockAdvance);
|
|
assert.ok(await expiring.getByText('META-CODE-1', { exact: true }).isVisible(), 'device code is still valid after 601 seconds');
|
|
assert.ok(await expiring.getByRole('button', { name: 'Waiting for browser authorization...', exact: true }).isDisabled());
|
|
await page.evaluate(() => { window.metaFixture.clockOffsetMs = 901_000; });
|
|
await page.getByText('Authorization timed out. Please initiate login again.', { exact: true }).waitFor();
|
|
assert.equal(await expiring.getByText('META-CODE-1', { exact: true }).count(), 0, 'expiry clears the device code');
|
|
assert.equal(await expiring.locator('.simple-mode-oauth-authorization').count(), 0, 'expiry clears the authorization link');
|
|
assert.ok(await expiring.getByRole('button', { name: 'Start Sign-In', exact: true }).isEnabled());
|
|
|
|
// Existing kimi-ai credentials continue to map to Kimi in Easy mode.
|
|
await open('view=easy&locale=en&kimiAi=1');
|
|
const kimi = page.locator('.simple-mode-provider-card').filter({ hasText: 'Kimi OAuth' });
|
|
await kimi.locator('.state-pill.success').waitFor();
|
|
assert.ok((await kimi.getAttribute('class')).split(' ').includes('connected'));
|
|
assert.equal((await calls()).filter(call => call.cmd === 'start_oauth_login').length, 0);
|
|
|
|
// Check the rendered labels and width constraints in both themes and viewport classes.
|
|
fs.mkdirSync('misc', { recursive: true });
|
|
for (const [theme, width] of [['light', 1280], ['dark', 390]]) {
|
|
await page.setViewportSize({ width, height: 900 });
|
|
await open(`locale=zh-CN&theme=${theme}`);
|
|
const zhCard = managementCard();
|
|
await zhCard.getByRole('button', { name: /开始登录/ }).click();
|
|
await zhCard.getByText('META-CODE-1', { exact: true }).waitFor();
|
|
assert.equal(await zhCard.getByText('设备码', { exact: true }).count(), 1);
|
|
assert.equal(await zhCard.getByRole('button', { name: '复制设备码', exact: true }).count(), 1);
|
|
assert.equal(await zhCard.getByRole('textbox').count(), 0);
|
|
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), `${theme} viewport overflows horizontally`);
|
|
await page.screenshot({ path: `misc/meta-oauth-${theme}.png`, fullPage: true });
|
|
|
|
await open(`view=easy&locale=zh-CN&theme=${theme}`);
|
|
const easyResponsive = easyCard();
|
|
await easyResponsive.getByRole('button', { name: /开始登录/ }).click();
|
|
await easyResponsive.getByText('META-CODE-1', { exact: true }).waitFor();
|
|
assert.equal(await easyResponsive.getByText('设备码', { exact: true }).count(), 1);
|
|
assert.equal(await easyResponsive.getByRole('button', { name: '复制设备码', exact: true }).count(), 1);
|
|
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), `Easy mode ${theme} viewport overflows horizontally`);
|
|
await easyResponsive.screenshot({ path: `misc/meta-oauth-easy-${theme}.png` });
|
|
}
|
|
|
|
assert.deepEqual(errors, []);
|
|
console.log('Meta OAuth browser checks passed: provider card, device code copy/open, refresh cancellation, stale poll isolation, success priority, retry, Easy mode expiry, Kimi credential compatibility and responsive labels.');
|
|
} finally {
|
|
await browser.close();
|
|
}
|
|
})().catch(error => { console.error(error); process.exitCode = 1; });
|