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

This commit is contained in:
Andras Schmelczer 2026-06-10 21:51:25 +01:00
parent 29de505fe2
commit 66da22d0c4
9 changed files with 290 additions and 73 deletions

View 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);
});
});