mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-22 08:25:21 +08:00
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 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
653f9e731d
commit
624d8efd97
@@ -94,7 +94,8 @@ const jaJP = {
|
||||
annualNote: '月額 {{price}} 相当',
|
||||
ownerOnly: '所有者または管理者のみ',
|
||||
choose: '支払いに進む',
|
||||
handoff: 'Space でログインしてお支払いください。選択内容は保持されます。',
|
||||
handoff:
|
||||
'LangBot アカウントでログインしてお支払いください。選択内容は保持されます。',
|
||||
quotas: 'ワークスペースの上限',
|
||||
unlimited: '無制限',
|
||||
retention: 'メッセージ履歴 · {{count}} 日間',
|
||||
|
||||
+220
-67
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
|
||||
@@ -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, /<CloudPlansDialog/);
|
||||
assert.match(workspaceSettingsPanelSource, /systemInfo\.cloud_service_url/);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user