Fix visual bugs
Some checks failed
CI / Frontend lint (push) Successful in 21s
CI / Backend tests (push) Successful in 23s
CI / Frontend unit tests (push) Successful in 21s
CI / Frontend build (push) Successful in 20s
Docker / build-and-push (push) Successful in 51s
CI / Playwright e2e (push) Failing after 2m0s
Some checks failed
CI / Frontend lint (push) Successful in 21s
CI / Backend tests (push) Successful in 23s
CI / Frontend unit tests (push) Successful in 21s
CI / Frontend build (push) Successful in 20s
Docker / build-and-push (push) Successful in 51s
CI / Playwright e2e (push) Failing after 2m0s
This commit is contained in:
parent
29de505fe2
commit
66da22d0c4
9 changed files with 290 additions and 73 deletions
108
frontend/e2e/navigation-no-fall.spec.ts
Normal file
108
frontend/e2e/navigation-no-fall.spec.ts
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Regression: navigating between pages must NOT replay the falling animation.
|
||||
*
|
||||
* The bug: `lt-page` (and its date-range slider) were reused across navigation,
|
||||
* so the previous page's stale `dateRange` was applied to the destination page's
|
||||
* towers on their first render. When the destination's blocks are NEWER than the
|
||||
* stale range they render off-screen (ascending) and then visibly "fall" in a
|
||||
* frame later when the slider corrects the range — i.e. all blocks fall at once.
|
||||
* The fix rebuilds the `lt-page` subtree on page-id change (fresh slider/filter),
|
||||
* matching a reload. This guards the destination ("Newer") page against falling.
|
||||
*
|
||||
* We detect a fall directly: hook the WAAPI `el.animate` used by `playFall` and
|
||||
* the `.descend`/`.ascend` CSS `transitionrun`, both on `lt-block` hosts. These
|
||||
* fire at the JS layer regardless of headless rendering, so the check is stable.
|
||||
*
|
||||
* The two pages are seeded into the offline cache with DISJOINT date ranges
|
||||
* (destination strictly newer) — the precise condition that triggered the bug.
|
||||
*/
|
||||
test.describe('navigation does not replay the falling animation', () => {
|
||||
test('navigating to a newer-dated page keeps blocks at rest', async ({ page }) => {
|
||||
const nowSec = Math.floor(Date.now() / 1000);
|
||||
const HOUR = 3600;
|
||||
const DAY = 86400;
|
||||
|
||||
await page.addInitScript(
|
||||
({ nowSec, HOUR, DAY }) => {
|
||||
const uuid = () => (crypto as Crypto).randomUUID();
|
||||
// baseAgeDays/spacingHrs place each page's done blocks in its own window.
|
||||
const tower = (name: string, h: number, baseAgeDays: number) => ({
|
||||
id: uuid(),
|
||||
name,
|
||||
base_color: { h, s: 0.7, l: 0.55 },
|
||||
blocks: [
|
||||
{ id: uuid(), tag: 't', description: 'pending', is_done: false, difficulty: 2, created_at: nowSec },
|
||||
...Array.from({ length: 6 }, (_unused, i) => ({
|
||||
id: uuid(),
|
||||
tag: 't',
|
||||
description: `done ${i}`,
|
||||
is_done: true,
|
||||
difficulty: 1 + (i % 3),
|
||||
created_at: nowSec - baseAgeDays * DAY - (i + 1) * 6 * HOUR,
|
||||
})),
|
||||
],
|
||||
});
|
||||
const mkPage = (name: string, h: number, baseAgeDays: number) => ({
|
||||
id: uuid(),
|
||||
name,
|
||||
hide_create_tower_button: false,
|
||||
keep_tasks_open: false,
|
||||
default_date_from: null,
|
||||
default_date_to: null,
|
||||
towers: [tower(name + ' A', h, baseAgeDays), tower(name + ' B', h + 0.2, baseAgeDays)],
|
||||
});
|
||||
// Page 0 ("Older") is the post-reload default; "Newer" is strictly more recent.
|
||||
const tree = { pages: [mkPage('Older', 0.05, 6), mkPage('Newer', 0.55, 1)] };
|
||||
const token = uuid();
|
||||
localStorage.setItem('life-towers.token.v4', token);
|
||||
localStorage.setItem('life-towers.cache.v4.' + token, JSON.stringify(tree));
|
||||
|
||||
// Fall detectors on lt-block hosts (WAAPI playFall + CSS descend/ascend).
|
||||
(window as unknown as { __falls: number }).__falls = 0;
|
||||
const isBlock = (el: EventTarget | null) =>
|
||||
el instanceof Element && el.tagName === 'LT-BLOCK';
|
||||
const origAnimate = Element.prototype.animate;
|
||||
Element.prototype.animate = function (kf: unknown, opts?: unknown) {
|
||||
try {
|
||||
const s = JSON.stringify(kf ?? '');
|
||||
if (isBlock(this) && (s.includes('translateY') || s.includes('transform')))
|
||||
(window as unknown as { __falls: number }).__falls++;
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return origAnimate.call(this, kf as any, opts as any);
|
||||
};
|
||||
document.addEventListener(
|
||||
'transitionrun',
|
||||
(e) => {
|
||||
if ((e as TransitionEvent).propertyName === 'transform' && isBlock(e.target))
|
||||
(window as unknown as { __falls: number }).__falls++;
|
||||
},
|
||||
true,
|
||||
);
|
||||
},
|
||||
{ nowSec, HOUR, DAY },
|
||||
);
|
||||
|
||||
await page.goto('/');
|
||||
// Lands on "Older"; wait for its stack to render and settle.
|
||||
await page.waitForSelector('lt-block', { timeout: 15000 });
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Reset the detector right before navigation, then switch to "Newer".
|
||||
await page.evaluate(() => ((window as unknown as { __falls: number }).__falls = 0));
|
||||
await page.locator('lt-select-add .top').first().click();
|
||||
await page.locator('lt-select-add .option', { hasText: 'Newer' }).first().click();
|
||||
|
||||
// Give any fall a full beat to fire (the animation is 1.5s).
|
||||
await page.waitForTimeout(1800);
|
||||
|
||||
const falls = await page.evaluate(() => (window as unknown as { __falls: number }).__falls);
|
||||
expect(falls, 'block fall animations fired on navigation').toBe(0);
|
||||
// Sanity: the destination page actually rendered its blocks.
|
||||
expect(await page.locator('lt-block').count()).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue