UNPKG

reactotron-react-native

Version:

A development tool to explore, inspect, and diagnose your React Native apps.

133 lines (108 loc) 4.84 kB
/** * Plugin-level tests for the expo/fetch side of the networking plugin * (onFetchOpen / onFetchResponse), driven through the real FetchInterceptor. */ // xhr-interceptor captures XMLHttpRequest.prototype methods at module load, so // a stub must exist before the plugin (which imports it) is required. /* eslint-disable @typescript-eslint/no-empty-function */ class FakeXMLHttpRequest { open() {} send() {} setRequestHeader() {} } /* eslint-enable @typescript-eslint/no-empty-function */ ;(globalThis as any).XMLHttpRequest = FakeXMLHttpRequest // Disable reason: must require after the XMLHttpRequest stub is installed. /* eslint-disable @typescript-eslint/no-var-requires */ const networking = require("./networking").default const { FetchInterceptor } = require("../fetch-interceptor") const { XHRInterceptor } = require("../xhr-interceptor") /* eslint-enable @typescript-eslint/no-var-requires */ function makeResponse( status: number, headersObj: Record<string, string>, bodyText = "", cloneThrows = false ) { return { status, headers: { get: (k: string) => headersObj[k.toLowerCase()] ?? null, forEach: (cb: (v: string, k: string) => void) => Object.entries(headersObj).forEach(([k, v]) => cb(v, k)), }, clone() { if (cloneThrows) throw new TypeError("Response body is already used") return this }, text: () => Promise.resolve(bodyText), } } const flushPromises = () => new Promise((resolve) => setTimeout(resolve, 0)) describe("networking plugin (expo/fetch path)", () => { const realFetch = globalThis.fetch let reactotron: { startTimer: () => () => number; apiResponse: jest.Mock } function connect(response: any, options: Record<string, unknown> = {}) { const fetchImpl: any = jest.fn(() => Promise.resolve(response)) reactotron = { startTimer: () => () => 42, apiResponse: jest.fn() } const plugin = networking({ fetch: fetchImpl, ...options })(reactotron as any) plugin.onConnect() return fetchImpl } afterEach(() => { FetchInterceptor.disableInterception() XHRInterceptor.disableInterception() globalThis.fetch = realFetch }) it("reports request and response through apiResponse", async () => { connect(makeResponse(200, { "content-type": "application/json" }, '{"ok":true}')) await (globalThis.fetch as any)("https://example.com/x?a=1&b=two+words", { method: "POST" }) await flushPromises() expect(reactotron.apiResponse).toHaveBeenCalledTimes(1) const [tronRequest, tronResponse, duration] = reactotron.apiResponse.mock.calls[0] expect(tronRequest.url).toBe("https://example.com/x?a=1&b=two+words") expect(tronRequest.method).toBe("POST") expect(tronRequest.params).toEqual({ a: "1", b: "two words" }) expect(tronResponse.status).toBe(200) expect(tronResponse.body).toEqual({ ok: true }) expect(duration).toBe(42) }) it("does not throw on malformed percent-encoding in query params", async () => { connect(makeResponse(200, { "content-type": "application/json" }, "{}")) // %E0%A4%A is malformed — decodeURIComponent would throw await expect( (globalThis.fetch as any)("https://example.com/x?q=%E0%A4%A&ok=1") ).resolves.toBeDefined() await flushPromises() expect(reactotron.apiResponse).toHaveBeenCalledTimes(1) const [tronRequest] = reactotron.apiResponse.mock.calls[0] // malformed value falls back to the raw string; valid values still decode expect(tronRequest.params).toEqual({ q: "%E0%A4%A", ok: "1" }) }) it("reports ~~~ unreadable ~~~ when clone() throws", async () => { connect(makeResponse(200, { "content-type": "application/json" }, "", true)) await (globalThis.fetch as any)("https://example.com/x") await flushPromises() expect(reactotron.apiResponse).toHaveBeenCalledTimes(1) const [, tronResponse] = reactotron.apiResponse.mock.calls[0] expect(tronResponse.body).toBe("~~~ unreadable ~~~") }) it("skips bodies for streaming content types without touching the response", async () => { const response = makeResponse(200, { "content-type": "text/event-stream" }) const cloneSpy = jest.spyOn(response, "clone") connect(response) await (globalThis.fetch as any)("https://example.com/stream") await flushPromises() expect(cloneSpy).not.toHaveBeenCalled() const [, tronResponse] = reactotron.apiResponse.mock.calls[0] expect(tronResponse.body).toBe("~~~ skipped ~~~") }) it("does not report requests matching ignoreUrls", async () => { connect(makeResponse(200, { "content-type": "application/json" }, "{}"), { ignoreUrls: /\/logs$/, }) await (globalThis.fetch as any)("https://example.com/logs") await flushPromises() expect(reactotron.apiResponse).not.toHaveBeenCalled() }) })