Skip to content
Open
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
148 changes: 148 additions & 0 deletions src/browser/CdpElectronWorkers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { describe, expect, it } from "vitest";

import type { InspectElectronPageInput } from "../domain/javascript/electronObservation.js";
import { CdpCaptureCompleteness } from "./CdpCaptureCompleteness.js";
import type { CdpConnection } from "./CdpConnection.js";
import type { CdpEndpointTarget } from "./CdpEndpoint.js";
import { captureElectronWorkers } from "./CdpElectronWorkers.js";

/** `authorizedElectronFile` only admits a file the host can resolve. */
const localFileUrl = import.meta.url;

const workerTarget = (
over: Readonly<Record<string, unknown>> = {},
): Readonly<Record<string, unknown>> => ({
targetId: "worker-1",
type: "service_worker",
url: localFileUrl,
openerId: "electron-page",
attached: true,
...over,
});

/** An Electron worker target whose `TargetInfo.type` is not delivered at all. */
const targetWithoutType: Readonly<Record<string, unknown>> = {
targetId: "worker-1",
url: localFileUrl,
openerId: "electron-page",
attached: true,
};

const capture = async (inventory: unknown) => {
const completeness = new CdpCaptureCompleteness();
const workers = await captureElectronWorkers({
connection: {
async send() {
return inventory;
},
} as unknown as CdpConnection,
target: {
id: "electron-page",
type: "page",
title: "Fixture",
url: localFileUrl,
attached: false,
} satisfies CdpEndpointTarget,
request: {} as InspectElectronPageInput,
frameIds: new Set<string>(),
completeness,
limitations: [],
});
return { workers, completeness: completeness.snapshot() };
};

