penpot/frontend/playwright/ui/visual-specs/visual-viewer.spec.js
Eva Marco c05b2e2ead
♻️ Use DS buttons and icons in the share link modal (#12562)
* ♻️ Use DS buttons and icons in the share link modal

- The close button becomes a ghost DS icon button named "Close"; the
  class that places it goes on the tooltip wrapper.
- "Get link", "Destroy link" and the confirmation buttons were raw
  inputs; they become DS primary, destructive and secondary buttons.
- The copy link button stays a raw segment of the link field; its icon
  becomes the DS clipboard icon, a bit larger than the legacy drawing.
- "Manage permissions" stays a link-style toggle; its arrow becomes the
  DS icon (the rotation still shows the state) and it says whether the
  permissions are expanded.

The page checkboxes and the permission selects stay for their own
groups. A new create share link fixture and a visual test cover the
modal before and after getting a link, with the permissions expanded,
and the destroy confirmation.

AI-assisted-by: claude-opus-5-5

* ♻️ Move share link modal styles closer to the DS

The share link stylesheet resolves its legacy sizes, spacing, mixins,
z-index and color aliases to DS values and tokens, with logical
properties. The copy button inlines %button-secondary with local color
variables, so it looks the same at rest, on hover, on focus and while
pressed; the visual test now also covers its hover.

The legacy typographies become the DS ones, each differing in one
detail: the title and "Manage permissions" use headline-small (12px
instead of 11px) and the texts body-small (line height 1.3 instead of
1.4), so the modal changes height by about 1px. refactor/ stays only
for the modal container, the link input and the page checkboxes.

AI-assisted-by: claude-opus-5-5

* ♻️ Use a DS icon button for the share link copy button

The copy button next to the link was a raw button with the secondary
button styles inlined in share_link.scss. It is now a secondary
icon-button* named "Copy link", so the .copy-button block goes away
(review feedback).

Visible changes: the DS secondary look (grey icon at rest, no lighter
background on hover) and a 32px wide button instead of 28px, so the
link text ends 4px earlier.

AI-assisted-by: claude-opus-5-5
2026-10-28 09:20:41 +01:00

492 lines
15 KiB
JavaScript

import { test, expect } from "@playwright/test";
import { ViewerPage } from "../pages/ViewerPage";
test.beforeEach(async ({ page }) => {
await ViewerPage.init(page);
});
const singleBoardFileId = "dd5cc0bb-91ff-81b9-8004-77df9cd3edb1";
const singleBoardPageId = "dd5cc0bb-91ff-81b9-8004-77df9cd3edb2";
test("User goes to an empty Viewer", async ({ page }) => {
const viewerPage = new ViewerPage(page);
await viewerPage.setupLoggedInUser();
await viewerPage.setupEmptyFile();
await viewerPage.goToViewer();
await expect(viewerPage.page.getByTestId("penpot-logo-link")).toBeVisible();
await expect(viewerPage.page).toHaveScreenshot();
});
test("User goes to the Viewer", async ({ page }) => {
const viewerPage = new ViewerPage(page);
await viewerPage.setupLoggedInUser();
await viewerPage.setupFileWithSingleBoard();
await viewerPage.goToViewer({
fileId: singleBoardFileId,
pageId: singleBoardPageId,
});
await expect(viewerPage.page.getByTestId("penpot-logo-link")).toBeVisible();
await expect(viewerPage.page).toHaveScreenshot();
});
test("User goes to the Viewer and opens zoom modal", async ({ page }) => {
const viewerPage = new ViewerPage(page);
await viewerPage.setupLoggedInUser();
await viewerPage.setupFileWithSingleBoard();
await viewerPage.goToViewer({
fileId: singleBoardFileId,
pageId: singleBoardPageId,
});
await viewerPage.page.getByTitle("Zoom").click();
await expect(viewerPage.page.getByTestId("penpot-logo-link")).toBeVisible();
await expect(viewerPage.page).toHaveScreenshot();
});
test("User goes to the Viewer Comments", async ({ page }) => {
const viewerPage = new ViewerPage(page);
await viewerPage.setupLoggedInUser();
await viewerPage.setupFileWithComments();
await viewerPage.goToViewer({
fileId: singleBoardFileId,
pageId: singleBoardPageId,
});
await viewerPage.showComments();
await viewerPage.showCommentsThread(1);
await expect(
viewerPage.page.getByRole("button", { name: "Post" }),
).toBeVisible();
await expect(viewerPage.page).toHaveScreenshot();
});
test("User opens Viewer comment list", async ({ page }) => {
const viewerPage = new ViewerPage(page);
await viewerPage.setupLoggedInUser();
await viewerPage.setupFileWithComments();
await viewerPage.goToViewer({
fileId: singleBoardFileId,
pageId: singleBoardPageId,
});
await viewerPage.showComments();
await viewerPage.page.getByTestId("viewer-comments-dropdown").click();
await viewerPage.page.getByText("Show comments list").click();
await expect(
viewerPage.page.getByRole("button", { name: "Show all comments" }),
).toBeVisible();
await expect(viewerPage.page).toHaveScreenshot();
});
test("User goes to the Viewer Inspect code", async ({ page }) => {
const viewerPage = new ViewerPage(page);
await viewerPage.setupLoggedInUser();
await viewerPage.setupFileWithComments();
await viewerPage.goToViewer({
fileId: singleBoardFileId,
pageId: singleBoardPageId,
});
await viewerPage.showCode();
await expect(
viewerPage.page.getByRole("button", {
name: "Toggle panel Size and position",
}),
).toBeVisible();
await expect(viewerPage.page).toHaveScreenshot();
});
test("User goes to the Viewer Inspect code, code tab", async ({ page }) => {
const viewerPage = new ViewerPage(page);
await viewerPage.setupLoggedInUser();
await viewerPage.setupFileWithComments();
await viewerPage.goToViewer({
fileId: singleBoardFileId,
pageId: singleBoardPageId,
});
await viewerPage.showCode();
await viewerPage.page
.getByRole("combobox")
.filter({ hasText: "Styles" })
.click();
await viewerPage.page.getByRole("option", { name: "Code" }).click();
await expect(
viewerPage.page.getByRole("button", { name: "Copy all code" }),
).toBeVisible();
await expect(viewerPage.page).toHaveScreenshot();
});
test("User opens Share modal", async ({ page }) => {
const viewerPage = new ViewerPage(page);
await viewerPage.setupLoggedInUser();
await viewerPage.setupFileWithSingleBoard();
await viewerPage.goToViewer({
fileId: singleBoardFileId,
pageId: singleBoardPageId,
});
await viewerPage.page.getByRole("button", { name: "Share" }).click();
await expect(
viewerPage.page.getByRole("button", { name: "Get link" }),
).toBeVisible();
await expect(viewerPage.page).toHaveScreenshot();
});
test.describe("Thumbnails panel", () => {
test("Shows the thumbnails panel collapsed and expanded", async ({
page,
}) => {
const viewerPage = new ViewerPage(page);
await viewerPage.setupLoggedInUser();
await viewerPage.setupFileWithMultipleBoards();
await viewerPage.goToViewer({
fileId: "dd5cc0bb-91ff-81b9-8004-77df9cd3edb0",
pageId: "dd5cc0bb-91ff-81b9-8004-77df9cd3edb3",
});
await page.locator("#current-frame").click();
const panel = page.locator("section").filter({ hasText: /boards/i });
await expect(panel).toBeVisible();
await page.mouse.move(640, 600);
// Collapsed: the summary with its expand and close buttons.
await expect(panel).toHaveScreenshot({
maxDiffPixelRatio: 0,
threshold: 0,
});
// The scroll handlers only show on hover.
await panel.locator("button").nth(3).hover();
await expect(panel).toHaveScreenshot({
maxDiffPixelRatio: 0,
threshold: 0,
});
await panel.locator("button").first().click();
await page.mouse.move(640, 700);
// Expanded: the expand arrow points up.
await expect(panel).toHaveScreenshot({
maxDiffPixelRatio: 0,
threshold: 0,
});
});
});
test.describe("Comments filter", () => {
test("Shows the comments filter closed and open", async ({ page }) => {
const viewerPage = new ViewerPage(page);
await viewerPage.setupLoggedInUser();
await viewerPage.setupFileWithComments();
await viewerPage.goToViewer({
fileId: singleBoardFileId,
pageId: singleBoardPageId,
});
await viewerPage.showComments();
const trigger = page.getByTestId("viewer-comments-dropdown");
await expect(trigger).toBeVisible();
await page.mouse.move(640, 600);
// Closed: the "Comments" trigger with its arrow.
await expect(trigger).toHaveScreenshot({
maxDiffPixelRatio: 0,
threshold: 0,
});
await trigger.click();
await expect(page.getByText("Show comments list")).toBeVisible();
await page.mouse.move(640, 600);
// Open: the filter menu with the selected option ticked.
const menu = page.getByText("Show comments list").locator("../..");
await expect(menu).toHaveScreenshot({
maxDiffPixelRatio: 0,
threshold: 0,
});
});
});
test.describe("Board navigation", () => {
test("Shows the previous, next and first board buttons", async ({ page }) => {
const viewerPage = new ViewerPage(page);
await viewerPage.setupLoggedInUser();
await viewerPage.setupFileWithMultipleBoards();
await viewerPage.goToViewer({
fileId: "dd5cc0bb-91ff-81b9-8004-77df9cd3edb0",
pageId: "dd5cc0bb-91ff-81b9-8004-77df9cd3edb3",
});
const next = page.getByRole("button", { name: "Next" });
await expect(next).toBeVisible();
await page.mouse.move(640, 300);
// Wait for the slide-in transition of the controls.
await page.waitForTimeout(800);
// First board: the next and first board buttons.
await expect(page).toHaveScreenshot({
maxDiffPixelRatio: 0,
threshold: 0,
});
await next.hover();
await expect(next).toHaveScreenshot({
maxDiffPixelRatio: 0,
threshold: 0,
});
await next.click();
const previous = page.getByRole("button", { name: "Previous" });
await previous.hover();
await expect(previous).toHaveScreenshot({
maxDiffPixelRatio: 0,
threshold: 0,
});
const first = page.locator("[class*='viewer-bottom'] button");
await first.hover();
await expect(first).toHaveScreenshot({
maxDiffPixelRatio: 0,
threshold: 0,
});
});
});
test.describe("Viewer header", () => {
test("Shows the header buttons and its menus", async ({ page }) => {
const viewerPage = new ViewerPage(page);
await viewerPage.setupLoggedInUser();
await viewerPage.setupFileWithComments();
await viewerPage.goToViewer({
fileId: singleBoardFileId,
pageId: singleBoardPageId,
});
const header = page.locator("header").first();
await expect(header).toBeVisible();
await page.mouse.move(640, 600);
// Interactions section.
await expect(header).toHaveScreenshot({
maxDiffPixelRatio: 0,
threshold: 0,
});
// Zoom menu.
await page.getByTitle("Zoom").click();
await expect(page.getByText("Fit - Scale down to fit")).toBeVisible();
await page.mouse.move(640, 600);
await expect(page).toHaveScreenshot({
clip: { x: 760, y: 0, width: 520, height: 260 },
maxDiffPixelRatio: 0,
threshold: 0,
});
await page.keyboard.press("Escape");
await page.mouse.click(640, 600);
// Sitemap menu.
await page
.locator("#current-frame")
.locator("..")
.locator("div")
.first()
.click();
await page.mouse.move(640, 600);
await expect(page).toHaveScreenshot({
clip: { x: 0, y: 0, width: 520, height: 200 },
maxDiffPixelRatio: 0,
threshold: 0,
});
await page.mouse.click(640, 600);
// Comments section, with its button selected.
await viewerPage.showComments();
await page.mouse.move(640, 600);
await expect(header).toHaveScreenshot({
maxDiffPixelRatio: 0,
threshold: 0,
});
});
});
test.describe("Inspect code tab", () => {
test("Shows the code tab buttons, expanded and collapsed", async ({
page,
}) => {
const viewerPage = new ViewerPage(page);
await viewerPage.setupLoggedInUser();
await viewerPage.setupFileWithComments();
await viewerPage.goToViewer({
fileId: singleBoardFileId,
pageId: singleBoardPageId,
});
await viewerPage.showCode();
await page.getByRole("combobox").filter({ hasText: "Styles" }).click();
await page.getByRole("option", { name: "Code" }).click();
const copyAll = page.getByRole("button", { name: "Copy all code" });
await expect(copyAll).toBeVisible();
// The CSS block header, with its toggle, expand and copy buttons.
const cssToggle = page.locator('button[data-type="css"]');
const cssHeader = cssToggle.locator("..");
await page.mouse.move(0, 0);
await expect(copyAll).toHaveScreenshot({
maxDiffPixelRatio: 0,
threshold: 0,
});
await expect(cssHeader).toHaveScreenshot({
maxDiffPixelRatio: 0,
threshold: 0,
});
await cssToggle.click();
await page.mouse.move(0, 0);
// Collapsed: the arrow turns.
await expect(cssHeader).toHaveScreenshot({
maxDiffPixelRatio: 0,
threshold: 0,
});
});
});
test.describe("Interactions menu", () => {
test("Shows the interactions menu closed and open", async ({ page }) => {
const viewerPage = new ViewerPage(page);
await viewerPage.setupLoggedInUser();
await viewerPage.setupFileWithMultipleBoards();
await viewerPage.goToViewer({
fileId: "dd5cc0bb-91ff-81b9-8004-77df9cd3edb0",
pageId: "dd5cc0bb-91ff-81b9-8004-77df9cd3edb3",
});
const trigger = page
.getByText("Interactions", { exact: true })
.locator("..");
await expect(trigger).toBeVisible();
await page.mouse.move(640, 600);
// Closed: the "Interactions" trigger with its arrow.
await expect(trigger).toHaveScreenshot({
maxDiffPixelRatio: 0,
threshold: 0,
});
await trigger.click();
const menu = page.getByText("Show interactions", { exact: true });
await expect(menu).toBeVisible();
await page.mouse.move(640, 600);
// Open: the menu with the selected mode ticked.
await expect(menu.locator("../..")).toHaveScreenshot({
maxDiffPixelRatio: 0,
threshold: 0,
});
});
});
test.describe("Copy buttons", () => {
const shot = { maxDiffPixelRatio: 0, threshold: 0 };
const openInspectTab = async (page, name) => {
const viewerPage = new ViewerPage(page);
await viewerPage.setupLoggedInUser();
await viewerPage.setupFileWithComments();
await viewerPage.goToViewer({
fileId: singleBoardFileId,
pageId: singleBoardPageId,
});
await viewerPage.showCode();
await page.getByRole("combobox").filter({ hasText: "Styles" }).click();
await page.getByRole("option", { name }).click();
};
test("Computed attribute rows", async ({ page }) => {
await openInspectTab(page, "Computed");
const row = page.locator("[class*='copy-wrapper']").first();
await expect(row).toBeVisible();
await page.mouse.move(10, 700);
await expect(row).toHaveScreenshot(shot);
await row.hover();
await expect(row).toHaveScreenshot(shot);
await row.click();
await expect(row).toHaveAttribute("data-active", "true");
await expect(row).toHaveScreenshot(shot);
});
test("Code block copy button", async ({ page }) => {
await openInspectTab(page, "Code");
const copy = page.locator("[class*='css-copy-btn']");
await expect(copy).toBeVisible();
await page.mouse.move(10, 700);
await expect(copy).toHaveScreenshot(shot);
await copy.hover();
await expect(copy).toHaveScreenshot(shot);
await copy.click();
await expect(copy).toHaveAttribute("data-active", "true");
await expect(copy).toHaveScreenshot(shot);
});
});
test.describe("Share link modal controls", () => {
const shot = { maxDiffPixelRatio: 0, threshold: 0 };
test("Get, copy and destroy link", async ({ page }) => {
const viewerPage = new ViewerPage(page);
await viewerPage.setupLoggedInUser();
await viewerPage.setupFileWithSingleBoard();
await viewerPage.mockRPC(
"create-share-link",
"viewer/create-share-link.json",
);
await viewerPage.goToViewer({
fileId: singleBoardFileId,
pageId: singleBoardPageId,
});
await page.getByRole("button", { name: "Share" }).click();
const getLink = page.getByRole("button", { name: "Get link" });
await expect(getLink).toBeVisible();
const modal = page.locator("[class*='share-link-dialog']");
await page.mouse.move(10, 700);
await expect(modal).toHaveScreenshot(shot);
await getLink.click();
const destroy = page.getByRole("button", { name: "Destroy link" });
await expect(destroy).toBeVisible();
await page.mouse.move(10, 700);
await expect(modal).toHaveScreenshot(shot);
// A raw button with a title before, a DS icon button named "Copy link" after.
await modal
.getByTitle("Copy link")
.or(modal.getByRole("button", { name: "Copy link" }))
.hover();
await expect(modal).toHaveScreenshot(shot);
await page.getByText("Manage permissions").click();
await page.mouse.move(10, 700);
await expect(modal).toHaveScreenshot(shot);
await destroy.click();
await expect(page.getByRole("button", { name: "Cancel" })).toBeVisible();
await page.mouse.move(10, 700);
await expect(modal).toHaveScreenshot(shot);
});
});