Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 150 additions & 0 deletions e2e/beta/specs/apps/analytics-dashboard-create-rename.spec.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>;

async function runAction(
page: import("@playwright/test").Page,
name: string,
data: Record<string, unknown>,
): Promise<ActionResult> {
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<string, string> = {},
): Promise<unknown> {
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<string, unknown>,
): Promise<void> {
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();
}
}
});
177 changes: 177 additions & 0 deletions e2e/beta/specs/apps/design-inline-source-save.spec.ts
Original file line number Diff line number Diff line change
@@ -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 = `<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><title>Beta inline source save</title></head>
<body><main data-agent-native-node-id="${NODE_ID}" data-agent-native-layer-name="Save target" style="color:rgb(153, 27, 27)">Save target</main></body>
</html>`;

async function postAction(
page: import("@playwright/test").Page,
name: string,
input: Record<string, unknown>,
): Promise<Record<string, unknown>> {
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<string, unknown>;
}

async function readSource(
page: import("@playwright/test").Page,
designId: string,
): Promise<string> {
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);
}
}
});
Loading