<!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Prototype visual editing mode for the preview app. Toggle the mode, pick elements (single or multiple), and edit margin, padding, border, background, static text, and text styles with live updates, then save changes back to code. - **New Features** - Pen tool button to enable/disable visual editing in the preview and toggle single/multi select; pro-only. - Inline toolbar anchored to the selected element for Margin (X/Y), Padding (X/Y), Border (width/radius/color), Background color, Edit Text (when static), and Text Style (font size/weight/color/font family). - Reads computed styles from the iframe and applies changes in real time; auto-appends px; overlay updates on scroll/resize. - Save/Discard dialog batches edits and writes Tailwind classes to source files via IPC; uses AST/recast to update className and text, replacing conflicting classes by prefix; supports multiple components. - New visual editor worker to get/apply styles and enable inline text editing via postMessage; selector client updated for coordinates streaming and highlight/deselect. - Proxy injects the visual editor client; new atoms track selected component, coordinates, and pending changes; component analysis flags dynamic styling and static text. - Uses runtimeId to correctly target and edit duplicate components. - **Dependencies** - Added @babel/parser for AST-based text updates. - Added recast for safer code transformations. <sup>Written for commit cdd50d33387a29103864f4743ae7570d64d61e93. Summary will update automatically on new commits.</sup> <!-- End of auto-generated description by cubic. -->
220 lines
6.8 KiB
TypeScript
220 lines
6.8 KiB
TypeScript
import { expect } from "@playwright/test";
|
|
import { testSkipIfWindows, Timeout } from "./helpers/test_helper";
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
|
|
testSkipIfWindows("edit style of one selected component", async ({ po }) => {
|
|
await po.setUpDyadPro();
|
|
await po.sendPrompt("tc=basic");
|
|
await po.clickTogglePreviewPanel();
|
|
await po.clickPreviewPickElement();
|
|
|
|
// Select a component
|
|
await po
|
|
.getPreviewIframeElement()
|
|
.contentFrame()
|
|
.getByRole("heading", { name: "Welcome to Your Blank App" })
|
|
.click();
|
|
|
|
// Wait for the toolbar to appear (check for the Margin button which is always visible)
|
|
const marginButton = po.page.getByRole("button", { name: "Margin" });
|
|
await expect(marginButton).toBeVisible({
|
|
timeout: Timeout.MEDIUM,
|
|
});
|
|
|
|
// Ensure the toolbar has proper coordinates before clicking
|
|
await expect(async () => {
|
|
const box = await marginButton.boundingBox();
|
|
expect(box).not.toBeNull();
|
|
expect(box!.y).toBeGreaterThan(0);
|
|
}).toPass({ timeout: Timeout.MEDIUM });
|
|
|
|
// Click on margin button to open the margin popover
|
|
await marginButton.click();
|
|
|
|
// Wait for the popover to fully open by checking for the popover content container
|
|
const marginDialog = po.page
|
|
.locator('[role="dialog"]')
|
|
.filter({ hasText: "Margin" });
|
|
await expect(marginDialog).toBeVisible({
|
|
timeout: Timeout.LONG,
|
|
});
|
|
|
|
// Edit margin - set horizontal margin
|
|
const marginXInput = po.page.getByLabel("Horizontal");
|
|
await marginXInput.fill("20");
|
|
|
|
// Edit margin - set vertical margin
|
|
const marginYInput = po.page.getByLabel("Vertical");
|
|
await marginYInput.fill("10");
|
|
|
|
// Close the popover by clicking outside or pressing escape
|
|
await po.page.keyboard.press("Escape");
|
|
|
|
// Check if the changes are applied to UI by verifying the visual changes dialog appears
|
|
await expect(po.page.getByText(/\d+ component[s]? modified/)).toBeVisible({
|
|
timeout: Timeout.MEDIUM,
|
|
});
|
|
|
|
// Save the changes
|
|
await po.page.getByRole("button", { name: "Save Changes" }).click();
|
|
|
|
// Wait for the success toast
|
|
await po.waitForToastWithText("Visual changes saved to source files");
|
|
|
|
// Verify that the changes are applied to codebase
|
|
await po.snapshotAppFiles({
|
|
name: "visual-editing-single-component-margin",
|
|
files: ["src/pages/Index.tsx"],
|
|
});
|
|
});
|
|
|
|
testSkipIfWindows("edit text of the selected component", async ({ po }) => {
|
|
await po.setUpDyadPro();
|
|
await po.sendPrompt("tc=basic");
|
|
await po.clickTogglePreviewPanel();
|
|
await po.clickPreviewPickElement();
|
|
|
|
// Click on component that contains static text
|
|
await po
|
|
.getPreviewIframeElement()
|
|
.contentFrame()
|
|
.getByRole("heading", { name: "Welcome to Your Blank App" })
|
|
.click();
|
|
|
|
// Wait for the toolbar to appear (check for the Margin button which is always visible)
|
|
await expect(po.page.getByRole("button", { name: "Margin" })).toBeVisible({
|
|
timeout: Timeout.MEDIUM,
|
|
});
|
|
|
|
// Get the iframe and access the content
|
|
const iframe = po.getPreviewIframeElement();
|
|
const frame = iframe.contentFrame();
|
|
|
|
// Find the heading element in the iframe
|
|
const heading = frame.getByRole("heading", {
|
|
name: "Welcome to Your Blank App",
|
|
});
|
|
|
|
await heading.dblclick();
|
|
|
|
// Wait for contentEditable to be enabled
|
|
await expect(async () => {
|
|
const isEditable = await heading.evaluate(
|
|
(el) => (el as HTMLElement).isContentEditable,
|
|
);
|
|
expect(isEditable).toBe(true);
|
|
}).toPass({ timeout: Timeout.MEDIUM });
|
|
|
|
// Clear the existing text and type new text
|
|
await heading.press("Meta+A");
|
|
await heading.type("Hello from E2E Test");
|
|
|
|
// Click outside to finish editing
|
|
await frame.locator("body").click({ position: { x: 10, y: 10 } });
|
|
|
|
// Verify the changes are applied in the UI
|
|
await expect(frame.getByText("Hello from E2E Test")).toBeVisible({
|
|
timeout: Timeout.MEDIUM,
|
|
});
|
|
|
|
// Verify the visual changes dialog appears
|
|
await expect(po.page.getByText(/\d+ component[s]? modified/)).toBeVisible({
|
|
timeout: Timeout.MEDIUM,
|
|
});
|
|
|
|
// Save the changes
|
|
await po.page.getByRole("button", { name: "Save Changes" }).click();
|
|
|
|
// Wait for the success toast
|
|
await po.waitForToastWithText("Visual changes saved to source files");
|
|
|
|
// Verify that the changes are applied to the codebase
|
|
await po.snapshotAppFiles({
|
|
name: "visual-editing-text-content",
|
|
files: ["src/pages/Index.tsx"],
|
|
});
|
|
});
|
|
|
|
testSkipIfWindows("discard changes", async ({ po }) => {
|
|
await po.setUpDyadPro();
|
|
await po.sendPrompt("tc=basic");
|
|
await po.clickTogglePreviewPanel();
|
|
await po.clickPreviewPickElement();
|
|
|
|
// Select a component
|
|
await po
|
|
.getPreviewIframeElement()
|
|
.contentFrame()
|
|
.getByRole("heading", { name: "Welcome to Your Blank App" })
|
|
.click();
|
|
|
|
// Wait for the toolbar to appear (check for the Margin button which is always visible)
|
|
const marginButton = po.page.getByRole("button", { name: "Margin" });
|
|
await expect(marginButton).toBeVisible({
|
|
timeout: Timeout.MEDIUM,
|
|
});
|
|
|
|
// Ensure the toolbar has proper coordinates before clicking
|
|
await expect(async () => {
|
|
const box = await marginButton.boundingBox();
|
|
expect(box).not.toBeNull();
|
|
expect(box!.y).toBeGreaterThan(0);
|
|
}).toPass({ timeout: Timeout.MEDIUM });
|
|
|
|
// Click on margin button to open the margin popover
|
|
await marginButton.click();
|
|
|
|
// Wait for the popover to fully open by checking for the popover content container
|
|
const marginDialog = po.page
|
|
.locator('[role="dialog"]')
|
|
.filter({ hasText: "Margin" });
|
|
await expect(marginDialog).toBeVisible({
|
|
timeout: Timeout.LONG,
|
|
});
|
|
|
|
// Edit margin
|
|
const marginXInput = po.page.getByLabel("Horizontal");
|
|
await marginXInput.fill("30");
|
|
|
|
const marginYInput = po.page.getByLabel("Vertical");
|
|
await marginYInput.fill("30");
|
|
|
|
// Close the popover
|
|
await po.page.keyboard.press("Escape");
|
|
|
|
// Wait for the popover to close
|
|
await expect(marginDialog).not.toBeVisible({
|
|
timeout: Timeout.MEDIUM,
|
|
});
|
|
|
|
// Check if the changes are applied to UI
|
|
await expect(po.page.getByText(/\d+ component[s]? modified/)).toBeVisible({
|
|
timeout: Timeout.MEDIUM,
|
|
});
|
|
|
|
// Take a snapshot of the app files before discarding
|
|
const appPathBefore = await po.getCurrentAppPath();
|
|
const appFileBefore = fs.readFileSync(
|
|
path.join(appPathBefore, "src", "pages", "Index.tsx"),
|
|
"utf-8",
|
|
);
|
|
|
|
// Discard the changes
|
|
await po.page.getByRole("button", { name: "Discard" }).click();
|
|
|
|
// Verify the visual changes dialog is gone
|
|
await expect(po.page.getByText(/\d+ component[s]? modified/)).not.toBeVisible(
|
|
{ timeout: Timeout.MEDIUM },
|
|
);
|
|
|
|
// Verify that the changes are NOT applied to codebase
|
|
const appFileAfter = fs.readFileSync(
|
|
path.join(appPathBefore, "src", "pages", "Index.tsx"),
|
|
"utf-8",
|
|
);
|
|
|
|
// The file content should be the same as before
|
|
expect(appFileAfter).toBe(appFileBefore);
|
|
});
|