Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions src/app/options/profile-summary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,13 @@ describe("profile summary formatting", () => {
it("formats record summary", () => {
const out = formatRecordingProfileSummary({
browser: "chromium",
selectorPolicy: "reliable",
device: "iPhone 13",
testIdAttribute: "data-qa",
loadStorage: ".auth/in.json",
saveStorage: ".auth/out.json",
});

expect(out).toContain("browser=chromium");
expect(out).toContain("selectorPolicy=reliable");
expect(out).toContain("loadStorage=.auth/in.json");
});

Expand Down
4 changes: 1 addition & 3 deletions src/app/options/profile-summary.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import type { RecordBrowser } from "../../core/recorder.js";
import type { SelectorPolicy } from "./record-profile.js";

export function formatRecordingProfileSummary(profile: {
browser: RecordBrowser;
selectorPolicy: SelectorPolicy;
device?: string;
testIdAttribute?: string;
loadStorage?: string;
saveStorage?: string;
}): string {
return `Recording profile: browser=${profile.browser}, selectorPolicy=${profile.selectorPolicy}, device=${profile.device ?? "(none)"}, testIdAttr=${profile.testIdAttribute ?? "(default)"}, loadStorage=${profile.loadStorage ?? "(none)"}, saveStorage=${profile.saveStorage ?? "(none)"}`;
return `Recording profile: browser=${profile.browser}, device=${profile.device ?? "(none)"}, testIdAttr=${profile.testIdAttribute ?? "(default)"}, loadStorage=${profile.loadStorage ?? "(none)"}, saveStorage=${profile.saveStorage ?? "(none)"}`;
}

export function formatImproveProfileSummary(profile: {
Expand Down
6 changes: 0 additions & 6 deletions src/app/options/record-profile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,12 @@ import { UserError } from "../../utils/errors.js";
import {
normalizeRecordUrl,
parseRecordBrowser,
parseSelectorPolicy,
resolveRecordProfile,
} from "./record-profile.js";

describe("resolveRecordProfile", () => {
it("applies CLI values and normalizes optionals", () => {
const out = resolveRecordProfile({
selectorPolicy: "raw",
browser: "firefox",
device: " iPhone 13 ",
testIdAttribute: " data-qa ",
Expand All @@ -19,7 +17,6 @@ describe("resolveRecordProfile", () => {
});

expect(out).toEqual({
selectorPolicy: "raw",
browser: "firefox",
device: "iPhone 13",
testIdAttribute: "data-qa",
Expand All @@ -31,20 +28,17 @@ describe("resolveRecordProfile", () => {

it("uses defaults when CLI values are unset", () => {
const out = resolveRecordProfile({});
expect(out.selectorPolicy).toBe("reliable");
expect(out.browser).toBe("chromium");
expect(out.outputDir).toBe("e2e");
});
});

describe("record-profile parsing", () => {
it("parses valid enums", () => {
expect(parseSelectorPolicy("RAW")).toBe("raw");
expect(parseRecordBrowser("Webkit")).toBe("webkit");
});

it("rejects invalid enums", () => {
expect(() => parseSelectorPolicy("fast")).toThrow(UserError);
expect(() => parseRecordBrowser("safari")).toThrow(UserError);
});

Expand Down
17 changes: 0 additions & 17 deletions src/app/options/record-profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@ import type { RecordBrowser } from "../../core/recorder.js";
import { PLAY_DEFAULT_TEST_DIR } from "../../core/play/play-defaults.js";
import { UserError } from "../../utils/errors.js";

export type SelectorPolicy = "reliable" | "raw";

export interface RecordProfileInput {
selectorPolicy?: string;
browser?: string;
device?: string;
testIdAttribute?: string;
Expand All @@ -15,7 +12,6 @@ export interface RecordProfileInput {
}

export interface ResolvedRecordProfile {
selectorPolicy: SelectorPolicy;
browser: RecordBrowser;
device?: string;
testIdAttribute?: string;
Expand All @@ -28,7 +24,6 @@ export function resolveRecordProfile(
input: RecordProfileInput
): ResolvedRecordProfile {
const profile: ResolvedRecordProfile = {
selectorPolicy: parseSelectorPolicy(input.selectorPolicy) ?? "reliable",
browser: parseRecordBrowser(input.browser) ?? "chromium",
outputDir: input.outputDir ?? PLAY_DEFAULT_TEST_DIR,
};
Expand All @@ -52,18 +47,6 @@ function cleanOptional(value: string | undefined): string | undefined {
return trimmed.length > 0 ? trimmed : undefined;
}

export function parseSelectorPolicy(value: string | undefined): SelectorPolicy | undefined {
if (!value) return undefined;
const normalized = value.trim().toLowerCase();
if (normalized === "reliable" || normalized === "raw") {
return normalized;
}
throw new UserError(
`Invalid selector policy: ${value}`,
"Use --selector-policy reliable or --selector-policy raw"
);
}

export function parseRecordBrowser(value: string | undefined): RecordBrowser | undefined {
if (!value) return undefined;
const normalized = value.trim().toLowerCase();
Expand Down
131 changes: 120 additions & 11 deletions src/app/services/record-service.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { UserError } from "../../utils/errors.js";

vi.mock("node:fs/promises", () => ({
default: {
readFile: vi.fn(),
writeFile: vi.fn(),
mkdir: vi.fn(),
},
}));

vi.mock("../../utils/chromium-runtime.js", () => ({
ensureChromiumAvailable: vi.fn(),
}));

vi.mock("../../core/recorder.js", () => ({
record: vi.fn(),
}));
vi.mock("../../core/recorder.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../core/recorder.js")>();
return {
...actual,
record: vi.fn(),
};
});

vi.mock("../../core/improve/improve.js", () => ({
improveTestFile: vi.fn(),
Expand All @@ -25,6 +37,7 @@ vi.mock("../../utils/ui.js", () => ({
},
}));

import fs from "node:fs/promises";
import { ensureChromiumAvailable } from "../../utils/chromium-runtime.js";
import { record } from "../../core/recorder.js";
import { improveTestFile } from "../../core/improve/improve.js";
Expand All @@ -34,14 +47,8 @@ import { runRecord } from "./record-service.js";
function mockRecordDefaults() {
vi.mocked(record).mockResolvedValue({
outputPath: "e2e/sample.yaml",
stats: {
selectorSteps: 1,
stableSelectors: 1,
fallbackSelectors: 0,
frameAwareSelectors: 0,
},
recordingMode: "jsonl",
degraded: false,
stepCount: 2,
recordingMode: "codegen",
});
vi.mocked(improveTestFile).mockResolvedValue({
report: {
Expand Down Expand Up @@ -283,3 +290,105 @@ describe("runRecord auto-improve", () => {
);
});
});

describe("runRecordFromFile", () => {
const validRecording = JSON.stringify({
title: "Login Flow",
steps: [
{ type: "navigate", url: "https://example.com/login" },
{ type: "click", selectors: [["aria/Submit[role=\"button\"]"]] },
],
});

beforeEach(() => {
vi.resetAllMocks();
vi.mocked(fs.readFile).mockResolvedValue(validRecording);
vi.mocked(fs.writeFile).mockResolvedValue();
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
vi.mocked(improveTestFile).mockResolvedValue({
report: {
testFile: "e2e/login-flow.yaml",
generatedAt: new Date().toISOString(),
providerUsed: "playwright",
summary: {
unchanged: 1,
improved: 0,
fallback: 0,
warnings: 0,
assertionCandidates: 0,
appliedAssertions: 0,
skippedAssertions: 0,
},
stepFindings: [],
assertionCandidates: [],
diagnostics: [],
},
reportPath: "e2e/login-flow.improve-report.json",
});
});

it("imports a valid DevTools recording JSON file", async () => {
await runRecord({ fromFile: "/tmp/recording.json" });

expect(fs.readFile).toHaveBeenCalledWith("/tmp/recording.json", "utf-8");
expect(fs.writeFile).toHaveBeenCalledWith(
expect.stringContaining("login-flow.yaml"),
expect.stringContaining("name: Login Flow"),
"utf-8"
);
expect(ui.success).toHaveBeenCalledWith(
expect.stringContaining("Test saved to")
);
});

it("throws UserError when file does not exist", async () => {
vi.mocked(fs.readFile).mockRejectedValue(new Error("ENOENT"));

await expect(
runRecord({ fromFile: "/tmp/missing.json" })
).rejects.toBeInstanceOf(UserError);
await expect(
runRecord({ fromFile: "/tmp/missing.json" })
).rejects.toThrow("Could not read file");
});

it("throws UserError when file contains no supported steps", async () => {
vi.mocked(fs.readFile).mockResolvedValue(
JSON.stringify({ title: "Empty", steps: [{ type: "scroll" }] })
);

await expect(
runRecord({ fromFile: "/tmp/empty.json" })
).rejects.toBeInstanceOf(UserError);
await expect(
runRecord({ fromFile: "/tmp/empty.json" })
).rejects.toThrow("No supported interactions");
});

it("uses recording title as test name, falling back to filename", async () => {
await runRecord({ fromFile: "/tmp/recording.json" });
expect(fs.writeFile).toHaveBeenCalledWith(
expect.stringContaining("login-flow.yaml"),
expect.stringContaining("name: Login Flow"),
"utf-8"
);

vi.mocked(fs.readFile).mockResolvedValue(
JSON.stringify({
steps: [{ type: "navigate", url: "https://example.com" }],
})
);
await runRecord({ fromFile: "/tmp/my-test.json" });
expect(fs.writeFile).toHaveBeenCalledWith(
expect.stringContaining("my-test.yaml"),
expect.stringContaining("name: my-test"),
"utf-8"
);
});

it("skips auto-improve when improve is false", async () => {
await runRecord({ fromFile: "/tmp/recording.json", improve: false });

expect(improveTestFile).not.toHaveBeenCalled();
});
});
Loading