Implement collapsible panels for the new room list (#32742)
* Add `react-resizable-panels` library * Implement a custom SeparatorView * Add a `LeftResizablePanelView` * Add a custom `GroupView` * Export everything from shared-components * Make it possible to track width/collapse state through settings * Add a view model to drive the views * Render views without disrupting the old room list * Fix lint error * Disable user interaction on collapsed panel * Prevent widgets fron hijacking pointer events * Expand to full width on separator click * Separator should be shown when focused via keyboard * Update tests * Use data-attribute for hover * Write stories for SeperatorView * Write vite tests for SeparatorView * Write tests for LeftResizablePanelView * More tests * Fix lint errors * Fix flakey border on the roomlst * Fix storybook axe violation * Update snapshots * Fix playwright tests * Fix sonarcloud issues * Use translated string * Add better js-doc comments * Rename `ResizerSnapshot` to `ResizerViewSnapshot` * Externalize react-resizable-panels * Link figma designs to stories * Write playwright tests * Update screenshots * Fix lint errors * Update more screenshots * Update more screenshots * Fix flaky toast test * Update apps/web/playwright/e2e/crypto/toasts.spec.ts Co-authored-by: Andy Balaam <andy.balaam@matrix.org> * Fix indentation --------- Co-authored-by: Andy Balaam <andy.balaam@matrix.org>
This commit is contained in:
co-authored by
Andy Balaam
parent
89261bbe00
commit
99e6ede9f1
@@ -49,6 +49,11 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
).toBeVisible();
|
||||
};
|
||||
|
||||
const scrollToBottomOfTimeline = async (page: Page) => {
|
||||
await page.locator(".mx_RoomView_MessageList").click();
|
||||
await page.mouse.wheel(0, 100);
|
||||
};
|
||||
|
||||
/**
|
||||
* Take snapshots of mx_EventTile_last on each layout, outputting log for reference/debugging.
|
||||
* @param detail The snapshot name. Used for outputting logs too.
|
||||
@@ -113,10 +118,12 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
`,
|
||||
mask: [page.getByTestId("audio-player-seek")],
|
||||
clip: undefined,
|
||||
hideJumpToBottomButton: true,
|
||||
};
|
||||
|
||||
// Take a snapshot of mx_EventTile_last on IRC layout
|
||||
screenshotOptions.clip = await page.locator(".mx_EventTile_last").boundingBox();
|
||||
await scrollToBottomOfTimeline(page);
|
||||
await expect(page).toMatchScreenshot(`${detail.replaceAll(" ", "-")}-irc-layout.png`, screenshotOptions);
|
||||
|
||||
// Take a snapshot on modern/group layout
|
||||
@@ -125,6 +132,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
await groupTile.locator(".mx_MessageTimestamp").click();
|
||||
await checkPlayerVisibility(groupTile);
|
||||
screenshotOptions.clip = await page.locator(".mx_EventTile_last").boundingBox();
|
||||
await scrollToBottomOfTimeline(page);
|
||||
await expect(page).toMatchScreenshot(`${detail.replaceAll(" ", "-")}-group-layout.png`, screenshotOptions);
|
||||
|
||||
// Take a snapshot on bubble layout
|
||||
@@ -133,6 +141,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
await bubbleTile.locator(".mx_MessageTimestamp").click();
|
||||
await checkPlayerVisibility(bubbleTile);
|
||||
screenshotOptions.clip = await page.locator(".mx_EventTile_last").boundingBox();
|
||||
await scrollToBottomOfTimeline(page);
|
||||
await expect(page).toMatchScreenshot(`${detail.replaceAll(" ", "-")}-bubble-layout.png`, screenshotOptions);
|
||||
};
|
||||
|
||||
|
||||
@@ -11,6 +11,16 @@ import { test, expect } from "../../element-web-test";
|
||||
import { createBot, deleteCachedSecrets, disableKeyBackup, logIntoElement, logIntoElementAndVerify } from "./utils";
|
||||
import { type Bot } from "../../pages/bot";
|
||||
|
||||
// Mask the background of the screenshot to avoid failing the test just because some
|
||||
// other component has changed its rendering.
|
||||
const screenshotOptions = {
|
||||
css: `
|
||||
.mx_ToastContainer {
|
||||
background-color: magenta !important;
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
test.describe("Key storage out of sync toast", () => {
|
||||
let recoveryKey: GeneratedSecretStorageKey;
|
||||
|
||||
@@ -37,15 +47,10 @@ test.describe("Key storage out of sync toast", () => {
|
||||
// playwright only evaluates the 'first()' call initially, not subsequent times it checks, so
|
||||
// it would always be checking the same toast, even if another one is now the first.
|
||||
await expect(page.getByRole("alert")).toHaveCount(2);
|
||||
// Mask the background of the screenshot to avoid failing the test just because some
|
||||
// other component have changed its rendering.
|
||||
await expect(page.getByRole("alert").first()).toMatchScreenshot("key-storage-out-of-sync-toast.png", {
|
||||
css: `
|
||||
.mx_ToastContainer {
|
||||
background-color: magenta !important;
|
||||
}
|
||||
`,
|
||||
});
|
||||
await expect(page.getByRole("alert").first()).toMatchScreenshot(
|
||||
"key-storage-out-of-sync-toast.png",
|
||||
screenshotOptions,
|
||||
);
|
||||
|
||||
await page.getByRole("button", { name: "Enter recovery key" }).click();
|
||||
|
||||
@@ -209,7 +214,10 @@ test.describe("Verify this device toast", () => {
|
||||
|
||||
await expect(page.getByRole("heading", { name: "Verify this device" })).toBeVisible();
|
||||
|
||||
await expect(page.locator(".mx_ToastContainer")).toMatchScreenshot("verify-this-device.png");
|
||||
await expect(page.locator(".mx_ToastContainer")).toMatchScreenshot(
|
||||
"verify-this-device.png",
|
||||
screenshotOptions,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { test, expect } from "../../../element-web-test";
|
||||
import type { Locator, Page } from "playwright-core";
|
||||
|
||||
test.describe("Collapsible Room list", () => {
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ page, app, user }) => {
|
||||
await app.closeNotificationToast();
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await app.client.createRoom({ name: `room${i}` });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Resize the panel and return the bounding box
|
||||
* @param pixels The number of pixels by which to resize the panel
|
||||
*/
|
||||
async function resize(page: Page, pixels: number): ReturnType<Locator["boundingBox"]> {
|
||||
const leftPanelLocator = page.getByTestId("left-panel");
|
||||
const boundingBox = await leftPanelLocator.boundingBox();
|
||||
|
||||
// Move mouse 2px to the right of the left-panel, this should be region that the user drags to resize the panel.
|
||||
const mouseX = boundingBox.x + boundingBox.width + 2;
|
||||
const mouseY = boundingBox.y + boundingBox.height / 2;
|
||||
|
||||
await page.mouse.move(mouseX, mouseY);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(mouseX + pixels, mouseY);
|
||||
|
||||
return boundingBox;
|
||||
}
|
||||
|
||||
test("should be possible to expand/contract the room list", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
await expect(page).toMatchScreenshot("room-list-collapse-default.png");
|
||||
const leftPanelLocator = page.getByTestId("left-panel");
|
||||
|
||||
// Contract the panel
|
||||
let previousBoundingBox = await resize(page, -50);
|
||||
let currentBoundingBox = await leftPanelLocator.boundingBox();
|
||||
expect(currentBoundingBox.width).toBeCloseTo(previousBoundingBox.width - 50, 0);
|
||||
|
||||
// Expand the panel
|
||||
previousBoundingBox = await resize(page, 30);
|
||||
currentBoundingBox = await leftPanelLocator.boundingBox();
|
||||
expect(currentBoundingBox.width).toBeCloseTo(previousBoundingBox.width + 30, 0);
|
||||
});
|
||||
|
||||
test(
|
||||
"should be possible to fully collapse and expand the left panel",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, user }) => {
|
||||
const leftPanelLocator = page.getByTestId("left-panel");
|
||||
|
||||
// Collapse the panel
|
||||
await resize(page, -300);
|
||||
let currentBoundingBox = await leftPanelLocator.boundingBox();
|
||||
expect(currentBoundingBox.width).toStrictEqual(0);
|
||||
|
||||
// Expect te separator to be shown
|
||||
const separator = page.getByRole("separator", { name: "Click or drag to expand" });
|
||||
await expect(separator).toBeInViewport();
|
||||
await expect(page).toMatchScreenshot("room-list-collapse-fully-collapsed.png");
|
||||
|
||||
// Should be possible to expand by clicking on the separator
|
||||
await separator.click();
|
||||
currentBoundingBox = await leftPanelLocator.boundingBox();
|
||||
expect(currentBoundingBox.width).toBeGreaterThan(365);
|
||||
|
||||
// Collapse the panel again
|
||||
await resize(page, -300);
|
||||
|
||||
// Check that the panel can be expanded by dragging the separator
|
||||
const separatorBoundingBox = await separator.boundingBox();
|
||||
const mouseX = separatorBoundingBox.x + separatorBoundingBox.width / 2;
|
||||
const mouseY = separatorBoundingBox.y + separatorBoundingBox.height / 2;
|
||||
await page.mouse.move(mouseX, mouseY);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(mouseX + 400, mouseY);
|
||||
expect(currentBoundingBox.width).toBeGreaterThan(365);
|
||||
},
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user