diff --git a/client/src/api/client.ts b/client/src/api/client.ts index 05d4edb..182276c 100644 --- a/client/src/api/client.ts +++ b/client/src/api/client.ts @@ -274,14 +274,12 @@ export const scenarioCredentials = { export interface CreateStepPayload { title?: string; execCode?: string; - validateCode?: string; } export interface UpdateStepPayload { title?: string; order?: number; execCode?: string; - validateCode?: string; } export const steps = { diff --git a/client/src/api/types.ts b/client/src/api/types.ts index 97c6cf7..e6ed21f 100644 --- a/client/src/api/types.ts +++ b/client/src/api/types.ts @@ -66,7 +66,6 @@ export interface ScenarioStep { order: number; title: string | null; execCode: string | null; - validateCode: string | null; createdAt: string; updatedAt: string; } diff --git a/client/src/i18n/locales/en.json b/client/src/i18n/locales/en.json index ec815a8..dd3c114 100644 --- a/client/src/i18n/locales/en.json +++ b/client/src/i18n/locales/en.json @@ -183,9 +183,7 @@ "form_session_placeholder": "e.g. my-session", "form_session_required": "Session name is required", "form_exec_code": "Exec code", - "form_exec_code_placeholder": "return await page.title();", - "form_validate_code": "Validate code", - "form_validate_code_placeholder": "return { success: result !== null, description: \"…\" };" + "form_exec_code_placeholder": "return await page.title();" }, "runs": { "title": "Runs", diff --git a/client/src/pages/scenario/CreateStepPage.tsx b/client/src/pages/scenario/CreateStepPage.tsx index 163b44a..d045aac 100644 --- a/client/src/pages/scenario/CreateStepPage.tsx +++ b/client/src/pages/scenario/CreateStepPage.tsx @@ -16,7 +16,6 @@ export function CreateStepPage() { const [scenario, setScenario] = useState(null); const [title, setTitle] = useState(''); const [execCode, setExecCode] = useState(''); - const [validateCode, setValidateCode] = useState(''); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); @@ -36,7 +35,6 @@ export function CreateStepPage() { await steps.create(id!, { title: title.trim() || undefined, execCode: execCode.trim() || undefined, - validateCode: validateCode.trim() || undefined, }); toast.success('Step created'); navigate(`/scenarios/${id}`); @@ -77,10 +75,6 @@ export function CreateStepPage() { -
- - -
diff --git a/client/src/pages/scenario/EditStepPage.tsx b/client/src/pages/scenario/EditStepPage.tsx index f7a0d3c..82b09aa 100644 --- a/client/src/pages/scenario/EditStepPage.tsx +++ b/client/src/pages/scenario/EditStepPage.tsx @@ -17,7 +17,6 @@ export function EditStepPage() { const [step, setStep] = useState(null); const [title, setTitle] = useState(''); const [execCode, setExecCode] = useState(''); - const [validateCode, setValidateCode] = useState(''); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); @@ -30,7 +29,6 @@ export function EditStepPage() { setStep(st); setTitle(st.title ?? ''); setExecCode(st.execCode ?? ''); - setValidateCode(st.validateCode ?? ''); }) .catch((err: Error) => setError(err.message)) .finally(() => setLoading(false)); @@ -44,7 +42,6 @@ export function EditStepPage() { await steps.update(id!, stepId!, { title: title.trim() || undefined, execCode: execCode.trim() || undefined, - validateCode: validateCode.trim() || undefined, }); toast.success('Step updated'); navigate(`/scenarios/${id}`); @@ -88,10 +85,6 @@ export function EditStepPage() {
-
- - -
diff --git a/package-lock.json b/package-lock.json index c9d6892..80f2209 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3532,6 +3532,21 @@ "url": "https://opencollective.com/pkgr" } }, + "node_modules/@playwright/test": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", + "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==", + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.29", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", @@ -15942,6 +15957,7 @@ "@nestjs/schedule": "^6.1.1", "@nestjs/swagger": "^11.2.6", "@nestjs/typeorm": "^11.0.1", + "@playwright/test": "^1.59.1", "acorn": "^8.16.0", "better-sqlite3": "^12.8.0", "class-transformer": "^0.5.1", diff --git a/server/package.json b/server/package.json index 3dda9ea..9a2dbbc 100644 --- a/server/package.json +++ b/server/package.json @@ -27,6 +27,7 @@ "@nestjs/schedule": "^6.1.1", "@nestjs/swagger": "^11.2.6", "@nestjs/typeorm": "^11.0.1", + "@playwright/test": "^1.59.1", "acorn": "^8.16.0", "better-sqlite3": "^12.8.0", "class-transformer": "^0.5.1", diff --git a/server/src/code-executor/code-executor.service.ts b/server/src/code-executor/code-executor.service.ts index c529169..2e80fcb 100644 --- a/server/src/code-executor/code-executor.service.ts +++ b/server/src/code-executor/code-executor.service.ts @@ -6,6 +6,7 @@ import { import { TraceLogger } from "../common/trace-logger"; import { parse } from "acorn"; import type { Page, BrowserContext } from "playwright"; +import { expect as playwrightExpect } from "@playwright/test"; import { dumpDom } from "./dom-helpers"; import type { EnvironmentData } from "../environment/environment.entity"; @@ -123,19 +124,28 @@ export class CodeExecutorService { "helpers", "console", "snippetArgs", + "expect", `return (async (page, context, helpers, ...args) => { ${snippetCode} })(page, context, helpers, ...snippetArgs)`, ); - return snippetFn(page, context, pageHelpers, fakeConsole, args); + return snippetFn( + page, + context, + pageHelpers, + fakeConsole, + args, + playwrightExpect, + ); }, }; - // Passing `console` as a named parameter shadows the global in the script scope. + // Passing `console` and `expect` as named parameters exposes them in script scope. const fn = new Function( "page", "context", "helpers", "console", "result", + "expect", `return (async (page, context, helpers) => { ${code} })(page, context, helpers)`, ); this.logger.debug("Executing user code"); @@ -145,6 +155,7 @@ export class CodeExecutorService { pageHelpers, fakeConsole, result, + playwrightExpect, ); return { result: execResult }; } catch (err) { diff --git a/server/src/mcp/mcp.service.ts b/server/src/mcp/mcp.service.ts index 3b5a209..96720fc 100644 --- a/server/src/mcp/mcp.service.ts +++ b/server/src/mcp/mcp.service.ts @@ -505,10 +505,6 @@ export class McpService { .string() .optional() .describe("Playwright JS code to execute (exec steps)"), - validateCode: z - .string() - .optional() - .describe("Validation JS code returning { success, description }"), }, }, async ({ scenarioId, ...dto }) => { @@ -569,7 +565,6 @@ export class McpService { .describe("New step type"), title: z.string().optional().describe("New step title"), execCode: z.string().optional().describe("New exec code"), - validateCode: z.string().optional().describe("New validation code"), }, }, async ({ scenarioId, stepId, ...dto }) => { @@ -785,11 +780,6 @@ export class McpService { .nullable() .optional() .describe("Exec/sign code"), - validateCode: z - .string() - .nullable() - .optional() - .describe("Validation code"), }), ) .describe("Ordered list of steps"), diff --git a/server/src/scenario/dto/create-scenario-step.dto.ts b/server/src/scenario/dto/create-scenario-step.dto.ts index bf77853..ac670ed 100644 --- a/server/src/scenario/dto/create-scenario-step.dto.ts +++ b/server/src/scenario/dto/create-scenario-step.dto.ts @@ -13,12 +13,4 @@ export class CreateScenarioStepDto { @IsNotEmpty() execCode?: string; - @ApiPropertyOptional({ - example: - 'return { success: result !== null, description: "title present" };', - }) - @IsOptional() - @IsString() - @IsNotEmpty() - validateCode?: string; } diff --git a/server/src/scenario/dto/scenario-export.dto.ts b/server/src/scenario/dto/scenario-export.dto.ts index 5e71eb4..ef50c4a 100644 --- a/server/src/scenario/dto/scenario-export.dto.ts +++ b/server/src/scenario/dto/scenario-export.dto.ts @@ -23,11 +23,6 @@ export class ScenarioStepExportDto { @IsNotEmpty() execCode: string | null; - @ApiPropertyOptional() - @IsOptional() - @IsString() - @IsNotEmpty() - validateCode: string | null; } export class ScenarioExportDto { diff --git a/server/src/scenario/dto/update-scenario-step.dto.ts b/server/src/scenario/dto/update-scenario-step.dto.ts index 11af1c2..a8a24f5 100644 --- a/server/src/scenario/dto/update-scenario-step.dto.ts +++ b/server/src/scenario/dto/update-scenario-step.dto.ts @@ -19,9 +19,4 @@ export class UpdateScenarioStepDto { @IsNotEmpty() execCode?: string; - @ApiPropertyOptional() - @IsOptional() - @IsString() - @IsNotEmpty() - validateCode?: string; } diff --git a/server/src/scenario/scenario-scheduler.service.ts b/server/src/scenario/scenario-scheduler.service.ts index 86da86e..4978d6a 100644 --- a/server/src/scenario/scenario-scheduler.service.ts +++ b/server/src/scenario/scenario-scheduler.service.ts @@ -17,11 +17,6 @@ import { ScenarioService } from "./scenario.service"; import { SnippetService } from "../snippet/snippet.service"; import { EnvironmentEntity, EnvironmentData } from "../environment/environment.entity"; -interface ValidateResult { - success: boolean; - description?: string; -} - interface BrowserHandle { browser: Browser; context: BrowserContext; @@ -188,25 +183,6 @@ export class ScenarioSchedulerService { snips, ); - if (step.validateCode) { - this.codeExecutor.validate(step.validateCode); - const { result } = await this.codeExecutor.execute( - page, - context, - step.validateCode, - this.stepLogger(stepRun.id, stepRun.runId), - getStepOutput, - creds, - env, - snips, - execOutput, - ); - const vr = this.parseValidateResult(result); - if (!vr.success) throw new Error(vr.description ?? "Validation failed"); - await this.passStepRun(stepRun, vr.description ?? null, execOutput); - return; - } - await this.passStepRun(stepRun, null, execOutput); } catch (err) { const msg = (err as Error).message ?? String(err); @@ -250,21 +226,6 @@ export class ScenarioSchedulerService { } } - // ── Validation helper ────────────────────────────────────────────────────── - - private parseValidateResult(raw: unknown): ValidateResult { - if (typeof raw === "boolean") return { success: raw }; - if (raw && typeof raw === "object") { - const r = raw as Record; - return { - success: Boolean(r["success"]), - description: - r["description"] != null ? String(r["description"]) : undefined, - }; - } - return { success: Boolean(raw) }; - } - // ── Pass / fail helpers ──────────────────────────────────────────────────── private async passStepRun( diff --git a/server/src/scenario/scenario-step.entity.ts b/server/src/scenario/scenario-step.entity.ts index 76112f9..f8d9086 100644 --- a/server/src/scenario/scenario-step.entity.ts +++ b/server/src/scenario/scenario-step.entity.ts @@ -32,9 +32,6 @@ export class ScenarioStepEntity { @Column({ type: "text", nullable: true }) execCode: string | null; - @Column({ type: "text", nullable: true }) - validateCode: string | null; - @CreateDateColumn() createdAt: Date; diff --git a/server/src/scenario/scenario.service.ts b/server/src/scenario/scenario.service.ts index 0716a72..16bc503 100644 --- a/server/src/scenario/scenario.service.ts +++ b/server/src/scenario/scenario.service.ts @@ -124,7 +124,6 @@ export class ScenarioService { scenarioId, title: dto.title ?? null, execCode: dto.execCode ?? null, - validateCode: dto.validateCode ?? null, }), ); await this.normalizeStepOrder(scenarioId); @@ -156,7 +155,6 @@ export class ScenarioService { order: step.order, title: dto.title ?? step.title, execCode: dto.execCode ?? step.execCode, - validateCode: dto.validateCode ?? step.validateCode, }); await this.stepRepo.save(step); @@ -404,7 +402,6 @@ export class ScenarioService { steps: scenario.steps.map((s) => ({ title: s.title, execCode: s.execCode, - validateCode: s.validateCode, })), }; } @@ -436,7 +433,6 @@ export class ScenarioService { order: index, title: s.title ?? null, execCode: s.execCode ?? null, - validateCode: s.validateCode ?? null, }), ); await this.stepRepo.save(steps); diff --git a/server/test/scenario.controller.spec.ts b/server/test/scenario.controller.spec.ts index b3a27ef..6ac8881 100644 --- a/server/test/scenario.controller.spec.ts +++ b/server/test/scenario.controller.spec.ts @@ -477,7 +477,6 @@ describe("ScenarioController", () => { order: 1, sessionName: "s", execCode: "return 1;", - validateCode: "return true;", }); const res = await request(app.getHttpServer()) @@ -536,25 +535,6 @@ describe("ScenarioController", () => { expect(step).not.toHaveProperty("updatedAt"); }); - it("exports null validateCode as null", async () => { - const sc = await createScenario("export-null-validate"); - await createStep(sc.id, { - order: 0, - sessionName: "s", - execCode: "return 1;", - }); - - const res = await request(app.getHttpServer()) - .get(`/scenarios/${sc.id}/export`) - .expect(200); - - const exported = yamlParse(res.text) as { - steps: Array<{ validateCode: string | null }>; - }; - - expect(exported.steps[0].validateCode).toBeNull(); - }); - it("returns 404 for unknown scenario", async () => { await request(app.getHttpServer()) .get("/scenarios/00000000-0000-0000-0000-000000000001/export") @@ -572,17 +552,14 @@ describe("ScenarioController", () => { { sessionName: "s", execCode: '{"keyId":"k","environmentName":"e"}', - validateCode: null, }, { sessionName: "s", execCode: "return 1;", - validateCode: "return true;", }, { sessionName: "s", execCode: '{"keyId":"k"}', - validateCode: null, }, ], }; @@ -622,7 +599,6 @@ describe("ScenarioController", () => { order: 0, sessionName: "rs", execCode: "return 42;", - validateCode: "return true;", }); const exportRes = await request(app.getHttpServer()) @@ -630,7 +606,7 @@ describe("ScenarioController", () => { .expect(200); const exported = yamlParse(exportRes.text) as { - steps: Array<{ execCode: string; validateCode: string | null }>; + steps: Array<{ execCode: string }>; }; const importRes = await request(app.getHttpServer()) @@ -643,9 +619,6 @@ describe("ScenarioController", () => { expect(importRes.body.steps[0].execCode).toBe( exported.steps[0].execCode, ); - expect(importRes.body.steps[0].validateCode).toBe( - exported.steps[0].validateCode, - ); }); it("imports with empty steps array", async () => {