penpot/frontend/playwright/ui/specs/text-options-missing-font.spec.js
Eva Marco c0221a9bf8
🐛 Hide "Create typography style" button for shapes with missing fonts (#11527)
The button let users convert a text shape's inline styles into a
typography asset even when the shape's font-id couldn't be resolved
(e.g. a custom/team font that was removed or isn't loaded), silently
baking a missing font into the new typography asset.

Guard the button on the font actually resolving via
app.main.fonts/fontsdb, in addition to the existing checks (no
typography or token already applied, single selection).

Added e2e coverage for all four conditions that must independently
hide the button: missing font, applied typography asset, multiple
selection with differing values, and applied typography token.

AI-assisted-by: claude-sonnet-5
2026-09-11 13:41:11 +02:00

202 lines
7.5 KiB
JavaScript

import { test, expect } from "@playwright/test";
import { WorkspacePage } from "../pages/WorkspacePage";
import { WasmWorkspacePage } from "../pages/WasmWorkspacePage";
// ---------------------------------------------------------------------------
// The "Create typography style" button (workspace.options.convert-to-typography)
// in the text options sidebar is only shown when ALL of these hold for the
// selected text shape(s) (src/app/main/ui/workspace/sidebar/options/menus/text.cljs):
// (and (some? font) (not typography) (not multiple?) (not applied-token-name))
// Each test below isolates one condition that must independently hide it:
// - font missing (font-id not registered in app.main.fonts/fontsdb)
// - a typography asset is applied (typography-ref-id set)
// - multiple shapes are selected with differing attributes
// - a typography design token is applied (applied-tokens :typography)
// ---------------------------------------------------------------------------
function convertToTypographyButton(workspace) {
return workspace.rightSidebar.getByRole("button", {
name: "Create typography style",
});
}
test.describe("font missing", () => {
// Fixture render-wasm/get-file-text-custom-fonts.json has a text shape
// ("Penpot & Dragons") using a custom team font-id and no typography/token
// applied - otherwise exactly the state that reveals the button once its
// font resolves. Toggling the get-font-variants mock between "the team owns
// this font" and "empty" simulates the font being present vs. missing.
const FILE = {
id: "434b0541-fa2f-802f-8006-59827d964a9b",
pageId: "434b0541-fa2f-802f-8006-59827d964a9c",
};
test.beforeEach(async ({ page }) => {
await WorkspacePage.init(page);
});
test("Create typography style button is hidden when the shape font is missing", async ({
page,
}) => {
const workspace = new WorkspacePage(page);
await workspace.setupEmptyFile();
await workspace.mockRPC(
/get\-file\?/,
"render-wasm/get-file-text-custom-fonts.json",
);
// The team does not own the shape's custom font, so it can't be resolved.
await workspace.mockRPC(
"get-font-variants?team-id=*",
"workspace/get-font-variants-empty.json",
);
await workspace.goToWorkspace({ fileId: FILE.id, pageId: FILE.pageId });
await workspace.clickLeafLayer("Penpot & Dragons");
await expect(convertToTypographyButton(workspace)).not.toBeVisible();
});
test("Create typography style button is visible once the shape font resolves", async ({
page,
}) => {
const workspace = new WorkspacePage(page);
await workspace.setupEmptyFile();
await workspace.mockRPC(
/get\-file\?/,
"render-wasm/get-file-text-custom-fonts.json",
);
// The team owns the shape's custom font, so it resolves normally.
await workspace.mockRPC(
"get-font-variants?team-id=*",
"render-wasm/get-font-variants-custom-fonts.json",
);
await workspace.goToWorkspace({ fileId: FILE.id, pageId: FILE.pageId });
await workspace.clickLeafLayer("Penpot & Dragons");
await expect(convertToTypographyButton(workspace)).toBeVisible();
});
});
test.describe("typography asset applied", () => {
// multiselection-typography.json: "Text with typography asset one" has a
// typography-ref-id pointing at an in-file typography asset (font
// gfont-agdasima, a built-in Google font that resolves with no extra
// mocking), and is not multi-selected or token-applied.
const FILE = {
id: "1062e0a0-8fe0-80ae-8007-e70b4993f5ef",
pageId: "1062e0a0-8fe0-80ae-8007-e70b4993f5f0",
};
test.beforeEach(async ({ page }) => {
await WorkspacePage.init(page);
});
test("Create typography style button is hidden when a typography asset is applied", async ({
page,
}) => {
const workspace = new WorkspacePage(page);
await workspace.setupEmptyFile();
await workspace.mockRPC(
/get\-file\?/,
"workspace/multiselection-typography.json",
);
await workspace.goToWorkspace({ fileId: FILE.id, pageId: FILE.pageId });
await workspace.clickLeafLayer("Text with typography asset one");
// Sanity check: the text options panel did render for this shape - the
// button is specifically hidden by the applied typography, not because
// the whole panel failed to show up.
await expect(
workspace.rightSidebar.getByRole("region", { name: "Text section" }),
).toBeVisible();
await expect(convertToTypographyButton(workspace)).not.toBeVisible();
});
});
test.describe("multiple selection", () => {
// get-file-text-multiple-selection.json has two text shapes sharing the
// same (resolvable, built-in) font-id but differing font-size, with no
// typography or token applied - so selecting both together isolates
// `multiple?` becoming true without also making the font unresolved.
const FILE = {
id: "434b0541-fa2f-802f-8006-6a827d964a9b",
pageId: "434b0541-fa2f-802f-8006-6a827d964a9c",
};
test.beforeEach(async ({ page }) => {
await WorkspacePage.init(page);
});
test("Create typography style button is hidden when multiple shapes with different values are selected", async ({
page,
}) => {
const workspace = new WorkspacePage(page);
await workspace.setupEmptyFile();
await workspace.mockRPC(
/get\-file\?/,
"workspace/get-file-text-multiple-selection.json",
);
await workspace.goToWorkspace({ fileId: FILE.id, pageId: FILE.pageId });
await workspace.clickLeafLayer("Text multiple selection one");
await expect(convertToTypographyButton(workspace)).toBeVisible();
await workspace.clickLeafLayer("Text multiple selection two", {
modifiers: ["Shift"],
});
await expect(convertToTypographyButton(workspace)).not.toBeVisible();
});
});
test.describe("typography token applied", () => {
// get-file-token-tooltip.json: "Text with token" has a typography design
// token applied (applied-tokens :typography) using font gfont-arizonia (a
// built-in Google font that resolves with no extra mocking).
test.beforeEach(async ({ page }) => {
await WasmWorkspacePage.init(page);
await WasmWorkspacePage.mockRPC(page, "get-teams", "get-teams-tokens.json");
});
test("Create typography style button is hidden when a typography token is applied", async ({
page,
}) => {
const workspace = new WasmWorkspacePage(page);
await workspace.mockConfigFlags(["enable-feature-token-input"]);
await workspace.setupEmptyFile();
await workspace.mockRPC("get-team?id=*", "workspace/get-team-tokens.json");
await workspace.mockRPC(
/get\-file\?/,
"workspace/get-file-token-tooltip.json",
);
await workspace.mockRPC(
/get\-file\-fragment\?/,
"workspace/get-file-fragment-tokens.json",
);
await workspace.mockRPC(
"update-file?id=*",
"workspace/update-file-create-rect.json",
);
await workspace.goToWorkspace({
fileId: "c7ce0794-0992-8105-8004-38f280443849",
pageId: "4530574a-7a0a-807b-8008-0107b2c4628e",
});
await page.getByRole("tab", { name: "Layers" }).click();
await workspace.layers
.getByTestId("layer-row")
.filter({ hasText: "Text with token" })
.click();
// Sanity check: the text options panel did render for this shape - the
// button is specifically hidden by the applied token, not because the
// whole panel failed to show up.
await expect(
workspace.rightSidebar.getByRole("region", { name: "Text section" }),
).toBeVisible();
await expect(convertToTypographyButton(workspace)).not.toBeVisible();
});
});