refactor(browser): open scenario sessions without a factory wrapper

This commit is contained in:
morluto 2026-10-10 00:48:49 +08:00
parent 87f0051496
commit d1983a4576
6 changed files with 45 additions and 69 deletions

View File

@ -35,11 +35,8 @@ export interface BrowserScenarioSessionPort {
redactError(error: unknown): string;
}
export interface BrowserScenarioSessionFactory {
open(
scenario: BrowserScenario,
options?: {
readonly signal?: AbortSignal;
},
): Promise<BrowserScenarioSessionPort>;
}
/** Acquire one scenario session whose lifecycle the caller owns. */
export type BrowserScenarioSessionOpener = (
scenario: BrowserScenario,
options: { readonly signal?: AbortSignal },
) => Promise<BrowserScenarioSessionPort>;

View File

@ -221,9 +221,9 @@ describe("PlaywrightBrowserScenarioProvider", () => {
it("makes reported event gaps ineligible for equality even in launch mode", async () => {
const session = new FakeSession("launch");
session.eventGaps = ["Popup frames before discovery are unavailable"];
const provider = new PlaywrightBrowserScenarioProvider({
open: () => Promise.resolve(session),
});
const provider = new PlaywrightBrowserScenarioProvider(() =>
Promise.resolve(session),
);
const result = await provider.captureScenario(
scenario({ events: ["frames"] }),
);
@ -237,9 +237,9 @@ describe("PlaywrightBrowserScenarioProvider", () => {
});
it("returns an initial state and every step beyond the former action ceiling", async () => {
const session = new FakeSession("launch");
const provider = new PlaywrightBrowserScenarioProvider({
open: () => Promise.resolve(session),
});
const provider = new PlaywrightBrowserScenarioProvider(() =>
Promise.resolve(session),
);
const result = await provider.captureScenario(scenario({ actions: 129 }));
if (!result.ok) throw result.error;
expect(result.value.scenario.action_count).toBe(129);
@ -249,9 +249,9 @@ describe("PlaywrightBrowserScenarioProvider", () => {
it("makes missing and truncated captures ineligible for equality", async () => {
const session = new FakeSession("launch", { incomplete: true });
const provider = new PlaywrightBrowserScenarioProvider({
open: () => Promise.resolve(session),
});
const provider = new PlaywrightBrowserScenarioProvider(() =>
Promise.resolve(session),
);
const result = await provider.captureScenario(
scenario({ captures: ["screenshot", "dom"] }),
);
@ -268,9 +268,9 @@ describe("PlaywrightBrowserScenarioProvider", () => {
const session = new FakeSession("launch", {
action: new Error("fixture secret action failure"),
});
const provider = new PlaywrightBrowserScenarioProvider({
open: () => Promise.resolve(session),
});
const provider = new PlaywrightBrowserScenarioProvider(() =>
Promise.resolve(session),
);
const result = await provider.captureScenario(scenario({ actions: 2 }));
if (!result.ok) throw result.error;
expect(result.value.steps.map(({ status }) => status)).toEqual([
@ -288,9 +288,9 @@ describe("PlaywrightBrowserScenarioProvider", () => {
it("disconnects external CDP sessions and marks pre-attach events missing", async () => {
const session = new FakeSession("connect");
const provider = new PlaywrightBrowserScenarioProvider({
open: () => Promise.resolve(session),
});
const provider = new PlaywrightBrowserScenarioProvider(() =>
Promise.resolve(session),
);
const result = await provider.captureScenario(
scenario({ mode: "connect" }),
);
@ -314,9 +314,9 @@ describe("PlaywrightBrowserScenarioProvider cleanup", () => {
action: new Error("fixture secret action failure"),
close: new Error("browser close failed"),
});
const provider = new PlaywrightBrowserScenarioProvider({
open: () => Promise.resolve(session),
});
const provider = new PlaywrightBrowserScenarioProvider(() =>
Promise.resolve(session),
);
const result = await provider.captureScenario(scenario());
expect(result.ok).toBe(false);
@ -378,9 +378,9 @@ describe("PlaywrightBrowserScenarioProvider operation failures", () => {
url: sanitizeBrowserUrl("https://app.example.test/"),
},
];
const provider = new PlaywrightBrowserScenarioProvider({
open: () => Promise.resolve(session),
});
const provider = new PlaywrightBrowserScenarioProvider(() =>
Promise.resolve(session),
);
const result = await provider.captureScenario(scenario());
expect(result.ok).toBe(false);
@ -462,9 +462,9 @@ describe("PlaywrightBrowserScenarioProvider cancellation and timeout", () => {
const session = new FakeSession("launch", {
capture: { call: 1, error },
});
const provider = new PlaywrightBrowserScenarioProvider({
open: () => Promise.resolve(session),
});
const provider = new PlaywrightBrowserScenarioProvider(() =>
Promise.resolve(session),
);
const result = await provider.captureScenario(scenario());
expect(result.ok).toBe(false);
@ -489,11 +489,9 @@ describe("PlaywrightBrowserScenarioProvider cancellation and timeout", () => {
describe("PlaywrightBrowserScenarioProvider request cancellation", () => {
it("does not open a session for an already-cancelled request", async () => {
let opens = 0;
const provider = new PlaywrightBrowserScenarioProvider({
open: () => {
opens += 1;
return Promise.resolve(new FakeSession("launch"));
},
const provider = new PlaywrightBrowserScenarioProvider(() => {
opens += 1;
return Promise.resolve(new FakeSession("launch"));
});
const controller = new AbortController();
controller.abort();
@ -508,9 +506,9 @@ describe("PlaywrightBrowserScenarioProvider request cancellation", () => {
const controller = new AbortController();
const session = new FakeSession("launch");
session.onPerform = () => controller.abort();
const provider = new PlaywrightBrowserScenarioProvider({
open: () => Promise.resolve(session),
});
const provider = new PlaywrightBrowserScenarioProvider(() =>
Promise.resolve(session),
);
const result = await provider.captureScenario(scenario(), {
signal: controller.signal,
});

View File

@ -25,7 +25,7 @@ import { BrowserObservationError } from "../domain/browserObservationError.js";
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
import { err, ok, type Result } from "../domain/result.js";
import type {
BrowserScenarioSessionFactory,
BrowserScenarioSessionOpener,
BrowserScenarioSessionPort,
} from "./BrowserScenarioSessionPort.js";
import {
@ -226,14 +226,14 @@ const globalCompleteness = (
};
const runScenario = async (
factory: BrowserScenarioSessionFactory,
openSession: BrowserScenarioSessionOpener,
scenario: BrowserScenario,
options: ExecutionOptions,
): Promise<BrowserScenarioCapture> => {
if (options.signal?.aborted === true)
throw new BrowserObservationError(OPERATION, "cancelled");
const startedAt = Date.now();
const session = await factory.open(
const session = await openSession(
scenario,
options.signal === undefined ? {} : { signal: options.signal },
);
@ -337,7 +337,7 @@ const runScenario = async (
/** Controlled Playwright/CDP scenario driver with exact process ownership. */
export class PlaywrightBrowserScenarioProvider implements BrowserScenarioCapturePort {
constructor(private readonly factory: BrowserScenarioSessionFactory) {}
constructor(private readonly openSession: BrowserScenarioSessionOpener) {}
identity(): ProviderIdentity {
return PLAYWRIGHT_BROWSER_SCENARIO_PROVIDER_IDENTITY;
@ -348,7 +348,7 @@ export class PlaywrightBrowserScenarioProvider implements BrowserScenarioCapture
options: ExecutionOptions = {},
): Promise<Result<BrowserScenarioCapture, AnalysisError>> {
try {
return ok(await runScenario(this.factory, scenario, options));
return ok(await runScenario(this.openSession, scenario, options));
} catch (cause: unknown) {
if (cause instanceof AnalysisError) return err(cause);
return err(

View File

@ -25,7 +25,7 @@ export interface OpenedScenarioBrowser {
}
/** Provider actions retained until the owned browser and profile are released. */
export interface PlaywrightScenarioCleanupResources {
interface PlaywrightScenarioCleanupResources {
readonly closeBrowser: () => Promise<void>;
readonly removeProfile: (() => Promise<void>) | undefined;
}

View File

@ -3,10 +3,7 @@ import { type BrowserContext, type Page } from "playwright-core";
import type { BrowserScenario } from "../domain/browserScenario.js";
import type { BrowserScenarioAction } from "../domain/browserScenarioValues.js";
import { BrowserObservationError } from "../domain/browserObservationError.js";
import type {
BrowserScenarioSessionFactory,
BrowserScenarioSessionPort,
} from "./BrowserScenarioSessionPort.js";
import type { BrowserScenarioSessionPort } from "./BrowserScenarioSessionPort.js";
import { BrowserScenarioSecrets } from "./BrowserScenarioSecrets.js";
import {
failBrowserScenarioOperation,
@ -311,19 +308,3 @@ export class PlaywrightScenarioSession implements BrowserScenarioSessionPort {
);
}
}
/** Production Playwright/CDP session factory. */
export class PlaywrightScenarioSessionFactory implements BrowserScenarioSessionFactory {
constructor(
private readonly environment: Readonly<Record<string, string | undefined>>,
) {}
open(
scenario: BrowserScenario,
options: {
readonly signal?: AbortSignal;
} = {},
): Promise<BrowserScenarioSessionPort> {
return PlaywrightScenarioSession.open(scenario, this.environment, options);
}
}

View File

@ -1,11 +1,11 @@
import type { BrowserScenarioCapturePort } from "../application/BrowserScenarioCapturePort.js";
import { PlaywrightBrowserScenarioProvider } from "../browser/PlaywrightBrowserScenarioProvider.js";
import { PlaywrightScenarioSessionFactory } from "../browser/PlaywrightScenarioSession.js";
import { PlaywrightScenarioSession } from "../browser/PlaywrightScenarioSession.js";
/** Construct a fresh provider without opening a target or acquiring an engine. */
export const createBrowserScenarioProvider = (
environment: Readonly<Record<string, string | undefined>>,
): BrowserScenarioCapturePort =>
new PlaywrightBrowserScenarioProvider(
new PlaywrightScenarioSessionFactory(environment),
new PlaywrightBrowserScenarioProvider((scenario, options) =>
PlaywrightScenarioSession.open(scenario, environment, options),
);