Files
webstudio/apps/builder/e2e/flows/sync-status.ts
Oleg Isonen d291e4b671 feat: Builder api runtime contracts guardrails for MCP (#5857)
This PR expands Webstudio MCP and CLI into a more complete agent
authoring interface. Agents can discover project capabilities, edit real
Webstudio content, use data/resources, verify visual output, and report
useful issues when workflows fail.

## MCP User Capabilities

- Create, update, duplicate, reorder, and delete pages.
- Create pages from templates and manage reusable page templates.
- Inspect project pages, instances, props, styles, assets, variables,
resources, and metadata.
- Insert Webstudio components and authored JSX fragments into pages.
- Edit text, props, styles, breakpoints, design tokens, and CSS
variables.
- Update page metadata, SEO fields, redirects, marketplace metadata, and
project settings.
- Create and use static data variables.
- Create and use dynamic resources fetched over HTTP.
- Use REST, GraphQL, and built-in system resources in authored pages.
- Bind data variables and resources to visible page content.
- Create resource-backed pages such as sitemap XML, structured SEO data,
and API-driven content sections.
- Preview current project state without requiring a publish step.
- Capture screenshots for visual verification.
- Verify responsive designs across desktop, tablet, and mobile
breakpoints.
- Use screenshot diff and OCR-style checks to inspect rendered output.
- Reset and refresh local MCP project state when needed.
- Discover available CLI/MCP commands from generated manuals.
- Use clearer examples for resources, data variables, JSX authoring,
previewing, and visual checks.
- Read marketplace product metadata without requiring a full project
snapshot.
- Get safer guidance when using low-level patch tools.
- Get a ready-to-copy Discord report template when MCP/CLI behavior is
confusing or broken.
2026-07-10 02:13:22 +02:00

99 lines
2.2 KiB
TypeScript

import type { Page } from "playwright";
type SyncStatusDotStatus = "idle" | "pending" | "saved" | "error";
const getSyncStatusDot = (page: Page) =>
page.getByRole("status", { name: /^Sync status:/ });
const syncStatusDotSelector = '[role="status"][aria-label^="Sync status:"]';
const getSyncStatusDotByStatus = ({
page,
status,
}: {
page: Page;
status: SyncStatusDotStatus;
}) =>
page.locator(
`[role="status"][aria-label^="Sync status:"][data-sync-status="${status}"]`
);
const getSyncStatusLabel = async (page: Page) =>
(await getSyncStatusDot(page).getAttribute("aria-label")) ??
"Sync status: error";
const waitForAnySyncStatus = async ({
page,
statuses,
timeout,
}: {
page: Page;
statuses: SyncStatusDotStatus[];
timeout: number;
}) => {
const handle = await page.waitForFunction(
({ selector, statuses }) => {
const dot = document.querySelector<HTMLElement>(selector);
const status = dot?.dataset.syncStatus;
if (status === undefined) {
return false;
}
if ((statuses as readonly string[]).includes(status)) {
return status;
}
return false;
},
{ selector: syncStatusDotSelector, statuses },
{ timeout }
);
return (await handle.jsonValue()) as SyncStatusDotStatus;
};
export const waitForSyncStatus = async ({
page,
status,
timeout = 10_000,
}: {
page: Page;
status: SyncStatusDotStatus;
timeout?: number;
}) => {
await getSyncStatusDot(page).waitFor({
state: "attached",
timeout,
});
await getSyncStatusDotByStatus({ page, status }).waitFor({ timeout });
};
export const waitForChangeToBeSaved = async ({
page,
timeout = 10_000,
}: {
page: Page;
timeout?: number;
}) => {
const firstStatus = await waitForAnySyncStatus({
page,
statuses: ["pending", "error"],
timeout,
});
if (firstStatus === "error") {
throw new Error(
`Expected change to save, received ${await getSyncStatusLabel(page)}`
);
}
const finalStatus = await waitForAnySyncStatus({
page,
statuses: ["saved", "idle", "error"],
timeout,
});
if (finalStatus === "error") {
throw new Error(
`Expected change to save, received ${await getSyncStatusLabel(page)}`
);
}
};