mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-22 08:25:21 +08:00
Merge pull request #2700 from langbot-app/test/quality-workspace-ui-oracles-20261017
test(web): replace Workspace source checks with runtime assertions
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { installLangBotApiMocks } from './fixtures/langbot-api';
|
||||
|
||||
for (const cleanup of ['close', 'expiry'] as const) {
|
||||
test(`QR start, polling and ${cleanup} cleanup retain the session Workspace`, async ({
|
||||
page,
|
||||
}) => {
|
||||
await installLangBotApiMocks(page, { authenticated: true });
|
||||
const calls: Array<{
|
||||
method: string;
|
||||
origin: string;
|
||||
path: string;
|
||||
workspace?: string;
|
||||
authorization?: string;
|
||||
}> = [];
|
||||
const endpoint = '/api/v1/platform/adapters/weixin/login';
|
||||
await page.route(`**${endpoint}**`, async (route) => {
|
||||
const request = route.request();
|
||||
const path = new URL(request.url()).pathname;
|
||||
calls.push({
|
||||
method: request.method(),
|
||||
origin: new URL(request.url()).origin,
|
||||
path,
|
||||
workspace: request.headers()['x-workspace-id'],
|
||||
authorization: request.headers().authorization,
|
||||
});
|
||||
const data =
|
||||
request.method() === 'POST'
|
||||
? {
|
||||
session_id: 'qr-session',
|
||||
qr_url: 'https://scan.example/qr',
|
||||
expire_at: Date.now() / 1000 + 8,
|
||||
}
|
||||
: { status: 'waiting' };
|
||||
await route.fulfill({ json: { code: 0, data } });
|
||||
});
|
||||
await page.goto('/home/bots');
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Models', exact: true }),
|
||||
).toBeVisible();
|
||||
const modules = await page.evaluate(() => {
|
||||
const resources = performance
|
||||
.getEntriesByType('resource')
|
||||
.map((entry) => entry.name);
|
||||
return {
|
||||
reactPath: resources.find((url) =>
|
||||
new URL(url).pathname.endsWith('/react.js'),
|
||||
)!,
|
||||
domPath: resources.find((url) =>
|
||||
new URL(url).pathname.endsWith('/react-dom_client.js'),
|
||||
)!,
|
||||
};
|
||||
});
|
||||
await page.clock.install();
|
||||
await page.evaluate(async ({ reactPath, domPath }) => {
|
||||
const reactModule = await import(reactPath);
|
||||
const React = reactModule.default || reactModule;
|
||||
const domModule = await import(domPath);
|
||||
const { createRoot } = domModule.default || domModule;
|
||||
const componentPath =
|
||||
'/src/app/home/components/qrcode-login/QrCodeLoginDialog.tsx';
|
||||
const { default: Dialog } = await import(componentPath);
|
||||
const contextPath = '/src/app/infra/http/workspaceContext.ts';
|
||||
(await import(contextPath)).setActiveWorkspaceUuid('qr-original');
|
||||
const host = document.createElement('div');
|
||||
document.body.appendChild(host);
|
||||
function Harness() {
|
||||
const [open, setOpen] = React.useState(true);
|
||||
return React.createElement(Dialog, {
|
||||
open,
|
||||
onOpenChange: setOpen,
|
||||
platform: 'weixin',
|
||||
onSuccess: () => {},
|
||||
});
|
||||
}
|
||||
createRoot(host).render(React.createElement(Harness));
|
||||
}, modules);
|
||||
await expect(page.getByRole('dialog').locator('img')).toBeVisible();
|
||||
await page.evaluate(async () => {
|
||||
const path = '/src/app/infra/http/workspaceContext.ts';
|
||||
(await import(path)).setActiveWorkspaceUuid('qr-other');
|
||||
});
|
||||
await page.clock.runFor(3100);
|
||||
await expect
|
||||
.poll(() => calls.some((call) => call.method === 'GET'))
|
||||
.toBe(true);
|
||||
if (cleanup === 'close') await page.keyboard.press('Escape');
|
||||
else await page.clock.runFor(6000);
|
||||
await expect
|
||||
.poll(() => calls.filter((call) => call.method === 'DELETE').length)
|
||||
.toBe(1);
|
||||
expect(calls).toEqual([
|
||||
{
|
||||
method: 'POST',
|
||||
origin: new URL(page.url()).origin,
|
||||
path: endpoint,
|
||||
workspace: 'qr-original',
|
||||
authorization: 'Bearer playwright-token',
|
||||
},
|
||||
...calls
|
||||
.filter((call) => call.method === 'GET')
|
||||
.map(() => ({
|
||||
method: 'GET',
|
||||
origin: new URL(page.url()).origin,
|
||||
path: `${endpoint}/status/qr-session`,
|
||||
workspace: 'qr-original',
|
||||
authorization: 'Bearer playwright-token',
|
||||
})),
|
||||
{
|
||||
method: 'DELETE',
|
||||
origin: new URL(page.url()).origin,
|
||||
path: `${endpoint}/qr-session`,
|
||||
workspace: 'qr-original',
|
||||
authorization: 'Bearer playwright-token',
|
||||
},
|
||||
]);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { installLangBotApiMocks } from './fixtures/langbot-api';
|
||||
|
||||
for (const fragment of [true, false]) {
|
||||
test(`direct launch assertion is fragment-only and cleared before exchange, fragment=${fragment}`, async ({
|
||||
page,
|
||||
}) => {
|
||||
await installLangBotApiMocks(page);
|
||||
await page.addInitScript(() => {
|
||||
const replace = history.replaceState.bind(history);
|
||||
history.replaceState = (...args) => {
|
||||
sessionStorage.setItem('last-replaced-url', String(args[2]));
|
||||
replace(...args);
|
||||
};
|
||||
});
|
||||
let body: unknown;
|
||||
let fragmentAtExchange: string | null = null;
|
||||
await page.route('**/api/v1/user/space/callback', async (route) => {
|
||||
body = route.request().postDataJSON();
|
||||
fragmentAtExchange = await page.evaluate(() => location.hash);
|
||||
await route.fulfill({
|
||||
status: 403,
|
||||
json: { code: 'launch_denied', msg: 'Denied for test' },
|
||||
});
|
||||
});
|
||||
const query = '/auth/space/callback?code=launch-code&state=launch-state';
|
||||
await page.goto(
|
||||
fragment
|
||||
? `${query}#workspace_uuid=target-workspace&launch_assertion=fragment-secret`
|
||||
: `${query}&workspace_uuid=target-workspace&launch_assertion=query-must-not-be-used`,
|
||||
);
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Back to Login', exact: true }),
|
||||
).toBeVisible();
|
||||
if (fragment) {
|
||||
expect(body).toEqual({
|
||||
code: 'launch-code',
|
||||
state: 'launch-state',
|
||||
redirect_uri: new URL('/auth/space/callback', page.url()).href,
|
||||
workspace_uuid: 'target-workspace',
|
||||
launch_assertion: 'fragment-secret',
|
||||
});
|
||||
expect(fragmentAtExchange).toBe('');
|
||||
} else {
|
||||
expect(body).toBeUndefined();
|
||||
expect(fragmentAtExchange).toBeNull();
|
||||
}
|
||||
if (fragment)
|
||||
expect(
|
||||
await page.evaluate(() => sessionStorage.getItem('last-replaced-url')),
|
||||
).toBe(query);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import {
|
||||
installLangBotApiMocks,
|
||||
makeWorkspaceEntry,
|
||||
} from './fixtures/langbot-api';
|
||||
|
||||
const cloudSystemInfo = {
|
||||
edition: 'cloud',
|
||||
deployment_mode: 'cloud',
|
||||
cloud_service_url: 'https://space.langbot.app',
|
||||
wizard_status: 'completed',
|
||||
};
|
||||
|
||||
for (const scenario of [
|
||||
'singleton-local',
|
||||
'singleton-cloud',
|
||||
'multiple',
|
||||
] as const) {
|
||||
test(`Workspace switcher visibility and placement for ${scenario}`, async ({
|
||||
page,
|
||||
}) => {
|
||||
const first = makeWorkspaceEntry(
|
||||
'layout-workspace',
|
||||
'A Workspace with a deliberately very long name to exercise truncation',
|
||||
scenario === 'singleton-local' ? 'local' : 'cloud_projection',
|
||||
);
|
||||
const workspaces =
|
||||
scenario === 'multiple'
|
||||
? [first, makeWorkspaceEntry('other-workspace', 'Other', 'local')]
|
||||
: [first];
|
||||
await installLangBotApiMocks(page, {
|
||||
authenticated: true,
|
||||
workspaces,
|
||||
storage: { langbot_active_workspace_uuid: first.workspace.uuid },
|
||||
});
|
||||
if (scenario !== 'singleton-local') {
|
||||
await page.route('**/api/v1/system/info', (route) =>
|
||||
route.fulfill({ json: { code: 0, data: cloudSystemInfo } }),
|
||||
);
|
||||
await page.route(
|
||||
/\/api\/v1\/workspaces\/layout-workspace\/(members|invitations)$/,
|
||||
(route) =>
|
||||
route.fulfill({
|
||||
json: { code: 0, data: { members: [], invitations: [] } },
|
||||
}),
|
||||
);
|
||||
}
|
||||
await page.goto('/home/bots');
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Models', exact: true }),
|
||||
).toBeVisible();
|
||||
const trigger = page.getByRole('button', {
|
||||
name: 'Switch Workspace',
|
||||
exact: true,
|
||||
});
|
||||
await expect(trigger).toHaveCount(scenario === 'singleton-local' ? 0 : 1);
|
||||
if (scenario === 'singleton-local') return;
|
||||
const sidebar = page.locator('[data-slot="sidebar-inner"]');
|
||||
const header = sidebar.locator('[data-slot="sidebar-header"]');
|
||||
const home = sidebar.getByText('Home', { exact: true });
|
||||
const navigation = sidebar.getByRole('button', {
|
||||
name: 'Models',
|
||||
exact: true,
|
||||
});
|
||||
const [headerBox, triggerBox, homeBox, navBox] = await Promise.all([
|
||||
header.boundingBox(),
|
||||
trigger.boundingBox(),
|
||||
home.boundingBox(),
|
||||
navigation.boundingBox(),
|
||||
]);
|
||||
expect(triggerBox!.y).toBeGreaterThanOrEqual(
|
||||
headerBox!.y + headerBox!.height,
|
||||
);
|
||||
expect(triggerBox!.y + triggerBox!.height).toBeLessThanOrEqual(homeBox!.y);
|
||||
expect(Math.abs(triggerBox!.x - navBox!.x)).toBeLessThanOrEqual(1);
|
||||
expect(
|
||||
Math.abs(triggerBox!.x + triggerBox!.width - navBox!.x - navBox!.width),
|
||||
).toBeLessThanOrEqual(1);
|
||||
await trigger.click();
|
||||
const row = page
|
||||
.getByRole('menuitem')
|
||||
.filter({ hasText: first.workspace.name });
|
||||
await expect(row).toBeVisible();
|
||||
await expect(
|
||||
row.getByRole('button', { name: 'Workspace Settings', exact: true }),
|
||||
).toBeVisible();
|
||||
const longName = row.getByText(first.workspace.name, { exact: true });
|
||||
expect(
|
||||
await longName.evaluate((element) => ({
|
||||
overflow: getComputedStyle(element).textOverflow,
|
||||
whiteSpace: getComputedStyle(element).whiteSpace,
|
||||
clipped: element.scrollWidth > element.clientWidth,
|
||||
})),
|
||||
).toEqual({ overflow: 'ellipsis', whiteSpace: 'nowrap', clipped: true });
|
||||
await row
|
||||
.getByRole('button', { name: 'Workspace Settings', exact: true })
|
||||
.click();
|
||||
await expect(
|
||||
page
|
||||
.getByRole('dialog')
|
||||
.getByRole('heading', { name: 'Workspace', exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page
|
||||
.getByRole('dialog')
|
||||
.getByRole('heading', { name: 'Members', exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('button', {
|
||||
name: 'Switch Workspace',
|
||||
exact: true,
|
||||
includeHidden: true,
|
||||
}),
|
||||
).toHaveCount(1);
|
||||
});
|
||||
}
|
||||
|
||||
test('the Cloud Workspace row shows its plan and opens plan upgrades in settings', async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspace = {
|
||||
...makeWorkspaceEntry(
|
||||
'plan-workspace',
|
||||
'Plan Workspace',
|
||||
'cloud_projection',
|
||||
),
|
||||
plan_name: 'Pro',
|
||||
};
|
||||
await installLangBotApiMocks(page, {
|
||||
authenticated: true,
|
||||
workspaces: [workspace],
|
||||
});
|
||||
await page.route('**/api/v1/system/info', (route) =>
|
||||
route.fulfill({ json: { code: 0, data: cloudSystemInfo } }),
|
||||
);
|
||||
await page.route('**/api/v1/workspaces/plan-workspace/members', (route) =>
|
||||
route.fulfill({ json: { code: 0, data: { members: [] } } }),
|
||||
);
|
||||
await page.route('**/api/v1/workspaces/plan-workspace/invitations', (route) =>
|
||||
route.fulfill({ json: { code: 0, data: { invitations: [] } } }),
|
||||
);
|
||||
let catalogs = 0;
|
||||
await page.route(
|
||||
'https://space.langbot.app/api/v1/cloud/v2/plans',
|
||||
async (route) => {
|
||||
catalogs++;
|
||||
await route.fulfill({ json: { code: 0, data: [] } });
|
||||
},
|
||||
);
|
||||
await page.goto('/home/bots');
|
||||
await page
|
||||
.getByRole('button', { name: 'Switch Workspace', exact: true })
|
||||
.click();
|
||||
const row = page.getByRole('menuitem').filter({ hasText: 'Plan Workspace' });
|
||||
await expect(row.getByText('Pro', { exact: true })).toBeVisible();
|
||||
await row
|
||||
.getByRole('button', { name: 'Workspace Settings', exact: true })
|
||||
.click();
|
||||
await expect(page.getByRole('dialog')).toBeVisible();
|
||||
await page
|
||||
.getByRole('button', { name: 'Compare plans', exact: true })
|
||||
.click();
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Compare plans', exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole('dialog', { includeHidden: true })).toHaveCount(
|
||||
2,
|
||||
);
|
||||
await expect.poll(() => catalogs).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('monitoring uses infrastructure Box health independently of Plugin Runtime status', async ({
|
||||
page,
|
||||
}) => {
|
||||
await installLangBotApiMocks(page, { authenticated: true });
|
||||
let runtimeRequests = 0;
|
||||
let scopedStatusRequests = 0;
|
||||
await page.route('**/api/v1/box/runtime-status', async (route) => {
|
||||
runtimeRequests++;
|
||||
await route.fulfill({
|
||||
json: {
|
||||
code: 0,
|
||||
data: {
|
||||
available: false,
|
||||
enabled: true,
|
||||
connector_error: 'Box infrastructure unavailable',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
await page.route('**/api/v1/box/status', async (route) => {
|
||||
scopedStatusRequests++;
|
||||
await route.fulfill({
|
||||
json: { code: 0, data: { available: true, enabled: true } },
|
||||
});
|
||||
});
|
||||
await page.goto('/home/monitoring');
|
||||
const card = page
|
||||
.locator('[data-slot="card"]')
|
||||
.filter({ hasText: 'System Status' });
|
||||
await expect(card.getByText('Box Runtime', { exact: true })).toBeVisible();
|
||||
await card.getByRole('button').click();
|
||||
const dialog = page.getByRole('dialog');
|
||||
await expect(
|
||||
dialog.getByText('Box infrastructure unavailable', { exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(dialog.getByText('Connected', { exact: true })).toHaveCount(1);
|
||||
await expect(dialog.getByText('Disconnected', { exact: true })).toHaveCount(
|
||||
1,
|
||||
);
|
||||
expect(runtimeRequests).toBeGreaterThan(0);
|
||||
expect(scopedStatusRequests).toBe(0);
|
||||
});
|
||||
@@ -73,6 +73,10 @@ test('workspace member list displays each member email without overlapping contr
|
||||
await expect(page.getByText('RockChinQ')).toBeVisible();
|
||||
await expect(page.getByText('[email protected]')).toBeVisible();
|
||||
await expect(page.getByText('Junyan Qin')).toBeVisible();
|
||||
const ownerRow = page
|
||||
.locator('[data-slot="item"]')
|
||||
.filter({ hasText: 'RockChinQ' });
|
||||
await expect(ownerRow.getByText('Owner', { exact: true })).toBeVisible();
|
||||
const longEmail = page.getByText(
|
||||
'[email protected]',
|
||||
);
|
||||
@@ -80,6 +84,7 @@ test('workspace member list displays each member email without overlapping contr
|
||||
await longEmail.scrollIntoViewIfNeeded();
|
||||
|
||||
const roleSelect = page.getByRole('combobox').last();
|
||||
await expect(roleSelect).toHaveText('Admin');
|
||||
const [emailBox, selectBox] = await Promise.all([
|
||||
longEmail.boundingBox(),
|
||||
roleSelect.boundingBox(),
|
||||
|
||||
@@ -2,6 +2,7 @@ import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
import ts from 'typescript';
|
||||
|
||||
const root = process.cwd();
|
||||
const dialogPath = path.join(
|
||||
@@ -12,43 +13,30 @@ const localeDir = path.join(root, 'src/i18n/locales');
|
||||
const backendUrlPath = path.join(root, 'src/app/infra/http/backendUrl.ts');
|
||||
|
||||
const dialogSource = fs.readFileSync(dialogPath, 'utf8');
|
||||
const backendUrlSource = fs.readFileSync(backendUrlPath, 'utf8');
|
||||
const { resolveBackendBaseUrl } = await import(
|
||||
`data:text/javascript;base64,${Buffer.from(
|
||||
ts.transpileModule(fs.readFileSync(backendUrlPath, 'utf8'), {
|
||||
compilerOptions: {
|
||||
module: ts.ModuleKind.ESNext,
|
||||
target: ts.ScriptTarget.ES2022,
|
||||
},
|
||||
}).outputText,
|
||||
).toString('base64')}`
|
||||
);
|
||||
|
||||
test('QR login resolves a proxy-root API base against the current origin', () => {
|
||||
assert.match(dialogSource, /const baseUrl = getBackendBaseUrl\(\)/);
|
||||
assert.match(backendUrlSource, /!configured \|\| configured === '\/'/);
|
||||
assert.match(backendUrlSource, /return normalizedOrigin/);
|
||||
assert.doesNotMatch(
|
||||
dialogSource,
|
||||
/import\.meta\.env\.VITE_API_BASE_URL \|\| window\.location\.origin/,
|
||||
);
|
||||
});
|
||||
|
||||
test('QR credential exchanges preserve the active Workspace scope', () => {
|
||||
assert.match(dialogSource, /getActiveWorkspaceUuid/);
|
||||
assert.match(
|
||||
dialogSource,
|
||||
/sessionWorkspaceUuidRef\.current = workspaceUuid/,
|
||||
);
|
||||
assert.match(
|
||||
dialogSource,
|
||||
/const workspaceUuid = sessionWorkspaceUuidRef\.current/,
|
||||
);
|
||||
assert.match(dialogSource, /sessionApiBaseRef\.current = cfg\.apiBase/);
|
||||
assert.match(
|
||||
dialogSource,
|
||||
/`\$\{baseUrlRef\.current\}\$\{sessionApiBaseRef\.current\}\/\$\{sessionIdRef\.current\}`/,
|
||||
);
|
||||
assert.match(dialogSource, /'X-Workspace-Id': workspaceUuid/);
|
||||
|
||||
const workspaceHeaderUses = dialogSource.match(
|
||||
/'X-Workspace-Id': workspaceUuid/g,
|
||||
);
|
||||
assert.equal(
|
||||
workspaceHeaderUses?.length,
|
||||
4,
|
||||
'start, poll, expiry cleanup, and dialog cleanup must all retain Workspace scope',
|
||||
);
|
||||
for (const [configured, expected] of [
|
||||
[undefined, 'https://core.example'],
|
||||
['', 'https://core.example'],
|
||||
['/', 'https://core.example'],
|
||||
['/proxy/', 'https://core.example/proxy'],
|
||||
['proxy', 'https://core.example/proxy'],
|
||||
['https://api.example/root/', 'https://api.example/root'],
|
||||
])
|
||||
assert.equal(
|
||||
resolveBackendBaseUrl(configured, 'https://core.example///'),
|
||||
expected,
|
||||
);
|
||||
});
|
||||
|
||||
test('WeChat QR login never reuses Feishu progress copy', () => {
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import test from 'node:test';
|
||||
|
||||
const source = fs.readFileSync(
|
||||
new URL('../../src/app/auth/space/callback/page.tsx', import.meta.url),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
test('direct launch assertion is fragment-only and removed before exchange', () => {
|
||||
assert.doesNotMatch(source, /searchParams\.get\(['"]launch_assertion['"]\)/);
|
||||
const readIndex = source.indexOf("fragmentParams.get('launch_assertion')");
|
||||
const clearIndex = source.indexOf('window.history.replaceState');
|
||||
const exchangeIndex = source.indexOf('handleOAuthCallback(', clearIndex);
|
||||
assert.ok(readIndex >= 0, 'fragment assertion read is missing');
|
||||
assert.ok(
|
||||
clearIndex > readIndex,
|
||||
'URL fragment is not cleared after copying the assertion',
|
||||
);
|
||||
assert.ok(
|
||||
exchangeIndex > clearIndex,
|
||||
'assertion exchange starts before the fragment is cleared',
|
||||
);
|
||||
});
|
||||
@@ -1,24 +0,0 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import test from 'node:test';
|
||||
|
||||
const panelSource = await readFile(
|
||||
new URL(
|
||||
'../../src/app/home/components/workspace-settings/WorkspaceSettingsPanel.tsx',
|
||||
import.meta.url,
|
||||
),
|
||||
'utf8',
|
||||
);
|
||||
const entitySource = await readFile(
|
||||
new URL('../../src/app/infra/entities/workspace.ts', import.meta.url),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
test('workspace member rows show display name, email, and role', () => {
|
||||
assert.match(entitySource, /display_name:\s*string/);
|
||||
assert.match(panelSource, /\{member\.display_name\}/);
|
||||
assert.match(
|
||||
panelSource,
|
||||
/<ItemDescription[^>]*>[\s\S]*?\{member\.email\}[\s\S]*?workspace\.roles\.\$\{member\.role\}[\s\S]*?<\/ItemDescription>/,
|
||||
);
|
||||
});
|
||||
@@ -18,103 +18,15 @@ const homeSidebarSource = readSource(
|
||||
const workspaceSettingsPanelSource = readSource(
|
||||
'src/app/home/components/workspace-settings/WorkspaceSettingsPanel.tsx',
|
||||
);
|
||||
const workspaceSwitcherSource = readSource(
|
||||
'src/app/home/components/workspace-settings/WorkspaceSwitcher.tsx',
|
||||
);
|
||||
|
||||
test('renders WorkspaceSwitcher only from HomeSidebar', () => {
|
||||
assert.doesNotMatch(homeLayoutSource, /<WorkspaceSwitcher\b/);
|
||||
assert.doesNotMatch(workspaceSettingsPanelSource, /<WorkspaceSwitcher\b/);
|
||||
assert.equal(homeSidebarSource.match(/<WorkspaceSwitcher\b/g)?.length, 1);
|
||||
});
|
||||
|
||||
test('places WorkspaceSwitcher between the sidebar header and Home navigation', () => {
|
||||
const headerEnd = homeSidebarSource.indexOf('</SidebarHeader>');
|
||||
const switcher = homeSidebarSource.indexOf('<WorkspaceSwitcher');
|
||||
const contentStart = homeSidebarSource.indexOf('<SidebarContent');
|
||||
const homeGroup = homeSidebarSource.indexOf(
|
||||
"<SidebarGroupLabel>{t('sidebar.home')}</SidebarGroupLabel>",
|
||||
);
|
||||
|
||||
assert.notEqual(headerEnd, -1);
|
||||
assert.notEqual(switcher, -1);
|
||||
assert.notEqual(contentStart, -1);
|
||||
assert.notEqual(homeGroup, -1);
|
||||
assert.ok(
|
||||
headerEnd < switcher,
|
||||
'WorkspaceSwitcher must follow SidebarHeader',
|
||||
);
|
||||
assert.ok(
|
||||
switcher < contentStart && switcher < homeGroup,
|
||||
'WorkspaceSwitcher must precede SidebarContent and the Home group',
|
||||
);
|
||||
});
|
||||
|
||||
test('hides WorkspaceSwitcher for the singleton local OSS workspace and keeps it for Cloud or multiple workspaces', () => {
|
||||
assert.match(
|
||||
workspaceSwitcherSource,
|
||||
/if \(!currentWorkspace\) return null;/,
|
||||
);
|
||||
assert.match(
|
||||
homeSidebarSource,
|
||||
/const workspaces = useWorkspaceBootstrap\(\);/,
|
||||
);
|
||||
assert.match(
|
||||
homeSidebarSource,
|
||||
/const showWorkspaceSwitcher\s*=\s*workspaces\.length\s*>\s*1\s*\|\|\s*currentWorkspace\?\.workspace\.source\s*===\s*'cloud_projection'/,
|
||||
);
|
||||
assert.match(
|
||||
homeSidebarSource,
|
||||
/\{showWorkspaceSwitcher\s*&&\s*\(\s*<div className="px-2[^>]*>[\s\S]*?<WorkspaceSwitcher/,
|
||||
);
|
||||
});
|
||||
|
||||
test('keeps Cloud workspace member management in Workspace Settings', () => {
|
||||
assert.match(workspaceSettingsPanelSource, /workspace\.inviteMember/);
|
||||
assert.doesNotMatch(workspaceSettingsPanelSource, /#workspace-members/);
|
||||
assert.doesNotMatch(workspaceSettingsPanelSource, /cloudMembersURL/);
|
||||
assert.doesNotMatch(workspaceSettingsPanelSource, /canManageCloudMembers/);
|
||||
});
|
||||
|
||||
test('keeps workspace plan and settings controls on the workspace row', () => {
|
||||
assert.match(workspaceSwitcherSource, /entry\.plan_name/);
|
||||
assert.match(
|
||||
workspaceSwitcherSource,
|
||||
/aria-label=\{t\('workspace\.settings'\)\}/,
|
||||
);
|
||||
assert.match(workspaceSwitcherSource, /className="size-8"/);
|
||||
assert.doesNotMatch(workspaceSwitcherSource, /workspace\.currentPlan/);
|
||||
assert.doesNotMatch(workspaceSwitcherSource, /workspace\.upgradePlan/);
|
||||
assert.doesNotMatch(workspaceSwitcherSource, /workspace\.roles/);
|
||||
assert.doesNotMatch(workspaceSwitcherSource, /entry\.membership\.role/);
|
||||
});
|
||||
|
||||
test('moves Cloud plan upgrades into Workspace Settings', () => {
|
||||
assert.match(workspaceSettingsPanelSource, /cloudPlans\.title/);
|
||||
assert.match(workspaceSettingsPanelSource, /<CloudPlansDialog/);
|
||||
assert.match(workspaceSettingsPanelSource, /systemInfo\.cloud_service_url/);
|
||||
});
|
||||
|
||||
test('aligns the workspace trigger with navigation entries on both sides and truncates long names', () => {
|
||||
assert.match(
|
||||
homeSidebarSource,
|
||||
/<div className="px-2[^>]*>[\s\S]*<WorkspaceSwitcher className="w-full/,
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
homeSidebarSource,
|
||||
/WorkspaceSwitcher className="[^"]*w-4\/5/,
|
||||
);
|
||||
assert.match(workspaceSwitcherSource, /h-9/);
|
||||
assert.match(workspaceSwitcherSource, /w-64/);
|
||||
assert.doesNotMatch(workspaceSwitcherSource, /min-w-80/);
|
||||
assert.match(workspaceSwitcherSource, /max-w-\[7rem\][^>]*truncate/);
|
||||
});
|
||||
|
||||
test('uses an infrastructure-level Box health endpoint in monitoring', () => {
|
||||
const statusCardSource = readSource(
|
||||
'src/app/home/monitoring/components/overview-cards/SystemStatusCards.tsx',
|
||||
);
|
||||
const backendClientSource = readSource('src/app/infra/http/BackendClient.ts');
|
||||
assert.match(backendClientSource, /getBoxRuntimeStatus/);
|
||||
assert.match(statusCardSource, /getBoxRuntimeStatus/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user