From b784c291fe9e91ac0dc2b92c03fbdb411a5d5698 Mon Sep 17 00:00:00 2001 From: Peter Dave Hello Date: Sun, 19 Jul 2026 05:59:23 +0800 Subject: [PATCH] Handle background fetch transport failures Reject background messaging and response-processing failures so callers do not remain pending when a request cannot complete. Add focused coverage for successful responses and both error paths. --- src/utils/fetch-bg.mjs | 1 + tests/unit/utils/fetch-bg.test.mjs | 87 ++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 tests/unit/utils/fetch-bg.test.mjs diff --git a/src/utils/fetch-bg.mjs b/src/utils/fetch-bg.mjs index 6d0cb52e4..f9b38f684 100644 --- a/src/utils/fetch-bg.mjs +++ b/src/utils/fetch-bg.mjs @@ -27,5 +27,6 @@ export function fetchBg(input, init) { ) } }) + .catch(reject) }) } diff --git a/tests/unit/utils/fetch-bg.test.mjs b/tests/unit/utils/fetch-bg.test.mjs new file mode 100644 index 000000000..3a37c56e5 --- /dev/null +++ b/tests/unit/utils/fetch-bg.test.mjs @@ -0,0 +1,87 @@ +import assert from 'node:assert/strict' +import { test } from 'node:test' +import Browser from 'webextension-polyfill' +import { fetchBg } from '../../../src/utils/fetch-bg.mjs' + +function mockSendMessage(t, implementation) { + const originalSendMessage = Browser.runtime.sendMessage + Object.defineProperty(Browser.runtime, 'sendMessage', { + configurable: true, + value: implementation, + }) + t.after(() => { + Object.defineProperty(Browser.runtime, 'sendMessage', { + configurable: true, + value: originalSendMessage, + }) + }) +} + +test('fetchBg returns the response received from the background script', async (t) => { + const calls = [] + mockSendMessage(t, async (message) => { + calls.push(message) + return [ + { + body: 'response body', + status: 201, + statusText: 'Created', + headers: { 'content-type': 'text/plain' }, + }, + null, + ] + }) + + const response = await fetchBg('https://example.com/resource', { method: 'POST' }) + + assert.deepEqual(calls, [ + { + type: 'FETCH', + data: { + input: 'https://example.com/resource', + init: { method: 'POST' }, + }, + }, + ]) + assert.equal(response.status, 201) + assert.equal(response.statusText, 'Created') + assert.equal(response.headers.get('content-type'), 'text/plain') + assert.equal(await response.text(), 'response body') +}) + +test('fetchBg rejects with the error received from the background script', async (t) => { + const backgroundError = { message: 'background fetch failed' } + mockSendMessage(t, async () => [null, backgroundError]) + + await assert.rejects(fetchBg('https://example.com'), (error) => { + assert.deepEqual(error, backgroundError) + return true + }) +}) + +test('fetchBg forwards sendMessage rejections', async (t) => { + const messageError = new Error('message channel closed') + mockSendMessage(t, async () => { + throw messageError + }) + + await assert.rejects(fetchBg('https://example.com'), (error) => { + assert.equal(error, messageError) + return true + }) +}) + +test('fetchBg forwards errors thrown while processing the response', async (t) => { + const processingError = new Error('invalid message response') + const messageResponse = { + [Symbol.iterator]() { + throw processingError + }, + } + mockSendMessage(t, async () => messageResponse) + + await assert.rejects(fetchBg('https://example.com'), (error) => { + assert.equal(error, processingError) + return true + }) +})