Create-project candidates come from the in-session user directory, so a fresh admin page never saw B/C unless the console refresh ran first. Co-authored-by: Cursor <cursoragent@cursor.com>
468 lines
22 KiB
TypeScript
468 lines
22 KiB
TypeScript
import { test, expect, type Page, type Browser } from '@playwright/test';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
import { GOVERNANCE_BLOCKED_HINT } from '../src/lib/blockedGovernanceCopy';
|
|
import { currentProjectBannerName } from '../src/lib/currentProjectRestore';
|
|
import {
|
|
ANY_TERMINAL_STATUS_RE,
|
|
evaluateSquadObservation,
|
|
} from '../src/lib/squadCompletionObservation';
|
|
import {
|
|
CATEGORY_TRIGGER_LABEL,
|
|
requiredCategoryForDocOpen,
|
|
} from '../src/lib/workflowCategoryNavigation';
|
|
|
|
/**
|
|
* M1 live: ledger-first GET. Projection missing must not become 404/409.
|
|
* Project 143 is read-only. Do not terminate or PUT workflow-plan.
|
|
*/
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const runAcceptance = process.env.RUN_UI_ACCEPTANCE === '1';
|
|
const ASSETS = process.env.UI_ACCEPTANCE_ASSETS_DIR
|
|
? path.resolve(process.env.UI_ACCEPTANCE_ASSETS_DIR)
|
|
: path.resolve(
|
|
__dirname,
|
|
'../../.doc/test-reports/assets/2026-09-14-document-reliability-pilot-live',
|
|
);
|
|
const SCREENSHOTS = path.join(ASSETS, 'screenshots');
|
|
fs.mkdirSync(SCREENSHOTS, { recursive: true });
|
|
|
|
const ADMIN_USER = process.env.ADMIN_USERNAME?.trim() || '';
|
|
const ADMIN_PASS = process.env.ADMIN_PASSWORD?.trim() || '';
|
|
const BASE = process.env.BASE_URL || 'http://127.0.0.1:8080';
|
|
const TARGET_DOC = 'doc_proj143_intake';
|
|
const suffix = `${Date.now().toString(36)}`;
|
|
const USER_B = `m1b_${suffix}`;
|
|
const USER_C = `m1c_${suffix}`;
|
|
const USER_D = `m1d_${suffix}`;
|
|
const INIT_PASS = `Init-${suffix}9`;
|
|
const FINAL_PASS_B = `PassB-${suffix}9`;
|
|
const FINAL_PASS_C = `PassC-${suffix}9`;
|
|
const FINAL_PASS_D = `PassD-${suffix}9`;
|
|
const PROJECT_NAME = `M1活验-${suffix}`;
|
|
|
|
test.skip(!runAcceptance, 'set RUN_UI_ACCEPTANCE=1 against the rebuilt dev stack');
|
|
|
|
const state: {
|
|
newDocId?: string;
|
|
newMarkdown?: string;
|
|
outsiderReady?: boolean;
|
|
} = {};
|
|
|
|
async function login(page: Page, username: string, password: string) {
|
|
await page.goto('/');
|
|
await page.fill('input[placeholder*="系统用户名"]', username);
|
|
await page.fill('input[placeholder*="登录密码"]', password);
|
|
await page.click('button:has-text("进入医疗协同工作台")');
|
|
await expect(page.getByText('新建立项会话')).toBeVisible({ timeout: 30_000 });
|
|
}
|
|
|
|
async function changePasswordFirstLogin(page: Page, oldPass: string, newPass: string) {
|
|
await expect(page.getByPlaceholder('请输入初始密码')).toBeVisible({ timeout: 15_000 });
|
|
await page.fill('input[placeholder="请输入初始密码"]', oldPass);
|
|
await page.fill('input[placeholder*="至少 6 位"]', newPass);
|
|
await page.fill('input[placeholder="再次输入新密码"]', newPass);
|
|
await page.getByRole('button', { name: /确认重置并进入系统/ }).click();
|
|
await expect(page.getByText('新建立项会话')).toBeVisible({ timeout: 30_000 });
|
|
}
|
|
|
|
async function logoutViaUI(page: Page) {
|
|
await page.locator('button[title="个人资料与系统偏好设置"]').click({ timeout: 15_000 });
|
|
const logout = page.getByRole('button', { name: '退出当前登录' });
|
|
await expect(logout).toBeVisible({ timeout: 10_000 });
|
|
await logout.click();
|
|
await expect(page.getByPlaceholder(/系统用户名/)).toBeVisible({ timeout: 20_000 });
|
|
}
|
|
|
|
async function sessionToken(page: Page): Promise<string> {
|
|
const token = await page.evaluate(() => {
|
|
try {
|
|
const raw = window.localStorage.getItem('smdf_auth_session');
|
|
if (!raw) return '';
|
|
const session = JSON.parse(raw) as { token?: string };
|
|
return session.token || '';
|
|
} catch {
|
|
return '';
|
|
}
|
|
});
|
|
if (!token) {
|
|
throw new Error('login did not persist smdf_auth_session.token');
|
|
}
|
|
return token;
|
|
}
|
|
|
|
async function getContent(page: Page, docId: string, token?: string) {
|
|
const headers = token ? { Authorization: `Bearer ${token}` } : {};
|
|
const res = await page.request.get(`${BASE}/api/v1/docgov/documents/${docId}/content`, {
|
|
headers,
|
|
});
|
|
let body: { markdown?: string; consistency?: string; error_code?: string } = {};
|
|
try {
|
|
body = (await res.json()) as typeof body;
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
return { status: res.status(), body };
|
|
}
|
|
|
|
function expectClosed404(status: number, body: { error_code?: string }, label: string) {
|
|
expect(status, `${label} ${JSON.stringify(body)}`).toBe(404);
|
|
expect(body.error_code, label).toBe('DOCUMENT_NOT_FOUND');
|
|
}
|
|
|
|
async function goToDutyStep(page: Page) {
|
|
const approval = page.locator('label', { hasText: '纳入人工审批' }).first();
|
|
const selectHint = page.getByText('选择要激活的已发布工作流版本。激活后将冻结配置,运行中不可切换。');
|
|
await expect(selectHint.or(approval)).toBeVisible({ timeout: 20_000 });
|
|
if (await approval.isVisible().catch(() => false)) {
|
|
return;
|
|
}
|
|
await page.getByRole('button', { name: /下一步/ }).click();
|
|
await expect(approval).toBeVisible({ timeout: 20_000 });
|
|
}
|
|
|
|
function dutyRoleSelect(page: Page, placeholder: '作者' | '审核' | '批准') {
|
|
return page
|
|
.locator('select')
|
|
.filter({ has: page.getByRole('option', { name: placeholder, exact: true }) })
|
|
.first();
|
|
}
|
|
|
|
function turnAssistantLocator(page: Page, userPromptSnippet: string) {
|
|
const userBubble = page
|
|
.locator('[data-chat-role="user"]')
|
|
.filter({ hasText: userPromptSnippet })
|
|
.last();
|
|
return {
|
|
userBubble,
|
|
assistant: userBubble.locator(
|
|
'xpath=ancestor::*[@data-index][1]/following-sibling::*[@data-index][1]//*[@data-chat-role="assistant"]',
|
|
),
|
|
};
|
|
}
|
|
|
|
async function waitForSquadCompletion(page: Page, userPromptSnippet: string, timeoutMs = 420_000) {
|
|
const { userBubble, assistant } = turnAssistantLocator(page, userPromptSnippet);
|
|
await expect(userBubble).toBeVisible({ timeout: 30_000 });
|
|
await expect(assistant).toBeVisible({ timeout: 30_000 });
|
|
const connErr = page.locator('text=连接异常');
|
|
const observe = async () =>
|
|
evaluateSquadObservation(
|
|
{
|
|
userPromptMatched: true,
|
|
assistantAdjacent: true,
|
|
status: (await assistant.getAttribute('data-chat-status')) || '',
|
|
hasVisibleError: await connErr.first().isVisible().catch(() => false),
|
|
},
|
|
{ requireInflight: false },
|
|
);
|
|
let verdict = await observe();
|
|
if (verdict.action === 'reject') {
|
|
throw new Error(`squad observation rejected: ${verdict.reason}`);
|
|
}
|
|
if (verdict.action === 'accept') {
|
|
return;
|
|
}
|
|
await expect(assistant).toHaveAttribute('data-chat-status', ANY_TERMINAL_STATUS_RE, {
|
|
timeout: timeoutMs,
|
|
});
|
|
verdict = await observe();
|
|
if (verdict.action !== 'accept') {
|
|
throw new Error(`squad terminal not accepted: ${verdict.reason}`);
|
|
}
|
|
}
|
|
|
|
async function openWorkflowDocByLabel(page: Page, label: RegExp) {
|
|
const category = requiredCategoryForDocOpen({ kind: 'workflow-label' });
|
|
const catTrigger = page.getByTestId('doc-category-trigger');
|
|
await expect(catTrigger).toBeVisible({ timeout: 15_000 });
|
|
await catTrigger.click();
|
|
const catItem = page.locator(`.dropdown-menu button[data-category="${category}"]`).first();
|
|
await catItem.click();
|
|
await expect(catTrigger).toContainText(CATEGORY_TRIGGER_LABEL[category], { timeout: 10_000 });
|
|
const trigger = page.getByTestId('doc-selector-trigger');
|
|
await expect(trigger).toBeVisible({ timeout: 15_000 });
|
|
await trigger.click();
|
|
const item = page.locator('.dropdown-menu button[data-doc-id]').filter({ hasText: label }).first();
|
|
if ((await item.count()) === 0) {
|
|
throw new Error(`workflow slot not in doc-selector menu: ${label}`);
|
|
}
|
|
const docId = (await item.getAttribute('data-doc-id')) || '';
|
|
await item.click();
|
|
if (!docId) {
|
|
throw new Error(`selected menu item missing data-doc-id for ${label}`);
|
|
}
|
|
return docId;
|
|
}
|
|
|
|
test.describe.serial('controlled document reload M1 live', () => {
|
|
test('project 143 content is 200 with consistency; anonymous is 401; unknown is 404', async ({
|
|
browser,
|
|
}) => {
|
|
if (!ADMIN_USER || !ADMIN_PASS) {
|
|
throw new Error('ADMIN_USERNAME and ADMIN_PASSWORD are required');
|
|
}
|
|
const page = await browser.newPage();
|
|
const contentHits: { status: number; consistency?: string; error?: string }[] = [];
|
|
page.on('response', async (res) => {
|
|
const u = res.url();
|
|
if (!u.includes(`/docgov/documents/${TARGET_DOC}/content`)) return;
|
|
let consistency: string | undefined;
|
|
let error: string | undefined;
|
|
try {
|
|
const json = (await res.json()) as { consistency?: string; error_code?: string };
|
|
consistency = json.consistency;
|
|
error = json.error_code;
|
|
} catch {
|
|
/* ignore non-json */
|
|
}
|
|
contentHits.push({ status: res.status(), consistency, error });
|
|
});
|
|
|
|
await login(page, ADMIN_USER, ADMIN_PASS);
|
|
await page.getByRole('complementary').getByTitle('对话协同会话', { exact: true }).click({ force: true });
|
|
await expect(page.getByRole('banner')).toContainText('对话协同会话', { timeout: 20_000 });
|
|
await page.screenshot({ path: path.join(SCREENSHOTS, '01-project-143.png'), fullPage: true });
|
|
|
|
const token = await sessionToken(page);
|
|
const authed = await getContent(page, TARGET_DOC, token);
|
|
expect(authed.status, `content ${JSON.stringify(authed.body)}`).toBe(200);
|
|
expect(authed.body.markdown, 'ledger markdown required').toBeTruthy();
|
|
expect(['aligned', 'missing', 'drift', 'empty', 'ledger_missing']).toContain(authed.body.consistency);
|
|
expect(authed.body.error_code ?? '').not.toMatch(/PROJECTION/);
|
|
await expect(page.getByText('加载失败,请点击重试')).toHaveCount(0);
|
|
|
|
await page.reload();
|
|
await expect(page.getByRole('banner')).toContainText('对话协同会话', { timeout: 20_000 });
|
|
const afterReload = await getContent(page, TARGET_DOC, token);
|
|
expect(afterReload.status).toBe(200);
|
|
expect(afterReload.body.markdown).toBeTruthy();
|
|
await expect(page.getByText('加载失败,请点击重试')).toHaveCount(0);
|
|
await page.screenshot({ path: path.join(SCREENSHOTS, '02-after-reload.png'), fullPage: true });
|
|
|
|
await logoutViaUI(page);
|
|
await login(page, ADMIN_USER, ADMIN_PASS);
|
|
await page.getByRole('complementary').getByTitle('对话协同会话', { exact: true }).click({ force: true });
|
|
await expect(page.getByRole('banner')).toContainText('对话协同会话', { timeout: 20_000 });
|
|
const afterRelogin = await getContent(page, TARGET_DOC, await sessionToken(page));
|
|
expect(afterRelogin.status).toBe(200);
|
|
expect(afterRelogin.body.markdown).toBeTruthy();
|
|
await page.screenshot({ path: path.join(SCREENSHOTS, '03-after-relogin.png'), fullPage: true });
|
|
|
|
const anon = await getContent(page, TARGET_DOC);
|
|
expect(anon.status).toBe(401);
|
|
|
|
const missing = await getContent(page, 'doc_does_not_exist_m1', await sessionToken(page));
|
|
expectClosed404(missing.status, missing.body, 'unknown doc');
|
|
|
|
const projection409 = contentHits.filter(
|
|
(h) => h.status === 409 || String(h.error || '').includes('PROJECTION'),
|
|
);
|
|
expect(projection409, JSON.stringify(contentHits)).toHaveLength(0);
|
|
await page.close();
|
|
});
|
|
|
|
test('non-member GET of project 143 is the same 404 as unknown', async ({ browser }) => {
|
|
if (!ADMIN_USER || !ADMIN_PASS) {
|
|
throw new Error('ADMIN_USERNAME and ADMIN_PASSWORD are required');
|
|
}
|
|
const admin = await browser.newPage();
|
|
await login(admin, ADMIN_USER, ADMIN_PASS);
|
|
await admin.locator('button[title*="系统管理与准入治理"]').click();
|
|
for (const [user, name, role] of [
|
|
[USER_B, `审核协作者-${suffix}`, 'role-engineer'],
|
|
[USER_C, `批准协作者-${suffix}`, 'role-engineer'],
|
|
[USER_D, `局外人-${suffix}`, 'role-guest'],
|
|
] as const) {
|
|
await admin.getByRole('button', { name: '开通受控员工账号' }).click();
|
|
const form = admin.locator('form').filter({ hasText: '用户名 (系统账号)' });
|
|
await form.locator('input[placeholder="如 li_si"]').fill(user);
|
|
await form.locator('input[placeholder="如 李四"]').fill(name);
|
|
const passBox = form.locator('xpath=.//label[contains(.,"临时初始密码")]/following::input[1]');
|
|
await passBox.fill(INIT_PASS);
|
|
await form.locator('select').selectOption(role);
|
|
await form.getByRole('button', { name: '确认开通账号' }).click();
|
|
await expect(form).toBeHidden({ timeout: 15_000 });
|
|
}
|
|
const adminHeading = admin.getByText('系统管理控制台', { exact: true });
|
|
const adminModal = adminHeading.locator('xpath=ancestor::div[contains(@style,"position: fixed")][1]');
|
|
await adminModal.locator('button').first().click();
|
|
await expect(adminHeading).toBeHidden({ timeout: 10_000 });
|
|
await admin.close();
|
|
|
|
const outsider = await browser.newPage();
|
|
await outsider.goto('/');
|
|
await outsider.fill('input[placeholder*="系统用户名"]', USER_D);
|
|
await outsider.fill('input[placeholder*="登录密码"]', INIT_PASS);
|
|
await outsider.click('button:has-text("进入医疗协同工作台")');
|
|
await changePasswordFirstLogin(outsider, INIT_PASS, FINAL_PASS_D);
|
|
const token = await sessionToken(outsider);
|
|
const closed = await getContent(outsider, TARGET_DOC, token);
|
|
expectClosed404(closed.status, closed.body, 'non-member 143');
|
|
const unknown = await getContent(outsider, 'doc_does_not_exist_m1', token);
|
|
expectClosed404(unknown.status, unknown.body, 'outsider unknown');
|
|
await outsider.screenshot({ path: path.join(SCREENSHOTS, '04-outsider-143.png'), fullPage: true });
|
|
await outsider.close();
|
|
state.outsiderReady = true;
|
|
});
|
|
|
|
test('new project PI compile then reload keeps ledger markdown', async ({ browser }) => {
|
|
if (!ADMIN_USER || !ADMIN_PASS) {
|
|
throw new Error('ADMIN_USERNAME and ADMIN_PASSWORD are required');
|
|
}
|
|
const reviewer = await browser.newPage();
|
|
await reviewer.goto('/');
|
|
await reviewer.fill('input[placeholder*="系统用户名"]', USER_B);
|
|
await reviewer.fill('input[placeholder*="登录密码"]', INIT_PASS);
|
|
await reviewer.click('button:has-text("进入医疗协同工作台")');
|
|
await changePasswordFirstLogin(reviewer, INIT_PASS, FINAL_PASS_B);
|
|
await reviewer.close();
|
|
|
|
const page = await browser.newPage();
|
|
const runCreates: { status: number; body: string }[] = [];
|
|
page.on('response', async (res) => {
|
|
if (res.url().includes('/api/v1/tasks/runs') && res.request().method() === 'POST') {
|
|
runCreates.push({ status: res.status(), body: await res.text().catch(() => '') });
|
|
}
|
|
});
|
|
await login(page, ADMIN_USER, ADMIN_PASS);
|
|
await page.locator('button[title*="系统管理与准入治理"]').click();
|
|
await expect(page.getByRole('button', { name: '开通受控员工账号' })).toBeVisible({ timeout: 15_000 });
|
|
const adminHeading = page.getByText('系统管理控制台', { exact: true });
|
|
const adminModal = adminHeading.locator('xpath=ancestor::div[contains(@style,"position: fixed")][1]');
|
|
await adminModal.locator('button').first().click();
|
|
await expect(adminHeading).toBeHidden({ timeout: 10_000 });
|
|
|
|
await page.getByRole('button', { name: '新建立项会话' }).click();
|
|
const heading = page.getByText('发起医疗器械新项目立项', { exact: true });
|
|
await expect(heading).toBeVisible({ timeout: 20_000 });
|
|
const modal = heading.locator('xpath=ancestor::div[contains(@style,"position: fixed")][1]');
|
|
await modal.locator('input[placeholder*="无线便携式"]').fill(PROJECT_NAME);
|
|
await modal.locator('textarea').fill('M1 活验:新建项目编制一条受控文档后刷新。');
|
|
for (const name of [`审核协作者-${suffix}`, `批准协作者-${suffix}`]) {
|
|
const memberName = modal.getByText(name, { exact: true });
|
|
if (await memberName.isVisible({ timeout: 5_000 }).catch(() => false)) {
|
|
await memberName.locator('xpath=ancestor::div[contains(@style,"cursor: pointer")][1]').click();
|
|
}
|
|
}
|
|
await modal.getByRole('button', { name: /确认立项/ }).click();
|
|
await expect(currentProjectBannerName(page, PROJECT_NAME)).toBeVisible({ timeout: 30_000 });
|
|
await page.screenshot({ path: path.join(SCREENSHOTS, '05-new-project.png'), fullPage: true });
|
|
|
|
const generatePrompt = '请编制用户初始需求收集表';
|
|
const input = page.getByPlaceholder(/向项目助手提问/);
|
|
await input.fill(generatePrompt);
|
|
await page.getByRole('button', { name: /发送/ }).click();
|
|
await expect(page.getByText(generatePrompt)).toBeVisible({ timeout: 20_000 });
|
|
await expect(page.getByText(GOVERNANCE_BLOCKED_HINT)).toBeVisible({ timeout: 45_000 });
|
|
const blocked = runCreates.find((row) => row.status === 202 && row.body.includes('blocked_pending_governance'));
|
|
if (!blocked) {
|
|
throw new Error(`expected 202 blocked run before activation, got ${JSON.stringify(runCreates)}`);
|
|
}
|
|
|
|
await page.getByRole('button', { name: '配置工作流' }).click();
|
|
await goToDutyStep(page);
|
|
const toggles = page.locator('label', { hasText: '纳入人工审批' });
|
|
const toggleCount = await toggles.count();
|
|
for (let i = 1; i < toggleCount; i += 1) {
|
|
const box = toggles.nth(i).locator('input[type="checkbox"]');
|
|
if (await box.isChecked()) await box.uncheck();
|
|
}
|
|
const firstToggle = toggles.first().locator('input[type="checkbox"]');
|
|
if (!(await firstToggle.isChecked())) await firstToggle.check();
|
|
const author = dutyRoleSelect(page, '作者');
|
|
await expect(author).toBeVisible({ timeout: 10_000 });
|
|
const values = (
|
|
await author.locator('option').evaluateAll((nodes) => nodes.map((n) => (n as HTMLOptionElement).value))
|
|
).filter(Boolean);
|
|
if (values.length < 3) {
|
|
throw new Error(`need 3 members for full duties, got ${values.length}`);
|
|
}
|
|
await author.selectOption(values[0]);
|
|
await dutyRoleSelect(page, '审核').selectOption(values[1]);
|
|
await dutyRoleSelect(page, '批准').selectOption(values[2]);
|
|
await page.getByRole('button', { name: /校验并预览/ }).click();
|
|
await expect(page.getByText('激活预览')).toBeVisible({ timeout: 20_000 });
|
|
await page.getByRole('button', { name: '确认激活' }).click();
|
|
await expect(page.getByText('运行中')).toBeVisible({ timeout: 20_000 });
|
|
await page.screenshot({ path: path.join(SCREENSHOTS, '06-activated.png'), fullPage: true });
|
|
|
|
const compilePrompt = '请基于本项目编制首轮《用户需求》摸底文档,覆盖预期用途、关键性能与适用法规框架,直接生成受控交付物。';
|
|
const taskRunWait = page.waitForResponse(
|
|
(r) => {
|
|
try {
|
|
return new URL(r.url()).pathname.includes('/api/v1/tasks/runs') && r.request().method() === 'POST';
|
|
} catch {
|
|
return false;
|
|
}
|
|
},
|
|
{ timeout: 60_000 },
|
|
);
|
|
await input.fill(compilePrompt);
|
|
await page.getByRole('button', { name: /发送/ }).click();
|
|
const taskRes = await taskRunWait;
|
|
if (taskRes.status() < 200 || taskRes.status() >= 300) {
|
|
throw new Error(`compile tasks/runs HTTP ${taskRes.status()}`);
|
|
}
|
|
await waitForSquadCompletion(page, '请基于本项目编制首轮', 420_000);
|
|
const docId = await openWorkflowDocByLabel(page, /用户初始需求|需求摸底|intake/i);
|
|
if (!/^doc_proj\d+_/.test(docId)) {
|
|
throw new Error(`expected server doc_proj* slot, got ${docId}`);
|
|
}
|
|
state.newDocId = docId;
|
|
const token = await sessionToken(page);
|
|
const first = await getContent(page, docId, token);
|
|
expect(first.status, JSON.stringify(first.body)).toBe(200);
|
|
expect(first.body.markdown).toBeTruthy();
|
|
expect(['aligned', 'missing', 'drift', 'empty', 'ledger_missing']).toContain(first.body.consistency);
|
|
expect(first.body.error_code ?? '').not.toMatch(/PROJECTION/);
|
|
state.newMarkdown = String(first.body.markdown);
|
|
await expect(page.getByText('加载失败,请点击重试')).toHaveCount(0);
|
|
await page.screenshot({ path: path.join(SCREENSHOTS, '07-new-doc.png'), fullPage: true });
|
|
|
|
await page.reload();
|
|
await expect(currentProjectBannerName(page, PROJECT_NAME)).toBeVisible({ timeout: 30_000 });
|
|
const afterReload = await getContent(page, docId, token);
|
|
expect(afterReload.status).toBe(200);
|
|
expect(afterReload.body.markdown).toBe(state.newMarkdown);
|
|
await expect(page.getByText('加载失败,请点击重试')).toHaveCount(0);
|
|
await page.screenshot({ path: path.join(SCREENSHOTS, '08-new-doc-reload.png'), fullPage: true });
|
|
|
|
await logoutViaUI(page);
|
|
await login(page, ADMIN_USER, ADMIN_PASS);
|
|
await page.getByRole('complementary').getByTitle(PROJECT_NAME, { exact: true }).click({ force: true });
|
|
await expect(currentProjectBannerName(page, PROJECT_NAME)).toBeVisible({ timeout: 30_000 });
|
|
const afterRelogin = await getContent(page, docId, await sessionToken(page));
|
|
expect(afterRelogin.status).toBe(200);
|
|
expect(afterRelogin.body.markdown).toBe(state.newMarkdown);
|
|
await page.screenshot({ path: path.join(SCREENSHOTS, '09-new-doc-relogin.png'), fullPage: true });
|
|
await page.close();
|
|
});
|
|
|
|
test('cross-project and wrong-stage GET stay DOCUMENT_NOT_FOUND', async ({ browser }: { browser: Browser }) => {
|
|
if (!state.newDocId) {
|
|
throw new Error('new project document id missing; compile test did not leave a ledger');
|
|
}
|
|
const outsider = await browser.newPage();
|
|
await login(outsider, USER_D, FINAL_PASS_D);
|
|
const tokenD = await sessionToken(outsider);
|
|
const cross143 = await getContent(outsider, TARGET_DOC, tokenD);
|
|
expectClosed404(cross143.status, cross143.body, 'D vs 143');
|
|
const crossNew = await getContent(outsider, state.newDocId, tokenD);
|
|
expectClosed404(crossNew.status, crossNew.body, 'D vs new project doc');
|
|
await outsider.close();
|
|
|
|
const reviewer = await browser.newPage();
|
|
await login(reviewer, USER_B, FINAL_PASS_B);
|
|
const tokenB = await sessionToken(reviewer);
|
|
const otherStage = state.newDocId.replace(/_[a-z0-9]+$/i, '_design_control');
|
|
const wrongDuty = await getContent(reviewer, otherStage, tokenB);
|
|
expectClosed404(wrongDuty.status, wrongDuty.body, 'B vs wrong stage-duty');
|
|
const stillClosed143 = await getContent(reviewer, TARGET_DOC, tokenB);
|
|
expectClosed404(stillClosed143.status, stillClosed143.body, 'B vs 143');
|
|
await reviewer.screenshot({ path: path.join(SCREENSHOTS, '10-fail-closed.png'), fullPage: true });
|
|
await reviewer.close();
|
|
});
|
|
});
|