test(web): replace account source checks with browser assertions

This commit is contained in:
huanghuoguoguo
2026-10-17 16:31:51 +08:00
parent c05aac538f
commit 61758c8727
9 changed files with 504 additions and 227 deletions
+103
View File
@@ -0,0 +1,103 @@
import { expect, test } from '@playwright/test';
import { installLangBotApiMocks } from './fixtures/langbot-api';
for (const invitation of [false, true]) {
test(`Cloud login starts standard OAuth, invitation=${invitation}`, async ({
page,
}) => {
await installLangBotApiMocks(page);
await page.route('**/api/v1/system/info', (route) =>
route.fulfill({
json: {
code: 0,
data: {
edition: 'cloud',
deployment_mode: 'cloud',
cloud_service_url: 'https://space.langbot.app',
wizard_status: 'completed',
},
},
}),
);
await page.route('**/api/v1/user/account-info', (route) =>
route.fulfill({
json: {
code: 0,
data: {
initialized: true,
password_login_enabled: false,
space_login_enabled: true,
authenticated_invitation_acceptance_enabled: true,
},
},
}),
);
if (invitation)
await page.addInitScript(() =>
sessionStorage.setItem(
'langbot_pending_invitation_token',
'invited-token',
),
);
let redirectUri: string | null = null;
await page.route('**/api/v1/user/space/authorize-url**', async (route) => {
redirectUri = new URL(route.request().url()).searchParams.get(
'redirect_uri',
);
await route.fulfill({
json: { code: 0, data: { authorize_url: '/oauth-provider' } },
});
});
await page.route('**/oauth-provider', (route) =>
route.fulfill({
contentType: 'text/html',
body: '<h1>OAuth provider</h1>',
}),
);
await page.goto(invitation ? '/login?invitation=1' : '/login');
await page
.getByRole('button', { name: 'Login with LangBot Account', exact: true })
.click();
await expect(page).toHaveURL(/\/oauth-provider$/);
expect(redirectUri).toBe(new URL('/auth/space/callback', page.url()).href);
expect(
await page.evaluate(() =>
sessionStorage.getItem('langbot_pending_invitation_token'),
),
).toBe(invitation ? 'invited-token' : null);
});
}
for (const [code, title, message] of [
[
'space_account_not_registered',
'Account not registered',
'No local account is registered for this LangBot Account email. Ask the Workspace owner for an invitation.',
],
[
'space_account_binding_required',
'LangBot Account connection required',
'This local account must connect a LangBot Account from Account settings before using LangBot Account login.',
],
]) {
test(`OAuth callback explains ${code}`, async ({ page }) => {
await installLangBotApiMocks(page);
await page.route('**/api/v1/user/space/callback', (route) =>
route.fulfill({
status: 403,
json: { code, msg: 'Generic upstream message' },
}),
);
await page.goto('/auth/space/callback?code=code&state=state');
await expect(page.getByText(title, { exact: true })).toBeVisible();
await expect(page.getByText(message, { exact: true })).toBeVisible();
await expect(
page.getByText('Generic upstream message', { exact: true }),
).toHaveCount(0);
await page
.getByRole('button', { name: 'Back to Login', exact: true })
.click();
await expect(page).toHaveURL(/\/login$/);
expect(await page.evaluate(() => localStorage.getItem('token'))).toBeNull();
});
}
+133
View File
@@ -0,0 +1,133 @@
import { expect, test } from '@playwright/test';
import { installLangBotApiMocks } from './fixtures/langbot-api';
for (const width of [1440, 390]) {
test(`bot setup and detail columns stay separated at ${width}px`, async ({
page,
}) => {
await page.setViewportSize({ width, height: 1000 });
await installLangBotApiMocks(page, { authenticated: true });
await page.goto('/home/bots?id=new');
const layout = page.locator('#bot-form > fieldset');
const basic = layout.locator('[data-guide="bot-basic"]');
const adapter = page.getByTestId('adapter-gallery');
await expect(basic).toBeVisible();
await expect(adapter).toBeVisible();
const basicBox = (await basic.boundingBox())!;
const adapterBox = (await adapter.boundingBox())!;
if (width >= 1024)
expect(adapterBox.x).toBeGreaterThan(basicBox.x + basicBox.width);
else expect(adapterBox.y).toBeGreaterThan(basicBox.y + basicBox.height);
await adapter.getByRole('button', { name: /Playwright Adapter/ }).click();
await page.locator('input[name="name"]').fill('Layout bot');
await page.getByRole('button', { name: /^Submit$/ }).click();
await expect(page).toHaveURL(/\/home\/bots\?id=bot-1$/);
const routing = page.locator('[data-guide="bot-routing"]');
await expect(routing).toBeVisible();
const cards = await layout.locator(':scope > [data-slot="card"]').all();
expect(cards).toHaveLength(2);
const left = (await cards[0].boundingBox())!;
const right = (await cards[1].boundingBox())!;
if (width >= 1024) expect(right.x).toBeGreaterThan(left.x + left.width);
else expect(right.y).toBeGreaterThan(left.y + left.height);
expect(
await page.evaluate(
() => document.documentElement.scrollWidth <= innerWidth,
),
).toBe(true);
await page.goto('/home/knowledge?id=new');
const form = page.locator('#kb-form');
await expect(form).toBeVisible();
const children = await form.locator(':scope > *').all();
expect(children.length).toBeGreaterThan(1);
for (let i = 1; i < children.length; i++) {
const previous = await children[i - 1].boundingBox();
const current = await children[i].boundingBox();
if (previous && current)
expect(current.y - previous.y - previous.height).toBeGreaterThanOrEqual(
20,
);
}
});
}
test('the dedicated connection selector hides its manifest controller and persists its value', async ({
page,
}) => {
await installLangBotApiMocks(page, { authenticated: true });
await page.route('**/api/v1/platform/adapters', (route) =>
route.fulfill({
json: {
code: 0,
data: {
adapters: [
{
name: 'connection-adapter',
label: { en_US: 'Connection adapter' },
description: { en_US: 'Both connection modes' },
spec: {
categories: ['testing'],
config: [
{
name: 'transport',
label: { en_US: 'Raw transport controller' },
type: 'select',
required: true,
default: 'persistent',
options: [
{ name: 'persistent', label: { en_US: 'Persistent' } },
{
name: 'webhook',
label: { en_US: 'Webhook transport' },
},
],
},
{
name: 'webhook_url',
label: { en_US: 'Webhook address' },
type: 'webhook-url',
show_if: {
field: 'transport',
operator: 'eq',
value: 'webhook',
},
},
{
name: 'credential',
label: { en_US: 'Adapter credential' },
type: 'string',
required: true,
default: '',
},
],
},
},
],
},
},
}),
);
await page.goto('/home/bots?id=new');
await page
.getByTestId('adapter-gallery')
.getByRole('button', { name: /Connection adapter/ })
.click();
await page.getByRole('button', { name: /^Submit$/ }).click();
await expect(page).toHaveURL(/\/home\/bots\?id=bot-1$/);
await page.getByRole('radio', { name: /^Webhook/ }).click();
await expect(page.getByText(/^Raw transport controller/)).toHaveCount(0);
await expect(
page.getByText('Webhook address', { exact: true }),
).toBeVisible();
await page.locator('input[name="credential"]').fill('persist-me');
const save = page.waitForRequest(
(request) =>
request.method() === 'PUT' &&
request.url().endsWith('/api/v1/platform/bots/bot-1'),
);
await page.getByRole('button', { name: /^Save and enable$/ }).click();
expect((await save).postDataJSON().adapter_config).toEqual({
transport: 'webhook',
credential: 'persist-me',
});
});
@@ -61,3 +61,52 @@ test('Cloud never exposes or requests storage analysis', async ({ page }) => {
);
expect(storageAnalysisRequests).toBe(0);
});
test('an OSS owner opens storage analysis from its storage-icon menu item', async ({
page,
}) => {
await installLangBotApiMocks(page, { authenticated: true });
let requests = 0;
await page.route('**/api/v1/system/storage-analysis', async (route) => {
requests++;
await route.fulfill({
json: {
code: 0,
data: {
generated_at: '2026-10-17T00:00:00Z',
total_size_bytes: 123,
sections: [],
cleanup_policy: {
uploaded_file_retention_days: 7,
log_retention_days: 14,
},
database: {
type: 'sqlite',
monitoring_counts: {},
binary_storage: { count: 0, size_bytes: 0 },
},
cleanup_candidates: { uploaded_files: [], log_files: [] },
tasks: {},
},
},
});
});
await page.goto('/home/bots');
await page.getByRole('button', { name: /admin@example\.com/i }).click();
const menuItem = page.getByRole('menuitem', {
name: 'Storage Analysis',
exact: true,
});
await expect(menuItem).toBeVisible();
await expect(menuItem.locator('svg.lucide-hard-drive')).toBeVisible();
await menuItem.click();
await expect(
page
.getByRole('dialog')
.getByRole('button', { name: 'Refresh', exact: true }),
).toBeVisible();
await expect(
page.getByRole('dialog').getByText('123 B', { exact: true }),
).toBeVisible();
await expect.poll(() => requests).toBeGreaterThan(0);
});
+4
View File
@@ -214,6 +214,7 @@ test('an OAuth-only OSS instance registers the invited email with a local passwo
email: '[email protected]',
password: 'invite-password-123',
});
expect(await page.evaluate(() => localStorage.getItem('token'))).toBeNull();
});
test('an authenticated OSS invitation requires logout before registration', async ({
@@ -402,8 +403,10 @@ test('Space OAuth accepts a pending invitation with the freshly authenticated ac
});
let acceptanceAuthorization = '';
let acceptanceBody: unknown;
await page.route('**/api/v1/invitations/accept', async (route) => {
acceptanceAuthorization = route.request().headers().authorization ?? '';
acceptanceBody = route.request().postDataJSON();
await route.fulfill({
status: 200,
contentType: 'application/json',
@@ -424,6 +427,7 @@ test('Space OAuth accepts a pending invitation with the freshly authenticated ac
timeout: 5_000,
});
expect(acceptanceAuthorization).toBe('Bearer fresh-invited-account-token');
expect(acceptanceBody).toEqual({ token: 'matching-invitation' });
expect(
await page.evaluate(() => ({
token: localStorage.getItem('token'),
+95 -61
View File
@@ -1,64 +1,98 @@
import { expect, test } from '@playwright/test';
import {
installLangBotApiMocks,
makeWorkspaceEntry,
} from './fixtures/langbot-api';
import { installLangBotApiMocks } from './fixtures/langbot-api';
test('an OSS local-only owner is prompted to bind before using LangBot Models', async ({
page,
}) => {
await installLangBotApiMocks(page, { authenticated: true });
await page.route('**/api/v1/user/space-credits', (route) =>
route.fulfill({
contentType: 'application/json',
body: JSON.stringify({
code: 0,
data: {
credits: null,
owner_space_bound: false,
is_workspace_owner: true,
},
msg: 'ok',
}),
}),
);
await page.route('**/api/v1/provider/providers', (route) =>
route.fulfill({
contentType: 'application/json',
body: JSON.stringify({
code: 0,
data: {
providers: [
{
uuid: 'langbot-models-provider',
name: 'LangBot Models',
requester: 'space-chat-completions',
base_url: '',
api_keys: [],
for (const role of ['owner', 'viewer'] as const) {
for (const bound of [false, true]) {
test(`LangBot Models uses Workspace billing for ${role}, owner bound=${bound}`, async ({
page,
}) => {
const workspace = makeWorkspaceEntry(
'billing-workspace',
'Billing Workspace',
'local',
);
workspace.membership.role = role;
await installLangBotApiMocks(page, {
authenticated: true,
workspaces: [workspace],
});
const billingHeaders: Array<Record<string, string>> = [];
await page.route('**/api/v1/user/space-credits', async (route) => {
billingHeaders.push(route.request().headers());
await route.fulfill({
json: {
code: 0,
data: {
credits: bound ? 12500 : null,
owner_space_bound: bound,
is_workspace_owner: role === 'owner',
},
],
},
msg: 'ok',
}),
}),
);
await page.route('**/api/v1/provider/requesters**', (route) =>
route.fulfill({
contentType: 'application/json',
body: JSON.stringify({ code: 0, data: { requesters: [] }, msg: 'ok' }),
}),
);
await page.route('**/api/v1/provider/models/**', (route) =>
route.fulfill({
contentType: 'application/json',
body: JSON.stringify({ code: 0, data: { models: [] }, msg: 'ok' }),
}),
);
await page.goto('/home?action=showModelSettings');
await expect(
page.getByText(
'The Workspace owner must connect a LangBot Account for LangBot Models.',
{ exact: true },
),
).toBeVisible();
});
},
});
});
await page.route('**/api/v1/provider/providers', (route) =>
route.fulfill({
json: {
code: 0,
data: {
providers: [
{
uuid: 'langbot-models-provider',
name: 'LangBot Models',
requester: 'space-chat-completions',
base_url: '',
api_keys: [],
},
],
},
},
}),
);
await page.route('**/api/v1/provider/requesters**', (route) =>
route.fulfill({ json: { code: 0, data: { requesters: [] } } }),
);
await page.route('**/api/v1/provider/models/**', (route) =>
route.fulfill({ json: { code: 0, data: { models: [] } } }),
);
await page.goto('/home?action=showModelSettings');
await expect.poll(() => billingHeaders.length).toBeGreaterThan(0);
expect(billingHeaders[0]).toMatchObject({
authorization: 'Bearer playwright-token',
'x-workspace-id': 'billing-workspace',
});
const dialog = page.getByRole('dialog');
await expect(
dialog.getByText(
'The Workspace owner must connect a LangBot Account for LangBot Models.',
{ exact: true },
),
).toHaveCount(bound ? 0 : 1);
await expect(
dialog.getByRole('button', {
name: 'Login with LangBot Account',
exact: true,
}),
).toHaveCount(role === 'owner' && !bound ? 1 : 0);
await expect(
dialog.getByText('2.50 Credits', { exact: true }),
).toHaveCount(bound ? 1 : 0);
if (bound) {
await expect(
dialog
.getByText('2.50 Credits', { exact: true })
.locator('..')
.getByRole('button'),
).toHaveCount(role === 'owner' ? 1 : 0);
}
await expect(
dialog.getByText(
"Uses the Workspace owner's LangBot Account billing and credits.",
{ exact: true },
),
).toHaveCount(role === 'viewer' && bound ? 1 : 0);
});
}
}
+120
View File
@@ -62,6 +62,7 @@ test('loads a Cloud plugin page through the authenticated asset route', async ({
});
let authenticatedAssetRequests = 0;
const assetHeaders: Array<Record<string, string>> = [];
let pageSdkRequests = 0;
let pageApiRequests = 0;
await page.route('**/api/v1/plugins/_sdk/page-sdk.js', async (route) => {
@@ -91,6 +92,7 @@ test('loads a Cloud plugin page through the authenticated asset route', async ({
'**/api/v1/plugins/langbot-team/LangRAG/authenticated-assets/**',
async (route) => {
authenticatedAssetRequests += 1;
assetHeaders.push(route.request().headers());
await route.fulfill({
status: 200,
contentType: 'text/html',
@@ -135,7 +137,125 @@ test('loads a Cloud plugin page through the authenticated asset route', async ({
'true',
);
expect(authenticatedAssetRequests).toBeGreaterThan(0);
expect(assetHeaders[0]).toMatchObject({
authorization: 'Bearer playwright-token',
'x-workspace-id': 'workspace-cloud',
});
expect(pageSdkRequests).toBe(1);
expect(pageApiRequests).toBe(1);
await expect(page.getByText('Loading...')).toHaveCount(0);
});
test('plugin asset hook replaces and revokes URLs on a same-document Workspace change', async ({
page,
}) => {
const first = makeWorkspaceEntry('asset-first', 'First', 'local');
const second = makeWorkspaceEntry('asset-second', 'Second', 'local');
await installLangBotApiMocks(page, {
authenticated: true,
workspaces: [first],
});
const headers: Array<Record<string, string>> = [];
await page.route(
'**/api/v1/plugins/test/asset/authenticated-assets/page.html',
async (route) => {
headers.push(route.request().headers());
await route.fulfill({
contentType: 'text/html',
body: `<h1>${headers.at(-1)?.['x-workspace-id']}</h1>`,
});
},
);
await page.goto('/home/bots');
await expect
.poll(() =>
page.evaluate(async () => {
const path = '/src/app/infra/http/currentWorkspaceStore.ts';
return (await import(path)).getCurrentWorkspaceSnapshot()?.workspace
.uuid;
}),
)
.toBe('asset-first');
await page.evaluate(async () => {
const resources = performance
.getEntriesByType('resource')
.map((entry) => entry.name);
const reactPath = resources.find((url) =>
new URL(url).pathname.endsWith('/react.js'),
)!;
const domPath = resources.find((url) =>
new URL(url).pathname.endsWith('/react-dom_client.js'),
)!;
const React = (await import(reactPath)).default;
const domModule = await import(domPath);
const { createRoot } = domModule.default || domModule;
const hookPath = '/src/hooks/useAuthenticatedPluginResource.ts';
const { useAuthenticatedPluginAsset } = await import(hookPath);
const host = document.createElement('main');
host.id = 'asset-hook';
document.getElementById('root')!.style.display = 'none';
document.body.appendChild(host);
const revoke = URL.revokeObjectURL.bind(URL);
URL.revokeObjectURL = (url) => {
host.dataset.revoked = JSON.stringify([
...JSON.parse(host.dataset.revoked || '[]'),
url,
]);
revoke(url);
};
function Resource() {
const resource = useAuthenticatedPluginAsset(
'test',
'asset',
'page.html',
);
return React.createElement(
'output',
{ 'data-testid': 'asset-url' },
resource.url,
);
}
function Harness() {
const [mounted, setMounted] = React.useState(true);
return React.createElement(
React.Fragment,
null,
mounted && React.createElement(Resource),
React.createElement(
'button',
{ onClick: () => setMounted(false) },
'Unmount resource',
),
);
}
createRoot(host).render(React.createElement(Harness));
});
const output = page.getByTestId('asset-url');
await expect(output).toHaveText(/^blob:/);
const oldURL = await output.textContent();
await page.evaluate(async (workspace) => {
const contextPath = '/src/app/infra/http/workspaceContext.ts';
const storePath = '/src/app/infra/http/currentWorkspaceStore.ts';
const context = await import(contextPath);
const store = await import(storePath);
context.setActiveWorkspaceUuid(workspace.workspace.uuid);
store.setCurrentWorkspaceSnapshot(workspace);
}, second);
await expect(output).toHaveText(/^blob:/);
await expect(output).not.toHaveText(oldURL!);
await expect
.poll(() => headers.map((item) => item['x-workspace-id']))
.toEqual(['asset-first', 'asset-second']);
expect(
headers.every((item) => item.authorization === 'Bearer playwright-token'),
).toBe(true);
const newURL = await output.textContent();
await expect
.poll(() => page.locator('#asset-hook').getAttribute('data-revoked'))
.toBe(JSON.stringify([oldURL]));
await page.getByRole('button', { name: 'Unmount resource' }).click();
await expect(output).toHaveCount(0);
await expect
.poll(() => page.locator('#asset-hook').getAttribute('data-revoked'))
.toBe(JSON.stringify([oldURL, newURL]));
});
@@ -1,19 +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/login/page.tsx', import.meta.url),
'utf8',
);
test('normal Cloud login uses the standard Space OAuth callback path', () => {
assert.doesNotMatch(source, /cloudEntry/);
assert.match(source, /getSpaceAuthorizeUrl\(redirectUri\)/);
});
test('invitation login uses the same OAuth callback before accepting the invitation', () => {
assert.doesNotMatch(source, /cloudEntry/);
assert.match(source, /const invitationToken = getPendingInvitationToken\(\)/);
assert.match(source, /acceptWorkspaceInvitation\(invitationToken\)/);
});
@@ -10,45 +10,6 @@ const root = path.resolve(
);
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
test('invited local registration returns to login instead of authenticating', () => {
const source = read('src/app/invitations/accept/page.tsx');
assert.doesNotMatch(
source,
/beginAuthenticatedSession\([\s\S]{0,120}response\.token/,
);
assert.match(source, /navigate\('\/login\?invitation=1'/);
});
test('authenticated invitation page offers logout while retaining invitation', () => {
const source = read('src/app/invitations/accept/page.tsx');
assert.match(source, /workspace\.logoutAndReturn/);
assert.match(source, /setPendingInvitationToken\(token\)/);
});
test('Space OAuth callback distinguishes unknown and unbound accounts by stable codes', () => {
const source = read('src/app/auth/space/callback/page.tsx');
assert.match(source, /space_account_not_registered/);
assert.match(source, /space_account_binding_required/);
});
test('models panel derives LangBot Models billing state from workspace owner', () => {
const source = read('src/app/home/components/models-dialog/ModelsPanel.tsx');
assert.match(source, /getWorkspaceSpaceBilling/);
assert.doesNotMatch(source, /getSpaceCredits\(\)/);
assert.match(source, /membership\.role === 'owner'/);
});
test('provider card represents owner and member owner-bound states explicitly', () => {
const source = read(
'src/app/home/components/models-dialog/components/ProviderCard.tsx',
);
assert.match(source, /isWorkspaceOwner/);
assert.match(source, /ownerSpaceBound/);
assert.match(source, /models\.ownerMustBindSpace/);
assert.match(source, /models\.usesOwnerSpaceBilling/);
assert.match(source, /isWorkspaceOwner && \(\s*<Button/);
});
test('workspace member controls never offer ownership transfer', () => {
const source = read(
'src/app/home/components/workspace-settings/WorkspaceSettingsPanel.tsx',
@@ -14,23 +14,6 @@ function readSource(relativePath) {
const homeSidebarSource = readSource(
'src/app/home/components/home-sidebar/HomeSidebar.tsx',
);
const botFormSource = readSource(
'src/app/home/bots/components/bot-form/BotForm.tsx',
);
const dynamicFormSource = readSource(
'src/app/home/components/dynamic-form/DynamicFormComponent.tsx',
);
const kbFormSource = readSource(
'src/app/home/knowledge/components/kb-form/KBForm.tsx',
);
const settingsDialogSource = readSource(
'src/app/home/components/settings-dialog/SettingsDialog.tsx',
);
const pluginPageSource = readSource('src/app/home/plugin-pages/page.tsx');
const authenticatedPluginResourceSource = readSource(
'src/hooks/useAuthenticatedPluginResource.ts',
);
test('hides the entire workspace switcher slot for a singleton local workspace', () => {
assert.match(homeSidebarSource, /useWorkspaceBootstrap/);
assert.match(
@@ -42,94 +25,3 @@ test('hides the entire workspace switcher slot for a singleton local workspace',
/\{showWorkspaceSwitcher\s*&&\s*\(\s*<div className="px-2[^>]*>\s*<WorkspaceSwitcher/,
);
});
test('keeps bot forms spaced with responsive setup and detail columns', () => {
const fieldset = botFormSource.match(
/<fieldset\s[\s\S]*?disabled=\{isLoading\}/,
)?.[0];
assert.ok(fieldset, 'the form must retain its loading-disabled fieldset');
assert.match(
fieldset,
/initBotId\s*\?\s*'grid gap-4 lg:h-full lg:min-h-0 lg:grid-cols-\[minmax\(0,2fr\)_minmax\(0,3fr\)\] lg:grid-rows-\[minmax\(0,1fr\)\]'\s*:\s*'grid items-start gap-6 lg:grid-cols-\[minmax\(16rem,0\.7fr\)_minmax\(0,2fr\)\]'/,
);
assert.match(kbFormSource, /<form[\s\S]*?className="space-y-6"/);
});
test('hides the manifest connection switch behind the dedicated selector', () => {
assert.match(
botFormSource,
/webhookField\?\.show_if\?\.field\s*\?\s*\[webhookField\.show_if\.field\]/,
);
assert.match(
botFormSource,
/hiddenItemNames=\{connectionModeControllerNames\}/,
);
assert.match(
dynamicFormSource,
/if \(hiddenItemNames\?\.includes\(config\.name\)\) return null/,
);
assert.doesNotMatch(
dynamicFormSource.match(
/const editableItems = useMemo\([\s\S]*?\n \);/,
)?.[0] ?? '',
/hiddenItemNames/,
);
});
test('does not expose storage analysis in Cloud settings or via a deep link', () => {
assert.match(
homeSidebarSource,
/canViewStorageAnalysis\s*&&\s*\(\s*<DropdownMenuItem[\s\S]*?openSettings\('storageAnalysis'\)/,
);
assert.match(
settingsDialogSource,
/const canViewStorageAnalysis\s*=\s*currentWorkspace\?\.workspace\.source\s*!==\s*'cloud_projection'\s*&&\s*canViewAudit/,
);
assert.match(
settingsDialogSource,
/item\.id === 'storageAnalysis'[\s\S]*?return canViewStorageAnalysis/,
);
assert.match(
settingsDialogSource,
/section === 'storageAnalysis' && !canViewStorageAnalysis/,
);
assert.match(
settingsDialogSource,
/section === 'storageAnalysis' &&\s*canViewStorageAnalysis &&\s*\(\s*<StorageAnalysisPanel/,
);
});
test('renders storage analysis with the storage icon in the user menu', () => {
assert.match(
homeSidebarSource,
/canViewStorageAnalysis\s*&&\s*\(\s*<DropdownMenuItem[\s\S]*?<HardDrive\s*\/>[\s\S]*?\{t\('storageAnalysis\.title'\)\}/,
);
});
test('loads plugin pages through the authenticated Workspace-scoped asset route', () => {
assert.match(pluginPageSource, /useAuthenticatedPluginAsset/);
assert.match(
pluginPageSource,
/useAuthenticatedPluginAsset\(\s*author,\s*pluginName,\s*pagePath,?\s*\)/,
);
assert.match(pluginPageSource, /src=\{assetUrl\}/);
assert.doesNotMatch(pluginPageSource, /getPluginAssetURL\(/);
assert.match(pluginPageSource, /plugins\.loadFailed/);
assert.match(pluginPageSource, /loadedAssetUrl !== assetUrl/);
});
test('revokes and reloads authenticated plugin resources when the Workspace changes', () => {
assert.match(authenticatedPluginResourceSource, /useCurrentWorkspace/);
assert.match(
authenticatedPluginResourceSource,
/const workspaceUuid = currentWorkspace\?\.workspace\.uuid;/,
);
assert.match(
authenticatedPluginResourceSource,
/\[author, name, filepath, resourceKey\]/,
);
assert.match(
authenticatedPluginResourceSource,
/resource\.key === resourceKey \? resource\.url : ''/,
);
});