diff --git a/e2e/beta/specs/apps/analytics-dashboard-create-rename.spec.ts b/e2e/beta/specs/apps/analytics-dashboard-create-rename.spec.ts new file mode 100644 index 00000000000..486ec873893 --- /dev/null +++ b/e2e/beta/specs/apps/analytics-dashboard-create-rename.spec.ts @@ -0,0 +1,150 @@ +import { expect, test } from "@playwright/test"; + +import { + assertSignedInOnBeta, + runMarker, + signedInContext, + skipUnlessAuthed, +} from "../../lib/authed"; +import { originFor, selectedSites, siteById } from "../../lib/fleet"; +import { BETA_E2E_TEST_TRAFFIC_HEADERS } from "../../lib/test-traffic"; + +skipUnlessAuthed(); + +const selected = new Set(selectedSites().map((site) => site.id)); +const site = siteById("analytics"); +const origin = originFor(site); +const ACTION_HEADERS = { + ...BETA_E2E_TEST_TRAFFIC_HEADERS, + "X-Agent-Native-Frontend": "1", +}; + +type ActionResult = Record; + +async function runAction( + page: import("@playwright/test").Page, + name: string, + data: Record, +): Promise { + const response = await page.request.post( + `${origin}/_agent-native/actions/${name}`, + { data, headers: ACTION_HEADERS }, + ); + const result = (await response.json().catch(() => ({}))) as ActionResult; + expect( + response.ok(), + `${name} should succeed (${response.status()}): ${JSON.stringify(result).slice(0, 500)}`, + ).toBe(true); + return result; +} + +async function readAction( + page: import("@playwright/test").Page, + name: string, + data: Record = {}, +): Promise { + const response = await page.request.get( + `${origin}/_agent-native/actions/${name}`, + { params: data, headers: ACTION_HEADERS }, + ); + const result: unknown = await response.json().catch(() => ({})); + expect( + response.ok(), + `${name} should succeed (${response.status()}): ${JSON.stringify(result).slice(0, 500)}`, + ).toBe(true); + return result; +} + +async function deleteAction( + page: import("@playwright/test").Page, + name: string, + data: Record, +): Promise { + const response = await page.request.delete( + `${origin}/_agent-native/actions/${name}`, + { data, headers: ACTION_HEADERS }, + ); + const result = await response.json().catch(() => ({})); + expect( + response.ok(), + `${name} should succeed (${response.status()}): ${JSON.stringify(result).slice(0, 500)}`, + ).toBe(true); +} + +test("Analytics beta creates, renames, and removes a SQL dashboard", async ({ + browser, +}) => { + test.skip( + !selected.has("analytics"), + "analytics is not in this run's selection", + ); + + const context = await signedInContext(browser, site, { seedModel: false }); + const page = await context.newPage(); + const dashboardId = `beta-e2e-${crypto.randomUUID()}`; + const marker = `${runMarker("analytics dashboard create rename")} ${dashboardId}`; + const renamed = `${marker} renamed`; + let createAttempted = false; + + try { + await assertSignedInOnBeta(context, site); + + createAttempted = true; + const created = await runAction(page, "update-dashboard", { + dashboardId, + config: { name: marker, panels: [] }, + }); + expect(created).toMatchObject({ dashboardId, name: marker }); + + const createdReadback = await readAction(page, "get-sql-dashboard", { + id: dashboardId, + }); + expect(createdReadback).toMatchObject({ + id: dashboardId, + kind: "sql", + name: marker, + title: marker, + }); + + const renamedResult = await runAction(page, "rename-dashboard", { + id: dashboardId, + name: renamed, + }); + expect(renamedResult).toMatchObject({ id: dashboardId, name: renamed }); + + const renamedReadback = await readAction(page, "get-sql-dashboard", { + id: dashboardId, + }); + expect(renamedReadback).toMatchObject({ + id: dashboardId, + kind: "sql", + name: renamed, + title: renamed, + }); + } finally { + try { + if (createAttempted) { + await deleteAction(page, "delete-sql-dashboard", { id: dashboardId }); + const deletedReadback = await page.request.get( + `${origin}/_agent-native/actions/get-sql-dashboard`, + { params: { id: dashboardId }, headers: ACTION_HEADERS }, + ); + expect(deletedReadback.status()).toBe(404); + + const dashboards = await readAction(page, "list-sql-dashboards", { + archived: "all", + hidden: "all", + }); + expect(Array.isArray(dashboards)).toBe(true); + expect( + (dashboards as Array<{ id?: string }>).some( + (dashboard) => dashboard.id === dashboardId, + ), + `SQL dashboard ${dashboardId} remained after cleanup`, + ).toBe(false); + } + } finally { + await context.close(); + } + } +}); diff --git a/e2e/beta/specs/apps/design-inline-source-save.spec.ts b/e2e/beta/specs/apps/design-inline-source-save.spec.ts new file mode 100644 index 00000000000..74d162f5ba6 --- /dev/null +++ b/e2e/beta/specs/apps/design-inline-source-save.spec.ts @@ -0,0 +1,177 @@ +import { expect, test } from "@playwright/test"; + +import { + assertSignedInOnBeta, + runMarker, + signedInContext, + skipUnlessAuthed, +} from "../../lib/authed"; +import { originFor, selectedSites, siteById } from "../../lib/fleet"; +import { BETA_E2E_TEST_TRAFFIC_HEADERS } from "../../lib/test-traffic"; + +skipUnlessAuthed(); + +const SITE = siteById("design"); +const ORIGIN = originFor(SITE); +const NODE_ID = "beta-inline-source-save-target"; +const COLOR = "rgb(22, 101, 52)"; +const ACTION_HEADERS = { + ...BETA_E2E_TEST_TRAFFIC_HEADERS, + "X-Agent-Native-Frontend": "1", +}; +const FIXTURE = ` + + Beta inline source save +
Save target
+`; + +async function postAction( + page: import("@playwright/test").Page, + name: string, + input: Record, +): Promise> { + const response = await page.request.post( + `${ORIGIN}/_agent-native/actions/${name}`, + { data: input, headers: ACTION_HEADERS }, + ); + if (!response.ok()) { + throw new Error( + `${name} failed: HTTP ${response.status()} ${await response.text()}`, + ); + } + return (await response.json()) as Record; +} + +async function readSource( + page: import("@playwright/test").Page, + designId: string, +): Promise { + const response = await page.request.get( + new URL("/_agent-native/actions/read-source-file", ORIGIN).href, + { params: { designId, path: "index.html" }, headers: ACTION_HEADERS }, + ); + if (!response.ok()) { + throw new Error(`read-source-file failed: HTTP ${response.status()}`); + } + const result = (await response.json()) as { content?: unknown }; + if (typeof result.content !== "string") { + throw new Error("read-source-file returned no source content"); + } + return result.content; +} + +test("Design inline source visual edits persist through reload and cleanup", async ({ + browser, +}) => { + test.skip( + !selectedSites().some((site) => site.id === SITE.id), + "design is not selected by BETA_E2E_APPS", + ); + const context = await signedInContext(browser, SITE, { seedModel: false }); + const page = await context.newPage(); + const designId = crypto.randomUUID(); + let primaryFailure: unknown; + try { + await assertSignedInOnBeta(context, SITE); + + const created = await postAction(page, "create-design", { + id: designId, + title: runMarker(`Design inline source save ${Date.now()}`), + projectType: "prototype", + }); + expect(created.id).toBe(designId); + await postAction(page, "create-file", { + designId, + filename: "index.html", + content: FIXTURE, + fileType: "html", + }); + + await page.goto(`${ORIGIN}/design/${designId}`, { + waitUntil: "domcontentloaded", + timeout: 90_000, + }); + await expect( + page.getByRole("button", { name: "Move", exact: true }), + ).toBeVisible({ + timeout: 45_000, + }); + const preview = page + .locator("iframe[data-design-preview-iframe][data-screen-iframe-id]") + .first() + .contentFrame(); + const target = preview.locator(`[data-agent-native-node-id="${NODE_ID}"]`); + await expect(target).toHaveCSS("color", "rgb(153, 27, 27)"); + + const edit = await postAction(page, "apply-visual-edit", { + source: { kind: "design-file", designId, filename: "index.html" }, + intent: { + kind: "style", + target: { nodeId: NODE_ID }, + property: "color", + value: COLOR, + }, + }); + expect(edit.persisted).toBe(true); + await expect.poll(() => readSource(page, designId)).toContain(COLOR); + + await page.reload({ waitUntil: "domcontentloaded" }); + await expect( + page.getByRole("button", { name: "Move", exact: true }), + ).toBeVisible({ + timeout: 45_000, + }); + await expect( + page + .locator("iframe[data-design-preview-iframe][data-screen-iframe-id]") + .first() + .contentFrame() + .locator(`[data-agent-native-node-id="${NODE_ID}"]`), + ).toHaveCSS("color", COLOR); + await expect.poll(() => readSource(page, designId)).toContain(COLOR); + } catch (error) { + primaryFailure = error; + throw error; + } finally { + const cleanupFailures: string[] = []; + try { + const existing = await page.request.get( + `${ORIGIN}/_agent-native/actions/get-design`, + { params: { id: designId }, headers: ACTION_HEADERS }, + ); + if (existing.status() === 200) { + const deleted = await postAction(page, "delete-design", { + id: designId, + }); + expect(deleted).toMatchObject({ id: designId, deleted: true }); + } else if (existing.status() !== 404) { + throw new Error( + `Could not verify test design before cleanup (${existing.status()})`, + ); + } + const readAfterDelete = await page.request.get( + `${ORIGIN}/_agent-native/actions/get-design`, + { params: { id: designId }, headers: ACTION_HEADERS }, + ); + expect(readAfterDelete.status()).toBe(404); + } catch (error) { + cleanupFailures.push( + `delete-design verification failed: ${error instanceof Error ? error.message : String(error)}`, + ); + } + try { + await context.close(); + } catch (error) { + cleanupFailures.push( + `context.close failed: ${error instanceof Error ? error.message : String(error)}`, + ); + } + if (cleanupFailures.length > 0) { + const message = `[beta-e2e] Design source save teardown failures: ${cleanupFailures.join("; ")}`; + if (primaryFailure) { + throw new AggregateError([primaryFailure, new Error(message)], message); + } + throw new Error(message); + } + } +});