mirror of
https://github.com/samvallad33/vestige.git
synced 2026-07-26 23:51:02 +02:00
Turn all 17 sparse organs from text-on-black voids into full-bleed, moving, bioluminescent WebGPU fields. Each cell maps to a real backend fact; verified live on the real brain with a decoded fill% + motion + zero-error probe. Foundation: one reusable LivingFieldPass (src/lib/observatory/field/) does splat -> separable-blur (render passes) -> membrane base coat -> HDR cells with orbital drift and a CPU pick-mirror. Each organ writes a ~20-line data->cells mapper (cell-layout.ts: layoutGalaxy / layoutRings / FIELD_HUE). Organs brought alive (fill% before -> after, all zero WebGPU errors): stats 0.8->80.9, observatory 10.8->81.4, graph 12.3->82.4, memories ->78.4, blackbox 18.2->83.3, contradictions 1.6->69.6, dreams 0.3->64.5, settings 2.7->76.4, reasoning 2.7->39.2, patterns 7.1->45.6, activation 10->64.9, feed ->89, explore ->83.3, intentions ->66.6, importance ->73.7, memory-prs ->74.4, schedule ->37.1. Backend surfaces: add api.memoryChangelog() + ChangelogResponse; surface suppress/unsuppress on the memories field (shift/alt-click -> scar). Tests: palace-launch-gate now asserts fillPct>30 + isAnimating (not just non-black), with a longer settle for reasoning's deep_reference and a raised timeout for the curated tour. all-routes-smoke treats settings as the WebGPU field it now is. Gates green: check 0/0, build ok, palace-launch-gate 22/22 + tour, all-routes-smoke 20/20. Protected work untouched: MemoryCinema, graph/cinema, observatory scene shaders, node-renderer, palace-map. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
241 lines
9.2 KiB
TypeScript
241 lines
9.2 KiB
TypeScript
// ─────────────────────────────────────────────────────────────────────────────
|
||
// v2.2 Pulse Toast — E2E behaviour proof
|
||
//
|
||
// These tests exercise the InsightToast overlay (mounted in the root layout
|
||
// at /src/routes/+layout.svelte) via the Settings page's "✦ Preview Pulse"
|
||
// button. That button calls `fireDemoSequence()` which pushes 4 synthetic
|
||
// toasts directly through the toast store — no WebSocket or MCP backend
|
||
// required. This is why the pulse-toast tests are backend-agnostic and
|
||
// should run reliably in CI without vestige-mcp.
|
||
//
|
||
// Coverage:
|
||
// 1. First toast appears within 500ms of clicking Preview Pulse
|
||
// 2. At peak of the 3.2s sequence, >= 2 toasts are visible simultaneously
|
||
// 3. Click-to-dismiss removes a toast
|
||
// 4. Hover pauses the 5.5s dwell timer (toast survives 8s hover)
|
||
// 5. Keyboard Enter on focused toast dismisses it
|
||
// 6. The `.toast-progress-fill` animation is paused during hover
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
||
import { test, expect, type Page } from '@playwright/test';
|
||
|
||
const SETTINGS_URL = '/dashboard/settings';
|
||
const TOAST = '.toast-item';
|
||
const PROGRESS_FILL = '.toast-progress-fill';
|
||
const PREVIEW_PULSE_TEXT = /Preview Pulse/i;
|
||
|
||
async function gotoSettings(page: Page) {
|
||
await page.goto(SETTINGS_URL);
|
||
// The Preview Pulse button lives inside the "Cognitive Operations" card.
|
||
// Wait for it before each test so clicks aren't racing hydration.
|
||
await expect(page.getByRole('button', { name: PREVIEW_PULSE_TEXT })).toBeVisible();
|
||
}
|
||
|
||
async function clearAllToasts(page: Page) {
|
||
// Dismiss any lingering toasts from a previous sub-test to keep counts clean.
|
||
const count = await page.locator(TOAST).count();
|
||
for (let i = 0; i < count; i++) {
|
||
const first = page.locator(TOAST).first();
|
||
if (await first.isVisible().catch(() => false)) {
|
||
await first.click({ timeout: 1000 }).catch(() => { /* race with auto-dismiss */ });
|
||
}
|
||
}
|
||
await expect(page.locator(TOAST)).toHaveCount(0, { timeout: 5000 });
|
||
}
|
||
|
||
async function firePulse(page: Page) {
|
||
await page.getByRole('button', { name: PREVIEW_PULSE_TEXT }).click();
|
||
}
|
||
|
||
// QUARANTINED (Jul 2026): the Pulse Toast is a pre-WebGPU-rework DOM
|
||
// notification component that no longer exists in the zero-DOM Cognitive OS
|
||
// dashboard (only a "Preview Pulse" trigger button remains in settings; the
|
||
// toast UI it popped was removed). These assertions look for DOM toast elements
|
||
// that are gone → "element(s) not found". Kept for history; unquarantine only
|
||
// if a DOM toast surface is reintroduced.
|
||
test.describe.fixme('v2.2 Pulse Toast — Demo sequence', () => {
|
||
test('1. first toast appears promptly after clicking Preview Pulse', async ({ page }) => {
|
||
await gotoSettings(page);
|
||
await clearAllToasts(page);
|
||
|
||
await firePulse(page);
|
||
|
||
// The first demo toast is pushed via setTimeout(..., 0) in
|
||
// fireDemoSequence. After the setTimeout, Svelte reactive pipeline
|
||
// ticks, then the toast-in CSS animation (0.32s) brings opacity from
|
||
// 0→1. On a warm Vite cache this is well under a second; on a cold
|
||
// start with parallel workers it can spike to 3–4s. The assertion
|
||
// target is "promptly" — not a perf SLA — so allow up to 5s.
|
||
await expect(page.locator(TOAST).first()).toBeVisible({ timeout: 5000 });
|
||
|
||
await page.screenshot({
|
||
path: 'e2e/screenshots/pulse-01-first-toast.png',
|
||
fullPage: false,
|
||
});
|
||
|
||
await clearAllToasts(page);
|
||
});
|
||
|
||
test('2. peak stack shows at least 2 toasts simultaneously', async ({ page }) => {
|
||
await gotoSettings(page);
|
||
await clearAllToasts(page);
|
||
|
||
await firePulse(page);
|
||
|
||
// The demo fires 4 toasts at i*800ms = 0, 800, 1600, 2400. The first
|
||
// toast has dwellMs 7000, so at t=2500 all 4 should coexist.
|
||
// Poll the count through the sequence and capture the peak.
|
||
let peak = 0;
|
||
const until = Date.now() + 3500;
|
||
while (Date.now() < until) {
|
||
const n = await page.locator(TOAST).count();
|
||
if (n > peak) peak = n;
|
||
await page.waitForTimeout(120);
|
||
}
|
||
|
||
expect(peak).toBeGreaterThanOrEqual(2);
|
||
|
||
await page.screenshot({
|
||
path: 'e2e/screenshots/pulse-02-peak-stack.png',
|
||
fullPage: false,
|
||
});
|
||
|
||
await clearAllToasts(page);
|
||
});
|
||
|
||
test('3. click-to-dismiss removes the clicked toast', async ({ page }) => {
|
||
await gotoSettings(page);
|
||
await clearAllToasts(page);
|
||
|
||
await firePulse(page);
|
||
await expect(page.locator(TOAST).first()).toBeVisible({ timeout: 1000 });
|
||
|
||
// Capture the toast's accessibility label so we can assert *that exact*
|
||
// toast disappeared, independent of how many siblings stayed on screen.
|
||
const first = page.locator(TOAST).first();
|
||
const label = await first.getAttribute('aria-label');
|
||
expect(label).toBeTruthy();
|
||
|
||
await first.click();
|
||
|
||
await expect(
|
||
page.locator(`${TOAST}[aria-label="${label}"]`)
|
||
).toHaveCount(0, { timeout: 2000 });
|
||
|
||
await clearAllToasts(page);
|
||
});
|
||
|
||
test('4. hover pauses the dwell timer (toast survives 8s hover)', async ({ page }) => {
|
||
await gotoSettings(page);
|
||
await clearAllToasts(page);
|
||
|
||
await firePulse(page);
|
||
// Let the full 3.2s demo sequence complete so no more prepends happen
|
||
// during the hover window. This avoids any subtle layout churn from
|
||
// new toasts being added above the one we're about to hover on.
|
||
await page.waitForTimeout(3500);
|
||
|
||
// After the sequence settles, the DOM stack (newest-first at index 0)
|
||
// is: [ConsolidationCompleted, MemorySuppressed, ConnectionDiscovered,
|
||
// DreamCompleted]. The last item (DreamCompleted) is the oldest
|
||
// and has the longest dwell (7000ms). It's anchored to the bottom of
|
||
// the stack — the most position-stable element. Hover-pause it and
|
||
// verify it outlives 8s of hover.
|
||
const stack = page.locator(TOAST);
|
||
const count = await stack.count();
|
||
expect(count).toBeGreaterThanOrEqual(2);
|
||
|
||
const target = stack.last();
|
||
const label = await target.getAttribute('aria-label');
|
||
expect(label).toBeTruthy();
|
||
|
||
await target.hover();
|
||
// Re-hover once after 3s and 6s to keep the mouse anchored on the
|
||
// element — defends against any micro-move that could trigger
|
||
// mouseleave. Pause is re-applied on each mouseenter, so this is safe.
|
||
await page.waitForTimeout(3000);
|
||
await target.hover();
|
||
await page.waitForTimeout(3000);
|
||
await target.hover();
|
||
await page.waitForTimeout(2200);
|
||
|
||
// If DreamCompleted's raw dwell (7s) without hover would have expired
|
||
// by now (t ≈ 8.2s after hover start); hover-pause must have kept it
|
||
// alive.
|
||
await expect(target).toBeVisible();
|
||
|
||
await page.screenshot({
|
||
path: 'e2e/screenshots/pulse-03-hover-pause.png',
|
||
fullPage: false,
|
||
});
|
||
|
||
// Mouseleave — move cursor far away to a non-interactive region.
|
||
// After leaving, dwell resumes with whatever time was remaining
|
||
// (>= 200ms floor). The toast should vanish within ~8s (generous
|
||
// to accommodate the 7000ms raw dwell plus a safety margin).
|
||
await page.mouse.move(0, 0);
|
||
await expect(
|
||
page.locator(`${TOAST}[aria-label="${label}"]`)
|
||
).toHaveCount(0, { timeout: 10_000 });
|
||
|
||
await clearAllToasts(page);
|
||
});
|
||
|
||
test('5. keyboard Enter on focused toast dismisses it', async ({ page }) => {
|
||
await gotoSettings(page);
|
||
await clearAllToasts(page);
|
||
|
||
await firePulse(page);
|
||
const first = page.locator(TOAST).first();
|
||
await expect(first).toBeVisible({ timeout: 1000 });
|
||
|
||
const label = await first.getAttribute('aria-label');
|
||
const target = page.locator(`${TOAST}[aria-label="${label}"]`);
|
||
|
||
// Toasts are <button> elements — they're focusable. focus() avoids
|
||
// relying on tab-order through the Settings page, which is unstable
|
||
// (depends on stat cards, form fields, and other affordances above it).
|
||
await target.focus();
|
||
await expect(target).toBeFocused();
|
||
|
||
await page.keyboard.press('Enter');
|
||
|
||
await expect(target).toHaveCount(0, { timeout: 2000 });
|
||
|
||
await clearAllToasts(page);
|
||
});
|
||
|
||
test('6. progress-fill animation is paused while hovering', async ({ page }) => {
|
||
await gotoSettings(page);
|
||
await clearAllToasts(page);
|
||
|
||
await firePulse(page);
|
||
const first = page.locator(TOAST).first();
|
||
await expect(first).toBeVisible({ timeout: 1000 });
|
||
|
||
// Hover, then read the computed animation-play-state on the inner fill.
|
||
// CSS rule: `.toast-item:hover .toast-progress-fill { animation-play-state: paused; }`.
|
||
await first.hover();
|
||
|
||
const playState = await first.locator(PROGRESS_FILL).evaluate(
|
||
(el) => window.getComputedStyle(el).animationPlayState
|
||
);
|
||
expect(playState).toBe('paused');
|
||
|
||
// Sanity: moving away resumes the animation.
|
||
await page.mouse.move(0, 0);
|
||
// Small settle so hover styles detach cleanly.
|
||
await page.waitForTimeout(100);
|
||
|
||
// The same toast may have already been dismissed in the small window.
|
||
// Guard the assertion so we don't fail on race between resume + dwell.
|
||
if (await first.isVisible().catch(() => false)) {
|
||
const running = await first.locator(PROGRESS_FILL).evaluate(
|
||
(el) => window.getComputedStyle(el).animationPlayState
|
||
);
|
||
expect(running).toBe('running');
|
||
}
|
||
|
||
await clearAllToasts(page);
|
||
});
|
||
});
|