From 624d8efd9749b1e26c61e71560639fc0b4803072 Mon Sep 17 00:00:00 2001 From: RockChinQ Date: Sat, 17 Oct 2026 01:54:05 +0800 Subject: [PATCH] test(web): align frontend checks with the plan comparison dialog - Use LangBot Account wording in the Japanese checkout handoff copy. - Assert the Workspace Settings plan entry opens CloudPlansDialog. - Format revision tests with Prettier. Co-Authored-By: Claude Opus 5.5 --- web/src/i18n/locales/ja-JP.ts | 3 +- web/tests/revisions/verify.mjs | 287 ++++++++++++++---- web/tests/unit/plan-revisions.test.mjs | 81 ++++- .../unit/workspace-switcher-layout.test.mjs | 5 +- 4 files changed, 292 insertions(+), 84 deletions(-) diff --git a/web/src/i18n/locales/ja-JP.ts b/web/src/i18n/locales/ja-JP.ts index 6e9b40eed..402e10b75 100644 --- a/web/src/i18n/locales/ja-JP.ts +++ b/web/src/i18n/locales/ja-JP.ts @@ -94,7 +94,8 @@ const jaJP = { annualNote: '月額 {{price}} 相当', ownerOnly: '所有者または管理者のみ', choose: '支払いに進む', - handoff: 'Space でログインしてお支払いください。選択内容は保持されます。', + handoff: + 'LangBot アカウントでログインしてお支払いください。選択内容は保持されます。', quotas: 'ワークスペースの上限', unlimited: '無制限', retention: 'メッセージ履歴 · {{count}} 日間', diff --git a/web/tests/revisions/verify.mjs b/web/tests/revisions/verify.mjs index 367ac2963..6d4ba1efb 100644 --- a/web/tests/revisions/verify.mjs +++ b/web/tests/revisions/verify.mjs @@ -1,72 +1,225 @@ -import {createServer} from 'vite'; -import {chromium} from '@playwright/test'; -import {fileURLToPath} from 'node:url'; +import { createServer } from 'vite'; +import { chromium } from '@playwright/test'; +import { fileURLToPath } from 'node:url'; import path from 'node:path'; -import {mkdir,writeFile} from 'node:fs/promises'; -const workdir=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'../../../..'); -const evidence=path.join(workdir,'evidence'); -await mkdir(evidence,{recursive:true}); -const servers=[];let browser; -const results=[]; -const latest=(mode)=>({uuid:'latest',name:'pro',revision:mode==='same'?1:2,display_name:{en_US:'Pro'},description:{en_US:'Workspace plan'},price_cny:'70',price_usd:'10',price_jpy:'1500',features:[],limits: mode==='equal'||mode==='same'?{'bots.max':2,'members.max':3}:{'bots.max':8,'members.max':5},entitlement_features:{mcp_stdio:!['same','equal'].includes(mode)},disabled_adapters:['same','equal'].includes(mode)?['telegram']:[],chat_history_retention_days:['same','equal'].includes(mode)?30:90,monthly_bonus_credits:['same','equal'].includes(mode)?10:20,is_active:true,subscription_disabled:mode==='paused',sort_order:1,highlight:false}); +import { mkdir, writeFile } from 'node:fs/promises'; +const workdir = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + '../../../..', +); +const evidence = path.join(workdir, 'evidence'); +await mkdir(evidence, { recursive: true }); +const servers = []; +let browser; +const results = []; +const latest = (mode) => ({ + uuid: 'latest', + name: 'pro', + revision: mode === 'same' ? 1 : 2, + display_name: { en_US: 'Pro' }, + description: { en_US: 'Workspace plan' }, + price_cny: '70', + price_usd: '10', + price_jpy: '1500', + features: [], + limits: + mode === 'equal' || mode === 'same' + ? { 'bots.max': 2, 'members.max': 3 } + : { 'bots.max': 8, 'members.max': 5 }, + entitlement_features: { mcp_stdio: !['same', 'equal'].includes(mode) }, + disabled_adapters: ['same', 'equal'].includes(mode) ? ['telegram'] : [], + chat_history_retention_days: ['same', 'equal'].includes(mode) ? 30 : 90, + monthly_bonus_credits: ['same', 'equal'].includes(mode) ? 10 : 20, + is_active: true, + subscription_disabled: mode === 'paused', + sort_order: 1, + highlight: false, +}); try { - for(const [repo,port] of [['space',4185],['core',4186]]){ - const server=await createServer({configFile:path.join(workdir,repo,'web/tests/revisions/vite.config.mjs')});await server.listen();servers.push(server); - } - browser=await chromium.launch({headless:true}); - for(const repo of ['space','core']){ - const modes=repo==='space'?['changed','same','equal','paused','removed','member','stripe','cancel-failure']:['changed','same','equal','paused','removed','member','unknown','error']; - for(const mode of modes){ - const page=await browser.newPage({viewport:{width:1360,height:1100}}); - page.setDefaultTimeout(120000); - const errors=[];page.on('pageerror',error=>{errors.push(error.message);console.error(error.message);}); - await page.route('**/*',async route=>{ - const url=new URL(route.request().url()); - if(url.hostname==='127.0.0.1')return route.continue(); - if(url.pathname==='/api/v1/cloud/v2/plans')return route.fulfill({status:mode==='error'?500:200,contentType:'application/json',body:JSON.stringify({code:0,data:mode==='removed'?[]:[latest(mode)]})}); - return route.abort(); - }); - const port=repo==='space'?4185:4186; - await page.goto(`http://127.0.0.1:${port}/tests/revisions/index.html?mode=${mode}${mode==='stripe'?'&intent=1':''}`,{waitUntil:'domcontentloaded',timeout:120000}); - if(repo==='core'&&mode==='error')await page.getByRole('alert').waitFor(); - else if(mode==='unknown')await page.getByText(/Current subscription details are unavailable/).waitFor(); - else await page.locator(repo==='space'?'[data-plan-comparison]':'[data-cloud-plans] article').first().waitFor(); - if(errors.length)throw new Error(`${repo}/${mode}: ${errors.join(';')}`); - if(await page.locator('[data-revision-comparison]').count())throw new Error('Standalone comparison remains'); - if(['same','equal'].includes(mode) && await page.locator('[data-owned-plan]').count())throw new Error('Identical benefits duplicated'); - const area=page.locator(repo==='space'?'[data-plan-comparison]':'[data-cloud-plans]'); - if(!['unknown','error'].includes(mode)) { - const copy=await area.innerText(); - if(/&#|revision|mcp stdio|workspace multi user|platform telegram/.test(copy))throw new Error('Internal metadata leaked into plan presentation'); - if(repo==='core'&&await area.locator('[data-changed], .bg-amber-500\\/10').count())throw new Error('Diff highlighting remains'); - if(['same','equal'].includes(mode)&&/Subscribed on|Latest quotas for this plan/.test(copy))throw new Error('Unnecessary comparison labels'); - } - if(mode==='changed') { - await area.getByText(/Subscribed on/).waitFor(); - await area.getByText('Latest quotas for this plan',{exact:true}).waitFor(); - const owned=page.locator('[data-owned-plan]'); - if(await owned.count()!==1)throw new Error('Missing integrated owned version'); - const horizontal=await owned.evaluate(el=>{const next=el.nextElementSibling;return !!next && Math.abs(next.getBoundingClientRect().y-el.getBoundingClientRect().y)<5 && next.getBoundingClientRect().x>el.getBoundingClientRect().x;}); - if(!horizontal)throw new Error('Owned and latest are not adjacent horizontally'); - } - if(repo==='space'&&mode==='stripe'){ - if(await page.getByRole('dialog').count())throw new Error('Deep link bypassed renewal gate'); - await page.getByRole('button',{name:'Manage subscription'}).first().click(); - await page.getByText('Server refresh confirmed cancellation').waitFor(); - if (!(await page.getByRole('dialog').count())) await page.getByRole('button',{name:/Upgrade to|Subscribe to|Choose/}).last().click(); - await page.getByRole('dialog').waitFor(); - } - if(repo==='space'&&mode==='cancel-failure'){ - await page.getByRole('button',{name:'Manage subscription'}).first().click(); - await page.getByText('Cancellation failed').waitFor(); - if(await page.getByRole('dialog').count())throw new Error('Failed cancellation opened checkout'); - } - if(mode==='member'&&repo==='core'&&await page.getByRole('link').count())throw new Error('Member purchase link'); - if(mode==='changed'||mode==='equal'||mode==='member'||mode==='paused')await page.screenshot({path:path.join(evidence,`${repo}-${mode}.png`),fullPage:true}); - results.push(`${repo}/${mode}: PASS`);console.log(results.at(-1));await page.close(); + for (const [repo] of [ + ['space', 4185], + ['core', 4186], + ]) { + const server = await createServer({ + configFile: path.join( + workdir, + repo, + 'web/tests/revisions/vite.config.mjs', + ), + }); + await server.listen(); + servers.push(server); + } + browser = await chromium.launch({ headless: true }); + for (const repo of ['space', 'core']) { + const modes = + repo === 'space' + ? [ + 'changed', + 'same', + 'equal', + 'paused', + 'removed', + 'member', + 'stripe', + 'cancel-failure', + ] + : [ + 'changed', + 'same', + 'equal', + 'paused', + 'removed', + 'member', + 'unknown', + 'error', + ]; + for (const mode of modes) { + const page = await browser.newPage({ + viewport: { width: 1360, height: 1100 }, + }); + page.setDefaultTimeout(120000); + const errors = []; + page.on('pageerror', (error) => { + errors.push(error.message); + console.error(error.message); + }); + await page.route('**/*', async (route) => { + const url = new URL(route.request().url()); + if (url.hostname === '127.0.0.1') return route.continue(); + if (url.pathname === '/api/v1/cloud/v2/plans') + return route.fulfill({ + status: mode === 'error' ? 500 : 200, + contentType: 'application/json', + body: JSON.stringify({ + code: 0, + data: mode === 'removed' ? [] : [latest(mode)], + }), + }); + return route.abort(); + }); + const port = repo === 'space' ? 4185 : 4186; + await page.goto( + `http://127.0.0.1:${port}/tests/revisions/index.html?mode=${mode}${mode === 'stripe' ? '&intent=1' : ''}`, + { waitUntil: 'domcontentloaded', timeout: 120000 }, + ); + if (repo === 'core' && mode === 'error') + await page.getByRole('alert').waitFor(); + else if (mode === 'unknown') + await page + .getByText(/Current subscription details are unavailable/) + .waitFor(); + else + await page + .locator( + repo === 'space' + ? '[data-plan-comparison]' + : '[data-cloud-plans] article', + ) + .first() + .waitFor(); + if (errors.length) + throw new Error(`${repo}/${mode}: ${errors.join(';')}`); + if (await page.locator('[data-revision-comparison]').count()) + throw new Error('Standalone comparison remains'); + if ( + ['same', 'equal'].includes(mode) && + (await page.locator('[data-owned-plan]').count()) + ) + throw new Error('Identical benefits duplicated'); + const area = page.locator( + repo === 'space' ? '[data-plan-comparison]' : '[data-cloud-plans]', + ); + if (!['unknown', 'error'].includes(mode)) { + const copy = await area.innerText(); + if ( + /&#|revision|mcp stdio|workspace multi user|platform telegram/.test( + copy, + ) + ) + throw new Error('Internal metadata leaked into plan presentation'); + if ( + repo === 'core' && + (await area.locator('[data-changed], .bg-amber-500\\/10').count()) + ) + throw new Error('Diff highlighting remains'); + if ( + ['same', 'equal'].includes(mode) && + /Subscribed on|Latest quotas for this plan/.test(copy) + ) + throw new Error('Unnecessary comparison labels'); + } + if (mode === 'changed') { + await area.getByText(/Subscribed on/).waitFor(); + await area + .getByText('Latest quotas for this plan', { exact: true }) + .waitFor(); + const owned = page.locator('[data-owned-plan]'); + if ((await owned.count()) !== 1) + throw new Error('Missing integrated owned version'); + const horizontal = await owned.evaluate((el) => { + const next = el.nextElementSibling; + return ( + !!next && + Math.abs( + next.getBoundingClientRect().y - el.getBoundingClientRect().y, + ) < 5 && + next.getBoundingClientRect().x > el.getBoundingClientRect().x + ); + }); + if (!horizontal) + throw new Error('Owned and latest are not adjacent horizontally'); + } + if (repo === 'space' && mode === 'stripe') { + if (await page.getByRole('dialog').count()) + throw new Error('Deep link bypassed renewal gate'); + await page + .getByRole('button', { name: 'Manage subscription' }) + .first() + .click(); + await page.getByText('Server refresh confirmed cancellation').waitFor(); + if (!(await page.getByRole('dialog').count())) + await page + .getByRole('button', { name: /Upgrade to|Subscribe to|Choose/ }) + .last() + .click(); + await page.getByRole('dialog').waitFor(); + } + if (repo === 'space' && mode === 'cancel-failure') { + await page + .getByRole('button', { name: 'Manage subscription' }) + .first() + .click(); + await page.getByText('Cancellation failed').waitFor(); + if (await page.getByRole('dialog').count()) + throw new Error('Failed cancellation opened checkout'); + } + if ( + mode === 'member' && + repo === 'core' && + (await page.getByRole('link').count()) + ) + throw new Error('Member purchase link'); + if ( + mode === 'changed' || + mode === 'equal' || + mode === 'member' || + mode === 'paused' + ) + await page.screenshot({ + path: path.join(evidence, `${repo}-${mode}.png`), + fullPage: true, + }); + results.push(`${repo}/${mode}: PASS`); + console.log(results.at(-1)); + await page.close(); + } } - } } finally { - await writeFile(path.join(evidence,'browser-results.txt'),results.join('\n')+'\n'); - if(browser)await browser.close();for(const server of servers)await server.close(); + await writeFile( + path.join(evidence, 'browser-results.txt'), + results.join('\n') + '\n', + ); + if (browser) await browser.close(); + for (const server of servers) await server.close(); } diff --git a/web/tests/unit/plan-revisions.test.mjs b/web/tests/unit/plan-revisions.test.mjs index ebf985005..d5c57f805 100644 --- a/web/tests/unit/plan-revisions.test.mjs +++ b/web/tests/unit/plan-revisions.test.mjs @@ -3,25 +3,80 @@ import test from 'node:test'; import { readFileSync } from 'node:fs'; import ts from 'typescript'; const m = { exports: {} }; -new Function('module', 'exports', ts.transpileModule(readFileSync(new URL('../../src/app/home/components/workspace-settings/plan-revisions.ts', import.meta.url), 'utf8'), {compilerOptions: {module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022}}).outputText)(m, m.exports); +new Function( + 'module', + 'exports', + ts.transpileModule( + readFileSync( + new URL( + '../../src/app/home/components/workspace-settings/plan-revisions.ts', + import.meta.url, + ), + 'utf8', + ), + { + compilerOptions: { + module: ts.ModuleKind.CommonJS, + target: ts.ScriptTarget.ES2022, + }, + }, + ).outputText, +)(m, m.exports); const { revisionRows } = m.exports; -const current = { name: 'pro', revision: 1, limits: {'bots.max': 2, 'chat_history.retention_days': 30}, monthly_bonus_credits: 10, entitlement_features: {'platform.telegram': false, mcp_stdio: false} }; +const current = { + name: 'pro', + revision: 1, + limits: { 'bots.max': 2, 'chat_history.retention_days': 30 }, + monthly_bonus_credits: 10, + entitlement_features: { 'platform.telegram': false, mcp_stdio: false }, +}; test('same version and revision-only changes do not duplicate comparisons', () => { - assert.ok(revisionRows(current, current).every(row => !row.changed)); - assert.ok(revisionRows(current, {...current, revision: 2}).every(row => !row.changed)); + assert.ok(revisionRows(current, current).every((row) => !row.changed)); + assert.ok( + revisionRows(current, { ...current, revision: 2 }).every( + (row) => !row.changed, + ), + ); }); test('compares effective quotas, credits, retention, platforms and feature switches', () => { - const latest = {name:'pro', revision:2, limits:{'bots.max':5}, chat_history_retention_days:60, monthly_bonus_credits:20, entitlement_features:{mcp_stdio:true}, disabled_adapters:[]}; - const changed = revisionRows(current, latest).filter(row => row.changed); - assert.deepEqual(changed.map(row => row.key).sort(), ['feature:mcp_stdio','feature:platform.telegram','limit:bots.max','limit:chat_history.retention_days','monthly_credits'].sort()); - assert.equal(changed.find(row => row.key === 'feature:platform.telegram').after, true); + const latest = { + name: 'pro', + revision: 2, + limits: { 'bots.max': 5 }, + chat_history_retention_days: 60, + monthly_bonus_credits: 20, + entitlement_features: { mcp_stdio: true }, + disabled_adapters: [], + }; + const changed = revisionRows(current, latest).filter((row) => row.changed); + assert.deepEqual( + changed.map((row) => row.key).sort(), + [ + 'feature:mcp_stdio', + 'feature:platform.telegram', + 'limit:bots.max', + 'limit:chat_history.retention_days', + 'monthly_credits', + ].sort(), + ); + assert.equal( + changed.find((row) => row.key === 'feature:platform.telegram').after, + true, + ); }); test('catalog representations normalize to snapshot values', () => { - const latest = {name:'pro',revision:2,limits:{'bots.max':2},chat_history_retention_days:30,monthly_bonus_credits:10,disabled_adapters:['telegram']}; - assert.ok(revisionRows(current, latest).every(row => !row.changed)); + const latest = { + name: 'pro', + revision: 2, + limits: { 'bots.max': 2 }, + chat_history_retention_days: 30, + monthly_bonus_credits: 10, + disabled_adapters: ['telegram'], + }; + assert.ok(revisionRows(current, latest).every((row) => !row.changed)); }); test('missing latest keeps current values without invented changes', () => { - const rows=revisionRows(current); - assert.equal(rows.find(row=>row.key==='limit:bots.max').before,2); - assert.ok(rows.every(row=>!row.changed)); + const rows = revisionRows(current); + assert.equal(rows.find((row) => row.key === 'limit:bots.max').before, 2); + assert.ok(rows.every((row) => !row.changed)); }); diff --git a/web/tests/unit/workspace-switcher-layout.test.mjs b/web/tests/unit/workspace-switcher-layout.test.mjs index 7b7e241f5..09e63aefa 100644 --- a/web/tests/unit/workspace-switcher-layout.test.mjs +++ b/web/tests/unit/workspace-switcher-layout.test.mjs @@ -90,9 +90,8 @@ test('keeps workspace plan and settings controls on the workspace row', () => { }); test('moves Cloud plan upgrades into Workspace Settings', () => { - assert.match(workspaceSettingsPanelSource, /workspace\.upgradePlan/); - assert.match(workspaceSettingsPanelSource, /cloudPortalURL/); - assert.match(workspaceSettingsPanelSource, /step=plan/); + assert.match(workspaceSettingsPanelSource, /cloudPlans\.title/); + assert.match(workspaceSettingsPanelSource, /