Release Device Tools v0.1.0
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
for (const path of ["/", "/deep/nested/device/"]) {
|
||||
test(`inspects capabilities locally at ${path}`, async ({ page }) => {
|
||||
const external: string[] = [];
|
||||
page.on("request", (request) => {
|
||||
if (!request.url().startsWith("http://127.0.0.1:4213"))
|
||||
external.push(request.url());
|
||||
});
|
||||
await page.goto(path);
|
||||
await expect(
|
||||
page.getByRole("heading", {
|
||||
name: "Inspect capabilities, not identity.",
|
||||
}),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Browser and display capabilities" }),
|
||||
).toBeVisible();
|
||||
await page.getByRole("button", { name: "Read permission states" }).click();
|
||||
await expect(page.locator(".probe-result").first()).toBeVisible();
|
||||
await expect(page.getByLabel("Redacted JSON report")).not.toContainText(
|
||||
"userAgent",
|
||||
);
|
||||
expect(external).toEqual([]);
|
||||
});
|
||||
}
|
||||
|
||||
test("keeps the installed capability lab available offline", async ({
|
||||
page,
|
||||
context,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
await page.evaluate(async () => navigator.serviceWorker.ready);
|
||||
await page.reload();
|
||||
await context.setOffline(true);
|
||||
try {
|
||||
await page.reload();
|
||||
await expect(
|
||||
page.getByRole("heading", {
|
||||
name: "Inspect capabilities, not identity.",
|
||||
}),
|
||||
).toBeVisible();
|
||||
} finally {
|
||||
await context.setOffline(false);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import { Workbench } from "../../src/components/Workbench";
|
||||
|
||||
beforeAll(() => {
|
||||
Object.defineProperty(HTMLMediaElement.prototype, "canPlayType", {
|
||||
configurable: true,
|
||||
value: vi.fn(() => "maybe"),
|
||||
});
|
||||
});
|
||||
|
||||
describe("Device Workbench", () => {
|
||||
it("shows passive evidence and a redacted report", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Workbench />);
|
||||
expect(
|
||||
screen.getByRole("heading", {
|
||||
name: "Inspect capabilities, not identity.",
|
||||
}),
|
||||
).toBeVisible();
|
||||
expect(screen.getByLabelText("Redacted JSON report")).not.toHaveTextContent(
|
||||
"userAgent",
|
||||
);
|
||||
await user.type(screen.getByLabelText("Filter capabilities"), "codec");
|
||||
expect(screen.getByRole("heading", { name: "Codecs" })).toBeVisible();
|
||||
expect(
|
||||
screen.queryByRole("heading", { name: "Display" }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("runs only the explicitly selected probe", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Workbench />);
|
||||
await user.click(
|
||||
screen.getByRole("button", { name: "Read permission states" }),
|
||||
);
|
||||
expect(await screen.findByRole("status")).toHaveTextContent(
|
||||
/Permissions API is unavailable|Permission states read/u,
|
||||
);
|
||||
expect(screen.getByText("1 run")).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,182 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
collectPassiveCapabilities,
|
||||
createRedactedReport,
|
||||
exportReportCsv,
|
||||
exportReportJson,
|
||||
runProbe,
|
||||
type Capability,
|
||||
type DeviceEnvironment,
|
||||
type ProbeResult,
|
||||
} from "../../src/core/device";
|
||||
|
||||
function environment(
|
||||
navigatorOverrides: Record<string, unknown> = {},
|
||||
): DeviceEnvironment {
|
||||
const fakeWindow = {
|
||||
innerWidth: 1234,
|
||||
innerHeight: 777,
|
||||
devicePixelRatio: 2.25,
|
||||
top: null,
|
||||
visualViewport: { width: 1200, height: 700 },
|
||||
matchMedia: (query: string) => ({ matches: query.includes("fine") }),
|
||||
WebGLRenderingContext: function WebGLRenderingContext() {},
|
||||
indexedDB: {},
|
||||
caches: {},
|
||||
} as unknown as Window;
|
||||
Object.defineProperty(fakeWindow, "top", { value: fakeWindow });
|
||||
const fakeDocument = {
|
||||
fullscreenEnabled: true,
|
||||
pictureInPictureEnabled: true,
|
||||
createElement: (name: string) =>
|
||||
name === "video" || name === "audio"
|
||||
? {
|
||||
canPlayType: (mime: string) =>
|
||||
mime.includes("opus") ? "probably" : "",
|
||||
}
|
||||
: {},
|
||||
} as unknown as Document;
|
||||
return {
|
||||
window: fakeWindow,
|
||||
document: fakeDocument,
|
||||
navigator: {
|
||||
onLine: true,
|
||||
maxTouchPoints: 5,
|
||||
mediaDevices: {
|
||||
getSupportedConstraints: () => ({
|
||||
width: true,
|
||||
echoCancellation: true,
|
||||
}),
|
||||
},
|
||||
...navigatorOverrides,
|
||||
} as unknown as DeviceEnvironment["navigator"],
|
||||
screen: {
|
||||
width: 2560,
|
||||
height: 1440,
|
||||
availWidth: 2500,
|
||||
availHeight: 1400,
|
||||
colorDepth: 24,
|
||||
orientation: { type: "landscape-primary" },
|
||||
} as Screen,
|
||||
secureContext: true,
|
||||
isolated: false,
|
||||
};
|
||||
}
|
||||
|
||||
describe("passive device inventory", () => {
|
||||
it("reports display, input, constraints, codecs and API presence without probing", () => {
|
||||
const capabilities = collectPassiveCapabilities(environment());
|
||||
expect(capabilities.find((item) => item.id === "viewport")?.value).toBe(
|
||||
"1234 × 777 CSS px",
|
||||
);
|
||||
expect(capabilities.find((item) => item.id === "touch")?.state).toBe(
|
||||
"available",
|
||||
);
|
||||
expect(
|
||||
capabilities.find((item) => item.id === "media-constraints")?.value,
|
||||
).toContain("echoCancellation");
|
||||
expect(capabilities.find((item) => item.id === "codec-opus")?.value).toBe(
|
||||
"Probably",
|
||||
);
|
||||
expect(capabilities.find((item) => item.id === "camera-api")?.state).toBe(
|
||||
"unavailable",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("explicit probes", () => {
|
||||
it("reads storage locally and preserves only buckets in a report", async () => {
|
||||
const result = await runProbe(
|
||||
"storage",
|
||||
environment({
|
||||
storage: {
|
||||
estimate: async () => ({ usage: 500_000_000, quota: 2_000_000_000 }),
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(result.state).toBe("complete");
|
||||
expect(result.details[0]?.value).toContain("MiB");
|
||||
const report = createRedactedReport([], [result]);
|
||||
const serialized = exportReportJson(report);
|
||||
expect(serialized).not.toContain("476.84");
|
||||
expect(serialized).toContain("25-to-49%");
|
||||
});
|
||||
|
||||
it("stops every user-media track immediately", async () => {
|
||||
const stop = vi.fn();
|
||||
const result = await runProbe(
|
||||
"camera",
|
||||
environment({
|
||||
mediaDevices: {
|
||||
getUserMedia: async () => ({ getTracks: () => [{ stop }, { stop }] }),
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(result.state).toBe("complete");
|
||||
expect(stop).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("queries permission state without treating unsupported names as failure", async () => {
|
||||
const query = vi.fn(async ({ name }: { name: string }) => {
|
||||
if (name === "camera") throw new TypeError("not supported");
|
||||
return { state: "prompt" };
|
||||
});
|
||||
const result = await runProbe(
|
||||
"permissions",
|
||||
environment({ permissions: { query } }),
|
||||
);
|
||||
expect(result.state).toBe("complete");
|
||||
expect(result.details.find((item) => item.label === "camera")?.value).toBe(
|
||||
"unsupported",
|
||||
);
|
||||
expect(query).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("redacted exports", () => {
|
||||
it("never emits exact high-entropy capability values", () => {
|
||||
const capabilities: Capability[] = [
|
||||
{
|
||||
id: "viewport",
|
||||
group: "Display",
|
||||
label: "Viewport",
|
||||
state: "available",
|
||||
value: "1234 × 777 CSS px",
|
||||
privacy: "high",
|
||||
explanation: "",
|
||||
},
|
||||
{
|
||||
id: "dpr",
|
||||
group: "Display",
|
||||
label: "DPR",
|
||||
state: "available",
|
||||
value: "2.25",
|
||||
privacy: "high",
|
||||
explanation: "",
|
||||
},
|
||||
{
|
||||
id: "touch",
|
||||
group: "Input",
|
||||
label: "Touch",
|
||||
state: "available",
|
||||
value: "5 maximum point(s)",
|
||||
privacy: "medium",
|
||||
explanation: "",
|
||||
},
|
||||
];
|
||||
const probe: ProbeResult = {
|
||||
id: "media-devices",
|
||||
state: "complete",
|
||||
summary: "done",
|
||||
details: [{ label: "videoinput", value: "7", redacted: "many" }],
|
||||
};
|
||||
const report = createRedactedReport(capabilities, [probe]);
|
||||
const json = exportReportJson(report);
|
||||
expect(json).not.toContain("1234");
|
||||
expect(json).not.toContain("2.25");
|
||||
expect(json).toContain("medium");
|
||||
expect(json).toContain("multi-touch");
|
||||
expect(exportReportCsv(report)).toContain("capability,viewport");
|
||||
expect(report.policy.join(" ")).toContain("No stable identifier");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user