Files
TORCH/test/fleet/console-view-navigation.test.mjs

356 lines
20 KiB
JavaScript

import assert from 'node:assert/strict';
import { once } from 'node:events';
import test from 'node:test';
import { chromium } from '@playwright/test';
import { createConsoleServer } from '../../src/console/server.mjs';
const views = [
['overview', 'Overview'],
['owner-briefing', 'Briefing'],
['flow-watch', 'Flow watch'],
['work', 'Work'],
['initiative-progress', 'Progress'],
['fleet', 'Fleet'],
['communications', 'Conversations'],
['organization', 'Organization'],
['evidence', 'Evidence'],
['delivery', 'Release gates'],
];
async function openDemo(t, viewport) {
const server = createConsoleServer();
server.listen(0, '127.0.0.1');
await once(server, 'listening');
let browser;
t.after(async () => {
await browser?.close();
await new Promise((resolve) => server.close(resolve));
});
browser = await chromium.launch({ headless: true });
const context = await browser.newContext({ viewport });
const page = await context.newPage();
await page.clock.install({ time: new Date('2026-10-01T12:00:00.000Z') });
await page.goto(`http://127.0.0.1:${server.address().port}/console?demo=1#work`);
await page.locator('#live-refresh-status[data-generation="1"]').waitFor();
return { page, context };
}
async function assertSelectedView(page, route, title) {
const selected = await page.locator('.console-workspace > [data-console-view]:not([hidden])').evaluateAll((sections) =>
[...new Set(sections.map((section) => section.dataset.consoleView))]);
assert.deepEqual(selected, [route], `only the ${route} view should be visible`);
assert.equal(await page.title(), route === 'overview' ? 'TORCH Dashboard' : `${title} | TORCH Local Fleet Console`);
assert.equal(await page.locator(`.console-rail a[aria-current="page"][href="#${route}"]`).count(), 1);
assert.equal(await page.locator('#project-name').isVisible(), true);
assert.equal(await page.locator('#snapshot-time').isVisible(), true);
}
test('SCN-console-view-title-mapping: Overview retains its dashboard title while other selected views stay contextual', async (t) => {
const { page } = await openDemo(t, { width: 1280, height: 900 });
const base = `http://127.0.0.1:${new URL(page.url()).port}/console?demo=1`;
await page.goto(`${base}#overview`);
await page.locator('#live-refresh-status[data-generation="1"]').waitFor();
await assertSelectedView(page, 'overview', 'Overview');
for (const [route, title] of views.filter(([route]) => route !== 'overview')) {
await page.locator(`.console-rail a[href="#${route}"]`).click();
await assertSelectedView(page, route, title);
}
await page.locator('.console-rail a[href="#overview"]').click();
await assertSelectedView(page, 'overview', 'Overview');
});
test('SCN-console-owner-briefing-evidence-limitation-stays-visible: collapsed provenance keeps the warning visible', async (t) => {
const { page } = await openDemo(t, { width: 1280, height: 900 });
await page.locator('.console-rail a[href="#owner-briefing"]').click();
const publication = page.locator('#owner-briefing-content .briefing-publication');
const warning = publication.locator(':scope > p.briefing-muted').filter({ hasText: 'Recorded evidence, not independent live verification.' });
const provenance = publication.locator('details.briefing-provenance').filter({ hasText: 'Publication time and reporting window' });
assert.equal(await warning.count(), 1);
assert.equal(await warning.isVisible(), true);
assert.match(await page.locator('#owner-briefing').innerText(), /not independent live verification/);
assert.equal(await provenance.count(), 1);
assert.equal(await provenance.evaluate((details) => details.open), false);
assert.equal(await provenance.locator('time').count(), 1, 'the exact publication time remains in compact metadata');
assert.equal(await provenance.locator('time').isVisible(), false, 'publication time stays inside the closed disclosure');
});
test('SCN-console-secondary-deeplink-focus-and-history: direct fragments and history land on the actual target', async (t) => {
const { page } = await openDemo(t, { width: 390, height: 844 });
const base = `http://127.0.0.1:${new URL(page.url()).port}/console?demo=1`;
const assertTarget = async (id, route, title) => {
await assertSelectedView(page, route, title);
const state = await page.locator(`#${id}`).evaluate((target) => {
const bounds = target.getBoundingClientRect();
return {
activeId: document.activeElement?.id,
top: bounds.top,
bottom: bounds.bottom,
height: innerHeight,
};
});
assert.equal(state.activeId, id, `the ${id} target must receive focus`);
assert.ok(state.top < state.height && state.bottom > 0, `${id} must intersect the viewport: ${JSON.stringify(state)}`);
};
await page.goto(`${base}#organization-proposals`);
await page.locator('#live-refresh-status[data-generation="1"]').waitFor();
await assertTarget('organization-proposals', 'organization', 'Organization');
await page.goto(`${base}#approval-request-list`);
await page.locator('#live-refresh-status[data-generation="1"]').waitFor();
await assertTarget('approval-request-list', 'flow-watch', 'Flow watch');
await page.goBack();
await assertTarget('organization-proposals', 'organization', 'Organization');
await page.goForward();
await assertTarget('approval-request-list', 'flow-watch', 'Flow watch');
});
test('SCN-console-operational-deeplink-layout-history-and-refresh: Fleet deep links settle after first render without stealing later navigation', async (t) => {
for (const viewport of [{ width: 1280, height: 900 }, { width: 390, height: 844 }]) {
const { page } = await openDemo(t, viewport);
const base = `http://127.0.0.1:${new URL(page.url()).port}/console?demo=1`;
const assertManagerWakes = async () => {
await assertSelectedView(page, 'fleet', 'Fleet');
const state = await page.locator('#manager-wakes').evaluate((target) => {
const bounds = target.getBoundingClientRect();
return {
activeId: document.activeElement?.id,
operationsOpen: target.closest('#fleet-operations')?.open,
top: bounds.top,
bottom: bounds.bottom,
height: innerHeight,
};
});
assert.equal(state.operationsOpen, true);
assert.equal(state.activeId, 'manager-wakes');
assert.ok(state.top >= 0 && state.bottom <= state.height,
`manager-wakes must be fully inside ${viewport.width}x${viewport.height}: ${JSON.stringify(state)}`);
};
await page.goto(`${base}#manager-wakes`);
await page.locator('#live-refresh-status[data-generation="1"]').waitFor();
await assertManagerWakes();
await page.goto(`${base}#overview`);
await page.locator('#live-refresh-status[data-generation="1"]').waitFor();
await page.locator('#attention-list a[href="#manager-wakes"]').click();
await assertManagerWakes();
await page.goBack();
await assertSelectedView(page, 'overview', 'Overview');
await page.goForward();
await assertManagerWakes();
await page.locator('.console-rail a[href="#work"]').click();
const beforeRefreshRender = await page.evaluate(() => {
window.scrollTo({ top: 347, behavior: 'instant' });
return window.scrollY;
});
assert.equal(beforeRefreshRender, 347, 'the deterministic scroll stimulus must establish its exact nonzero baseline');
await page.locator('#refresh-console').evaluate((button) => {
button.dispatchEvent(new MouseEvent('click', { bubbles: true }));
});
await page.locator('#live-refresh-status[data-generation="2"]').waitFor();
assert.equal(new URL(page.url()).hash, '#work');
await assertSelectedView(page, 'work', 'Work');
assert.notEqual(await page.evaluate(() => document.activeElement?.id), 'manager-wakes');
assert.equal(await page.evaluate(() => scrollY), beforeRefreshRender,
'a later refresh must retain the exact pre-refresh user scroll position');
}
});
test('SCN-console-priority-preview-invalidates-on-newer-evidence: refresh preserves editable drafts and rejects the old preview', async (t) => {
const { page } = await openDemo(t, { width: 1280, height: 900 });
const form = page.locator('.priority-change-form[data-task-id="API-08"]');
const ticket = form.locator('xpath=../..');
await ticket.locator('.priority-change > summary').click();
await form.locator('[name="priority"]').selectOption('urgent');
await form.locator('[name="reason"]').fill('Preserve this unsent explanation across a newer task revision.');
await form.getByRole('button', { name: 'Review change', exact: true }).click();
await form.locator('[data-priority-confirm]').waitFor();
const revisionN = Number(await form.getAttribute('data-revision'));
const staleToken = await form.getAttribute('data-preview-token');
assert.equal(await form.locator('[data-priority-preview] dd').nth(2).innerText(), String(revisionN));
const revisionN1 = await page.evaluate(async (expectedRevision) => {
const path = '/api/backlog/tasks/API-08/priority';
const change = { priority: 'low', reason: 'Separate normal sample API action advances authoritative evidence.', expectedRevision };
const preview = await globalThis.TorchConsoleDemo.request(`${path}/preview`, change);
const applied = await globalThis.TorchConsoleDemo.request(path, { ...change, token: preview.token, planHash: preview.planHash });
return applied.task.revision;
}, revisionN);
assert.equal(revisionN1, revisionN + 1);
await page.locator('#refresh-console').click();
await page.locator('#live-refresh-status[data-generation="2"]').waitFor();
assert.equal(Number(await form.getAttribute('data-revision')), revisionN1);
assert.equal(await ticket.locator('.priority').textContent(), 'low');
assert.equal(await form.getAttribute('data-preview-token'), null);
assert.equal(await form.locator('[data-priority-preview]').isVisible(), false);
assert.equal(await form.locator('[data-priority-confirm]').count(), 0);
assert.equal(await form.locator('[name="priority"]').inputValue(), 'urgent');
assert.equal(await form.locator('[name="reason"]').inputValue(), 'Preserve this unsent explanation across a newer task revision.');
assert.equal(await form.locator('[name="priority"]').isDisabled(), false);
assert.equal(await form.locator('[name="reason"]').isDisabled(), false);
assert.match(await form.locator('.priority-change-status').innerText(), new RegExp(`revision ${revisionN1}`));
await page.locator('.console-rail a[href="#overview"]').click();
await page.locator('.console-rail a[href="#work"]').click();
await page.locator('#refresh-console').click();
await page.locator('#live-refresh-status[data-generation="3"]').waitFor();
const preservedForm = page.locator('.priority-change-form[data-task-id="API-08"]');
const preservedTicket = preservedForm.locator('xpath=../..');
assert.equal(Number(await preservedForm.getAttribute('data-revision')), revisionN1);
assert.equal(await preservedTicket.locator('.priority').textContent(), 'low');
assert.equal(await preservedForm.getAttribute('data-preview-token'), null);
assert.equal(await preservedForm.locator('[data-priority-preview]').isVisible(), false);
assert.equal(await preservedForm.locator('[data-priority-confirm]').count(), 0);
assert.equal(await preservedForm.locator('[name="priority"]').inputValue(), 'urgent');
assert.equal(await preservedForm.locator('[name="reason"]').inputValue(), 'Preserve this unsent explanation across a newer task revision.');
assert.equal(await preservedForm.locator('[name="priority"]').isDisabled(), false);
assert.equal(await preservedForm.locator('[name="reason"]').isDisabled(), false);
await preservedForm.getByRole('button', { name: 'Review change', exact: true }).click();
await preservedForm.locator('[data-priority-confirm]').waitFor();
const currentToken = await preservedForm.getAttribute('data-preview-token');
assert.notEqual(currentToken, staleToken);
assert.equal(await preservedForm.locator('[data-priority-preview] dd').nth(2).innerText(), String(revisionN1));
await page.locator('#refresh-console').click();
await page.locator('#live-refresh-status[data-generation="4"]').waitFor();
const currentForm = page.locator('.priority-change-form[data-task-id="API-08"]');
assert.equal(await currentForm.getAttribute('data-preview-token'), currentToken);
assert.equal(await currentForm.locator('[data-priority-preview]').isVisible(), true);
assert.equal(await currentForm.locator('[data-priority-confirm]').isVisible(), true);
assert.equal(await currentForm.locator('[name="priority"]').inputValue(), 'urgent');
assert.equal(await currentForm.locator('[name="reason"]').inputValue(), 'Preserve this unsent explanation across a newer task revision.');
});
test('SCN-console-current-preview-and-draft-survive-unchanged-refresh: a current preview and its fields remain intact', async (t) => {
const { page } = await openDemo(t, { width: 1280, height: 900 });
const form = page.locator('.priority-change-form[data-task-id="API-08"]');
const ticket = form.locator('xpath=../..');
await ticket.locator('.priority-change > summary').click();
await form.locator('[name="priority"]').selectOption('urgent');
await form.locator('[name="reason"]').fill('Keep this current unsent explanation after an unchanged refresh.');
await form.getByRole('button', { name: 'Review change', exact: true }).click();
await form.locator('[data-priority-confirm]').waitFor();
const revision = await form.getAttribute('data-revision');
const token = await form.getAttribute('data-preview-token');
await page.locator('#refresh-console').click();
await page.locator('#live-refresh-status[data-generation="2"]').waitFor();
assert.equal(await form.getAttribute('data-revision'), revision);
assert.equal(await form.getAttribute('data-preview-token'), token);
assert.equal(await form.locator('[data-priority-preview]').isVisible(), true);
assert.equal(await form.locator('[data-priority-confirm]').isVisible(), true);
assert.equal(await form.locator('[name="priority"]').inputValue(), 'urgent');
assert.equal(await form.locator('[name="reason"]').inputValue(), 'Keep this current unsent explanation after an unchanged refresh.');
});
test('SCN-console-view-navigation: fragments, direct loads, Back and Forward select one addressable view', async (t) => {
const { page } = await openDemo(t, { width: 1280, height: 900 });
await assertSelectedView(page, 'work', 'Work');
await page.locator('.console-rail a[href="#fleet"]').click();
await assertSelectedView(page, 'fleet', 'Fleet');
await page.locator('.console-rail a[href="#evidence"]').click();
await assertSelectedView(page, 'evidence', 'Evidence');
await page.goBack();
await assertSelectedView(page, 'fleet', 'Fleet');
await page.goForward();
await assertSelectedView(page, 'evidence', 'Evidence');
await page.goto(`http://127.0.0.1:${new URL(page.url()).port}/console?demo=1#approval-request-list`);
await page.locator('#live-refresh-status[data-generation="1"]').waitFor();
await assertSelectedView(page, 'flow-watch', 'Flow watch');
});
test('SCN-console-operational-deeplink-fleet: manager-wakes belongs to Fleet across direct loads and history', async (t) => {
const { page } = await openDemo(t, { width: 1280, height: 900 });
const base = `http://127.0.0.1:${new URL(page.url()).port}/console?demo=1`;
const assertManagerWakesTarget = async () => {
await assertSelectedView(page, 'fleet', 'Fleet');
assert.equal(await page.locator('#fleet-operations').isVisible(), true);
assert.equal(await page.locator('#fleet-operations').evaluate((details) => details.open), true,
'the Operations disclosure must open for its nested target');
const target = await page.locator('#manager-wakes').evaluate((element) => {
const bounds = element.getBoundingClientRect();
return { activeId: document.activeElement?.id, top: bounds.top, bottom: bounds.bottom, height: innerHeight };
});
assert.equal(target.activeId, 'manager-wakes', 'the actual fragment target must receive focus');
assert.ok(target.top < target.height && target.bottom > 0,
`manager-wakes must intersect the viewport: ${JSON.stringify(target)}`);
};
await page.goto(`${base}#manager-wakes`);
await page.locator('#live-refresh-status[data-generation="1"]').waitFor();
await assertManagerWakesTarget();
await page.locator('.console-rail a[href="#fleet"]').click();
await assertSelectedView(page, 'fleet', 'Fleet');
await page.goBack();
await assertManagerWakesTarget();
await page.goForward();
await assertSelectedView(page, 'fleet', 'Fleet');
await page.goto(`${base}#overview`);
await page.locator('#live-refresh-status[data-generation="1"]').waitFor();
await page.locator('#attention-list a[href="#manager-wakes"]').click();
await assertManagerWakesTarget();
await page.goBack();
await assertSelectedView(page, 'overview', 'Overview');
await page.goForward();
await assertManagerWakesTarget();
await page.locator('.console-rail a[href="#work"]').click();
await assertSelectedView(page, 'work', 'Work');
assert.equal(await page.locator('#fleet-operations').isVisible(), false,
'operational panels must be hidden outside the selected Fleet view');
});
test('SCN-console-mobile-navigation: every destination is visible at 390x844 without horizontal hunting', async (t) => {
const { page } = await openDemo(t, { width: 390, height: 844 });
assert.equal(await page.locator('.console-rail a').count(), views.length);
for (const [route, title] of views) {
const link = page.locator(`.console-rail a[href="#${route}"]`);
assert.equal(await link.isVisible(), true, `${title} navigation must remain visible`);
await link.click();
await assertSelectedView(page, route, title);
const widths = await page.evaluate(() => ({
viewport: innerWidth,
document: document.documentElement.scrollWidth,
navigation: document.querySelector('.console-rail').scrollWidth,
navigationClient: document.querySelector('.console-rail').clientWidth,
}));
assert.ok(widths.document <= widths.viewport, `document overflows horizontally: ${JSON.stringify(widths)}`);
assert.ok(widths.navigation <= widths.navigationClient, `navigation requires horizontal scrolling: ${JSON.stringify(widths)}`);
}
});
test('SCN-console-view-drafts: navigation and refresh retain unsent edits and the explicit owner preview boundary', async (t) => {
const { page } = await openDemo(t, { width: 1280, height: 900 });
const titleInput = page.locator('#task-create-form [name="title"]');
await page.locator('.task-create-panel > summary').click();
await titleInput.fill('Unsent route-change draft');
await page.locator('.console-rail a[href="#flow-watch"]').click();
const approval = page.locator('.approval-decision-form').first();
await approval.locator('[name="decision"]').selectOption('approved');
await approval.locator('[name="note"]').fill('Review remains explicit after navigation.');
await approval.locator('[data-approval-preview]').click();
await approval.locator('[data-approval-confirm]:visible').waitFor();
const previewToken = await approval.getAttribute('data-preview-token');
await page.locator('.console-rail a[href="#work"]').click();
await page.locator('#refresh-console').click();
await page.locator('#live-refresh-status[data-generation="2"]').waitFor();
assert.equal(await titleInput.inputValue(), 'Unsent route-change draft');
await page.locator('.console-rail a[href="#flow-watch"]').click();
assert.equal(await approval.getAttribute('data-preview-token'), previewToken);
assert.equal(await approval.locator('[data-approval-confirm]').isVisible(), true);
assert.equal(await approval.locator('[name="decision"]').isDisabled(), true);
assert.equal(await approval.locator('[data-approval-edit]').isVisible(), true);
assert.match(await approval.locator('.approval-decision-status').innerText(), /bound to this request revision/);
assert.equal(await page.locator('#approval-request-count').innerText(), '1 open');
});