mirror of
https://github.com/morluto/rea
synced 2026-10-27 22:45:19 -04:00
refactor(test): share browser and apktool fixture lifecycles
This commit is contained in:
@@ -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)}`,
|
||||
})),
|
||||
);
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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" }),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: [],
|
||||
});
|
||||
@@ -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 };
|
||||
};
|
||||
Reference in New Issue
Block a user