fix(frontend): binder le fetch global à globalThis (pairing Firefox) (#13)
Firefox exige que fetch soit appelé sur Window : passer la référence nue `fetch` en dépendance déclenchait « 'fetch' called on an object that does not implement interface Window » au pairing. Nouveau helper defaultFetch() qui binde globalThis.fetch, utilisé par webSession. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -7,9 +7,37 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
|
||||
import type { FetchLike } from "./httpInvoker";
|
||||
import { HttpInvoker } from "./httpInvoker";
|
||||
import { HttpInvoker, defaultFetch } from "./httpInvoker";
|
||||
import { HttpProjectGateway, HttpGitGateway } from "./requestResponseGateways";
|
||||
|
||||
/**
|
||||
* Installs a global `fetch` that records its receiver (`this`), runs `body`, then
|
||||
* restores the original. A real browser's `fetch` rejects a non-global receiver
|
||||
* (Firefox: "'fetch' called on an object that does not implement interface
|
||||
* Window"); jsdom does not enforce it, so the receiver is asserted explicitly.
|
||||
*/
|
||||
async function withRecordingGlobalFetch(
|
||||
body: () => Promise<void>,
|
||||
): Promise<unknown[]> {
|
||||
const original = globalThis.fetch;
|
||||
const receivers: unknown[] = [];
|
||||
globalThis.fetch = function (this: unknown) {
|
||||
receivers.push(this);
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({}),
|
||||
text: async () => "{}",
|
||||
});
|
||||
} as unknown as typeof globalThis.fetch;
|
||||
try {
|
||||
await body();
|
||||
} finally {
|
||||
globalThis.fetch = original;
|
||||
}
|
||||
return receivers;
|
||||
}
|
||||
|
||||
/** Builds a fake fetch that records the request and returns `body`. */
|
||||
function fakeFetch(
|
||||
body: unknown,
|
||||
@ -110,6 +138,52 @@ describe("HttpInvoker", () => {
|
||||
code: "TRANSPORT_ERROR",
|
||||
});
|
||||
});
|
||||
|
||||
// Regression: the default global `fetch` was stored unbound, so calling it as
|
||||
// `this.fetchImpl(…)` handed it the invoker as receiver — a TypeError in
|
||||
// Firefox/Chrome at pairing. It must stay bound to `globalThis`.
|
||||
it("calls the default global fetch with globalThis as receiver", async () => {
|
||||
const receivers = await withRecordingGlobalFetch(async () => {
|
||||
const http = new HttpInvoker({ baseUrl: "https://host" });
|
||||
await http.invoke("health");
|
||||
});
|
||||
|
||||
expect(receivers).toHaveLength(1);
|
||||
expect(receivers[0]).toBe(globalThis);
|
||||
});
|
||||
|
||||
it("leaves an injected fetchImpl untouched (no receiver rebinding)", async () => {
|
||||
const { fetchImpl, calls } = fakeFetch({ ok: true });
|
||||
const http = new HttpInvoker({ baseUrl: "https://host", fetchImpl });
|
||||
|
||||
await http.invoke("health");
|
||||
|
||||
expect(calls).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("defaultFetch", () => {
|
||||
it("returns the global fetch bound to globalThis", async () => {
|
||||
const receivers = await withRecordingGlobalFetch(async () => {
|
||||
const fn = defaultFetch();
|
||||
// Called as a bare reference (worst case: no receiver at all).
|
||||
await fn("https://host/x");
|
||||
});
|
||||
|
||||
expect(receivers).toEqual([globalThis]);
|
||||
});
|
||||
|
||||
it("passes through a missing global fetch instead of throwing at construction", () => {
|
||||
const original = globalThis.fetch;
|
||||
// @ts-expect-error — simulating an environment without a global fetch.
|
||||
delete globalThis.fetch;
|
||||
try {
|
||||
// Must not throw here; the failure surfaces at call time, as before.
|
||||
expect(defaultFetch()).toBeUndefined();
|
||||
} finally {
|
||||
globalThis.fetch = original;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("request/response gateways preserve the Tauri command contract", () => {
|
||||
|
||||
Reference in New Issue
Block a user