import { test, expect } from "@playwright/test"; import { WasmWorkspacePage } from "../pages/WasmWorkspacePage"; const FILE = { id: "3b0d758a-8c9d-8013-8006-52c8337e5c72", pageId: "3b0d758a-8c9d-8013-8006-52c8337e5c73", }; test.beforeEach(async ({ page }) => { await WasmWorkspacePage.init(page); // WASM_FLAGS already enables render-wasm; add the WASM text editor on top. await WasmWorkspacePage.mockConfigFlags(page, [ "enable-feature-text-editor-wasm", ]); }); async function openEditorAndSelectAll(workspace) { await workspace.clickLeafLayer("this is a text"); // Enter edit mode (waits until the typography controls are ready) and then // select every character so the sidebar reflects the combined styles of the // whole text via the WASM editor path. await workspace.textEditor.startEditing(); await workspace.page.keyboard.press("ControlOrMeta+a"); } test.describe("BUG 10502 - Mixed families and variants", () => { test("Multiple variants of the same font family", async ({ page }) => { const workspace = new WasmWorkspacePage(page, { textEditor: true }); await workspace.setupEmptyFile(); await workspace.mockGetFile( "text-editor/get-file-10502-mixed-variants.json", ); await workspace.goToWorkspace(FILE); await workspace.waitForFirstRender(); await openEditorAndSelectAll(workspace); // The whole selection shares a single font family, so it must be shown even // though the variants differ. const fontFamily = workspace.rightSidebar.getByTitle("Font Family"); await expect(fontFamily).toContainText("Source Sans Pro"); // The variants differ across the selection, so the variant dropdown shows the // "mixed" placeholder. const fontVariant = workspace.rightSidebar .getByTitle("Font Style") .getByRole("combobox"); await expect(fontVariant).toHaveText("--"); }); test("Mixed font families appear as such in the dropdown", async ({ page, }) => { const workspace = new WasmWorkspacePage(page, { textEditor: true }); await workspace.setupEmptyFile(); await workspace.mockGetFile( "text-editor/get-file-10502-mixed-families.json", ); // Serve a stand-in TTF for Sora so the render doesn't wait on a real fetch. // Glyphs are irrelevant here: the assertion only inspects the sidebar. await workspace.mockGoogleFont("sora", "render-wasm/assets/ebgaramond.ttf"); await workspace.goToWorkspace(FILE); await workspace.waitForFirstRender(); await openEditorAndSelectAll(workspace); // The selection mixes two different font families (Source Sans Pro and Sora), // so the font family dropdown reports it as mixed. const fontFamily = workspace.rightSidebar.getByTitle("Font Family"); await expect(fontFamily).toContainText("Mixed Font Families"); }); }); test.describe("BUG 10530 - Empty text box left behind when leaving the editor", () => { test("An empty text box is removed when leaving the editor by clicking outside", async ({ page, }) => { const workspace = new WasmWorkspacePage(page, { textEditor: true }); await workspace.setupEmptyFile(); await workspace.goToWorkspace(); await workspace.waitForFirstRender(); const layerRows = workspace.layers.getByTestId("layer-row"); await expect(layerRows).toHaveCount(0); // Draw an empty text box await workspace.createTextShape(200, 150, 320, 210); // The shape exists while it is being edited await expect(layerRows).toHaveCount(1); // Leave the editor by clicking outside await workspace.clickAt(500, 400); await expect(layerRows).toHaveCount(0); }); test("A non-empty text box is kept when leaving the editor by clicking outside", async ({ page, }) => { const workspace = new WasmWorkspacePage(page, { textEditor: true }); await workspace.setupEmptyFile(); await workspace.goToWorkspace(); await workspace.waitForFirstRender(); const layerRows = workspace.layers.getByTestId("layer-row"); // A text box with content must survive leaving the editor by clicking outside. await workspace.createTextShape(200, 150, 320, 210, "hello"); await workspace.clickAt(500, 400); await expect(layerRows).toHaveCount(1); }); }); test("BUG 10467 - Auto-width text captures every typed character", async ({ page, }) => { const workspace = new WasmWorkspacePage(page, { textEditor: true }); await workspace.setupEmptyFile(); await workspace.goToWorkspace(); await workspace.waitForFirstRender(); const layerRows = workspace.layers.getByTestId("layer-row"); // A single click with the text tool creates an auto-width text box by default await workspace.createAutoWidthTextShape(200, 150, "hello world"); // Leave the editor to finalize the content await workspace.textEditor.stopEditing(); // Assert the whole typed text made it into the shape await workspace.layers.getByTestId("layer-row").first().click(); await workspace.waitForSelectedShapeName("hello world"); }); test.describe("BUG 10910 - Text is not replaced when there is a selection", () => { // Non-ascii on purpose: selection offsets are counted in characters. test("Typing over a selection replaces it", async ({ page }) => { const workspace = new WasmWorkspacePage(page, { textEditor: true }); await workspace.setupEmptyFile(); await workspace.goToWorkspace(); await workspace.waitForFirstRender(); await workspace.createAutoWidthTextShape(200, 150, "Añadir"); await page.keyboard.press("ControlOrMeta+a"); await page.keyboard.type("nuevo"); await workspace.textEditor.stopEditing(); await workspace.layers.getByTestId("layer-row").first().click(); await workspace.waitForSelectedShapeName("nuevo"); }); test("Typing over a selection that contains emoji replaces it", async ({ page, }) => { const workspace = new WasmWorkspacePage(page, { textEditor: true }); await workspace.setupEmptyFile(); await workspace.goToWorkspace(); await workspace.waitForFirstRender(); await workspace.createAutoWidthTextShape(200, 150, "Hola 😀"); await page.keyboard.press("ControlOrMeta+a"); await page.keyboard.type("ok"); await workspace.textEditor.stopEditing(); await workspace.layers.getByTestId("layer-row").first().click(); await workspace.waitForSelectedShapeName("ok"); }); test("Backspace deletes the selection", async ({ page }) => { const workspace = new WasmWorkspacePage(page, { textEditor: true }); await workspace.setupEmptyFile(); await workspace.goToWorkspace(); await workspace.waitForFirstRender(); await workspace.createAutoWidthTextShape(200, 150, "Añadir texto"); await page.keyboard.press("ControlOrMeta+a"); await page.keyboard.press("Backspace"); await page.keyboard.type("ok"); await workspace.textEditor.stopEditing(); await workspace.layers.getByTestId("layer-row").first().click(); await workspace.waitForSelectedShapeName("ok"); }); }); test("BUG 10531 - Entering the editor auto-selects the whole text", async ({ page, }) => { const workspace = new WasmWorkspacePage(page, { textEditor: true }); await workspace.setupEmptyFile(); await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json"); await workspace.goToWorkspace(); await workspace.waitForFirstRender(); // Select the existing text shape and enter edit mode via Enter await workspace.clickLeafLayer("Lorem ipsum"); await workspace.textEditor.startEditing(); // Copying while editing exports only the selected text as raw text. // Since we just entered the editor, the whole text should be selected. await workspace.copy("keyboard"); // Assert the text was copied correctly const copiedText = await page.evaluate(() => navigator.clipboard.readText()); expect(copiedText).toBe("Lorem ipsum"); }); test.describe("BUG 10934 - Double-clicking a text side handle sets auto-size", () => { // Sets up the workspace and loads a text shape whose size is larger than its text async function setupFixedSizeText(page) { const workspace = new WasmWorkspacePage(page, { textEditor: true }); // Enable token inputs so they use the new component with accessible DOM await workspace.mockConfigFlags(["enable-feature-token-input"]); await workspace.setupEmptyFile(); await workspace.mockGetFile("text-editor/get-file-fixed-size-text.json"); await workspace.goToWorkspace(); await workspace.waitForFirstRender(); // Select the text and zoom to fit, so it is fully visible in the viewport await workspace.clickLeafLayer("Fixed text"); await page.keyboard.press("Shift+1"); await workspace.waitForIdle(); return workspace; } async function doubleClickSideHandle(workspace, position) { const handle = workspace.viewport.getByTestId( `resize-side-handler-${position}`, ); await handle.waitFor(); const box = await handle.boundingBox(); await workspace.page.mouse.dblclick( box.x + box.width / 2, box.y + box.height / 2, ); } function measureInput(workspace, name) { return workspace.rightSidebar .getByRole("region", { name: "shape-measures-section" }) .getByRole("textbox", { name, exact: true }); } test("Double-clicking the right handle switches to auto-width", async ({ page, }) => { const workspace = await setupFixedSizeText(page); const widthInput = workspace.rightSidebar .getByRole("region", { name: "shape-measures-section" }) .getByRole("textbox", { name: "Width", exact: true }); const initialWidth = Number(await widthInput.inputValue()); await doubleClickSideHandle(workspace, "right"); // Assert auto-width is selected and that the width has shrunk. The resize // is debounced, so poll the value (auto-retrying) rather than reading once. await expect( workspace.rightSidebar.getByRole("button", { name: "Auto width", pressed: true, }), ).toBeVisible(); await expect .poll(async () => Number(await widthInput.inputValue())) .toBeLessThan(initialWidth); }); test("Double-clicking the bottom handle switches to auto-height", async ({ page, }) => { const workspace = await setupFixedSizeText(page); const heightInput = workspace.rightSidebar .getByRole("region", { name: "shape-measures-section" }) .getByRole("textbox", { name: "Height", exact: true }); const initialHeight = Number(await heightInput.inputValue()); await doubleClickSideHandle(workspace, "bottom"); // Assert auto-height is selected and that the height has shrunk. The resize // is debounced, so poll the value (auto-retrying) rather than reading once. await expect( workspace.rightSidebar.getByRole("button", { name: "Auto height", pressed: true, }), ).toBeVisible(); await expect .poll(async () => Number(await heightInput.inputValue())) .toBeLessThan(initialHeight); }); });