diff --git a/server/test/__mocks__/playwright.ts b/server/test/__mocks__/playwright.ts index 17793e1..37ef6b5 100644 --- a/server/test/__mocks__/playwright.ts +++ b/server/test/__mocks__/playwright.ts @@ -9,6 +9,7 @@ const makePage = () => ({ const makeContext = () => ({ newPage: jest.fn().mockResolvedValue(makePage()), + cookies: jest.fn().mockResolvedValue([]), addCookies: jest.fn().mockResolvedValue(undefined), addInitScript: jest.fn().mockResolvedValue(undefined), }); diff --git a/server/test/browser.controller.spec.ts b/server/test/browser.controller.spec.ts index 6cf32d7..3e32fac 100644 --- a/server/test/browser.controller.spec.ts +++ b/server/test/browser.controller.spec.ts @@ -8,8 +8,8 @@ import { Repository } from "typeorm"; /** * Browser controller integration tests. * - * POST /open and POST /exec need a running Playwright browser. We test - * validation rejections (no browser launched) and session-not-found paths, + * POST /open and POST /exec need a running Playwright browser. We test + * validation rejections and auto-create behavior for unknown named sessions, * which are safe to run in a headless CI environment. */ describe("BrowserController", () => { @@ -54,11 +54,16 @@ describe("BrowserController", () => { .expect(400); }); - it("returns 404 when session does not exist", async () => { - await request(app.getHttpServer()) + it("auto-creates missing named session and succeeds", async () => { + const res = await request(app.getHttpServer()) .post("/open") .send({ sessionName: "no-such-session", url: "https://example.com" }) - .expect(404); + .expect(201); + expect(res.body).toMatchObject({ + url: expect.any(String), + title: expect.any(String), + content: expect.any(String), + }); }); it("succeeds without a session (sessionless open)", async () => { @@ -115,11 +120,12 @@ describe("BrowserController", () => { .expect(400); }); - it("returns 404 when session does not exist", async () => { - await request(app.getHttpServer()) + it("auto-creates missing named session and succeeds", async () => { + const res = await request(app.getHttpServer()) .post("/exec") .send({ sessionName: "no-such-session", code: "return 1;" }) - .expect(404); + .expect(201); + expect(res.body).toEqual({ result: 1 }); }); it("succeeds without a session (sessionless exec)", async () => { diff --git a/server/test/scenario-step-output.spec.ts b/server/test/scenario-step-output.spec.ts index 1127eab..7e36403 100644 --- a/server/test/scenario-step-output.spec.ts +++ b/server/test/scenario-step-output.spec.ts @@ -15,6 +15,7 @@ import { DataSource } from "typeorm"; import { buildTestApp } from "./app.harness"; import { ScenarioService } from "../src/scenario/scenario.service"; import { ScenarioSchedulerService } from "../src/scenario/scenario-scheduler.service"; +import { EnvironmentEntity } from "../src/environment/environment.entity"; describe("ScenarioRunStepEntity.output + helpers.getStepOutput", () => { let app: INestApplication; @@ -52,18 +53,26 @@ describe("ScenarioRunStepEntity.output + helpers.getStepOutput", () => { scenarioId: string, _order: number, execCode: string, - sessionName = "output-test-session", ): Promise { const step = await scenarioService.createStep(scenarioId, { - sessionName, execCode, }); return step.id; } + /** Create a test environment and return its id. */ + async function createEnvironment( + name = `output-env-${Math.random().toString(36).slice(2, 8)}`, + ): Promise { + const repo = dataSource.getRepository(EnvironmentEntity); + const env = await repo.save(repo.create({ name, data: {} })); + return env.id; + } + /** Trigger a run and process it to completion via the scheduler. */ async function runScenario(scenarioId: string): Promise { - const run = await scenarioService.createRun(scenarioId); + const environmentId = await createEnvironment(); + const run = await scenarioService.createRun(scenarioId, environmentId); // Drive the scheduler directly — keeps tests synchronous and fast. await scheduler.pickUpPendingRuns(); // Wait for the run to reach a terminal state (max 10 s). diff --git a/server/test/scenario.controller.spec.ts b/server/test/scenario.controller.spec.ts index 03a5fae..b3a27ef 100644 --- a/server/test/scenario.controller.spec.ts +++ b/server/test/scenario.controller.spec.ts @@ -41,6 +41,27 @@ describe("ScenarioController", () => { return res.body as { id: string }; } + async function createEnvironment(name = `env-${Math.random()}`) { + const res = await request(app.getHttpServer()) + .post("/environments") + .send({ name, data: {} }) + .expect(201); + return res.body as { id: string; name: string }; + } + + async function createRun(scenarioId: string) { + const env = await createEnvironment(); + const res = await request(app.getHttpServer()) + .post(`/scenarios/${scenarioId}/run`) + .send({ environmentId: env.id }) + .expect(201); + return res.body as { + id: string; + status: string; + stepRuns: Array<{ status: string }>; + }; + } + // ── POST /scenarios ──────────────────────────────────────────────────────── describe("POST /scenarios", () => { @@ -190,25 +211,22 @@ describe("ScenarioController", () => { const res = await request(app.getHttpServer()) .post(`/scenarios/${sc.id}/steps`) .send({ - order: 0, - sessionName: "my-session", execCode: "return 1;", }) .expect(201); expect(res.body.id).toBeDefined(); expect(res.body.order).toBe(0); - expect(res.body.sessionName).toBe("my-session"); }); it("creates a step without execCode", async () => { const sc = await createScenario(); const res = await request(app.getHttpServer()) .post(`/scenarios/${sc.id}/steps`) - .send({ order: 0, sessionName: "session-x" }) + .send({ title: "step without exec" }) .expect(201); - expect(res.body.sessionName).toBe("session-x"); + expect(res.body.title).toBe("step without exec"); expect(res.body.execCode).toBeNull(); }); @@ -371,15 +389,13 @@ describe("ScenarioController", () => { await createStep(sc.id, { order: 1 }); await createStep(sc.id, { order: 2 }); - const res = await request(app.getHttpServer()) - .post(`/scenarios/${sc.id}/run`) - .expect(201); + const res = await createRun(sc.id); - expect(res.body.status).toBe("pending"); - expect(Array.isArray(res.body.stepRuns)).toBe(true); - expect(res.body.stepRuns).toHaveLength(3); + expect(res.status).toBe("pending"); + expect(Array.isArray(res.stepRuns)).toBe(true); + expect(res.stepRuns).toHaveLength(3); - const statuses = res.body.stepRuns.map( + const statuses = res.stepRuns.map( (s: { status: string }) => s.status, ); expect(statuses[0]).toBe("pending"); @@ -388,8 +404,10 @@ describe("ScenarioController", () => { }); it("returns 404 for unknown scenario", async () => { + const env = await createEnvironment(); await request(app.getHttpServer()) .post("/scenarios/00000000-0000-0000-0000-000000000001/run") + .send({ environmentId: env.id }) .expect(404); }); }); @@ -400,9 +418,7 @@ describe("ScenarioController", () => { it("returns paginated runs with stepRuns embedded", async () => { const sc = await createScenario(); await createStep(sc.id, { order: 0 }); - await request(app.getHttpServer()) - .post(`/scenarios/${sc.id}/run`) - .expect(201); + await createRun(sc.id); const res = await request(app.getHttpServer()) .get(`/scenarios/${sc.id}/runs`) @@ -415,9 +431,7 @@ describe("ScenarioController", () => { it("filters by status", async () => { const sc = await createScenario(); await createStep(sc.id, { order: 0 }); - await request(app.getHttpServer()) - .post(`/scenarios/${sc.id}/run`) - .expect(201); + await createRun(sc.id); const pendingRes = await request(app.getHttpServer()) .get(`/scenarios/${sc.id}/runs?status=pending`) @@ -675,10 +689,8 @@ describe("ScenarioController", () => { it("returns run with stepRuns (with scenarioStep) and logs array", async () => { const sc = await createScenario(); await createStep(sc.id, { order: 0 }); - const runRes = await request(app.getHttpServer()) - .post(`/scenarios/${sc.id}/run`) - .expect(201); - const runId = runRes.body.id; + const runRes = await createRun(sc.id); + const runId = runRes.id; const res = await request(app.getHttpServer()) .get(`/scenarios/${sc.id}/run/${runId}`) @@ -696,10 +708,8 @@ describe("ScenarioController", () => { await createStep(sc.id, { order: 0 }); await createStep(sc.id, { order: 1 }); await createStep(sc.id, { order: 2 }); - const runRes = await request(app.getHttpServer()) - .post(`/scenarios/${sc.id}/run`) - .expect(201); - const runId = runRes.body.id; + const runRes = await createRun(sc.id); + const runId = runRes.id; const res = await request(app.getHttpServer()) .get(`/scenarios/${sc.id}/run/${runId}`) @@ -720,10 +730,8 @@ describe("ScenarioController", () => { const sc1 = await createScenario(); const sc2 = await createScenario(); await createStep(sc1.id, { order: 0 }); - const runRes = await request(app.getHttpServer()) - .post(`/scenarios/${sc1.id}/run`) - .expect(201); - const runId = runRes.body.id; + const runRes = await createRun(sc1.id); + const runId = runRes.id; await request(app.getHttpServer()) .get(`/scenarios/${sc2.id}/run/${runId}`) @@ -745,10 +753,8 @@ describe("ScenarioController", () => { it("returns 200 with run data immediately when run is already terminal", async () => { const sc = await createScenario(); await createStep(sc.id, { order: 0 }); - const runRes = await request(app.getHttpServer()) - .post(`/scenarios/${sc.id}/run`) - .expect(201); - const runId = runRes.body.id; + const runRes = await createRun(sc.id); + const runId = runRes.id; // Manually mark run as pass so wait resolves immediately const dataSource = app.get(DataSource); @@ -769,10 +775,8 @@ describe("ScenarioController", () => { it("returns the run in fail state when it has failed", async () => { const sc = await createScenario(); await createStep(sc.id, { order: 0 }); - const runRes = await request(app.getHttpServer()) - .post(`/scenarios/${sc.id}/run`) - .expect(201); - const runId = runRes.body.id; + const runRes = await createRun(sc.id); + const runId = runRes.id; const dataSource = app.get(DataSource); await dataSource.query(