- 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
209 lines
6.9 KiB
TypeScript
209 lines
6.9 KiB
TypeScript
import { INestApplication } from "@nestjs/common";
|
|
import { getRepositoryToken } from "@nestjs/typeorm";
|
|
import request from "supertest";
|
|
import { buildTestApp } from "./app.harness";
|
|
import { SessionEntity } from "../src/session/session.entity";
|
|
import { Repository } from "typeorm";
|
|
|
|
/**
|
|
* Browser controller integration tests.
|
|
*
|
|
* 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", () => {
|
|
let app: INestApplication;
|
|
let sessionRepo: Repository<SessionEntity>;
|
|
|
|
const FAKE_SESSION = "test-browser-session";
|
|
|
|
beforeAll(async () => {
|
|
app = await buildTestApp();
|
|
sessionRepo = app.get<Repository<SessionEntity>>(
|
|
getRepositoryToken(SessionEntity),
|
|
);
|
|
|
|
// Seed a session with minimal but valid JSON so the browser code can
|
|
// deserialise it (it will still fail to open a real page, tested separately)
|
|
await sessionRepo.save(
|
|
sessionRepo.create({
|
|
sessionName: FAKE_SESSION,
|
|
token: "fake-token",
|
|
cookies: "[]",
|
|
localStorage: "{}",
|
|
}),
|
|
);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await app.close();
|
|
});
|
|
|
|
// ── POST /open ─────────────────────────────────────────────────────────────
|
|
|
|
describe("POST /open", () => {
|
|
it("returns 400 when body is empty", async () => {
|
|
await request(app.getHttpServer()).post("/open").send({}).expect(400);
|
|
});
|
|
|
|
it("returns 400 when url is missing", async () => {
|
|
await request(app.getHttpServer())
|
|
.post("/open")
|
|
.send({ sessionName: FAKE_SESSION })
|
|
.expect(400);
|
|
});
|
|
|
|
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(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 () => {
|
|
const res = await request(app.getHttpServer())
|
|
.post("/open")
|
|
.send({ url: "https://example.com" })
|
|
.expect(201);
|
|
expect(res.body).toMatchObject({
|
|
url: expect.any(String),
|
|
title: expect.any(String),
|
|
content: expect.any(String),
|
|
});
|
|
});
|
|
|
|
it("returns only selector content when selector is provided", async () => {
|
|
const res = await request(app.getHttpServer())
|
|
.post("/open")
|
|
.send({ url: "https://example.com", selector: "#mock" })
|
|
.expect(201);
|
|
expect(res.body.content).toBe('<div id="mock">mock content</div>');
|
|
});
|
|
|
|
it("returns selector text content in reader mode", async () => {
|
|
const res = await request(app.getHttpServer())
|
|
.post("/open")
|
|
.send({
|
|
url: "https://example.com",
|
|
selector: "#mock",
|
|
readerMode: true,
|
|
})
|
|
.expect(201);
|
|
expect(res.body.content).toBe("mock content");
|
|
});
|
|
});
|
|
|
|
// ── POST /exec ─────────────────────────────────────────────────────────────
|
|
|
|
describe("POST /exec", () => {
|
|
it("returns 400 when body is empty", async () => {
|
|
await request(app.getHttpServer()).post("/exec").send({}).expect(400);
|
|
});
|
|
|
|
it("returns 400 when code is missing", async () => {
|
|
await request(app.getHttpServer())
|
|
.post("/exec")
|
|
.send({ sessionName: FAKE_SESSION })
|
|
.expect(400);
|
|
});
|
|
|
|
it("returns 400 when code has a syntax error", async () => {
|
|
await request(app.getHttpServer())
|
|
.post("/exec")
|
|
.send({ sessionName: FAKE_SESSION, code: "this is not valid {{{" })
|
|
.expect(400);
|
|
});
|
|
|
|
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(201);
|
|
expect(res.body).toEqual({ result: 1 });
|
|
});
|
|
|
|
it("succeeds without a session (sessionless exec)", async () => {
|
|
const res = await request(app.getHttpServer())
|
|
.post("/exec")
|
|
.send({ code: "return 42;" })
|
|
.expect(201);
|
|
expect(res.body).toEqual({ result: 42 });
|
|
});
|
|
|
|
it("exposes environment via context.env in a named session", async () => {
|
|
const res = await request(app.getHttpServer())
|
|
.post("/exec")
|
|
.send({
|
|
sessionName: "no-such-session",
|
|
code: "return context.env.BASE_URL;",
|
|
environment: { BASE_URL: "https://env.example.com" },
|
|
})
|
|
.expect(201);
|
|
expect(res.body).toEqual({ result: "https://env.example.com" });
|
|
});
|
|
|
|
it("exposes environment via context.env in a sessionless exec", async () => {
|
|
const res = await request(app.getHttpServer())
|
|
.post("/exec")
|
|
.send({
|
|
code: "return context.env.KEY;",
|
|
environment: { KEY: "value123" },
|
|
})
|
|
.expect(201);
|
|
expect(res.body).toEqual({ result: "value123" });
|
|
});
|
|
|
|
it("exposes credentials via context.getCredential in a named session", async () => {
|
|
const res = await request(app.getHttpServer())
|
|
.post("/exec")
|
|
.send({
|
|
sessionName: "no-such-session",
|
|
code: "return context.getCredential('admin');",
|
|
credentials: { admin: { username: "user1", password: "pass1" } },
|
|
})
|
|
.expect(201);
|
|
expect(res.body).toEqual({
|
|
result: { username: "user1", password: "pass1" },
|
|
});
|
|
});
|
|
|
|
it("exposes credentials via context.getCredential in a sessionless exec", async () => {
|
|
const res = await request(app.getHttpServer())
|
|
.post("/exec")
|
|
.send({
|
|
code: "return context.getCredential('svc');",
|
|
credentials: { svc: { token: "abc" } },
|
|
})
|
|
.expect(201);
|
|
expect(res.body).toEqual({ result: { token: "abc" } });
|
|
});
|
|
|
|
it("throws when getCredential is called with an unknown alias", async () => {
|
|
const res = await request(app.getHttpServer())
|
|
.post("/exec")
|
|
.send({
|
|
code: "return context.getCredential('missing');",
|
|
credentials: {},
|
|
})
|
|
.expect(500);
|
|
expect(res.body.message).toMatch(/missing/);
|
|
});
|
|
|
|
it("environment and credentials default to empty when omitted", async () => {
|
|
const res = await request(app.getHttpServer())
|
|
.post("/exec")
|
|
.send({
|
|
code: "return Object.keys(context.env).length === 0 ? 'empty' : 'not-empty';",
|
|
})
|
|
.expect(201);
|
|
expect(res.body).toEqual({ result: "empty" });
|
|
});
|
|
});
|
|
});
|