mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-10-27 07:51:52 +00:00
TASK-conversation-console-workflow: refresh stale previews and focus deep links
This commit is contained in:
1 parent
a67a04d57f
commit
ffff333f48
4 files changed
+187
-10
No files matched your search
+20
-4
@@ -2570,10 +2570,14 @@ test_integrity_note:
|
||||
given: "The existing dashboard scenarios and eleven screenshot destinations run in their established order"
|
||||
when: "The driver selects each target section's supported fragment immediately before its unchanged checks and captures"
|
||||
then: "Each original assertion and capture executes against the selected view without changing capture order, viewport coverage, artifact destination, or artifact safeguards"
|
||||
- id: SCN-dashboard-draft-preview-invalidates-on-newer-evidence
|
||||
given: "A task priority preview is created from task evidence revision N"
|
||||
when: "A separate confirmed sample action advances that task to authoritative revision N+1, the owner navigates away and back, and refresh loads the newer evidence"
|
||||
then: "The revision N token, preview, and confirmation control are absent; only a newly generated preview bound to revision N+1 can be presented"
|
||||
- id: SCN-console-priority-preview-invalidates-on-newer-evidence
|
||||
given: "A task priority preview and unsent priority and reason values are created from task evidence revision N"
|
||||
when: "A separate confirmed sample API action advances that task to authoritative revision N+1 and Console refresh loads the newer evidence"
|
||||
then: "The revision N token, preview, and confirmation control are absent; revision N+1 is visible, the unsent values remain intact and editable, and only a newly generated preview bound to N+1 can be presented"
|
||||
- id: SCN-console-current-preview-and-draft-survive-unchanged-refresh
|
||||
given: "A task has an active priority preview and unsent priority and reason values bound to its current evidence revision"
|
||||
when: "Console refresh returns the same task revision"
|
||||
then: "The preview token, review panel, confirmation boundary, and unsent field values remain available unchanged"
|
||||
- id: SCN-console-view-navigation
|
||||
given: "The Console has addressable work, fleet, evidence, and flow-watch views"
|
||||
when: "A reader opens fragments, follows navigation, and uses browser Back and Forward"
|
||||
@@ -2582,6 +2586,14 @@ test_integrity_note:
|
||||
given: "The Console is rendered at a 390x844 viewport"
|
||||
when: "Each supported destination is selected"
|
||||
then: "Every navigation destination remains visible and the document and navigation have no horizontal overflow"
|
||||
- id: SCN-console-view-title-mapping
|
||||
given: "The Console loads Overview and a reader selects each other supported view"
|
||||
when: "The reader returns to Overview after visiting contextual views"
|
||||
then: "Overview has the exact title TORCH Dashboard on initial and return navigation; all other views keep their exact contextual titles"
|
||||
- id: SCN-console-secondary-deeplink-focus-and-history
|
||||
given: "The Console is loaded at the organization-proposals or approval-request-list fragment"
|
||||
when: "The reader opens either direct link and uses browser Back and Forward"
|
||||
then: "Exactly the matching view is selected and the actual secondary target is focused inside the viewport"
|
||||
- id: SCN-console-view-drafts
|
||||
given: "A task draft and an owner approval preview are present"
|
||||
when: "The reader changes views and refreshes the sample Console"
|
||||
@@ -2589,11 +2601,15 @@ test_integrity_note:
|
||||
observable_outcomes:
|
||||
- "Selected fragment, visible view, original assertion results, and the unchanged ordered screenshot set"
|
||||
- "Revision N+1 displayed after refresh, with the old preview token and confirmation control unavailable"
|
||||
- "Unsent priority and reason values retained and editable after newer evidence invalidates the old preview"
|
||||
- "An unchanged current-revision preview, confirmation boundary, and draft retained after refresh"
|
||||
- "A regenerated preview token and displayed revision bound to N+1"
|
||||
- "Exact Overview and contextual view titles, plus focused in-viewport secondary targets across direct loads and browser history"
|
||||
- "No browser requests to live project APIs and no changes to tracked screenshot expectations"
|
||||
determinism_controls:
|
||||
- "The existing isolated sample Console, fixed Playwright clock, and fixed desktop/mobile viewports"
|
||||
- "Newer evidence is introduced through the sample's supported preview/confirm API before an explicit refresh"
|
||||
- "The browser test advances only the isolated sample API; it never submits the priority form or mutates a live project"
|
||||
- "No network, live project mutation, retry, sleep, timeout, golden update, or direct fixture-source change"
|
||||
anti_cheat_rationale:
|
||||
prevents:
|
||||
|
||||
@@ -332,14 +332,28 @@ try {
|
||||
'revision N preview must not remain presented after revision N+1 is refreshed');
|
||||
assert.equal(await refreshedForm.locator('[data-priority-confirm]').count(), 0,
|
||||
'the stale revision N preview cannot be confirmed as current');
|
||||
assert.equal(await refreshedForm.locator('[name="priority"]').inputValue(), 'urgent', 'the unsent priority choice must survive invalidation');
|
||||
assert.equal(await refreshedForm.locator('[name="reason"]').inputValue(), 'Review this change against the current task evidence.',
|
||||
'the unsent reason must survive invalidation');
|
||||
assert.equal(await refreshedForm.locator('[name="priority"]').isDisabled(), false, 'the preserved priority choice remains editable');
|
||||
assert.equal(await refreshedForm.locator('[name="reason"]').isDisabled(), false, 'the preserved reason remains editable');
|
||||
assert.match(await refreshedForm.locator('.priority-change-status').innerText(), new RegExp(`revision ${revisionN1}`),
|
||||
'the visible status identifies current task evidence');
|
||||
await refreshedTicket.locator('.priority-change > summary').click();
|
||||
await refreshedForm.locator('[name="priority"]').selectOption('urgent');
|
||||
await refreshedForm.locator('[name="reason"]').fill('Review again using the refreshed task evidence.');
|
||||
await refreshedForm.getByRole('button', { name: 'Review change', exact: true }).click();
|
||||
await refreshedForm.locator('[data-priority-confirm]').waitFor();
|
||||
assert.equal(await refreshedForm.locator('[data-priority-preview]').locator('dd').nth(2).innerText(), String(revisionN1));
|
||||
assert.notEqual(await refreshedForm.getAttribute('data-preview-token'), staleToken,
|
||||
'a current preview must be regenerated from revision N+1');
|
||||
const currentToken = await refreshedForm.getAttribute('data-preview-token');
|
||||
await stalePage.locator('#refresh-console').click();
|
||||
await stalePage.locator('#live-refresh-status[data-generation="3"]').waitFor();
|
||||
assert.equal(await refreshedForm.getAttribute('data-preview-token'), currentToken,
|
||||
'unchanged current evidence must retain its active preview');
|
||||
assert.equal(await refreshedForm.locator('[data-priority-preview]').isVisible(), true);
|
||||
assert.equal(await refreshedForm.locator('[data-priority-confirm]').isVisible(), true);
|
||||
assert.equal(await refreshedForm.locator('[name="priority"]').inputValue(), 'urgent');
|
||||
assert.equal(await refreshedForm.locator('[name="reason"]').inputValue(), 'Review this change against the current task evidence.');
|
||||
await stalePage.close();
|
||||
assert.deepEqual(errors, []);
|
||||
assert.deepEqual(apiRequests, [], 'demo must never contact live project APIs');
|
||||
|
||||
+41
-3
@@ -45,15 +45,19 @@ function installConsoleViewRouting() {
|
||||
});
|
||||
const workspace = document.querySelector('.console-workspace');
|
||||
if (workspace) workspace.dataset.currentView = view;
|
||||
document.title = `${definition.title} | TORCH Local Fleet Console`;
|
||||
document.title = view === 'overview' ? 'TORCH Dashboard' : `${definition.title} | TORCH Local Fleet Console`;
|
||||
const announcement = document.querySelector('#console-view-status');
|
||||
if (announcement) announcement.textContent = `Viewing ${definition.title}.`;
|
||||
|
||||
if (focus) {
|
||||
const targetId = decodeHash(hash);
|
||||
const target = document.getElementById(targetId);
|
||||
if (target && target.dataset.consoleView !== view && target.closest('[data-console-view]')) {
|
||||
target.scrollIntoView?.({ block: 'start' });
|
||||
const targetView = target?.closest('[data-console-view]')?.dataset.consoleView;
|
||||
if (target && targetId !== view && targetView === view) {
|
||||
if (!target.hasAttribute('tabindex') && !target.matches('a[href], button, input, select, textarea, summary')) {
|
||||
target.tabIndex = -1;
|
||||
}
|
||||
target.scrollIntoView?.({ block: 'start', behavior: 'instant' });
|
||||
target.focus?.({ preventScroll: true });
|
||||
} else {
|
||||
const labelledBy = visible[0]?.getAttribute('aria-labelledby');
|
||||
@@ -92,6 +96,39 @@ refreshProtection.remember();
|
||||
let renderingSnapshot = false;
|
||||
let heldPanels = 0;
|
||||
|
||||
function invalidateStalePriorityPreviews(tasks = []) {
|
||||
const currentById = new Map(tasks.map((task) => [task.id, task]));
|
||||
for (const form of document.querySelectorAll('.priority-change-form[data-preview-token]')) {
|
||||
const task = currentById.get(form.dataset.taskId);
|
||||
const currentRevision = Number(task?.revision);
|
||||
const previewRevision = Number(form.dataset.revision);
|
||||
if (!task || !Number.isSafeInteger(currentRevision) || currentRevision === previewRevision) continue;
|
||||
|
||||
const ticket = form.closest('.task-ticket');
|
||||
if (!ticket) continue;
|
||||
const priority = form.querySelector('[name="priority"]')?.value;
|
||||
const reason = form.querySelector('[name="reason"]')?.value;
|
||||
const openDetails = new Set([...ticket.querySelectorAll('details[open]')]
|
||||
.map((details) => details.querySelector(':scope > summary')?.textContent));
|
||||
const template = document.createElement('template');
|
||||
template.innerHTML = taskCard(task, currentBacklogHealth).trim();
|
||||
const refreshedTicket = template.content.firstElementChild;
|
||||
if (!refreshedTicket) continue;
|
||||
const refreshedForm = refreshedTicket.querySelector('.priority-change-form');
|
||||
if (refreshedForm) {
|
||||
const priorityField = refreshedForm.querySelector('[name="priority"]');
|
||||
const reasonField = refreshedForm.querySelector('[name="reason"]');
|
||||
if (priorityField && priority) priorityField.value = priority;
|
||||
if (reasonField) reasonField.value = reason ?? '';
|
||||
priorityStatus(refreshedForm, `Task evidence advanced to revision ${currentRevision}. The older preview was cleared; review the current task before previewing again.`);
|
||||
}
|
||||
refreshedTicket.querySelectorAll('details').forEach((details) => {
|
||||
if (openDetails.has(details.querySelector(':scope > summary')?.textContent)) details.open = true;
|
||||
});
|
||||
ticket.replaceWith(refreshedTicket);
|
||||
}
|
||||
}
|
||||
|
||||
const escapeHtml = (value) => {
|
||||
const element = document.createElement('span');
|
||||
element.textContent = value ?? '';
|
||||
@@ -1044,6 +1081,7 @@ const refreshScheduler = globalThis.TorchLiveRefresh.createScheduler({
|
||||
heldPanels = 0;
|
||||
renderingSnapshot = true;
|
||||
try { render(snapshot); } finally { renderingSnapshot = false; }
|
||||
invalidateStalePriorityPreviews(snapshot.backlog ?? []);
|
||||
refreshProtection.remember({ preserve: editedBeforeRender });
|
||||
$('#console-error').hidden = true;
|
||||
const status = $('#live-refresh-status');
|
||||
|
||||
@@ -39,12 +39,121 @@ 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(), `${title} | TORCH Local Fleet Console`);
|
||||
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-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-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 form.getByRole('button', { name: 'Review change', exact: true }).click();
|
||||
await form.locator('[data-priority-confirm]').waitFor();
|
||||
const currentToken = await form.getAttribute('data-preview-token');
|
||||
assert.notEqual(currentToken, staleToken);
|
||||
assert.equal(await form.locator('[data-priority-preview] dd').nth(2).innerText(), String(revisionN1));
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
Reference in new issue
Block a user