refactor(test): share browser and apktool fixture lifecycles

This commit is contained in:
morluto
2026-10-21 05:50:50 +08:00
parent 1bb2fa23de
commit 7dd6941393
9 changed files with 461 additions and 647 deletions
+9 -105
View File
@@ -1,6 +1,6 @@
import { expect, it } from "vitest";
import { createWebPageInspection } from "../../tests/support/browser/webPageInspection.js";
import { webPageInspectionSchema } from "../domain/browserObservationSchemas.js";
import {
AnalysisCancelledError,
AnalysisOutputError,
@@ -13,7 +13,6 @@ import type {
} from "../domain/webBundleAnalysis.js";
import { analyzeWebBundleInputSchema } from "../domain/webBundleAnalysis.js";
import { analyzeCapturedWebScript } from "../domain/webBundleAnalyzer.js";
import { createWebTextArtifact } from "../domain/webContentArtifact.js";
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
import { err, ok } from "../domain/result.js";
import {
@@ -373,107 +372,12 @@ const deferred = <T>(): {
};
const inspection = (sources: readonly string[]) =>
webPageInspectionSchema.parse({
browser: {
product: "Fake Chrome",
protocol_version: "1.3",
revision: "1",
user_agent: "fake",
js_version: "1",
},
target: {
target_id: "page-1",
type: "page",
title: "App",
url: `${origin}/app`,
createWebPageInspection(
sources.map((source, index) => ({
source,
scriptKey: `scr_${String(index + 1).repeat(64)}`,
url: `${origin}/assets/${String(index)}.js`,
origin,
attached: false,
},
capture_window: {
started_at: "2026-07-14T00:00:00.000Z",
ended_at: "2026-07-14T00:00:01.000Z",
observation_ms: 1_000,
},
completeness: {
status: "attach_limited",
conditions: ["attach_limited"],
policy_filtered_sections: [],
attach_limited_sections: ["network_requests"],
truncated_sections: [],
unavailable_sections: [],
excluded: [],
dropped_events: {
scripts: 0,
network_requests: 0,
console_events: 0,
websocket_connections: 0,
websocket_frames: 0,
webmcp_tools: 0,
timeline_events: 0,
total: 0,
},
},
frames: [],
dom: { total_nodes: 0, nodes: [] },
accessibility: {
total_nodes: 0,
text_capture: {
status: "not_approved",
retained_bytes: 0,
excluded_fields: 0,
},
nodes: [],
},
scripts: {
total: sources.length,
items: sources.map((source, index) => ({
script_key: `scr_${String(index + 1).repeat(64)}`,
url: `${origin}/assets/${String(index)}.js`,
origin,
cdp_hash: `hash-${String(index)}`,
length: Buffer.byteLength(source),
is_module: true,
language: "JavaScript",
source_map_url: null,
resource_reconciliation: {
status: "unmatched",
reason: "no_exact_sanitized_url",
},
source: {
included: true,
artifact: createWebTextArtifact(source, "text/javascript"),
},
})),
},
resources: [],
network: {
requests: [],
websocket_connections: [],
coverage_started_at: "2026-07-14T00:00:00.000Z",
prior_activity_available: false,
},
console: {
events: [],
coverage_started_at: "2026-07-14T00:00:00.000Z",
prior_activity_available: false,
},
workers: [],
metadata: {
responses: [],
dom_urls: [],
agent_hints: [],
excluded_dom_urls: 0,
headers_allowlisted: true,
},
storage: {
origin,
usage_bytes: null,
quota_bytes: null,
local_storage_keys: [],
session_storage_keys: [],
indexed_db_names: [],
cache_names: [],
values_redacted: true,
},
limitations: [],
});
cdpHash: `hash-${String(index)}`,
})),
);
+47 -74
View File
@@ -216,6 +216,51 @@ const policyResponse = (method: string): unknown =>
? { result: { value: null } }
: {};
const prepareLateStopRetry = async () => {
const { cleanup, page, storage } = fixture();
const cancellation = new AbortController();
storage?.close.mockResolvedValue({ loadingStopped: false });
const attached = connectedScenario("block");
const session = await PlaywrightScenarioSession.open(
attached,
{},
{ signal: cancellation.signal },
);
const { promise: held, resolve: finishRestoration } = deferred();
const { promise: started, resolve: restorationStarted } = deferred();
const { promise: transportClosed, resolve: signalTransportClosed } =
deferred();
mocks.policyTransportClose.mockImplementation(async () => {
signalTransportClosed();
});
mocks.policySend.mockImplementation(async (method: string) => {
if (method === "Runtime.evaluate") {
restorationStarted();
await held;
}
return policyResponse(method);
});
vi.useFakeTimers();
try {
const firstClose = session.close(false).then(
() => ({ ok: true as const }),
(cause: unknown) => ({ ok: false as const, cause }),
);
await started;
await vi.advanceTimersByTimeAsync(5_001);
await expect(firstClose).resolves.toMatchObject({ ok: false });
finishRestoration();
await transportClosed;
await vi.advanceTimersByTimeAsync(0);
cancellation.abort();
return { cleanup, page, session };
} catch (cause) {
vi.useRealTimers();
throw cause;
}
};
beforeEach(() => {
vi.clearAllMocks();
mocks.policySend.mockReset();
@@ -661,44 +706,8 @@ it("retries a failed connected-page stop when service workers are allowed", asyn
});
it("retains a late fallback stop failure for retry", async () => {
const { cleanup, page, storage } = fixture();
const cancellation = new AbortController();
storage?.close.mockResolvedValue({ loadingStopped: false });
const attached = connectedScenario("block");
const session = await PlaywrightScenarioSession.open(
attached,
{},
{ signal: cancellation.signal },
);
const { promise: held, resolve: finishRestoration } = deferred();
const { promise: started, resolve: restorationStarted } = deferred();
const { promise: transportClosed, resolve: signalTransportClosed } =
deferred();
mocks.policyTransportClose.mockImplementation(async () => {
signalTransportClosed();
});
mocks.policySend.mockImplementation(async (method: string) => {
if (method === "Runtime.evaluate") {
restorationStarted();
await held;
}
return policyResponse(method);
});
vi.useFakeTimers();
const { cleanup, page, session } = await prepareLateStopRetry();
try {
const firstClose = session.close(false).then(
() => ({ ok: true as const }),
(cause: unknown) => ({ ok: false as const, cause }),
);
await started;
await vi.advanceTimersByTimeAsync(5_001);
await expect(firstClose).resolves.toMatchObject({ ok: false });
finishRestoration();
await transportClosed;
await vi.advanceTimersByTimeAsync(0);
cancellation.abort();
const stopFailure = new Error("late main-frame stop failed");
const detachedFrame = {
evaluate: vi.fn(async () => {
@@ -734,44 +743,8 @@ it("retains a late fallback stop failure for retry", async () => {
});
it("coalesces concurrent late-stop close retries", async () => {
const { cleanup, page, storage } = fixture();
const cancellation = new AbortController();
storage?.close.mockResolvedValue({ loadingStopped: false });
const attached = connectedScenario("block");
const session = await PlaywrightScenarioSession.open(
attached,
{},
{ signal: cancellation.signal },
);
const { promise: held, resolve: finishRestoration } = deferred();
const { promise: started, resolve: restorationStarted } = deferred();
const { promise: transportClosed, resolve: signalTransportClosed } =
deferred();
mocks.policyTransportClose.mockImplementation(async () => {
signalTransportClosed();
});
mocks.policySend.mockImplementation(async (method: string) => {
if (method === "Runtime.evaluate") {
restorationStarted();
await held;
}
return policyResponse(method);
});
vi.useFakeTimers();
const { cleanup, page, session } = await prepareLateStopRetry();
try {
const firstClose = session.close(false).then(
() => ({ ok: true as const }),
(cause: unknown) => ({ ok: false as const, cause }),
);
await started;
await vi.advanceTimersByTimeAsync(5_001);
await expect(firstClose).resolves.toMatchObject({ ok: false });
finishRestoration();
await transportClosed;
await vi.advanceTimersByTimeAsync(0);
cancellation.abort();
const { promise: heldStop, resolve: finishStop } = deferred();
const { promise: startedStop, resolve: stopStarted } = deferred();
page.evaluate.mockImplementation(async (script: unknown) => {
+8 -105
View File
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { createWebPageInspection } from "../../tests/support/browser/webPageInspection.js";
import { webPageInspectionSchema } from "./browserObservationSchemas.js";
import { analyzeJavaScriptStaticSource } from "./javascript/javascriptStaticAnalysis.js";
import {
analyzeCapturedWebBundle,
@@ -1146,112 +1146,15 @@ describe("deep captured web bundle syntax", () => {
});
const inspection = (source: string) =>
webPageInspectionSchema.parse({
browser: {
product: "Fake Chrome",
protocol_version: "1.3",
revision: "1",
user_agent: "fake",
js_version: "1",
},
target: {
target_id: "page-1",
type: "page",
title: "App",
url: `${origin}/app`,
createWebPageInspection([
{
source,
scriptKey: `scr_${"1".repeat(64)}`,
url: `${origin}/assets/app.js`,
origin,
attached: false,
cdpHash: "hash",
},
capture_window: {
started_at: "2026-07-14T00:00:00.000Z",
ended_at: "2026-07-14T00:00:01.000Z",
observation_ms: 1_000,
},
completeness: {
status: "attach_limited",
conditions: ["attach_limited"],
policy_filtered_sections: [],
attach_limited_sections: ["network_requests"],
truncated_sections: [],
unavailable_sections: [],
excluded: [],
dropped_events: {
scripts: 0,
network_requests: 0,
console_events: 0,
websocket_connections: 0,
websocket_frames: 0,
webmcp_tools: 0,
timeline_events: 0,
total: 0,
},
},
frames: [],
dom: { total_nodes: 0, nodes: [] },
accessibility: {
total_nodes: 0,
text_capture: {
status: "not_approved",
retained_bytes: 0,
excluded_fields: 0,
},
nodes: [],
},
scripts: {
total: 1,
items: [
{
script_key: `scr_${"1".repeat(64)}`,
url: `${origin}/assets/app.js`,
origin,
cdp_hash: "hash",
length: Buffer.byteLength(source),
is_module: true,
language: "JavaScript",
source_map_url: null,
resource_reconciliation: {
status: "unmatched",
reason: "no_exact_sanitized_url",
},
source: {
included: true,
artifact: createWebTextArtifact(source, "text/javascript"),
},
},
],
},
resources: [],
network: {
requests: [],
websocket_connections: [],
coverage_started_at: "2026-07-14T00:00:00.000Z",
prior_activity_available: false,
},
console: {
events: [],
coverage_started_at: "2026-07-14T00:00:00.000Z",
prior_activity_available: false,
},
workers: [],
metadata: {
responses: [],
dom_urls: [],
agent_hints: [],
excluded_dom_urls: 0,
headers_allowlisted: true,
},
storage: {
origin,
usage_bytes: null,
quota_bytes: null,
local_storage_keys: [],
session_storage_keys: [],
indexed_db_names: [],
cache_names: [],
values_redacted: true,
},
limitations: [],
});
]);
describe("per-script web bundle analysis", () => {
it("keeps parser failures as analyzed outcomes with their own coverage", () => {
@@ -2,17 +2,39 @@ import { expect, it } from "vitest";
import { CdpBrowserProvider } from "../../../src/browser/CdpBrowserProvider.js";
import { observeWebSessionInputSchema } from "../../../src/domain/browserSession.js";
import { startFakeCdpBrowser } from "../../fixtures/fakeCdpBrowser.js";
import {
startFakeCdpBrowser,
type FakeOptions,
} from "../../fixtures/fakeCdpBrowser.js";
import { trackBrowser } from "./cdpBrowserProvider.support.js";
it("fails closed on an untimestamped cross-origin main-frame commit during clock sampling", async () => {
const observeDuringClockSampling = async (
commandEvents: NonNullable<FakeOptions["commandEvents"]>,
) => {
const browser = await startFakeCdpBrowser({
eventsBeforeReply: ["Performance.getMetrics"],
commandResult: (command) =>
command.method === "Performance.getMetrics"
? { metrics: [{ name: "Timestamp", value: 20 }] }
: undefined,
commandEvents: (command) => {
commandEvents,
});
trackBrowser(browser);
const result = await new CdpBrowserProvider().observeSession(
observeWebSessionInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
observation_ms: 20,
}),
);
if (!result.ok) throw result.error;
return { browser, observation: result.value };
};
it("fails closed on an untimestamped cross-origin main-frame commit during clock sampling", async () => {
const { observation } = await observeDuringClockSampling(
(command, origin) => {
if (command.method !== "Performance.getMetrics") return undefined;
return [
{
@@ -34,27 +56,17 @@ it("fails closed on an untimestamped cross-origin main-frame commit during clock
timestamp: 21,
type: "Document",
frameId: "frame-main",
redirectResponse: { url: `${browser.allowedOrigin}/prior` },
request: { url: `${browser.allowedOrigin}/later` },
redirectResponse: { url: `${origin}/prior` },
request: { url: `${origin}/later` },
},
},
];
},
});
trackBrowser(browser);
const result = await new CdpBrowserProvider().observeSession(
observeWebSessionInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
observation_ms: 20,
}),
);
if (!result.ok) throw result.error;
expect(result.value.window.end_reason).toBe("target_left_scope");
expect(result.value.target.final_url).toBeNull();
expect(result.value.timeline).toContainEqual(
expect(observation.window.end_reason).toBe("target_left_scope");
expect(observation.target.final_url).toBeNull();
expect(observation.timeline).toContainEqual(
expect.objectContaining({
type: "navigation_committed",
loader_id: "outside-loader",
@@ -63,19 +75,14 @@ it("fails closed on an untimestamped cross-origin main-frame commit during clock
url: null,
}),
);
expect(result.value.timeline).not.toContainEqual(
expect(observation.timeline).not.toContainEqual(
expect.objectContaining({ request_id: "later-in-window-request" }),
);
});
it("fails closed on an untimestamped out-of-scope main-frame request during clock sampling", async () => {
const browser = await startFakeCdpBrowser({
eventsBeforeReply: ["Performance.getMetrics"],
commandResult: (command) =>
command.method === "Performance.getMetrics"
? { metrics: [{ name: "Timestamp", value: 20 }] }
: undefined,
commandEvents: (command, origin) => {
const { observation } = await observeDuringClockSampling(
(command, origin) => {
if (command.method !== "Performance.getMetrics") return undefined;
return [
{
@@ -101,20 +108,10 @@ it("fails closed on an untimestamped out-of-scope main-frame request during cloc
},
];
},
});
trackBrowser(browser);
const result = await new CdpBrowserProvider().observeSession(
observeWebSessionInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
observation_ms: 20,
}),
);
if (!result.ok) throw result.error;
expect(result.value.window.end_reason).toBe("target_left_scope");
expect(result.value.timeline).toContainEqual(
expect(observation.window.end_reason).toBe("target_left_scope");
expect(observation.timeline).toContainEqual(
expect.objectContaining({
type: "navigation_requested",
timestamp: null,
@@ -122,19 +119,14 @@ it("fails closed on an untimestamped out-of-scope main-frame request during cloc
url: null,
}),
);
expect(result.value.timeline).not.toContainEqual(
expect(observation.timeline).not.toContainEqual(
expect.objectContaining({ request_id: "later-in-window-request" }),
);
});
it("fails closed on an untimestamped out-of-scope redirect during clock sampling", async () => {
const browser = await startFakeCdpBrowser({
eventsBeforeReply: ["Performance.getMetrics"],
commandResult: (command) =>
command.method === "Performance.getMetrics"
? { metrics: [{ name: "Timestamp", value: 20 }] }
: undefined,
commandEvents: (command, origin) => {
const { observation } = await observeDuringClockSampling(
(command, origin) => {
if (command.method !== "Performance.getMetrics") return undefined;
return [
{
@@ -162,21 +154,11 @@ it("fails closed on an untimestamped out-of-scope redirect during clock sampling
},
];
},
});
trackBrowser(browser);
const result = await new CdpBrowserProvider().observeSession(
observeWebSessionInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
observation_ms: 20,
}),
);
if (!result.ok) throw result.error;
expect(result.value.window.end_reason).toBe("target_left_scope");
expect(result.value.target.final_url).toBeNull();
expect(result.value.timeline).toContainEqual(
expect(observation.window.end_reason).toBe("target_left_scope");
expect(observation.target.final_url).toBeNull();
expect(observation.timeline).toContainEqual(
expect.objectContaining({
type: "redirect",
request_id: "unknown-time-outside-redirect",
@@ -185,7 +167,7 @@ it("fails closed on an untimestamped out-of-scope redirect during clock sampling
url: null,
}),
);
expect(result.value.timeline).not.toContainEqual(
expect(observation.timeline).not.toContainEqual(
expect.objectContaining({ request_id: "later-in-window-request" }),
);
});
+179 -209
View File
@@ -14,6 +14,7 @@ import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import {
processTest as it,
waitForExit,
type TestProcesses,
} from "../../support/process/processFixture.js";
const chromeExecutable = [
@@ -34,6 +35,87 @@ const chromeExecutable = [
const CHROME_STARTUP_TIMEOUT_MS = 60_000;
const CHROME_TEST_TIMEOUT_MS = CHROME_STARTUP_TIMEOUT_MS + 30_000;
const inspectChromePage = async ({
processes,
executable,
profilePrefix,
page,
contentType,
pathname,
}: {
processes: TestProcesses;
executable: string;
profilePrefix: string;
page: string;
contentType: string;
pathname: string;
}) => {
const profile = await createTestTempDirectory(profilePrefix);
const server = createServer((_request, response) => {
response.writeHead(200, {
"content-type": `${contentType}; charset=utf-8`,
});
response.end(page);
});
await new Promise<void>((resolve) => {
server.listen(0, "127.0.0.1", resolve);
});
const address = server.address();
if (address === null || typeof address === "string")
throw new Error("SVG fixture server did not bind a TCP port");
const origin = `http://127.0.0.1:${String(address.port)}`;
const pageUrl = `${origin}${pathname}`;
let stderr = "";
const child = processes.spawn(executable, [
"--headless=new",
...(process.env.REA_BROWSER_NO_SANDBOX === "true" ? ["--no-sandbox"] : []),
"--remote-debugging-address=127.0.0.1",
"--remote-debugging-port=0",
`--user-data-dir=${profile}`,
"--no-first-run",
"--no-default-browser-check",
"--disable-background-networking",
"--disable-component-update",
"--disable-dev-shm-usage",
"--disable-sync",
"--metrics-recording-only",
pageUrl,
]);
child.stdout.on("data", () => undefined);
child.stderr.on("data", (chunk: Buffer) => {
if (stderr.length < 8_192) stderr += chunk.toString("utf8");
});
try {
const port = await waitForBrowserDevtoolsPort({
child,
executable,
activePortPath: join(profile, "DevToolsActivePort"),
stderr: () => stderr,
timeoutMs: CHROME_STARTUP_TIMEOUT_MS,
});
const endpoint = `http://127.0.0.1:${String(port)}`;
const provider = new CdpBrowserProvider();
const targetId = await pageTarget(provider, endpoint, origin, pageUrl);
const result = await provider.inspectPage(
inspectWebPageInputSchema.parse({
cdp_endpoint: endpoint,
allowed_origins: [origin],
target_id: targetId,
observation_ms: 0,
}),
);
if (!result.ok) throw result.error;
return { origin, pageUrl, inspection: result.value };
} finally {
child.kill("SIGTERM");
if (!(await waitForExit(child, 2_000))) child.kill("SIGKILL");
await waitForExit(child, 2_000);
await new Promise<void>((resolve) => {
server.close(() => resolve());
});
}
};
// Issue 1586's page, plus the HTML, image, use, and foreign-origin cases the
// real snapshot must distinguish. The XHTML page is the prefixed-svg gap
// reported on PR 1602. The tests are skipped when no Chrome-family executable
@@ -84,133 +166,78 @@ it.skipIf(chromeExecutable === undefined)(
async ({ processes }) => {
const executable = chromeExecutable;
if (executable === undefined) return;
const profile = await createTestTempDirectory("rea-svg-xlink-");
const server = createServer((_request, response) => {
response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
response.end(pageHtml);
const { origin, inspection } = await inspectChromePage({
processes,
executable,
profilePrefix: "rea-svg-xlink-",
page: pageHtml,
contentType: "text/html",
pathname: "/x.html",
});
await new Promise<void>((resolve) => {
server.listen(0, "127.0.0.1", resolve);
});
const address = server.address();
if (address === null || typeof address === "string")
throw new Error("SVG fixture server did not bind a TCP port");
const origin = `http://127.0.0.1:${String(address.port)}`;
const pageUrl = `${origin}/x.html`;
let stderr = "";
const child = processes.spawn(executable, [
"--headless=new",
...(process.env.REA_BROWSER_NO_SANDBOX === "true"
? ["--no-sandbox"]
: []),
"--remote-debugging-address=127.0.0.1",
"--remote-debugging-port=0",
`--user-data-dir=${profile}`,
"--no-first-run",
"--no-default-browser-check",
"--disable-background-networking",
"--disable-component-update",
"--disable-dev-shm-usage",
"--disable-sync",
"--metrics-recording-only",
pageUrl,
]);
child.stdout.on("data", () => undefined);
child.stderr.on("data", (chunk: Buffer) => {
if (stderr.length < 8_192) stderr += chunk.toString("utf8");
});
try {
const port = await waitForBrowserDevtoolsPort({
child,
executable,
activePortPath: join(profile, "DevToolsActivePort"),
stderr: () => stderr,
timeoutMs: CHROME_STARTUP_TIMEOUT_MS,
});
const endpoint = `http://127.0.0.1:${String(port)}`;
const provider = new CdpBrowserProvider();
const targetId = await pageTarget(provider, endpoint, origin, pageUrl);
const result = await provider.inspectPage(
inspectWebPageInputSchema.parse({
cdp_endpoint: endpoint,
allowed_origins: [origin],
target_id: targetId,
observation_ms: 0,
const urls = inspection.metadata.dom_urls;
const html = inspection.dom.nodes.find(
(node) =>
node.node_name === "A" && node.attribute_names.includes("xlink:href"),
);
expect(html).toBeDefined();
expect(urls.some((url) => url.node_index === html?.index)).toBe(false);
expect(urls).toEqual(
expect.arrayContaining([
expect.objectContaining({
attribute: "href",
url: `${origin}/xl-s2h`,
destination_scope: "approved",
}),
);
if (!result.ok) throw result.error;
const urls = result.value.metadata.dom_urls;
const html = result.value.dom.nodes.find(
(node) =>
node.node_name === "A" && node.attribute_names.includes("xlink:href"),
);
expect(html).toBeDefined();
expect(urls.some((url) => url.node_index === html?.index)).toBe(false);
expect(urls).toEqual(
expect.arrayContaining([
expect.objectContaining({
attribute: "href",
url: `${origin}/href-wins`,
destination_scope: "approved",
}),
expect.objectContaining({
attribute: "href",
url: `${origin}/xl-image`,
destination_scope: "approved",
}),
expect.objectContaining({
attribute: "href",
url: `${origin}/xl-use`,
destination_scope: "approved",
}),
expect.objectContaining({
attribute: "href",
url: null,
destination_scope: "outside_policy",
}),
...[
"/html-desc",
"/html-title",
"/xl-nested-desc",
"/xl-nested-title",
"/xl-moved-desc",
"/xl-moved-title",
"/xl-created-desc",
"/xl-created-title",
"/xl-created-nested-desc",
"/xl-created-nested-title",
].map((path) =>
expect.objectContaining({
attribute: "href",
url: `${origin}/xl-s2h`,
url: `${origin}${path}`,
destination_scope: "approved",
}),
expect.objectContaining({
attribute: "href",
url: `${origin}/href-wins`,
destination_scope: "approved",
}),
expect.objectContaining({
attribute: "href",
url: `${origin}/xl-image`,
destination_scope: "approved",
}),
expect.objectContaining({
attribute: "href",
url: `${origin}/xl-use`,
destination_scope: "approved",
}),
expect.objectContaining({
attribute: "href",
url: null,
destination_scope: "outside_policy",
}),
...[
"/html-desc",
"/html-title",
"/xl-nested-desc",
"/xl-nested-title",
"/xl-moved-desc",
"/xl-moved-title",
"/xl-created-desc",
"/xl-created-title",
"/xl-created-nested-desc",
"/xl-created-nested-title",
].map((path) =>
expect.objectContaining({
attribute: "href",
url: `${origin}${path}`,
destination_scope: "approved",
}),
),
]),
);
expect(
urls.some(
(url) =>
url.url?.includes("/xl-html") === true ||
url.url?.includes("/xl-math") === true ||
url.url?.endsWith("/xlink-loses") === true,
),
).toBe(false);
expect(urls.every((url) => url.attribute === "href")).toBe(true);
expect(urls).toHaveLength(15);
} finally {
child.kill("SIGTERM");
if (!(await waitForExit(child, 2_000))) child.kill("SIGKILL");
await waitForExit(child, 2_000);
await new Promise<void>((resolve) => {
server.close(() => resolve());
});
}
]),
);
expect(
urls.some(
(url) =>
url.url?.includes("/xl-html") === true ||
url.url?.includes("/xl-math") === true ||
url.url?.endsWith("/xlink-loses") === true,
),
).toBe(false);
expect(urls.every((url) => url.attribute === "href")).toBe(true);
expect(urls).toHaveLength(15);
},
CHROME_TEST_TIMEOUT_MS,
);
@@ -277,99 +304,42 @@ for (const { format, page, contentType, nodeNames, link, destinations } of [
async ({ processes }) => {
const executable = chromeExecutable;
if (executable === undefined) return;
const profile = await createTestTempDirectory("rea-svg-xhtml-");
const server = createServer((_request, response) => {
response.writeHead(200, {
"content-type": `${contentType}; charset=utf-8`,
});
response.end(page);
const { origin, pageUrl, inspection } = await inspectChromePage({
processes,
executable,
profilePrefix: "rea-svg-xhtml-",
page,
contentType,
pathname: "/page.xhtml",
});
await new Promise<void>((resolve) => {
server.listen(0, "127.0.0.1", resolve);
});
const address = server.address();
if (address === null || typeof address === "string")
throw new Error("XHTML fixture server did not bind a TCP port");
const origin = `http://127.0.0.1:${String(address.port)}`;
const pageUrl = `${origin}/page.xhtml`;
let stderr = "";
const child = processes.spawn(executable, [
"--headless=new",
...(process.env.REA_BROWSER_NO_SANDBOX === "true"
? ["--no-sandbox"]
: []),
"--remote-debugging-address=127.0.0.1",
"--remote-debugging-port=0",
`--user-data-dir=${profile}`,
"--no-first-run",
"--no-default-browser-check",
"--disable-background-networking",
"--disable-component-update",
"--disable-dev-shm-usage",
"--disable-sync",
"--metrics-recording-only",
pageUrl,
]);
child.stdout.on("data", () => undefined);
child.stderr.on("data", (chunk: Buffer) => {
if (stderr.length < 8_192) stderr += chunk.toString("utf8");
});
try {
const port = await waitForBrowserDevtoolsPort({
child,
executable,
activePortPath: join(profile, "DevToolsActivePort"),
stderr: () => stderr,
timeoutMs: CHROME_STARTUP_TIMEOUT_MS,
});
const endpoint = `http://127.0.0.1:${String(port)}`;
const provider = new CdpBrowserProvider();
const targetId = await pageTarget(provider, endpoint, origin, pageUrl);
const result = await provider.inspectPage(
inspectWebPageInputSchema.parse({
cdp_endpoint: endpoint,
allowed_origins: [origin],
target_id: targetId,
observation_ms: 0,
}),
);
if (!result.ok) throw result.error;
const urls = result.value.metadata.dom_urls;
for (const name of nodeNames) {
expect(
result.value.dom.nodes.some((node) => node.node_name === name),
).toBe(true);
}
const urls = inspection.metadata.dom_urls;
for (const name of nodeNames) {
expect(
result.value.dom.nodes.some(
(node) =>
node.node_name === link.name &&
node.attribute_names.includes(link.attribute),
),
inspection.dom.nodes.some((node) => node.node_name === name),
).toBe(true);
expect(urls).toEqual(
destinations.map((destination) =>
expect.objectContaining({
attribute: "href",
url:
destination === null
? null
: destination === ""
? pageUrl
: `${origin}${destination}`,
destination_scope:
destination === null ? "outside_policy" : "approved",
}),
),
);
} finally {
child.kill("SIGTERM");
if (!(await waitForExit(child, 2_000))) child.kill("SIGKILL");
await waitForExit(child, 2_000);
await new Promise<void>((resolve) => {
server.close(() => resolve());
});
}
expect(
inspection.dom.nodes.some(
(node) =>
node.node_name === link.name &&
node.attribute_names.includes(link.attribute),
),
).toBe(true);
expect(urls).toEqual(
destinations.map((destination) =>
expect.objectContaining({
attribute: "href",
url:
destination === null
? null
: destination === ""
? pageUrl
: `${origin}${destination}`,
destination_scope:
destination === null ? "outside_policy" : "approved",
}),
),
);
},
CHROME_TEST_TIMEOUT_MS,
);
@@ -1,51 +1,9 @@
import { expect, it } from "vitest";
import { Cli } from "incur";
import pino from "pino";
import type { ApktoolResourceAnalysisPort } from "../../../src/application/apktool/ApktoolResourceAnalysisPort.js";
import { createAnalysisExecution } from "../../../src/application/AnalysisProvider.js";
import { registerApktoolCommands } from "../../../src/cli/apktoolCommands.js";
import { presentOmittableDefaults } from "../../../src/cli/omittableDefaults.js";
import type { ApktoolRequest } from "../../../src/domain/apktool/apktoolResourceAnalysis.js";
import { ok } from "../../../src/domain/result.js";
const invoke = async (argv: readonly string[]) => {
const requests: ApktoolRequest[] = [];
const provider: ApktoolResourceAnalysisPort = {
async inspectAvailability() {
throw new Error("unexpected provider probe");
},
async close() {},
async execute(request) {
requests.push(request);
return ok(
createAnalysisExecution(
{ strings: [] },
{ id: "apktool", name: "Apktool", version: null },
),
);
},
};
const cli = Cli.create("rea", { sync: false });
registerApktoolCommands(cli, pino({ level: "silent" }), {}, provider);
presentOmittableDefaults(cli);
let stdout = "";
let exitCode = 0;
await cli.serve([...argv], {
env: {},
exit: (code) => {
exitCode = code;
},
stdout: (text) => {
stdout += text;
},
});
return { exitCode, stdout, requests };
};
import { invokeApktoolCli } from "../../support/cli/apktoolCli.js";
it("dispatches --no-strings as include_strings false", async () => {
const result = await invoke([
const result = await invokeApktoolCli([
"decode-android-resources",
"/targets/app.apk",
"--no-strings",
@@ -62,7 +20,7 @@ it("dispatches --no-strings as include_strings false", async () => {
});
it("keeps string projection enabled when the flag is omitted", async () => {
const result = await invoke([
const result = await invokeApktoolCli([
"decode-android-resources",
"/targets/app.apk",
"--json",
@@ -78,7 +36,7 @@ it("keeps string projection enabled when the flag is omitted", async () => {
});
it("documents the supported negative flag in command help", async () => {
const result = await invoke(["decode-android-resources", "--help"]);
const result = await invokeApktoolCli(["decode-android-resources", "--help"]);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("--no-strings");
+6 -48
View File
@@ -1,48 +1,6 @@
import { expect, it } from "vitest";
import { Cli } from "incur";
import pino from "pino";
import type { ApktoolResourceAnalysisPort } from "../../../src/application/apktool/ApktoolResourceAnalysisPort.js";
import { createAnalysisExecution } from "../../../src/application/AnalysisProvider.js";
import { registerApktoolCommands } from "../../../src/cli/apktoolCommands.js";
import { presentOmittableDefaults } from "../../../src/cli/omittableDefaults.js";
import type { ApktoolRequest } from "../../../src/domain/apktool/apktoolResourceAnalysis.js";
import { ok } from "../../../src/domain/result.js";
const invoke = async (argv: readonly string[]) => {
const requests: ApktoolRequest[] = [];
const provider: ApktoolResourceAnalysisPort = {
async inspectAvailability() {
throw new Error("unexpected provider probe");
},
async close() {},
async execute(request) {
requests.push(request);
return ok(
createAnalysisExecution(
{ strings: [] },
{ id: "apktool", name: "Apktool", version: null },
),
);
},
};
const cli = Cli.create("rea", { sync: false });
registerApktoolCommands(cli, pino({ level: "silent" }), {}, provider);
presentOmittableDefaults(cli);
let stdout = "";
let exitCode = 0;
await cli.serve([...argv], {
env: {},
exit: (code) => {
exitCode = code;
},
stdout: (text) => {
stdout += text;
},
});
return { exitCode, stdout, requests };
};
import { invokeApktoolCli } from "../../support/cli/apktoolCli.js";
/**
* Locales the apktool input owner admits. The owner's own rejection message
@@ -64,7 +22,7 @@ const OWNER_ADMITTED_LOCALES = [
] as const;
it("forwards the table-only string choice through the CLI", async () => {
const result = await invoke([
const result = await invokeApktoolCli([
"decode-android-resources",
"/targets/app.apk",
"--string-tables-only",
@@ -88,7 +46,7 @@ it("forwards the table-only string choice through the CLI", async () => {
it("forwards every locale the apktool input owner admits", async () => {
for (const locale of OWNER_ADMITTED_LOCALES) {
const result = await invoke([
const result = await invokeApktoolCli([
"decode-android-resources",
"/targets/app.apk",
"--locale",
@@ -107,7 +65,7 @@ it("forwards every locale the apktool input owner admits", async () => {
});
it("forwards repeated --locales to project multiple localized tables", async () => {
const result = await invoke([
const result = await invokeApktoolCli([
"decode-android-resources",
"/targets/app.apk",
"--locales",
@@ -133,7 +91,7 @@ it("forwards repeated --locales to project multiple localized tables", async ()
it.each(["not a locale", "en-r419", "es-r419"])(
"refuses the invalid locale %s before invoking Apktool",
async (locale) => {
const result = await invoke([
const result = await invokeApktoolCli([
"decode-android-resources",
"/targets/app.apk",
"--locale",
@@ -151,7 +109,7 @@ it.each(["not a locale", "en-r419", "es-r419"])(
);
it("forwards --no-strings with --locale preserving caller options", async () => {
const result = await invoke([
const result = await invokeApktoolCli([
"decode-android-resources",
"/targets/app.apk",
"--no-strings",
+122
View File
@@ -0,0 +1,122 @@
import { webPageInspectionSchema } from "../../../src/domain/browserObservationSchemas.js";
import { createWebTextArtifact } from "../../../src/domain/webContentArtifact.js";
export interface WebPageScriptFixture {
source: string;
scriptKey: string;
url: string;
origin: string;
cdpHash: string;
}
const origin = "https://app.example.test";
export const createWebPageInspection = (
scripts: readonly WebPageScriptFixture[],
) =>
webPageInspectionSchema.parse({
browser: {
product: "Fake Chrome",
protocol_version: "1.3",
revision: "1",
user_agent: "fake",
js_version: "1",
},
target: {
target_id: "page-1",
type: "page",
title: "App",
url: `${origin}/app`,
origin,
attached: false,
},
capture_window: {
started_at: "2026-07-14T00:00:00.000Z",
ended_at: "2026-07-14T00:00:01.000Z",
observation_ms: 1_000,
},
completeness: {
status: "attach_limited",
conditions: ["attach_limited"],
policy_filtered_sections: [],
attach_limited_sections: ["network_requests"],
truncated_sections: [],
unavailable_sections: [],
excluded: [],
dropped_events: {
scripts: 0,
network_requests: 0,
console_events: 0,
websocket_connections: 0,
websocket_frames: 0,
webmcp_tools: 0,
timeline_events: 0,
total: 0,
},
},
frames: [],
dom: { total_nodes: 0, nodes: [] },
accessibility: {
total_nodes: 0,
text_capture: {
status: "not_approved",
retained_bytes: 0,
excluded_fields: 0,
},
nodes: [],
},
scripts: {
total: scripts.length,
items: scripts.map(
({ source, scriptKey, url, origin: scriptOrigin, cdpHash }) => ({
script_key: scriptKey,
url,
origin: scriptOrigin,
cdp_hash: cdpHash,
length: Buffer.byteLength(source),
is_module: true,
language: "JavaScript",
source_map_url: null,
resource_reconciliation: {
status: "unmatched",
reason: "no_exact_sanitized_url",
},
source: {
included: true,
artifact: createWebTextArtifact(source, "text/javascript"),
},
}),
),
},
resources: [],
network: {
requests: [],
websocket_connections: [],
coverage_started_at: "2026-07-14T00:00:00.000Z",
prior_activity_available: false,
},
console: {
events: [],
coverage_started_at: "2026-07-14T00:00:00.000Z",
prior_activity_available: false,
},
workers: [],
metadata: {
responses: [],
dom_urls: [],
agent_hints: [],
excluded_dom_urls: 0,
headers_allowlisted: true,
},
storage: {
origin,
usage_bytes: null,
quota_bytes: null,
local_storage_keys: [],
session_storage_keys: [],
indexed_db_names: [],
cache_names: [],
values_redacted: true,
},
limitations: [],
});
+44
View File
@@ -0,0 +1,44 @@
import { Cli } from "incur";
import pino from "pino";
import type { ApktoolResourceAnalysisPort } from "../../../src/application/apktool/ApktoolResourceAnalysisPort.js";
import { createAnalysisExecution } from "../../../src/application/AnalysisProvider.js";
import { registerApktoolCommands } from "../../../src/cli/apktoolCommands.js";
import { presentOmittableDefaults } from "../../../src/cli/omittableDefaults.js";
import type { ApktoolRequest } from "../../../src/domain/apktool/apktoolResourceAnalysis.js";
import { ok } from "../../../src/domain/result.js";
export const invokeApktoolCli = async (argv: readonly string[]) => {
const requests: ApktoolRequest[] = [];
const provider: ApktoolResourceAnalysisPort = {
async inspectAvailability() {
throw new Error("unexpected provider probe");
},
async close() {},
async execute(request) {
requests.push(request);
return ok(
createAnalysisExecution(
{ strings: [] },
{ id: "apktool", name: "Apktool", version: null },
),
);
},
};
const cli = Cli.create("rea", { sync: false });
registerApktoolCommands(cli, pino({ level: "silent" }), {}, provider);
presentOmittableDefaults(cli);
let stdout = "";
let exitCode = 0;
await cli.serve([...argv], {
env: {},
exit: (code) => {
exitCode = code;
},
stdout: (text) => {
stdout += text;
},
});
return { exitCode, stdout, requests };
};