diff --git a/web/e2e/board.spec.ts b/web/e2e/board.spec.ts index fef8411..7a8886a 100644 --- a/web/e2e/board.spec.ts +++ b/web/e2e/board.spec.ts @@ -64,10 +64,16 @@ test("M-3: прокрутка доски после возврата из кар await expect(page.locator(".board .card[data-key]").first()).toBeVisible(); const y = await page.evaluate(() => { window.scrollTo(0, document.documentElement.scrollHeight); return window.scrollY; }); expect(y).toBeGreaterThan(100); - // событие прокрутки приходит на следующем кадре: без ожидания страница уходит раньше, чем доска запомнит позицию - await page.evaluate(() => new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)))); - // щелчок без прокрутки к элементу: иначе Playwright сам сдвинет страницу перед переходом - await page.evaluate(() => (document.querySelector(".board .card[data-key] a.open-card") as HTMLElement).click()); + // реальный щелчок по ⓘ карточки, видимой в окне после прокрутки (иначе Playwright сам сдвинет страницу) + const key = await page.evaluate(() => { + const vh = window.innerHeight; + const a = [...document.querySelectorAll(".board .card[data-key] a.open-card")] + .find((e) => { const r = e.getBoundingClientRect(); return r.top > 200 && r.bottom < vh - 20; }); + return a?.closest(".card")?.dataset.key ?? null; + }); + expect(key).not.toBeNull(); + const link = page.locator(`.board .card[data-key="${key}"] a.open-card`); + await link.click(); await expect(page).toHaveURL(/\/app\/issues\//); await page.getByRole("link", { name: "Доска", exact: true }).click(); await expect.poll(() => page.evaluate(() => window.scrollY)).toBeGreaterThan(y - 2); diff --git a/web/src/board/components/BoardCanvas.test.tsx b/web/src/board/components/BoardCanvas.test.tsx index a8d6ce8..3ee4efd 100644 --- a/web/src/board/components/BoardCanvas.test.tsx +++ b/web/src/board/components/BoardCanvas.test.tsx @@ -141,4 +141,22 @@ describe("BoardCanvas", () => { Object.defineProperty(window, "scrollY", { configurable: true, value: 0 }); vi.restoreAllMocks(); }); + + it("M-3: запоздавшее scroll после ухода (прокрутка зажата в 0) не затирает запомненное", () => { + vi.spyOn(window, "scrollTo").mockImplementation(() => {}); + const handlers: EventListener[] = []; + const add = window.addEventListener.bind(window); + vi.spyOn(window, "addEventListener").mockImplementation((t: string, h: EventListenerOrEventListenerObject, o?: boolean | AddEventListenerOptions) => { + if (t === "scroll") handlers.push(h as EventListener); + add(t, h, o); + }); + const r = renderWithProviders(); + Object.defineProperty(window, "scrollY", { configurable: true, value: 640 }); + fireEvent.scroll(window); + r.unmount(); + Object.defineProperty(window, "scrollY", { configurable: true, value: 0 }); + handlers.forEach((h) => h(new Event("scroll"))); // событие, дошедшее до слушателя уже после снятия доски + expect(readBoardScroll()).toMatchObject({ y: 640 }); + vi.restoreAllMocks(); + }); }); diff --git a/web/src/board/components/BoardCanvas.tsx b/web/src/board/components/BoardCanvas.tsx index 1f8865b..4915326 100644 --- a/web/src/board/components/BoardCanvas.tsx +++ b/web/src/board/components/BoardCanvas.tsx @@ -146,7 +146,9 @@ export function BoardCanvas({ vis, byDate, snap, today, store, sprintSel, emptyT // под короткую страницу карточки useEffect(() => { const w = wrapRef.current; - const save = () => saveBoardScroll({ x: window.scrollX, y: window.scrollY, left: w?.scrollLeft ?? 0 }); + // после ухода со страницы браузер зажимает прокрутку короткой страницы в 0 и шлёт scroll: доску уже убрали из DOM — + // такое событие не должно затирать запомненное + const save = () => { if (w && !w.isConnected) return; saveBoardScroll({ x: window.scrollX, y: window.scrollY, left: w?.scrollLeft ?? 0 }); }; window.addEventListener("scroll", save, { passive: true }); w?.addEventListener("scroll", save, { passive: true }); return () => { window.removeEventListener("scroll", save); w?.removeEventListener("scroll", save); };