Files
clean-architecture-frontend…/tests/unit/browser-file-download.test.ts
T
DongHyeonkaandClaude Opus 5 ba79060a83 fix: execute canonical browser download targets
Replace the boolean browser-managed target validator with
resolveBrowserManagedTarget, which returns the parsed canonical absolute URL,
and hand that exact value to the host. Previously the raw href was passed on,
so a relative target was re-resolved against document.baseURI and a hostile
<base> could send the navigation to an origin the policy never approved.

STO-08 stays UNVERIFIED: the capability spec does not exercise the system
picker, so the receiver-binding hypothesis is neither reproduced nor refuted
and no source change was made for it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 23:23:46 +09:00

947 lines
26 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import type {
BrowserManagedDownloadCapabilityReceipt,
BrowserManagedDownloadCapabilityResolver,
DownloadDeliveryPort,
DownloadSource,
DownloadStrategy,
FileByteSource,
} from "../../src/application/ports/browser-file-storage/file.ts";
import {
DEFAULT_OBJECT_URL_RELEASE_GRACE_MS,
createDownloadDeliveryAdapter,
type SaveFileHandle,
} from "../../src/adapters/browser-files/download-delivery-adapter.ts";
import { ObjectUrlLeaseRegistry } from "../../src/adapters/browser-files/object-url-lease.ts";
import {
BrowserFilePolicyRegistry,
browserFilePolicyReference,
} from "../../src/adapters/browser-files/browser-file-policy-registry.ts";
const HARD_LIMITS = Object.freeze({
hardMaxObjectUrlBytes: 64,
hardMaxTransferBytes: 128,
});
const capabilityReceipt =
"capability:download-1" as BrowserManagedDownloadCapabilityReceipt;
const policyByStrategy = Object.freeze({
BROWSER_MANAGED: browserFilePolicyReference(
"download",
"browser-managed-pdf",
),
PROMPT_AND_STREAM: browserFilePolicyReference(
"download",
"prompt-and-stream-pdf",
),
BOUNDED_OBJECT_URL: browserFilePolicyReference(
"download",
"bounded-object-url-pdf",
),
});
const policies = new BrowserFilePolicyRegistry({
profiles: (
Object.entries(policyByStrategy) as Array<
[DownloadStrategy, (typeof policyByStrategy)[DownloadStrategy]]
>
).map(([strategy, reference]) => ({
reference,
download: {
strategy,
mediaType: "application/pdf",
safeExtension: ".pdf",
maxTransferBytes: 32,
maxBufferedBytes: 16,
integrity: "OPTIONAL" as const,
},
})),
hardLimits: {
maxInspectionBytes: 64,
maxRetainedFileBytes: 128,
maxPreviewBytes: 64,
maxObjectUrlBytes: 64,
maxTransferBytes: 128,
},
});
function capabilityResolver(
href: (resourceId: string) => string = () => "/unused",
overrides: Readonly<
Partial<{
capabilityReceipt: BrowserManagedDownloadCapabilityReceipt;
resourceId: string;
mediaType: string;
safeExtension: string;
maxBytes: number;
expectedSha256: string;
expiresAtEpochMs: number;
}>
> = {},
): BrowserManagedDownloadCapabilityResolver {
return {
resolve(input) {
return {
ok: true,
value: {
capabilityReceipt:
overrides.capabilityReceipt ??
input.capabilityReceipt,
href: href(input.resourceId),
resourceId:
overrides.resourceId ?? input.resourceId,
mediaType:
overrides.mediaType ?? "application/pdf",
safeExtension: overrides.safeExtension ?? ".pdf",
maxBytes: overrides.maxBytes ?? 32,
...(overrides.expectedSha256
? { expectedSha256: overrides.expectedSha256 }
: {}),
expiresAtEpochMs:
overrides.expiresAtEpochMs ??
Number.MAX_SAFE_INTEGER,
},
};
},
};
}
function successfulChunk(bytes: Uint8Array) {
return Object.freeze({ ok: true as const, value: bytes });
}
function byteSource(
chunks: readonly Uint8Array[],
byteLength: number | null = chunks.reduce(
(total, chunk) => total + chunk.byteLength,
0,
),
afterChunk?: (index: number) => void,
): FileByteSource {
return Object.freeze({
byteLength,
async *stream(
signal: AbortSignal,
): AsyncIterable<ReturnType<typeof successfulChunk>> {
for (const [index, chunk] of chunks.entries()) {
if (signal.aborted) {
throw new DOMException("aborted", "AbortError");
}
yield successfulChunk(chunk);
afterChunk?.(index);
}
},
});
}
function deliveryInput(
source: DownloadSource,
strategy: DownloadStrategy,
signal = new AbortController().signal,
) {
return {
policy: policyByStrategy[strategy],
source,
suggestedFileName: "../../invoice.exe.pdf",
maxTransferBytes: 32,
maxBufferedBytes: 16,
signal,
onProgress: vi.fn(),
} satisfies Parameters<DownloadDeliveryPort["deliver"]>[0];
}
function writableHandle(events: string[]): SaveFileHandle {
return Object.freeze({
async createWritable() {
return new WritableStream<Uint8Array>({
write(chunk) {
events.push(`write:${chunk.byteLength}`);
},
close() {
events.push("close");
},
abort() {
events.push("abort");
},
});
},
});
}
describe("browser download delivery", () => {
it("captures composition dependencies instead of re-reading mutable option objects", async () => {
const originalHandoff = vi.fn();
const replacedHandoff = vi.fn();
const originalResolve = vi.fn(
capabilityResolver(
() => "/downloads/composition-bound",
).resolve,
);
const replacedResolve = vi.fn(
capabilityResolver(
() => "https://evil.example/replaced",
).resolve,
);
const host = { handoff: originalHandoff };
const browserManagedCapabilities = {
resolve: originalResolve,
};
const adapter = createDownloadDeliveryAdapter({
...HARD_LIMITS,
policies,
host,
browserManagedCapabilities,
baseOrigin: "https://app.example",
createTransferId: () => "transfer:composition",
userActivation: { isActive: true },
});
host.handoff = replacedHandoff;
browserManagedCapabilities.resolve = replacedResolve;
expect(
await adapter.deliver(
deliveryInput(
{
kind: "BROWSER_MANAGED_RESOURCE",
resourceId: "artifact-1",
capabilityReceipt,
},
"BROWSER_MANAGED",
),
),
).toMatchObject({
ok: true,
value: { kind: "BROWSER_HANDOFF" },
});
expect(originalResolve).toHaveBeenCalledOnce();
expect(replacedResolve).not.toHaveBeenCalled();
expect(originalHandoff).toHaveBeenCalledOnce();
expect(replacedHandoff).not.toHaveBeenCalled();
});
it("sanitizes browser-managed handoff and reports only handoff truth", async () => {
const handoff = vi.fn();
const adapter = createDownloadDeliveryAdapter({
...HARD_LIMITS,
policies,
host: { handoff },
browserManagedCapabilities: capabilityResolver(
(resourceId) => `/downloads/${resourceId}`,
),
baseOrigin: "https://app.example",
createTransferId: () => "transfer:1",
userActivation: { isActive: true },
});
expect(
await adapter.deliver(
deliveryInput(
{
kind: "BROWSER_MANAGED_RESOURCE",
resourceId: "artifact-1",
capabilityReceipt,
},
"BROWSER_MANAGED",
),
),
).toEqual({
ok: true,
value: { kind: "BROWSER_HANDOFF", transferId: "transfer:1" },
});
// STO-02. Parse once, then execute exactly what was validated.
expect(handoff).toHaveBeenCalledWith(
"https://app.example/downloads/artifact-1",
"invoice_exe.pdf",
);
});
it("executes the canonical target instead of a document-base-relative href", async () => {
const handoff = vi.fn();
const adapter = createDownloadDeliveryAdapter({
...HARD_LIMITS,
policies,
host: { handoff },
// A relative href the host would otherwise resolve against a hostile
// document <base href="https://evil.example/">.
browserManagedCapabilities: capabilityResolver(() => "downloads/a"),
baseOrigin: "https://app.example",
createTransferId: () => "transfer:1",
userActivation: { isActive: true },
});
expect(
await adapter.deliver(
deliveryInput(
{
kind: "BROWSER_MANAGED_RESOURCE",
resourceId: "artifact-1",
capabilityReceipt,
},
"BROWSER_MANAGED",
),
),
).toMatchObject({ ok: true });
expect(handoff).toHaveBeenCalledWith(
"https://app.example/downloads/a",
"invoice_exe.pdf",
);
for (const [href] of handoff.mock.calls) {
expect(String(href).startsWith("https://app.example/")).toBe(true);
expect(String(href)).not.toContain("evil.example");
}
});
it("rejects cross-origin or query-bearing browser-managed targets", async () => {
const handoff = vi.fn();
const adapter = createDownloadDeliveryAdapter({
...HARD_LIMITS,
policies,
host: { handoff },
browserManagedCapabilities: capabilityResolver(
() => "https://evil.example/a?token=secret",
),
baseOrigin: "https://app.example",
userActivation: { isActive: true },
});
expect(
await adapter.deliver(
deliveryInput(
{
kind: "BROWSER_MANAGED_RESOURCE",
resourceId: "artifact-1",
capabilityReceipt,
},
"BROWSER_MANAGED",
),
),
).toMatchObject({
ok: false,
error: { code: "POLICY_REJECTED" },
});
expect(handoff).not.toHaveBeenCalled();
});
it("requires an exact, unexpired, server-bound synchronous capability receipt", async () => {
const source = {
kind: "BROWSER_MANAGED_RESOURCE" as const,
resourceId: "artifact-1",
capabilityReceipt,
};
const cases = [
{
resolver: capabilityResolver(() => "/unused", {
capabilityReceipt:
"capability:other" as BrowserManagedDownloadCapabilityReceipt,
}),
expectedCode: "POLICY_REJECTED",
},
{
resolver: capabilityResolver(undefined, {
resourceId: "artifact-2",
}),
expectedCode: "POLICY_REJECTED",
},
{
resolver: capabilityResolver(undefined, {
mediaType: "text/plain",
}),
expectedCode: "POLICY_REJECTED",
},
{
resolver: capabilityResolver(undefined, {
expiresAtEpochMs: 99,
}),
expectedCode: "EXPIRED_RESOURCE",
},
] as const;
for (const testCase of cases) {
const handoff = vi.fn();
const adapter = createDownloadDeliveryAdapter({
...HARD_LIMITS,
policies,
host: { handoff },
browserManagedCapabilities: testCase.resolver,
now: () => 100,
baseOrigin: "https://app.example",
userActivation: { isActive: true },
});
expect(
await adapter.deliver(
deliveryInput(source, "BROWSER_MANAGED"),
),
).toMatchObject({
ok: false,
error: { code: testCase.expectedCode },
});
expect(handoff).not.toHaveBeenCalled();
}
const missingReceipt = {
kind: "BROWSER_MANAGED_RESOURCE",
resourceId: "artifact-1",
} as unknown as DownloadSource;
const adapter = createDownloadDeliveryAdapter({
...HARD_LIMITS,
policies,
host: { handoff: vi.fn() },
browserManagedCapabilities: capabilityResolver(),
userActivation: { isActive: true },
});
expect(
await adapter.deliver(
deliveryInput(missingReceipt, "BROWSER_MANAGED"),
),
).toMatchObject({
ok: false,
error: { code: "INVALID_INPUT" },
});
});
it("streams with backpressure and succeeds only after close", async () => {
const events: string[] = [];
const controller = new AbortController();
const handle = writableHandle(events);
const adapter = createDownloadDeliveryAdapter({
...HARD_LIMITS,
policies,
host: { handoff: vi.fn() },
browserManagedCapabilities: capabilityResolver(),
showSaveFilePicker: async () => handle,
createTransferId: () => "transfer:stream",
progressMinIntervalMs: 0,
userActivation: { isActive: true },
});
const input = deliveryInput(
{
kind: "GENERATED",
bytes: byteSource([
new Uint8Array([1, 2]),
new Uint8Array([3]),
]),
},
"PROMPT_AND_STREAM",
controller.signal,
);
expect(await adapter.deliver(input)).toEqual({
ok: true,
value: {
kind: "SAVED",
transferId: "transfer:stream",
bytesWritten: 3,
integrity: "NOT_PROVIDED",
},
});
expect(events).toEqual(["write:2", "write:1", "close"]);
expect(input.onProgress.mock.calls.map(([progress]) => progress.phase)).toEqual(
["PREPARING", "TRANSFERRING", "TRANSFERRING", "VERIFYING", "FINALIZING"],
);
});
it("snapshots the generated source before awaiting the save picker", async () => {
const events: string[] = [];
let resolvePicker:
| ((handle: SaveFileHandle) => void)
| undefined;
const originalStream = vi.fn(
async function* () {
yield successfulChunk(new Uint8Array([1, 2]));
},
);
const replacedStream = vi.fn(
async function* () {
yield successfulChunk(new Uint8Array([9]));
},
);
const bytes = {
byteLength: 2,
stream: originalStream,
};
const source = {
kind: "GENERATED" as const,
bytes,
};
const request = deliveryInput(
source,
"PROMPT_AND_STREAM",
);
const adapter = createDownloadDeliveryAdapter({
...HARD_LIMITS,
policies,
host: { handoff: vi.fn() },
browserManagedCapabilities: capabilityResolver(),
showSaveFilePicker: () =>
new Promise((resolve) => {
resolvePicker = resolve;
}),
createTransferId: () => "transfer:snapshot",
userActivation: { isActive: true },
});
const pending = adapter.deliver(request);
bytes.byteLength = 1;
bytes.stream = replacedStream;
(
request as { source: DownloadSource }
).source = {
kind: "BROWSER_MANAGED_RESOURCE",
resourceId: "replaced",
capabilityReceipt,
};
resolvePicker?.(writableHandle(events));
expect(await pending).toMatchObject({
ok: true,
value: { kind: "SAVED", bytesWritten: 2 },
});
expect(originalStream).toHaveBeenCalledOnce();
expect(replacedStream).not.toHaveBeenCalled();
expect(events).toEqual(["write:2", "close"]);
});
it("does not turn a completed close into a late abort failure", async () => {
const controller = new AbortController();
const handle: SaveFileHandle = {
async createWritable() {
return new WritableStream<Uint8Array>({
close() {
controller.abort();
},
});
},
};
const adapter = createDownloadDeliveryAdapter({
...HARD_LIMITS,
policies,
host: { handoff: vi.fn() },
browserManagedCapabilities: capabilityResolver(),
showSaveFilePicker: async () => handle,
createTransferId: () => "transfer:committed",
userActivation: { isActive: true },
});
expect(
await adapter.deliver(
deliveryInput(
{
kind: "GENERATED",
bytes: byteSource([new Uint8Array([1])]),
},
"PROMPT_AND_STREAM",
controller.signal,
),
),
).toMatchObject({
ok: true,
value: { kind: "SAVED", transferId: "transfer:committed" },
});
});
it("aborts the writable before close on integrity failure", async () => {
const events: string[] = [];
const adapter = createDownloadDeliveryAdapter({
...HARD_LIMITS,
policies,
host: { handoff: vi.fn() },
browserManagedCapabilities: capabilityResolver(),
showSaveFilePicker: async () => writableHandle(events),
createIntegrityVerifier: () => ({
update: () => {
events.push("hash");
},
verify: () => false,
}),
userActivation: { isActive: true },
});
const input = {
...deliveryInput(
{
kind: "GENERATED" as const,
bytes: byteSource([new Uint8Array([1, 2])]),
expectedSha256: "a".repeat(64),
},
"PROMPT_AND_STREAM",
),
};
expect(await adapter.deliver(input)).toMatchObject({
ok: false,
error: { code: "INTEGRITY_FAILED" },
});
expect(events).toEqual(["hash", "write:2", "abort"]);
});
it("distinguishes save-picker dismissal from active cancellation", async () => {
const dismissed = createDownloadDeliveryAdapter({
...HARD_LIMITS,
policies,
host: { handoff: vi.fn() },
browserManagedCapabilities: capabilityResolver(),
showSaveFilePicker: async () => {
throw new DOMException("dismissed", "AbortError");
},
userActivation: { isActive: true },
});
expect(
await dismissed.deliver(
deliveryInput(
{
kind: "GENERATED",
bytes: byteSource([new Uint8Array([1])]),
},
"PROMPT_AND_STREAM",
),
),
).toEqual({
ok: true,
value: { kind: "DISMISSED" },
});
const controller = new AbortController();
let rejectPicker:
| ((reason: DOMException) => void)
| undefined;
const aborted = createDownloadDeliveryAdapter({
...HARD_LIMITS,
policies,
host: { handoff: vi.fn() },
browserManagedCapabilities: capabilityResolver(),
showSaveFilePicker: () =>
new Promise((_, reject: (reason: DOMException) => void) => {
rejectPicker = reject;
}),
userActivation: { isActive: true },
});
const pending = aborted.deliver(
deliveryInput(
{
kind: "GENERATED",
bytes: byteSource([new Uint8Array([1])]),
},
"PROMPT_AND_STREAM",
controller.signal,
),
);
controller.abort();
rejectPicker?.(new DOMException("closed", "AbortError"));
expect(await pending).toMatchObject({
ok: false,
error: { code: "ABORTED" },
});
});
it("bounds Blob buffering and revokes the lease after handoff", async () => {
const revoked: string[] = [];
const leases = new ObjectUrlLeaseRegistry({
createObjectURL: () => "blob:download-1",
revokeObjectURL: (url) => revoked.push(url),
});
let scheduled: (() => void) | undefined;
let scheduledDelay: number | undefined;
const handoff = vi.fn();
const adapter = createDownloadDeliveryAdapter({
...HARD_LIMITS,
policies,
host: { handoff },
browserManagedCapabilities: capabilityResolver(),
objectUrls: leases,
scheduler: {
setTimeout(callback, delayMs) {
scheduled = callback;
scheduledDelay = delayMs;
return 1;
},
},
createTransferId: () => "transfer:blob",
userActivation: { isActive: true },
});
expect(
await adapter.deliver(
deliveryInput(
{
kind: "GENERATED",
bytes: byteSource([
new Uint8Array([1, 2]),
new Uint8Array([3]),
]),
},
"BOUNDED_OBJECT_URL",
),
),
).toEqual({
ok: true,
value: {
kind: "BROWSER_HANDOFF",
transferId: "transfer:blob",
},
});
expect(handoff).toHaveBeenCalledWith(
"blob:download-1",
"invoice_exe.pdf",
);
expect(leases.activeLeaseCount).toBe(1);
expect(scheduledDelay).toBe(
DEFAULT_OBJECT_URL_RELEASE_GRACE_MS,
);
scheduled?.();
expect(leases.activeLeaseCount).toBe(0);
expect(revoked).toEqual(["blob:download-1"]);
});
it("captures object URL methods at registry construction", () => {
const originalCreate = vi.fn(() => "blob:captured");
const originalRevoke = vi.fn();
const replacedCreate = vi.fn(() => "blob:replaced");
const replacedRevoke = vi.fn();
const urlApi = {
createObjectURL: originalCreate,
revokeObjectURL: originalRevoke,
};
const leases = new ObjectUrlLeaseRegistry(urlApi);
urlApi.createObjectURL = replacedCreate;
urlApi.revokeObjectURL = replacedRevoke;
const lease = leases.create(new Blob(["safe"]));
lease.release();
expect(lease.url).toBe("blob:captured");
expect(originalCreate).toHaveBeenCalledOnce();
expect(originalRevoke).toHaveBeenCalledWith(
"blob:captured",
);
expect(replacedCreate).not.toHaveBeenCalled();
expect(replacedRevoke).not.toHaveBeenCalled();
});
it("rejects a stream that crosses the bounded Blob cap", async () => {
const handoff = vi.fn();
const adapter = createDownloadDeliveryAdapter({
...HARD_LIMITS,
policies,
host: { handoff },
browserManagedCapabilities: capabilityResolver(),
userActivation: { isActive: true },
});
const input = {
...deliveryInput(
{
kind: "GENERATED" as const,
bytes: byteSource(
[new Uint8Array(10), new Uint8Array(10)],
null,
),
},
"BOUNDED_OBJECT_URL",
),
maxBufferedBytes: 16,
};
expect(await adapter.deliver(input)).toMatchObject({
ok: false,
error: { code: "LIMIT_EXCEEDED" },
});
expect(handoff).not.toHaveBeenCalled();
});
it("rejects caller limits above runtime hard caps before side effects", async () => {
const handoff = vi.fn();
const showSaveFilePicker = vi.fn(async () => writableHandle([]));
const stream = vi.fn(
async function* (): AsyncIterable<
ReturnType<typeof successfulChunk>
> {
yield successfulChunk(new Uint8Array([1]));
},
);
const adapter = createDownloadDeliveryAdapter({
hardMaxObjectUrlBytes: 8,
hardMaxTransferBytes: 12,
policies,
host: { handoff },
browserManagedCapabilities: capabilityResolver(),
showSaveFilePicker,
userActivation: { isActive: true },
});
const source = {
kind: "GENERATED" as const,
bytes: { byteLength: null, stream },
};
expect(
await adapter.deliver({
...deliveryInput(source, "BOUNDED_OBJECT_URL"),
maxBufferedBytes: 9,
maxTransferBytes: 12,
}),
).toMatchObject({
ok: false,
error: { code: "LIMIT_EXCEEDED" },
});
expect(
await adapter.deliver({
...deliveryInput(source, "PROMPT_AND_STREAM"),
maxBufferedBytes: 8,
maxTransferBytes: 13,
}),
).toMatchObject({
ok: false,
error: { code: "LIMIT_EXCEEDED" },
});
expect(
await adapter.deliver({
...deliveryInput(
{
kind: "GENERATED",
bytes: { byteLength: 13, stream },
},
"PROMPT_AND_STREAM",
),
maxBufferedBytes: 8,
maxTransferBytes: 12,
}),
).toMatchObject({
ok: false,
error: { code: "LIMIT_EXCEEDED" },
});
expect(stream).not.toHaveBeenCalled();
expect(showSaveFilePicker).not.toHaveBeenCalled();
expect(handoff).not.toHaveBeenCalled();
});
it("aborts streaming when actual bytes cross the transfer hard cap", async () => {
const events: string[] = [];
const adapter = createDownloadDeliveryAdapter({
hardMaxObjectUrlBytes: 8,
hardMaxTransferBytes: 8,
policies,
host: { handoff: vi.fn() },
browserManagedCapabilities: capabilityResolver(),
showSaveFilePicker: async () => writableHandle(events),
userActivation: { isActive: true },
});
expect(
await adapter.deliver({
...deliveryInput(
{
kind: "GENERATED",
bytes: byteSource(
[new Uint8Array(5), new Uint8Array(4)],
null,
),
},
"PROMPT_AND_STREAM",
),
maxBufferedBytes: 8,
maxTransferBytes: 8,
}),
).toMatchObject({
ok: false,
error: { code: "LIMIT_EXCEEDED" },
});
expect(events).toEqual(["write:5", "abort"]);
});
it("treats a declared-length mismatch as integrity failure", async () => {
const adapter = createDownloadDeliveryAdapter({
...HARD_LIMITS,
policies,
host: { handoff: vi.fn() },
browserManagedCapabilities: capabilityResolver(),
userActivation: { isActive: true },
});
expect(
await adapter.deliver(
deliveryInput(
{
kind: "GENERATED",
bytes: byteSource([new Uint8Array([1])], 2),
},
"BOUNDED_OBJECT_URL",
),
),
).toMatchObject({
ok: false,
error: { code: "INTEGRITY_FAILED" },
});
});
it("maps typed and defensive raw stream failures to closed download errors", async () => {
const adapter = createDownloadDeliveryAdapter({
...HARD_LIMITS,
policies,
host: { handoff: vi.fn() },
browserManagedCapabilities: capabilityResolver(),
userActivation: { isActive: true },
});
const common = {
...deliveryInput(
{
kind: "GENERATED" as const,
bytes: {
byteLength: null,
async *stream() {
yield {
ok: false as const,
error: {
code: "NOT_READABLE" as const,
operation: "FILE_READ" as const,
retryable: true,
recovery: "REOPEN" as const,
},
};
},
},
},
"BOUNDED_OBJECT_URL",
),
};
expect(await adapter.deliver(common)).toMatchObject({
ok: false,
error: {
code: "NOT_READABLE",
operation: "DOWNLOAD",
},
});
expect(
await adapter.deliver({
...common,
source: {
kind: "GENERATED",
bytes: {
byteLength: null,
async *stream(signal: AbortSignal) {
if (signal.aborted) {
yield {
ok: false as const,
error: {
code: "ABORTED" as const,
operation: "FILE_READ" as const,
retryable: false,
recovery: "NONE" as const,
},
};
return;
}
throw new DOMException(
"native detail",
"NotReadableError",
);
},
},
},
}),
).toMatchObject({
ok: false,
error: {
code: "NOT_READABLE",
operation: "DOWNLOAD",
},
});
});
});