diff --git a/tests/runner-e2e/PRODUCT-JOURNEYS.md b/tests/runner-e2e/PRODUCT-JOURNEYS.md index 296dbc6309f..ed5f8c2cc83 100644 --- a/tests/runner-e2e/PRODUCT-JOURNEYS.md +++ b/tests/runner-e2e/PRODUCT-JOURNEYS.md @@ -33,6 +33,10 @@ never start a server or depend on a tenant filesystem. | `stop-redirect` | Native agent; three work runs; qualified Docker oracle | Download a verified checkpoint, Stop an active revision, require native cancellation acknowledgement, submit a new direction once and verify completion without original-run activity | | `cloud-onboarding` | Signed-in admitted Cloud account; fresh workspace; qualified bounded provider route | Create workspace in the portal, connect API key and native Codex agent in the wizard, complete a task, download and independently check its artifact | +The shared Studio helper waits for the original saved file body before typing. +An editor control can mount before the file query completes; editing that empty +draft can otherwise lose the original content when the file arrives. + Supported acquisitions are managed self-hosted (`local`), registered existing Cloud (`local` or `daytona` when present), and new Cloud. New Cloud must start with onboarding. Its context can then satisfy the identical task-properties diff --git a/tests/runner-e2e/playwright-support.config.ts b/tests/runner-e2e/playwright-support.config.ts index 8d2bd94f902..a21d78db448 100644 --- a/tests/runner-e2e/playwright-support.config.ts +++ b/tests/runner-e2e/playwright-support.config.ts @@ -3,7 +3,7 @@ import { defineConfig } from "@playwright/test"; /** Browser-only harness regressions: no Paperclip instance or provider credentials. */ export default defineConfig({ testDir: ".", - testMatch: ["native-task-reply.spec.ts", "screenshot-readiness.spec.ts", "service-worker-reload.spec.ts", "lost-send.spec.ts", "chat-restart.spec.ts", "settings-toggle.spec.ts", "browser-bootstrap-diagnostics.spec.ts", "service-worker-module-loading.spec.ts", "browser-bootstrap-recovery.spec.ts", "blocker-input.spec.ts", "native-blocker-visible.spec.ts", "copilot-permission-card.spec.ts", "github-review-check.spec.ts", "antigravity-journey-browser.spec.ts"], + testMatch: ["skill-studio-flow.spec.ts", "native-task-reply.spec.ts", "screenshot-readiness.spec.ts", "service-worker-reload.spec.ts", "lost-send.spec.ts", "chat-restart.spec.ts", "settings-toggle.spec.ts", "browser-bootstrap-diagnostics.spec.ts", "service-worker-module-loading.spec.ts", "browser-bootstrap-recovery.spec.ts", "blocker-input.spec.ts", "native-blocker-visible.spec.ts", "copilot-permission-card.spec.ts", "github-review-check.spec.ts", "antigravity-journey-browser.spec.ts"], workers: 1, retries: 0, timeout: 10_000, diff --git a/tests/runner-e2e/skill-studio-flow.spec.ts b/tests/runner-e2e/skill-studio-flow.spec.ts new file mode 100644 index 00000000000..a27c3ff32b8 --- /dev/null +++ b/tests/runner-e2e/skill-studio-flow.spec.ts @@ -0,0 +1,45 @@ +import { expect, test } from "@playwright/test"; +import { editCreatedSkillThroughStudio } from "./skill-studio-flow.js"; + +for (const hydrationMs of [0, 1500]) test(`Studio preserves the loaded file before editing (${hydrationMs}ms hydration)`, async ({ page }) => { + const skillId = "owned-skill", skillName = "owned-qa-skill", marker = "Studio edit marker: owned-campaign"; + let saved = "Verify checks.", saves = 0; + await page.exposeFunction("saveSkill", (body: string) => { saved = body; saves++; }); + await page.route("https://fixture.test/**", async route => { + const studio = new URL(route.request().url()).pathname.includes("/skills/studio/"); + const body = studio ? ` +
+ + ` : `
+ + `; + await route.fulfill({ contentType: "text/html", body }); + }); + await page.goto("https://fixture.test/QA/issues/QA-1"); + await editCreatedSkillThroughStudio({ page, skillId, skillName, marker, deadlineAt: Date.now() + 8000, + read: async () => ({ content: saved }) }); + expect(saves).toBe(1); + expect(saved).toContain("Verify checks."); + expect(saved).toContain(marker); +}); + +test("Studio never edits or saves an unhydrated file", async ({ page }) => { + let saves = 0, inputs = 0; + await page.exposeFunction("savedUnloadedFile", () => { saves++; }); + await page.exposeFunction("editedUnloadedFile", () => { inputs++; }); + await page.route("https://fixture.test/**", route => route.fulfill({ contentType: "text/html", body: + `
+ +
+ ` })); + await page.goto("https://fixture.test/QA/issues/QA-1"); + await expect(editCreatedSkillThroughStudio({ page, skillId: "id", skillName: "owned", marker: "New content", deadlineAt: Date.now() + 7000, + read: async () => ({}) })).rejects.toThrow(); + expect(inputs).toBe(0); + expect(saves).toBe(0); +}); diff --git a/tests/runner-e2e/skill-studio-flow.ts b/tests/runner-e2e/skill-studio-flow.ts index 7b12ba3e82c..686bfa1a0e4 100644 --- a/tests/runner-e2e/skill-studio-flow.ts +++ b/tests/runner-e2e/skill-studio-flow.ts @@ -16,6 +16,9 @@ export async function editCreatedSkillThroughStudio(input: { await expect(page).toHaveURL(new RegExp(`/skills/studio/${input.skillId}$`)); await expect(page.getByRole("combobox").filter({ hasText: skillName })).toBeVisible(); const editor = page.getByRole("textbox", { name: "editable markdown", exact: true }); + // Studio mounts the editor before its file query completes. Wait for the + // saved body, otherwise hydration can replace edits made to the empty draft. + await expect(editor).toContainText("Verify checks."); await editor.click(); await editor.press("ControlOrMeta+End"); await editor.press("Enter");