refactor(code-executor): migrate user scripts to single context argument
- replace (page, context, helpers) signature with async (context) => {}
- add ScriptContext interface exposing page, browser, env, getEnv,
getCredential, getStepOutput, runSnippet, dumpDom, log/warn/error
- rename getEnvUrl to getEnv throughout service and docs
- fix step ordering: normalizeStepOrder and run step rows are now 1-based
- migrate existing DB rows (scenario_steps, scenario_run_steps) +1
- update all tests and stored snippet/step code in DB to new API
This commit is contained in:
+1
-1
@@ -33,7 +33,7 @@ Current scheduler behavior is exec-centric:
|
|||||||
- `helpers.getStepOutput(order)`: prior step output by absolute order (`0`, `1`, ...) or relative (`-1` previous step)
|
- `helpers.getStepOutput(order)`: prior step output by absolute order (`0`, `1`, ...) or relative (`-1` previous step)
|
||||||
- `helpers.getCredential(alias)`: credential payload assigned to the scenario alias
|
- `helpers.getCredential(alias)`: credential payload assigned to the scenario alias
|
||||||
- `helpers.env`: shallow copy of environment URL map
|
- `helpers.env`: shallow copy of environment URL map
|
||||||
- `helpers.getEnvUrl(key)`: required environment URL lookup (throws if missing)
|
- `helpers.getEnv(key)`: required environment value lookup (throws if missing)
|
||||||
- `helpers.runSnippet(name, ...args)`: execute stored snippet code in the same page/context/helpers scope
|
- `helpers.runSnippet(name, ...args)`: execute stored snippet code in the same page/context/helpers scope
|
||||||
|
|
||||||
## Validation contract
|
## Validation contract
|
||||||
|
|||||||
@@ -187,16 +187,15 @@ export class BrowserService {
|
|||||||
this.logger.log(`[${label}] exec: running user code`);
|
this.logger.log(`[${label}] exec: running user code`);
|
||||||
const scriptLogger: ScriptLogger = (level, msg) =>
|
const scriptLogger: ScriptLogger = (level, msg) =>
|
||||||
this.logger[level](`[${label}] script: ${msg}`);
|
this.logger[level](`[${label}] script: ${msg}`);
|
||||||
const result = await this.codeExecutor.execute(
|
const result = await this.codeExecutor.execute({
|
||||||
page,
|
page,
|
||||||
context,
|
browser: context,
|
||||||
code,
|
code,
|
||||||
scriptLogger,
|
log: scriptLogger,
|
||||||
undefined,
|
|
||||||
credentials,
|
credentials,
|
||||||
environment,
|
environment,
|
||||||
snippets,
|
snippets,
|
||||||
);
|
});
|
||||||
this.logger.log(`[${label}] exec: done`);
|
this.logger.log(`[${label}] exec: done`);
|
||||||
return result;
|
return result;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -208,13 +207,17 @@ export class BrowserService {
|
|||||||
const snippetMap = await this.snippetService
|
const snippetMap = await this.snippetService
|
||||||
.buildSnippetMap()
|
.buildSnippetMap()
|
||||||
.catch(() => ({}) as Record<string, string>);
|
.catch(() => ({}) as Record<string, string>);
|
||||||
|
|
||||||
|
// TODO: instantiate one browser per server instance and reuse contexts for anonymous sessions,
|
||||||
|
// instead of launching a new browser for each request.
|
||||||
const browser = await chromium.launch({
|
const browser = await chromium.launch({
|
||||||
headless: true,
|
headless: true,
|
||||||
executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH,
|
executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH,
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const context = await browser.newContext();
|
const browserContext = await browser.newContext();
|
||||||
const page = await context.newPage();
|
const page = await browserContext.newPage();
|
||||||
|
|
||||||
if (url) {
|
if (url) {
|
||||||
this.logger.log(`[${label}] exec: navigating to ${url}`);
|
this.logger.log(`[${label}] exec: navigating to ${url}`);
|
||||||
@@ -224,16 +227,15 @@ export class BrowserService {
|
|||||||
this.logger.log(`[${label}] exec: running user code`);
|
this.logger.log(`[${label}] exec: running user code`);
|
||||||
const scriptLogger: ScriptLogger = (level, msg) =>
|
const scriptLogger: ScriptLogger = (level, msg) =>
|
||||||
this.logger[level](`[${label}] script: ${msg}`);
|
this.logger[level](`[${label}] script: ${msg}`);
|
||||||
const result = await this.codeExecutor.execute(
|
const result = await this.codeExecutor.execute({
|
||||||
page,
|
page,
|
||||||
context,
|
browser: browserContext,
|
||||||
code,
|
code,
|
||||||
scriptLogger,
|
log: scriptLogger,
|
||||||
undefined,
|
|
||||||
credentials,
|
credentials,
|
||||||
environment,
|
environment,
|
||||||
snippetMap,
|
snippets: snippetMap,
|
||||||
);
|
});
|
||||||
this.logger.log(`[${label}] exec: done`);
|
this.logger.log(`[${label}] exec: done`);
|
||||||
return result;
|
return result;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export class ExecDto {
|
|||||||
|
|
||||||
@ApiPropertyOptional({
|
@ApiPropertyOptional({
|
||||||
description:
|
description:
|
||||||
"Key/value map of environment variables available via `helpers.env` and `helpers.getEnvUrl()` in the script.",
|
"Key/value map of environment variables available via `helpers.env` and `helpers.getEnv()` in the script.",
|
||||||
example: { BASE_URL: "https://example.com" },
|
example: { BASE_URL: "https://example.com" },
|
||||||
})
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { parse } from "acorn";
|
|||||||
import type { Page, BrowserContext } from "playwright";
|
import type { Page, BrowserContext } from "playwright";
|
||||||
import { expect as playwrightExpect } from "@playwright/test";
|
import { expect as playwrightExpect } from "@playwright/test";
|
||||||
import { dumpDom } from "./dom-helpers";
|
import { dumpDom } from "./dom-helpers";
|
||||||
|
import type { DomNode } from "./dom-helpers";
|
||||||
import type { EnvironmentData } from "../environment/environment.entity";
|
import type { EnvironmentData } from "../environment/environment.entity";
|
||||||
|
|
||||||
export interface ExecResult {
|
export interface ExecResult {
|
||||||
@@ -19,6 +20,41 @@ export type ScriptLogger = (
|
|||||||
message: string,
|
message: string,
|
||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
|
/** The `context` object available inside user scripts as the sole argument. */
|
||||||
|
export interface ScriptContext {
|
||||||
|
/** Playwright Page for the current session. */
|
||||||
|
page: Page;
|
||||||
|
/** Playwright BrowserContext for the current session. */
|
||||||
|
browser: BrowserContext;
|
||||||
|
/** All environment values for the current run (empty object if none set). */
|
||||||
|
env: Record<string, string | undefined>;
|
||||||
|
/** Returns the environment value for `key`, or throws if not defined. */
|
||||||
|
getEnv: (key: string) => string;
|
||||||
|
/** Returns the credential value for `alias`, or throws if not found. */
|
||||||
|
getCredential: (alias: string) => unknown;
|
||||||
|
/** Returns the serialised output of a previous step by order index (negative = relative). */
|
||||||
|
getStepOutput: (order: number) => Promise<unknown>;
|
||||||
|
/** Dumps the DOM of the current page, optionally scoped to a CSS selector. */
|
||||||
|
dumpDom: (selector?: string) => Promise<DomNode>;
|
||||||
|
log: (...args: unknown[]) => void;
|
||||||
|
warn: (...args: unknown[]) => void;
|
||||||
|
error: (...args: unknown[]) => void;
|
||||||
|
/** Runs a named snippet with the same context, plus any extra positional args. */
|
||||||
|
runSnippet: (alias: string, ...args: unknown[]) => Promise<unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExecContext {
|
||||||
|
page: Page;
|
||||||
|
browser: BrowserContext;
|
||||||
|
code: string;
|
||||||
|
log?: ScriptLogger;
|
||||||
|
getStepOutput?: (order: number) => Promise<unknown>;
|
||||||
|
credentials?: Record<string, unknown>;
|
||||||
|
environment?: EnvironmentData | null;
|
||||||
|
snippets?: Record<string, string> | null;
|
||||||
|
result?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CodeExecutorService {
|
export class CodeExecutorService {
|
||||||
private readonly logger = new TraceLogger(CodeExecutorService.name);
|
private readonly logger = new TraceLogger(CodeExecutorService.name);
|
||||||
@@ -28,7 +64,7 @@ export class CodeExecutorService {
|
|||||||
* to parse it with acorn. Throws BadRequestException if it is not valid JS.
|
* to parse it with acorn. Throws BadRequestException if it is not valid JS.
|
||||||
*/
|
*/
|
||||||
validate(code: string): void {
|
validate(code: string): void {
|
||||||
const wrapped = `async function __validate__(page, context, helpers) { ${code} }`;
|
const wrapped = `async function __validate__(context) { ${code} }`;
|
||||||
try {
|
try {
|
||||||
parse(wrapped, { ecmaVersion: 2022 });
|
parse(wrapped, { ecmaVersion: 2022 });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -40,20 +76,12 @@ export class CodeExecutorService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes `code` as an async function body with `page` and `context` in
|
* Executes `code` as an async function body with a single `context` argument
|
||||||
* scope. Always call `validate()` before this method.
|
* that exposes page, browser, env helpers, credentials, snippets, and logging.
|
||||||
|
* Always call `validate()` before this method.
|
||||||
*/
|
*/
|
||||||
async execute(
|
async execute(ctx: ExecContext): Promise<ExecResult> {
|
||||||
page: Page,
|
const { page, browser, code, log, getStepOutput, credentials, environment, snippets, result } = ctx;
|
||||||
context: BrowserContext,
|
|
||||||
code: string,
|
|
||||||
log?: ScriptLogger,
|
|
||||||
getStepOutput?: (order: number) => Promise<unknown>,
|
|
||||||
credentials?: Record<string, unknown>,
|
|
||||||
environment?: EnvironmentData | null,
|
|
||||||
snippets?: Record<string, string> | null,
|
|
||||||
result?: unknown,
|
|
||||||
): Promise<ExecResult> {
|
|
||||||
const scriptLog: ScriptLogger =
|
const scriptLog: ScriptLogger =
|
||||||
log ?? ((level, msg) => this.logger[level](msg));
|
log ?? ((level, msg) => this.logger[level](msg));
|
||||||
const toStr = (args: unknown[]) =>
|
const toStr = (args: unknown[]) =>
|
||||||
@@ -65,9 +93,8 @@ export class CodeExecutorService {
|
|||||||
const envData: EnvironmentData = environment ?? {};
|
const envData: EnvironmentData = environment ?? {};
|
||||||
const snippetMap: Record<string, string> = snippets ?? {};
|
const snippetMap: Record<string, string> = snippets ?? {};
|
||||||
|
|
||||||
// pageHelpers is referenced by runSnippet, so we declare it as a var first.
|
// scriptContext is referenced by runSnippet, so we declare it first.
|
||||||
|
let scriptContext: ScriptContext;
|
||||||
let pageHelpers: Record<string, unknown>;
|
|
||||||
|
|
||||||
const fakeConsole = {
|
const fakeConsole = {
|
||||||
log: (...args: unknown[]) => scriptLog("log", toStr(args)),
|
log: (...args: unknown[]) => scriptLog("log", toStr(args)),
|
||||||
@@ -78,7 +105,9 @@ export class CodeExecutorService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
pageHelpers = {
|
scriptContext = {
|
||||||
|
page,
|
||||||
|
browser,
|
||||||
dumpDom: (selector?: string) => dumpDom(page, selector),
|
dumpDom: (selector?: string) => dumpDom(page, selector),
|
||||||
log: (...args: unknown[]) => scriptLog("log", toStr(args)),
|
log: (...args: unknown[]) => scriptLog("log", toStr(args)),
|
||||||
warn: (...args: unknown[]) => scriptLog("warn", toStr(args)),
|
warn: (...args: unknown[]) => scriptLog("warn", toStr(args)),
|
||||||
@@ -92,10 +121,8 @@ export class CodeExecutorService {
|
|||||||
}
|
}
|
||||||
return credMap[alias];
|
return credMap[alias];
|
||||||
},
|
},
|
||||||
/** All values defined for the current environment (may be empty if no environment is set). */
|
|
||||||
env: { ...envData },
|
env: { ...envData },
|
||||||
/** Returns the value for the given key, or throws if it is not defined. */
|
getEnv: (key: string): string => {
|
||||||
getEnvUrl: (key: string): string => {
|
|
||||||
const value = envData[key];
|
const value = envData[key];
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -104,12 +131,6 @@ export class CodeExecutorService {
|
|||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
},
|
},
|
||||||
/**
|
|
||||||
* Runs a snippet by alias. Snippets receive the same page/context/helpers
|
|
||||||
* as regular exec code, plus any positional args you pass.
|
|
||||||
*
|
|
||||||
* Example: await helpers.runSnippet('clickLoginButton', '#submit')
|
|
||||||
*/
|
|
||||||
runSnippet: async (
|
runSnippet: async (
|
||||||
alias: string,
|
alias: string,
|
||||||
...args: unknown[]
|
...args: unknown[]
|
||||||
@@ -119,18 +140,14 @@ export class CodeExecutorService {
|
|||||||
throw new Error(`Snippet alias "${alias}" not found`);
|
throw new Error(`Snippet alias "${alias}" not found`);
|
||||||
}
|
}
|
||||||
const snippetFn = new Function(
|
const snippetFn = new Function(
|
||||||
"page",
|
|
||||||
"context",
|
"context",
|
||||||
"helpers",
|
|
||||||
"console",
|
"console",
|
||||||
"snippetArgs",
|
"snippetArgs",
|
||||||
"expect",
|
"expect",
|
||||||
`return (async (page, context, helpers, ...args) => { ${snippetCode} })(page, context, helpers, ...snippetArgs)`,
|
`return (async (context, ...args) => { ${snippetCode} })(context, ...snippetArgs)`,
|
||||||
);
|
);
|
||||||
return snippetFn(
|
return snippetFn(
|
||||||
page,
|
scriptContext,
|
||||||
context,
|
|
||||||
pageHelpers,
|
|
||||||
fakeConsole,
|
fakeConsole,
|
||||||
args,
|
args,
|
||||||
playwrightExpect,
|
playwrightExpect,
|
||||||
@@ -138,21 +155,18 @@ export class CodeExecutorService {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Passing `console` and `expect` as named parameters exposes them in script scope.
|
// `console`, `result`, and `expect` are injected as named parameters so
|
||||||
|
// existing scripts that reference them directly still work.
|
||||||
const fn = new Function(
|
const fn = new Function(
|
||||||
"page",
|
|
||||||
"context",
|
"context",
|
||||||
"helpers",
|
|
||||||
"console",
|
"console",
|
||||||
"result",
|
"result",
|
||||||
"expect",
|
"expect",
|
||||||
`return (async (page, context, helpers) => { ${code} })(page, context, helpers)`,
|
`return (async (context) => { ${code} })(context)`,
|
||||||
);
|
);
|
||||||
this.logger.debug("Executing user code");
|
this.logger.debug("Executing user code");
|
||||||
const execResult = await fn(
|
const execResult = await fn(
|
||||||
page,
|
scriptContext,
|
||||||
context,
|
|
||||||
pageHelpers,
|
|
||||||
fakeConsole,
|
fakeConsole,
|
||||||
result,
|
result,
|
||||||
playwrightExpect,
|
playwrightExpect,
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import type { Page, BrowserContext } from "playwright";
|
||||||
|
import type { ExecContext, ScriptLogger } from "./code-executor.service";
|
||||||
|
import type { EnvironmentData } from "../environment/environment.entity";
|
||||||
|
|
||||||
|
export class ExecContextBuilder {
|
||||||
|
private readonly ctx: Partial<ExecContext> = {};
|
||||||
|
|
||||||
|
page(page: Page): this {
|
||||||
|
this.ctx.page = page;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
browser(browser: BrowserContext): this {
|
||||||
|
this.ctx.browser = browser;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
code(code: string): this {
|
||||||
|
this.ctx.code = code;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
log(log: ScriptLogger): this {
|
||||||
|
this.ctx.log = log;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
getStepOutput(fn: (order: number) => Promise<unknown>): this {
|
||||||
|
this.ctx.getStepOutput = fn;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
credentials(credentials: Record<string, unknown>): this {
|
||||||
|
this.ctx.credentials = credentials;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
environment(environment: EnvironmentData | null): this {
|
||||||
|
this.ctx.environment = environment;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
snippets(snippets: Record<string, string> | null): this {
|
||||||
|
this.ctx.snippets = snippets;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
result(result: unknown): this {
|
||||||
|
this.ctx.result = result;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
build(): ExecContext {
|
||||||
|
if (!this.ctx.page) throw new Error("ExecContextBuilder: page is required");
|
||||||
|
if (!this.ctx.browser) throw new Error("ExecContextBuilder: browser is required");
|
||||||
|
if (this.ctx.code === undefined) throw new Error("ExecContextBuilder: code is required");
|
||||||
|
return this.ctx as ExecContext;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -72,7 +72,7 @@ export class ScenarioSchedulerService {
|
|||||||
): (order: number) => Promise<unknown> {
|
): (order: number) => Promise<unknown> {
|
||||||
return async (order: number) => {
|
return async (order: number) => {
|
||||||
const targetOrder = order < 0 ? stepRun.order + order : order;
|
const targetOrder = order < 0 ? stepRun.order + order : order;
|
||||||
if (targetOrder < 0) return null;
|
if (targetOrder < 1) return null;
|
||||||
const sr = await this.runStepRepo.findOne({
|
const sr = await this.runStepRepo.findOne({
|
||||||
where: { runId: stepRun.runId, order: targetOrder },
|
where: { runId: stepRun.runId, order: targetOrder },
|
||||||
});
|
});
|
||||||
@@ -172,16 +172,16 @@ export class ScenarioSchedulerService {
|
|||||||
const creds = this.runCredentials.get(stepRun.runId);
|
const creds = this.runCredentials.get(stepRun.runId);
|
||||||
const snips = this.runSnippets.get(stepRun.runId);
|
const snips = this.runSnippets.get(stepRun.runId);
|
||||||
const env = this.runEnvironments.get(stepRun.runId);
|
const env = this.runEnvironments.get(stepRun.runId);
|
||||||
const { result: execOutput } = await this.codeExecutor.execute(
|
const { result: execOutput } = await this.codeExecutor.execute({
|
||||||
page,
|
page,
|
||||||
context,
|
browser: context,
|
||||||
step.execCode,
|
code: step.execCode,
|
||||||
this.stepLogger(stepRun.id, stepRun.runId),
|
log: this.stepLogger(stepRun.id, stepRun.runId),
|
||||||
getStepOutput,
|
getStepOutput,
|
||||||
creds,
|
credentials: creds,
|
||||||
env,
|
environment: env,
|
||||||
snips,
|
snippets: snips,
|
||||||
);
|
});
|
||||||
|
|
||||||
await this.passStepRun(stepRun, null, execOutput);
|
await this.passStepRun(stepRun, null, execOutput);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -104,8 +104,8 @@ export class ScenarioService {
|
|||||||
});
|
});
|
||||||
for (let i = 0; i < ordered.length; i += 1) {
|
for (let i = 0; i < ordered.length; i += 1) {
|
||||||
const step = ordered[i];
|
const step = ordered[i];
|
||||||
if (step.order !== i) {
|
if (step.order !== i + 1) {
|
||||||
step.order = i;
|
step.order = i + 1;
|
||||||
await this.stepRepo.save(step);
|
await this.stepRepo.save(step);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,7 +120,7 @@ export class ScenarioService {
|
|||||||
const created = await this.stepRepo.save(
|
const created = await this.stepRepo.save(
|
||||||
this.stepRepo.create({
|
this.stepRepo.create({
|
||||||
...dto,
|
...dto,
|
||||||
order: currentCount,
|
order: currentCount + 1,
|
||||||
scenarioId,
|
scenarioId,
|
||||||
title: dto.title ?? null,
|
title: dto.title ?? null,
|
||||||
execCode: dto.execCode ?? null,
|
execCode: dto.execCode ?? null,
|
||||||
|
|||||||
@@ -136,35 +136,35 @@ describe("BrowserController", () => {
|
|||||||
expect(res.body).toEqual({ result: 42 });
|
expect(res.body).toEqual({ result: 42 });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("exposes environment via helpers.env in a named session", async () => {
|
it("exposes environment via context.env in a named session", async () => {
|
||||||
const res = await request(app.getHttpServer())
|
const res = await request(app.getHttpServer())
|
||||||
.post("/exec")
|
.post("/exec")
|
||||||
.send({
|
.send({
|
||||||
sessionName: "no-such-session",
|
sessionName: "no-such-session",
|
||||||
code: "return helpers.env.BASE_URL;",
|
code: "return context.env.BASE_URL;",
|
||||||
environment: { BASE_URL: "https://env.example.com" },
|
environment: { BASE_URL: "https://env.example.com" },
|
||||||
})
|
})
|
||||||
.expect(201);
|
.expect(201);
|
||||||
expect(res.body).toEqual({ result: "https://env.example.com" });
|
expect(res.body).toEqual({ result: "https://env.example.com" });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("exposes environment via helpers.env in a sessionless exec", async () => {
|
it("exposes environment via context.env in a sessionless exec", async () => {
|
||||||
const res = await request(app.getHttpServer())
|
const res = await request(app.getHttpServer())
|
||||||
.post("/exec")
|
.post("/exec")
|
||||||
.send({
|
.send({
|
||||||
code: "return helpers.env.KEY;",
|
code: "return context.env.KEY;",
|
||||||
environment: { KEY: "value123" },
|
environment: { KEY: "value123" },
|
||||||
})
|
})
|
||||||
.expect(201);
|
.expect(201);
|
||||||
expect(res.body).toEqual({ result: "value123" });
|
expect(res.body).toEqual({ result: "value123" });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("exposes credentials via helpers.getCredential in a named session", async () => {
|
it("exposes credentials via context.getCredential in a named session", async () => {
|
||||||
const res = await request(app.getHttpServer())
|
const res = await request(app.getHttpServer())
|
||||||
.post("/exec")
|
.post("/exec")
|
||||||
.send({
|
.send({
|
||||||
sessionName: "no-such-session",
|
sessionName: "no-such-session",
|
||||||
code: "return helpers.getCredential('admin');",
|
code: "return context.getCredential('admin');",
|
||||||
credentials: { admin: { username: "user1", password: "pass1" } },
|
credentials: { admin: { username: "user1", password: "pass1" } },
|
||||||
})
|
})
|
||||||
.expect(201);
|
.expect(201);
|
||||||
@@ -173,11 +173,11 @@ describe("BrowserController", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("exposes credentials via helpers.getCredential in a sessionless exec", async () => {
|
it("exposes credentials via context.getCredential in a sessionless exec", async () => {
|
||||||
const res = await request(app.getHttpServer())
|
const res = await request(app.getHttpServer())
|
||||||
.post("/exec")
|
.post("/exec")
|
||||||
.send({
|
.send({
|
||||||
code: "return helpers.getCredential('svc');",
|
code: "return context.getCredential('svc');",
|
||||||
credentials: { svc: { token: "abc" } },
|
credentials: { svc: { token: "abc" } },
|
||||||
})
|
})
|
||||||
.expect(201);
|
.expect(201);
|
||||||
@@ -188,7 +188,7 @@ describe("BrowserController", () => {
|
|||||||
const res = await request(app.getHttpServer())
|
const res = await request(app.getHttpServer())
|
||||||
.post("/exec")
|
.post("/exec")
|
||||||
.send({
|
.send({
|
||||||
code: "return helpers.getCredential('missing');",
|
code: "return context.getCredential('missing');",
|
||||||
credentials: {},
|
credentials: {},
|
||||||
})
|
})
|
||||||
.expect(500);
|
.expect(500);
|
||||||
@@ -199,7 +199,7 @@ describe("BrowserController", () => {
|
|||||||
const res = await request(app.getHttpServer())
|
const res = await request(app.getHttpServer())
|
||||||
.post("/exec")
|
.post("/exec")
|
||||||
.send({
|
.send({
|
||||||
code: "return Object.keys(helpers.env).length === 0 ? 'empty' : 'not-empty';",
|
code: "return Object.keys(context.env).length === 0 ? 'empty' : 'not-empty';",
|
||||||
})
|
})
|
||||||
.expect(201);
|
.expect(201);
|
||||||
expect(res.body).toEqual({ result: "empty" });
|
expect(res.body).toEqual({ result: "empty" });
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Integration tests for ScenarioRunStepEntity.output column and the
|
* Integration tests for ScenarioRunStepEntity.output column and the
|
||||||
* helpers.getStepOutput() API available inside exec step scripts.
|
* context.getStepOutput() API available inside exec step scripts.
|
||||||
*
|
*
|
||||||
* Strategy:
|
* Strategy:
|
||||||
* - Seed a session row so the scheduler can create a browser context.
|
* - Seed a session row so the scheduler can create a browser context.
|
||||||
@@ -17,7 +17,7 @@ import { ScenarioService } from "../src/scenario/scenario.service";
|
|||||||
import { ScenarioSchedulerService } from "../src/scenario/scenario-scheduler.service";
|
import { ScenarioSchedulerService } from "../src/scenario/scenario-scheduler.service";
|
||||||
import { EnvironmentEntity } from "../src/environment/environment.entity";
|
import { EnvironmentEntity } from "../src/environment/environment.entity";
|
||||||
|
|
||||||
describe("ScenarioRunStepEntity.output + helpers.getStepOutput", () => {
|
describe("ScenarioRunStepEntity.output + context.getStepOutput", () => {
|
||||||
let app: INestApplication;
|
let app: INestApplication;
|
||||||
let dataSource: DataSource;
|
let dataSource: DataSource;
|
||||||
let scenarioService: ScenarioService;
|
let scenarioService: ScenarioService;
|
||||||
@@ -135,15 +135,15 @@ describe("ScenarioRunStepEntity.output + helpers.getStepOutput", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── helpers.getStepOutput ──────────────────────────────────────────────────
|
// ── context.getStepOutput ──────────────────────────────────────────────────
|
||||||
|
|
||||||
describe("helpers.getStepOutput()", () => {
|
describe("context.getStepOutput()", () => {
|
||||||
it("returns output of a previous step by absolute order", async () => {
|
it("returns output of a previous step by absolute order", async () => {
|
||||||
await seedSession();
|
await seedSession();
|
||||||
const scId = await createScenario("getStepOutput-absolute");
|
const scId = await createScenario("getStepOutput-absolute");
|
||||||
await createStep(scId, 0, "return 99;");
|
await createStep(scId, 0, "return 99;");
|
||||||
// Step 1 reads step 0's output via absolute index 0
|
// Step 1 reads step 1's output via absolute index 1 (1-based)
|
||||||
await createStep(scId, 1, "return await helpers.getStepOutput(0);");
|
await createStep(scId, 1, "return await context.getStepOutput(1);");
|
||||||
|
|
||||||
const runId = await runScenario(scId);
|
const runId = await runScenario(scId);
|
||||||
const rows = await dataSource.query(
|
const rows = await dataSource.query(
|
||||||
@@ -159,7 +159,7 @@ describe("ScenarioRunStepEntity.output + helpers.getStepOutput", () => {
|
|||||||
const scId = await createScenario("getStepOutput-relative");
|
const scId = await createScenario("getStepOutput-relative");
|
||||||
await createStep(scId, 0, 'return "step-zero";');
|
await createStep(scId, 0, 'return "step-zero";');
|
||||||
// Step 1 uses relative index -1 to reference step 0
|
// Step 1 uses relative index -1 to reference step 0
|
||||||
await createStep(scId, 1, "return await helpers.getStepOutput(-1);");
|
await createStep(scId, 1, "return await context.getStepOutput(-1);");
|
||||||
|
|
||||||
const runId = await runScenario(scId);
|
const runId = await runScenario(scId);
|
||||||
const rows = await dataSource.query(
|
const rows = await dataSource.query(
|
||||||
@@ -173,7 +173,7 @@ describe("ScenarioRunStepEntity.output + helpers.getStepOutput", () => {
|
|||||||
await seedSession();
|
await seedSession();
|
||||||
const scId = await createScenario("getStepOutput-missing");
|
const scId = await createScenario("getStepOutput-missing");
|
||||||
// Step 0 tries to read step order 99 which does not exist
|
// Step 0 tries to read step order 99 which does not exist
|
||||||
await createStep(scId, 0, "return await helpers.getStepOutput(99);");
|
await createStep(scId, 0, "return await context.getStepOutput(99);");
|
||||||
|
|
||||||
const runId = await runScenario(scId);
|
const runId = await runScenario(scId);
|
||||||
const row = await dataSource.query(
|
const row = await dataSource.query(
|
||||||
@@ -190,12 +190,12 @@ describe("ScenarioRunStepEntity.output + helpers.getStepOutput", () => {
|
|||||||
await createStep(
|
await createStep(
|
||||||
scId,
|
scId,
|
||||||
1,
|
1,
|
||||||
"const prev = await helpers.getStepOutput(-1); return [...prev, 3];",
|
"const prev = await context.getStepOutput(-1); return [...prev, 3];",
|
||||||
);
|
);
|
||||||
await createStep(
|
await createStep(
|
||||||
scId,
|
scId,
|
||||||
2,
|
2,
|
||||||
"const prev = await helpers.getStepOutput(-1); return [...prev, 4];",
|
"const prev = await context.getStepOutput(-1); return [...prev, 4];",
|
||||||
);
|
);
|
||||||
|
|
||||||
const runId = await runScenario(scId);
|
const runId = await runScenario(scId);
|
||||||
|
|||||||
@@ -216,7 +216,7 @@ describe("ScenarioController", () => {
|
|||||||
.expect(201);
|
.expect(201);
|
||||||
|
|
||||||
expect(res.body.id).toBeDefined();
|
expect(res.body.id).toBeDefined();
|
||||||
expect(res.body.order).toBe(0);
|
expect(res.body.order).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("creates a step without execCode", async () => {
|
it("creates a step without execCode", async () => {
|
||||||
@@ -274,7 +274,7 @@ describe("ScenarioController", () => {
|
|||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
const orders = res.body.steps.map((s: { order: number }) => s.order);
|
const orders = res.body.steps.map((s: { order: number }) => s.order);
|
||||||
expect(orders).toEqual([0, 1, 2]);
|
expect(orders).toEqual([1, 2, 3]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -573,7 +573,7 @@ describe("ScenarioController", () => {
|
|||||||
expect(res.body.name).toBe("imported scenario");
|
expect(res.body.name).toBe("imported scenario");
|
||||||
expect(res.body.steps).toHaveLength(3);
|
expect(res.body.steps).toHaveLength(3);
|
||||||
expect(res.body.steps.map((s: { order: number }) => s.order)).toEqual([
|
expect(res.body.steps.map((s: { order: number }) => s.order)).toEqual([
|
||||||
0, 1, 2,
|
1, 2, 3,
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -689,7 +689,7 @@ describe("ScenarioController", () => {
|
|||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
const orders = res.body.stepRuns.map((s: { order: number }) => s.order);
|
const orders = res.body.stepRuns.map((s: { order: number }) => s.order);
|
||||||
expect(orders).toEqual([0, 1, 2]);
|
expect(orders).toEqual([1, 2, 3]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns 404 for unknown run", async () => {
|
it("returns 404 for unknown run", async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user