332 lines
9.6 KiB
TypeScript
332 lines
9.6 KiB
TypeScript
import { expect, test } from "../support/browser/strict-browser-test.ts";
|
|
|
|
test("selects, inspects, streams, and previews a real browser File", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto("/");
|
|
await page.evaluate(async () => {
|
|
const modulePath =
|
|
"/src/adapters/browser-files/index.ts";
|
|
const {
|
|
createBrowserFileRuntime,
|
|
browserFilePolicyReference,
|
|
} = await import(/* @vite-ignore */ modulePath);
|
|
const input = document.createElement("input");
|
|
input.id = "capability-file-input";
|
|
input.type = "file";
|
|
const label = document.createElement("label");
|
|
label.htmlFor = input.id;
|
|
label.textContent = "PNG file";
|
|
const button = document.createElement("button");
|
|
button.type = "button";
|
|
button.textContent = "Choose test file";
|
|
document.body.append(label, input, button);
|
|
|
|
const filePolicy = browserFilePolicyReference(
|
|
"avatar",
|
|
"select-inspect-preview-png",
|
|
);
|
|
const runtime = createBrowserFileRuntime({
|
|
input,
|
|
policies: [
|
|
{
|
|
reference: filePolicy,
|
|
selection: {
|
|
policyId: "avatar-v1",
|
|
purpose: "avatar",
|
|
classification: "PERSONAL",
|
|
multiple: false,
|
|
maxCount: 1,
|
|
maxFileBytes: 1024,
|
|
maxTotalBytes: 1024,
|
|
allowEmpty: false,
|
|
accept: [
|
|
{
|
|
mediaType: "image/png",
|
|
extensions: [".png"],
|
|
},
|
|
],
|
|
},
|
|
inspection: {
|
|
policyId: "png-signature-v1",
|
|
maxInspectionBytes: 16,
|
|
acceptedSignatures: [
|
|
{
|
|
mediaType: "image/png",
|
|
extensions: [".png"],
|
|
patterns: [
|
|
{
|
|
offset: 0,
|
|
bytes: [137, 80, 78, 71, 13, 10, 26, 10],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
preview: {
|
|
allowedMediaTypes: ["image/png"],
|
|
maxPreviewBytes: 1024,
|
|
},
|
|
},
|
|
],
|
|
limits: {
|
|
hardMaxPreviewBytes: 1024,
|
|
hardMaxObjectUrlBytes: 1024,
|
|
hardMaxTransferBytes: 4096,
|
|
},
|
|
download: {
|
|
host: Object.freeze({ handoff() {} }),
|
|
browserManagedCapabilities: {
|
|
resolve() {
|
|
return {
|
|
ok: false as const,
|
|
error: {
|
|
code: "POLICY_REJECTED" as const,
|
|
operation: "DOWNLOAD" as const,
|
|
retryable: false,
|
|
recovery: "NONE" as const,
|
|
},
|
|
};
|
|
},
|
|
},
|
|
},
|
|
});
|
|
const state = globalThis as typeof globalThis & {
|
|
__browserFileResult?: Promise<unknown>;
|
|
};
|
|
button.addEventListener("click", () => {
|
|
state.__browserFileResult = (async () => {
|
|
const selected = await runtime.baselinePicker.select({
|
|
policy: filePolicy,
|
|
});
|
|
if (
|
|
!selected.ok ||
|
|
selected.value.kind !== "SELECTED" ||
|
|
selected.value.files.length !== 1
|
|
) {
|
|
runtime.dispose();
|
|
return selected;
|
|
}
|
|
const candidate = selected.value.files[0]!;
|
|
const signal = new AbortController().signal;
|
|
const inspected = await runtime.content.inspect({
|
|
ref: candidate.ref,
|
|
policy: filePolicy,
|
|
signal,
|
|
});
|
|
const range = await runtime.content.readRange({
|
|
ref: candidate.ref,
|
|
offset: 1,
|
|
length: 4,
|
|
signal,
|
|
});
|
|
const opened = await runtime.content.openSource({
|
|
ref: candidate.ref,
|
|
signal,
|
|
});
|
|
const streamed: number[] = [];
|
|
if (opened.ok) {
|
|
for await (const chunk of opened.value.stream(signal)) {
|
|
if (chunk.ok) streamed.push(...chunk.value);
|
|
}
|
|
}
|
|
const verificationReceipt = inspected.ok
|
|
? inspected.value.verificationReceipt
|
|
: null;
|
|
if (!verificationReceipt) {
|
|
runtime.dispose();
|
|
return inspected;
|
|
}
|
|
const preview = await runtime.previews.create({
|
|
ref: candidate.ref,
|
|
verificationReceipt,
|
|
policy: filePolicy,
|
|
maxPreviewBytes: 1024,
|
|
signal,
|
|
});
|
|
let previewBytes: number[] = [];
|
|
let previewProtocol: string | null = null;
|
|
if (preview.ok) {
|
|
previewProtocol = new URL(preview.value.url).protocol;
|
|
previewBytes = Array.from(
|
|
new Uint8Array(await (await fetch(preview.value.url)).arrayBuffer()),
|
|
);
|
|
preview.value.release();
|
|
}
|
|
runtime.baselinePicker.release(candidate.ref);
|
|
runtime.dispose();
|
|
return {
|
|
selected,
|
|
inspected,
|
|
range: range.ok ? Array.from(range.value) : range,
|
|
streamed,
|
|
preview: preview.ok ? { ok: true } : preview,
|
|
previewBytes,
|
|
previewProtocol,
|
|
};
|
|
})();
|
|
});
|
|
});
|
|
|
|
const chooserPromise = page.waitForEvent("filechooser");
|
|
await page.getByRole("button", { name: "Choose test file" }).click();
|
|
const chooser = await chooserPromise;
|
|
const bytes = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10, 1, 2, 3, 4]);
|
|
await chooser.setFiles({
|
|
name: "sample.png",
|
|
mimeType: "image/png",
|
|
buffer: bytes,
|
|
});
|
|
|
|
const result = await page.evaluate(async () => {
|
|
const state = globalThis as typeof globalThis & {
|
|
__browserFileResult?: Promise<unknown>;
|
|
};
|
|
return await state.__browserFileResult;
|
|
});
|
|
expect(result).toMatchObject({
|
|
selected: {
|
|
ok: true,
|
|
value: {
|
|
kind: "SELECTED",
|
|
files: [{ sizeBytes: bytes.byteLength, source: "NATIVE_INPUT" }],
|
|
},
|
|
},
|
|
inspected: {
|
|
ok: true,
|
|
value: {
|
|
byteLength: bytes.byteLength,
|
|
detectedMediaType: "image/png",
|
|
signature: "MATCHED",
|
|
},
|
|
},
|
|
range: [80, 78, 71, 13],
|
|
streamed: Array.from(bytes),
|
|
preview: { ok: true },
|
|
previewBytes: Array.from(bytes),
|
|
previewProtocol: "blob:",
|
|
});
|
|
});
|
|
|
|
test("hands a bounded generated Blob to the real browser download path", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto("/");
|
|
await page.evaluate(async () => {
|
|
const modulePath = "/src/adapters/browser-files/index.ts";
|
|
const {
|
|
createBrowserFileRuntime,
|
|
createAnchorDownloadHost,
|
|
browserFilePolicyReference,
|
|
} = await import(/* @vite-ignore */ modulePath);
|
|
const input = document.createElement("input");
|
|
input.type = "file";
|
|
input.id = "unused-download-input";
|
|
const label = document.createElement("label");
|
|
label.htmlFor = input.id;
|
|
label.textContent = "Unused file control";
|
|
const button = document.createElement("button");
|
|
button.type = "button";
|
|
button.textContent = "Download generated file";
|
|
document.body.append(label, input, button);
|
|
const downloadPolicy = browserFilePolicyReference(
|
|
"generated-export",
|
|
"bounded-object-url-bin",
|
|
);
|
|
const runtime = createBrowserFileRuntime({
|
|
input,
|
|
policies: [
|
|
{
|
|
reference: downloadPolicy,
|
|
download: {
|
|
strategy: "BOUNDED_OBJECT_URL",
|
|
mediaType: "application/octet-stream",
|
|
safeExtension: ".bin",
|
|
maxTransferBytes: 1024,
|
|
maxBufferedBytes: 1024,
|
|
integrity: "OPTIONAL",
|
|
},
|
|
},
|
|
],
|
|
limits: {
|
|
hardMaxPreviewBytes: 1024,
|
|
hardMaxObjectUrlBytes: 1024,
|
|
hardMaxTransferBytes: 4096,
|
|
},
|
|
download: {
|
|
host: createAnchorDownloadHost(document),
|
|
browserManagedCapabilities: {
|
|
resolve() {
|
|
return {
|
|
ok: false as const,
|
|
error: {
|
|
code: "POLICY_REJECTED" as const,
|
|
operation: "DOWNLOAD" as const,
|
|
retryable: false,
|
|
recovery: "NONE" as const,
|
|
},
|
|
};
|
|
},
|
|
},
|
|
},
|
|
});
|
|
const state = globalThis as typeof globalThis & {
|
|
__browserDownloadResult?: Promise<unknown>;
|
|
};
|
|
button.addEventListener("click", () => {
|
|
state.__browserDownloadResult = runtime.downloads
|
|
.deliver({
|
|
policy: downloadPolicy,
|
|
source: {
|
|
kind: "GENERATED",
|
|
bytes: {
|
|
byteLength: 5,
|
|
async *stream() {
|
|
yield {
|
|
ok: true as const,
|
|
value: new Uint8Array([1, 2]),
|
|
};
|
|
yield {
|
|
ok: true as const,
|
|
value: new Uint8Array([3, 4, 5]),
|
|
};
|
|
},
|
|
},
|
|
},
|
|
suggestedFileName: "report",
|
|
maxTransferBytes: 1024,
|
|
maxBufferedBytes: 1024,
|
|
signal: new AbortController().signal,
|
|
onProgress() {},
|
|
})
|
|
.finally(() => runtime.dispose());
|
|
});
|
|
});
|
|
|
|
const downloadPromise = page.waitForEvent("download");
|
|
await page
|
|
.getByRole("button", { name: "Download generated file" })
|
|
.click();
|
|
const download = await downloadPromise;
|
|
const stream = await download.createReadStream();
|
|
const chunks: Buffer[] = [];
|
|
for await (const chunk of stream) {
|
|
chunks.push(Buffer.from(chunk));
|
|
}
|
|
const result = await page.evaluate(async () => {
|
|
const state = globalThis as typeof globalThis & {
|
|
__browserDownloadResult?: Promise<unknown>;
|
|
};
|
|
return await state.__browserDownloadResult;
|
|
});
|
|
|
|
expect(download.suggestedFilename()).toBe("report.bin");
|
|
expect(Buffer.concat(chunks)).toEqual(Buffer.from([1, 2, 3, 4, 5]));
|
|
expect(result).toMatchObject({
|
|
ok: true,
|
|
value: {
|
|
kind: "BROWSER_HANDOFF",
|
|
},
|
|
});
|
|
});
|