describe("Electron worker target inventory", () => {
it("preserves an explicit empty inventory", async () => {
const result = await capture({ targetInfos: [] });

expect(result.workers).toEqual([]);
expect(result.completeness.unavailable_sections).not.toContain("workers");
expect(result.completeness.excluded).not.toContainEqual(
expect.objectContaining({ section: "workers" }),
);
});

it.each([undefined, { length: 1 }, "worker-one"])(
"reports a missing or non-array target inventory as unknown: %s",
async (targetInfos) => {
const result = await capture({ targetInfos });

expect(result.workers).toEqual([]);
expect(result.completeness).toMatchObject({
unavailable_sections: ["workers"],
excluded: [
{ section: "workers", reason: "invalid_protocol_value", count: null },
],
});
},
);

it("records a target entry that carries no type discriminator", async () => {
const result = await capture({ targetInfos: [targetWithoutType] });

expect(result.workers).toEqual([]);
expect(result.completeness.unavailable_sections).toContain("workers");
expect(result.completeness.excluded).toContainEqual({
section: "workers",
reason: "invalid_protocol_value",
count: 1,
});
});

it.each([
["null", null],
["numeric", 42],
["empty", ""],
] as const)(
"records a target entry whose type is an undecodable %s value",
async (_label, type) => {
const result = await capture({ targetInfos: [workerTarget({ type })] });

expect(result.workers).toEqual([]);
expect(result.completeness.excluded).toContainEqual({
section: "workers",
reason: "invalid_protocol_value",
count: 1,
});
},
);

it("keeps a decodable non-worker type out of the malformed ledger", async () => {
const result = await capture({
targetInfos: [workerTarget({ type: "page" })],
});

expect(result.workers).toEqual([]);
expect(result.completeness.unavailable_sections).not.toContain("workers");
expect(result.completeness.excluded).not.toContainEqual(
expect.objectContaining({ section: "workers" }),
);
});

it("retains valid worker records beside malformed entries", async () => {
const result = await capture({
targetInfos: [
null,
targetWithoutType,
{ url: localFileUrl, type: "worker", openerId: "electron-page" },
workerTarget({ targetId: "worker-2" }),
],
});

expect(result.workers.map((worker) => worker.target_id)).toEqual([
"worker-2",
]);
expect(result.completeness.unavailable_sections).toContain("workers");
expect(result.completeness.excluded).toContainEqual({
section: "workers",
reason: "invalid_protocol_value",
count: 3,
});
expect(result.completeness.excluded).not.toContainEqual(
expect.objectContaining({
section: "workers",
reason: "out_of_target_scope",
}),
);
});
});
30 changes: 23 additions & 7 deletions src/browser/CdpElectronWorkers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import type {
} from "../domain/javascript/electronObservation.js";
import type { CdpConnection } from "./CdpConnection.js";
import { CdpCaptureCompleteness } from "./CdpCaptureCompleteness.js";
import { recordValue, cdpStringValue } from "./CdpCaptureValues.js";
import {
admitRecords,
recordValue,
cdpStringValue,
} from "./CdpCaptureValues.js";
import type { CdpEndpointTarget } from "./CdpEndpoint.js";
import { optionalCdpCommand } from "./CdpOptionalCommand.js";
import { authorizedElectronFile } from "./ElectronFileScope.js";
Expand Down Expand Up @@ -36,18 +40,24 @@ export const captureElectronWorkers = async (input: {
}
const workers: ElectronPageInspection["workers"] = [];
// A missing target list or a non-record entry is unreadable, not empty.
const targetInfos = recordValue(result)?.targetInfos;
if (!Array.isArray(targetInfos)) {
const targetInfos = admitRecords(recordValue(result)?.targetInfos);
if (targetInfos === undefined) {
input.completeness.exclude("workers", "invalid_protocol_value", null);
return [];
}
let malformedEntries = 0;
for (const entry of targetInfos) {
const target = recordValue(entry);
if (target === undefined) {
input.completeness.exclude("workers", "invalid_protocol_value");
malformedEntries += 1;
continue;
}
// An undecodable discriminator cannot prove the entry is not a worker.
const type = cdpStringValue(target.type);
if (type === undefined || type.length === 0) {
malformedEntries += 1;
continue;
}
const type = cdpStringValue(target.type) ?? "";
if (!type.includes("worker")) continue;
const openerTargetId = boundedTargetField(target.openerId);
const parentFrameId = boundedTargetField(target.parentFrameId);
Expand All @@ -65,11 +75,11 @@ export const captureElectronWorkers = async (input: {
}
const targetId = boundedTargetField(target.targetId);
if (targetId === null) {
input.completeness.exclude("workers", "invalid_protocol_value");
malformedEntries += 1;
continue;
}
if (typeof target.attached !== "boolean") {
input.completeness.exclude("workers", "invalid_protocol_value");
malformedEntries += 1;
continue;
}
workers.push({
Expand All @@ -81,6 +91,12 @@ export const captureElectronWorkers = async (input: {
parent_frame_id: parentFrameId,
});
}
if (malformedEntries > 0)
input.completeness.exclude(
"workers",
"invalid_protocol_value",
malformedEntries,
);
return workers.sort((left, right) =>
compareUnicodeCodePoints(left.target_id, right.target_id),
);
Expand Down
60 changes: 53 additions & 7 deletions src/browser/CdpPageCaptureWorkers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,22 @@ const capture = async (inventory: unknown) => {
return { workers, completeness: events.completeness.snapshot() };
};

/** A page-owned worker `TargetInfo` with no `type` discriminator at all. */
const targetWithoutType = () => ({
targetId: "worker-1",
url: `${origin}/worker.js`,
openerId: "page",
attached: true,
});

const workerTarget = (
over: Readonly<Record<string, unknown>> = {},
): Readonly<Record<string, unknown>> => ({
...targetWithoutType(),
type: "service_worker",
...over,
});

describe("web worker target inventory", () => {
it("preserves an explicit empty inventory", async () => {
const result = await capture({ targetInfos: [] });
Expand Down Expand Up @@ -74,13 +90,7 @@ describe("web worker target inventory", () => {
const result = await capture({
targetInfos: [
null,
{
targetId: "worker-1",
type: "service_worker",
url: `${origin}/worker.js`,
attached: true,
openerId: "page",
},
workerTarget(),
{
targetId: "worker-missing-url",
type: "worker",
Expand Down Expand Up @@ -124,4 +134,40 @@ describe("web worker target inventory", () => {
}),
);
});

it.each([
["absent", undefined],
["null", null],
["numeric", 42],
["empty", ""],
] as const)(
"records a target entry whose type is %s instead of deciding it is not a worker",
async (label, type) => {
const result = await capture({
targetInfos: [
label === "absent" ? targetWithoutType() : workerTarget({ type }),
],
});

expect(result.workers).toEqual([]);
expect(result.completeness.unavailable_sections).toContain("workers");
expect(result.completeness.excluded).toContainEqual({
section: "workers",
reason: "invalid_protocol_value",
count: 1,
});
},
);

it("keeps a decodable non-worker type out of the malformed ledger", async () => {
const result = await capture({
targetInfos: [workerTarget({ type: "page" })],
});

expect(result.workers).toEqual([]);
expect(result.completeness.unavailable_sections).not.toContain("workers");
expect(result.completeness.excluded).not.toContainEqual(
expect.objectContaining({ section: "workers" }),
);
});
});
Loading