Files
moreminimore-vibe/e2e-tests/visual_editing.spec.ts
Mohamed Aziz Mejri 352d4330ed Visual editor (Pro only) (#1828)
<!-- 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. -->
2025-12-09 13:09:19 -08:00

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);
});