mirror of
https://github.com/penpot/penpot.git
synced 2026-09-24 04:46:14 +00:00
* 🐛 Preserve layers panel scroll position across tab switches Fixes #7440. Switching between the Layers, Assets and Tokens tabs in the workspace left sidebar unmounts the active panel component, causing its scroll position to reset to the top on re-entry. Add a module-level `scroll-positions` atom keyed by page-id. The layers scroll handler now also saves the current scrollTop value into the atom; a `mf/with-effect` on the page-id dep restores it whenever the `layers-toolbox*` component mounts or the page changes. Co-Authored-By: Paperclip <noreply@paperclip.ing> * 🐛 Preserve sidebar scroll positions across tab switches Replace the Layers-only global atom with a scroll store held in a use-var in left-sidebar*, shared by the Layers, Assets and Tokens panels through a new sidebar.scroll helper. Positions are keyed per panel and page (or token set) and restore waits for list content to settle, so deep positions in lazily rendered lists survive tab switches. Closes #7440. AI-assisted-by: muse-spark-1.3-contributor * 🐛 Add e2e coverage for sidebar scroll preservation Port the regression tests from closed PR #7544 for issue #7440, adapted to the current ref-based implementation and fixtures: async restore needs polled assertions, and setup uses the shared tokens helpers. Also add data-scroll-container hooks to the Assets and Tokens scroll containers so the specs can locate them. AI-assisted-by: muse-spark-1.3-contributor * 📎 Fix rebase issue --------- Co-authored-by: Sumit Ridhal <sridhal@redhat.com> Co-authored-by: Paperclip <noreply@paperclip.ing>
130 lines
4.6 KiB
JavaScript
130 lines
4.6 KiB
JavaScript
import { test, expect } from "@playwright/test";
|
|
import { BaseWebSocketPage } from "../pages/BaseWebSocketPage";
|
|
import { WasmWorkspacePage } from "../pages/WasmWorkspacePage";
|
|
import { setupTokensFileRender, unfoldTokenType } from "./tokens/helpers";
|
|
|
|
// Regression tests for issue #7440: scroll position in the left sidebar
|
|
// tabs (Layers / Assets / Tokens) must survive tab switches.
|
|
// Ported from closed PR #7544 and adapted to the current implementation
|
|
// (scroll store held in the sidebar + async restore on mount) and to the
|
|
// current fixtures and page objects.
|
|
|
|
// We allow a small tolerance because content can shift slightly on remount.
|
|
const POSITION_TOLERANCE = 20; // pixels
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await WasmWorkspacePage.init(page);
|
|
await BaseWebSocketPage.mockRPC(page, "get-teams", "get-teams-tokens.json");
|
|
});
|
|
|
|
async function visibleScrollContainers(page) {
|
|
const containers = page.locator(
|
|
'[data-testid="left-sidebar"] [data-scroll-container="true"]',
|
|
);
|
|
const count = await containers.count();
|
|
const visible = [];
|
|
for (let i = 0; i < count; i++) {
|
|
const el = containers.nth(i);
|
|
// eslint-disable-next-line no-await-in-loop
|
|
if (await el.evaluate((node) => !!node && node.offsetParent !== null)) {
|
|
visible.push(el);
|
|
}
|
|
}
|
|
return visible;
|
|
}
|
|
|
|
async function scrollableContainers(page) {
|
|
const visible = await visibleScrollContainers(page);
|
|
const scrollable = [];
|
|
for (const el of visible) {
|
|
// eslint-disable-next-line no-await-in-loop
|
|
if (
|
|
await el.evaluate((node) => node.scrollHeight > node.clientHeight + 100)
|
|
) {
|
|
scrollable.push(el);
|
|
}
|
|
}
|
|
return scrollable;
|
|
}
|
|
|
|
async function scrollToBottom(locator) {
|
|
await locator.evaluate((el) => {
|
|
el.scrollTop = el.scrollHeight;
|
|
});
|
|
// Programmatic scrolls dispatch scroll events asynchronously; wait for
|
|
// the save handler before switching tabs.
|
|
await locator.page().waitForTimeout(150);
|
|
}
|
|
|
|
async function expectScrollRestored(locator, expected) {
|
|
// Restore runs in a post-mount effect with frame retries, so poll
|
|
// instead of asserting immediately after the tab click.
|
|
await expect
|
|
.poll(async () => locator.evaluate((el) => el.scrollTop), {
|
|
timeout: 10000,
|
|
})
|
|
.toBeGreaterThanOrEqual(expected - POSITION_TOLERANCE);
|
|
}
|
|
|
|
test("Sidebar scroll position preserved when switching tabs", async ({
|
|
page,
|
|
}) => {
|
|
const { tokensSidebar } = await setupTokensFileRender(page);
|
|
await unfoldTokenType(tokensSidebar, "color");
|
|
|
|
const containers = await scrollableContainers(page);
|
|
expect(
|
|
containers.length,
|
|
"Tokens tab should have a scrollable container",
|
|
).toBeGreaterThan(0);
|
|
const scrollEl = containers[0];
|
|
|
|
await scrollToBottom(scrollEl);
|
|
const initialPos = await scrollEl.evaluate((el) => el.scrollTop);
|
|
expect(initialPos).toBeGreaterThan(0);
|
|
|
|
await page.getByRole("tab", { name: "Assets" }).click();
|
|
await page.getByRole("tab", { name: "Tokens" }).click();
|
|
|
|
const restored = (await scrollableContainers(page))[0];
|
|
await expectScrollRestored(restored, initialPos);
|
|
});
|
|
|
|
test("Sidebar maintains independent scroll positions per tab", async ({
|
|
page,
|
|
}) => {
|
|
const { tokensSidebar } = await setupTokensFileRender(page);
|
|
await unfoldTokenType(tokensSidebar, "color");
|
|
|
|
// Scroll Tokens to the bottom.
|
|
const tokensContainers = await scrollableContainers(page);
|
|
expect(
|
|
tokensContainers.length,
|
|
"Tokens tab should have a scrollable container",
|
|
).toBeGreaterThan(0);
|
|
await scrollToBottom(tokensContainers[0]);
|
|
const tokensPos = await tokensContainers[0].evaluate((el) => el.scrollTop);
|
|
expect(tokensPos).toBeGreaterThan(0);
|
|
|
|
// Switch to Layers: fresh panel, must start at top (not contaminated
|
|
// by the Tokens position saved under a different key).
|
|
await page.getByRole("tab", { name: "Layers" }).click();
|
|
const layersVisible = await visibleScrollContainers(page);
|
|
expect(layersVisible.length).toBeGreaterThan(0);
|
|
const layersPos = await layersVisible[0].evaluate((el) => el.scrollTop);
|
|
expect(layersPos).toBe(0);
|
|
|
|
// Back to Tokens: its own position must be restored, not Layers'.
|
|
|
|
// Back to Tokens: its own position must be restored, not Layers'.
|
|
await page.getByRole("tab", { name: "Tokens" }).click();
|
|
const tokensRestored = (await scrollableContainers(page))[0];
|
|
await expectScrollRestored(tokensRestored, tokensPos);
|
|
|
|
// And back to Layers: still untouched at top.
|
|
await page.getByRole("tab", { name: "Layers" }).click();
|
|
const layersAgain = await visibleScrollContainers(page);
|
|
const layersAgainPos = await layersAgain[0].evaluate((el) => el.scrollTop);
|
|
expect(layersAgainPos).toBe(0);
|
|
});
|