mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-23 17:05:28 +08:00
Merge pull request #2699 from langbot-app/test/quality-account-ui-oracles-20261017
test(web): replace account and privacy source checks with browser behavior
This commit is contained in:
@@ -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();
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,6 +137,10 @@ 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);
|
||||
@@ -289,3 +295,116 @@ for (const pendingAsset of [false, true]) {
|
||||
}
|
||||
});
|
||||
}
|
||||
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]));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user