Files
DemonGatanjieu-Anomalous_Mo…/tests/ai_side.html
T

116 lines
6.7 KiB
HTML

<!doctype html>
<meta charset="utf-8">
<title>AI stage browser regression</title>
<link rel="stylesheet" href="../web/styles/40-ai-side.css">
<link rel="stylesheet" href="../web/styles/38-ai-panels.css">
<link rel="stylesheet" href="../web/styles/45-ai-side-views.css">
<style>
body { background: #202027; color: #eee; font: 14px sans-serif; --amb-link: #a58af9; --amb-text-soft: #ccc; --amb-text-dim: #aaa; --amb-ink-rgb: 255,255,255; }
#stage { width: 620px; height: 420px; border: 1px solid #777; }
#results { white-space: pre-wrap; }
</style>
<!-- Only the unused ComfyUI canvas boundary is stubbed; panel DOM and CSS are real. -->
<script type="importmap">{"imports":{"/scripts/app.js":"data:text/javascript,export const app = {};","/scripts/api.js":"data:text/javascript,export const api = new EventTarget();"}}</script>
<div id="results">Running…</div>
<div id="switch"></div>
<div id="stage" class="anomalous-ai-stage"></div>
<script type="module">
import { renderStage, stageNews, stageBadge, focusPanel } from '../web/modules/ui_ai_side.js';
import { sideSwitch } from '../web/modules/ui_ai_side_views.js';
const box = document.querySelector('#stage');
const results = document.querySelector('#results');
const stage = {};
const ctx = { numberOf: id => id, tab() {}, changed() {}, close() {
stage.hidden = true;
box.style.display = 'none';
} };
const panel = id => ({ id, kind: 'panel', panel: { id, title: `Panel ${id}`, blocks:
Array.from({ length: 25 }, (_, n) => ({ type: 'text', text: `${id}: line ${n + 1}` })) }, answers: {} });
let task = { id: 'historical', steps: [panel('1')] };
const settle = () => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
const body = () => box.querySelector('.anomalous-ai-stage-body');
const bottom = () => body().scrollHeight - body().clientHeight - body().scrollTop < 2;
const check = (value, label) => { if (!value) throw Error(label); results.textContent += `PASS ${label}\n`; };
async function draw() { const news = stageNews(task, stage); renderStage(box, task, stage, ctx, news.arrived); await settle(); }
try {
results.textContent = '';
const badgeState = {};
const badgeTask = { id: 'badge-test', steps: [panel('a')] };
stageNews(badgeTask, badgeState);
check(stageBadge(badgeTask, badgeState, false).count === 1 && !badgeState.panelUnread, 'historical content has a count without a false unread dot');
badgeTask.steps.push(panel('b'));
check(stageBadge(badgeTask, badgeState, false).unread, 'new panel while hidden lights unread dot');
check(stageBadge(badgeTask, badgeState, false).unread, 'refresh keeps unread dot');
check(!stageBadge(badgeTask, badgeState, true).unread, 'opening the stage clears unread dot');
badgeTask.steps[0].panel.title = 'Older panel edited';
check(stageBadge(badgeTask, badgeState, false).unread, 'editing an older panel lights unread dot too');
const badgeBar = sideSwitch({ view: 'map' }, true, { stage: stageBadge(badgeTask, badgeState, false), asking: 0 }, () => {});
document.querySelector('#switch').append(badgeBar);
check(badgeBar.firstChild.classList.contains('is-unread') && badgeBar.firstChild.querySelector('.is-total').textContent === '2',
'stage button renders muted total and unread indicator');
check(badgeBar.firstChild.getAttribute('aria-label').includes('有未查看'), 'unread is also announced to assistive technology');
check(!stageBadge(badgeTask, badgeState, true).unread, 'viewing the update clears the dot');
badgeTask.steps[0].panel.title = 'Live update';
check(!stageBadge(badgeTask, badgeState, true).unread, 'visible updates do not create unread reminders');
stageNews({ id: 'other-badge-task', steps: [] }, badgeState);
const emptyBadge = stageBadge({ steps: [] }, badgeState, false);
check(emptyBadge.count === 0 && !emptyBadge.unread, 'another empty task clears count and unread state');
await draw();
check(bottom() && body().scrollTop > 100, 'historical single tall panel opens at bottom');
task.steps.push(panel('2'));
await draw();
check(bottom(), 'new tall panel follows to its bottom');
check([...body().children].map(el => el.dataset.panelId).join(',') === '1,2', 'oldest first');
task.steps[1].panel.blocks.push({ type: 'text', text: 'new content '.repeat(80) });
await draw();
check(bottom(), 'update_panel follows with unchanged panel count');
const img = document.createElement('img');
body().lastElementChild.append(img);
img.style.height = '250px';
img.dispatchEvent(new Event('load'));
await settle();
check(bottom(), 'late image layout follows to bottom');
body().scrollTop = 100;
body().dispatchEvent(new Event('scroll'));
task.steps[1].panel.blocks.push({ type: 'text', text: 'another update' });
await draw();
check(body().scrollTop === 100 && stage.unseen, 'reading earlier content stays put on update');
box.querySelector('.anomalous-ai-stage-new').click();
await settle();
check(bottom() && !stage.unseen && !box.querySelector('.anomalous-ai-stage-new'), 'see latest resumes bottom follow');
focusPanel(stage, '1');
await draw();
const deadline = performance.now() + 3000;
while (body().scrollTop >= 10 && performance.now() < deadline) await settle();
check(body().scrollTop < 10, 'timeline link goes to panel start');
const unchanged = body().firstElementChild;
await draw();
check(body().firstElementChild === unchanged, 'unchanged panels keep their DOM');
task = { id: 'hidden', steps: [panel('3')] };
box.style.display = 'none';
await draw();
check(!stage.placed, 'hidden stage waits for visible layout');
box.style.display = 'flex';
await draw();
check(bottom() && stage.placed, 'showing stage tab positions latest');
task.steps[0].panel.blocks.push({ type: 'input', id: 'draft', question: 'Draft' });
await draw();
const draft = box.querySelector('input');
draft.value = 'keep this draft';
const keptPanel = body().firstElementChild;
box.querySelector('[aria-label="关闭展台"]').click();
check(stage.hidden && !box.clientHeight, 'close button hides stage');
await draw();
check(stage.hidden, 'task refresh does not reopen stage');
focusPanel(stage, '3');
box.style.display = 'flex';
await draw();
check(!stage.hidden && body().firstElementChild === keptPanel && draft.value === 'keep this draft',
'timeline entry reopens without discarding drafts');
stage.hidden = true;
stageNews({ id: 'another-task', steps: [] }, stage);
check(!stage.hidden, 'another task resets closed state');
results.textContent += 'ALL PASSED';
} catch (error) { results.textContent += `FAIL ${error.stack}`; }
</script>