diff --git a/tests/unit/utils/get-core-content-text.test.mjs b/tests/unit/utils/get-core-content-text.test.mjs
new file mode 100644
index 000000000..8fbf44403
--- /dev/null
+++ b/tests/unit/utils/get-core-content-text.test.mjs
@@ -0,0 +1,111 @@
+import assert from 'node:assert/strict'
+import { createRequire } from 'node:module'
+import { after, afterEach, test } from 'node:test'
+import { JSDOM } from 'jsdom'
+
+const require = createRequire(import.meta.url)
+const readability = require('@mozilla/readability')
+const originalIsProbablyReaderable = readability.isProbablyReaderable
+readability.isProbablyReaderable = () => false
+const { getCoreContentText } = await import('../../../src/utils/get-core-content-text.mjs')
+
+const globalNames = ['document', 'location', 'Node']
+const originalDescriptors = new Map(
+ globalNames.map((name) => [name, Object.getOwnPropertyDescriptor(globalThis, name)]),
+)
+let dom
+
+after(() => {
+ readability.isProbablyReaderable = originalIsProbablyReaderable
+})
+
+const setDOM = (html, url = 'https://example.com/') => {
+ dom = new JSDOM(html, { url })
+
+ for (const name of globalNames) {
+ Object.defineProperty(globalThis, name, {
+ value: dom.window[name],
+ configurable: true,
+ })
+ }
+}
+
+afterEach(() => {
+ dom?.window.close()
+ dom = undefined
+
+ for (const [name, descriptor] of originalDescriptors) {
+ if (descriptor) {
+ Object.defineProperty(globalThis, name, descriptor)
+ } else {
+ delete globalThis[name]
+ }
+ }
+})
+
+test('getCoreContentText prefers the known-site selector over a generic article', () => {
+ setDOM(
+ `
+ Known site content
+ Generic article content
+ `,
+ 'https://www.google.com/search?q=test',
+ )
+
+ assert.equal(getCoreContentText(), 'Known site content')
+})
+
+test('getCoreContentText uses an article and normalizes its text', () => {
+ setDOM(`
+ First paragraph
+
+ Second,,
+ `)
+
+ assert.equal(getCoreContentText(), 'FirstparagraphSecond')
+})
+
+test('getCoreContentText falls back to a small document body', (t) => {
+ t.mock.method(console, 'log', () => {})
+ setDOM('
Body fallback content')
+
+ assert.equal(getCoreContentText(), 'Body fallback content')
+})
+
+test('getCoreContentText falls back to the largest body child', (t) => {
+ t.mock.method(console, 'log', () => {})
+ setDOM(`
+
+ Largest content
+
+
+ `)
+
+ document.body.getBoundingClientRect = () => ({ width: 100, height: 100 })
+ document.querySelector('#content').getBoundingClientRect = () => ({ width: 80, height: 80 })
+ document.querySelector('aside').getBoundingClientRect = () => ({ width: 20, height: 20 })
+
+ assert.equal(getCoreContentText(), 'Largest content')
+})
+
+test('getCoreContentText uses a major nested content element', (t) => {
+ t.mock.method(console, 'log', () => {})
+ setDOM(`
+
+
+ Nested content
+
+
+
+ `)
+
+ document.body.getBoundingClientRect = () => ({ width: 100, height: 100 })
+ document.querySelector('#content').getBoundingClientRect = () => ({ width: 80, height: 80 })
+ document.querySelector('#nested-content').getBoundingClientRect = () => ({
+ width: 60,
+ height: 60,
+ })
+ document.querySelector('aside').getBoundingClientRect = () => ({ width: 10, height: 10 })
+
+ assert.equal(getCoreContentText(), 'Nested content')
+})
diff --git a/tests/unit/utils/get-possible-element-by-query-selector.test.mjs b/tests/unit/utils/get-possible-element-by-query-selector.test.mjs
new file mode 100644
index 000000000..048e70193
--- /dev/null
+++ b/tests/unit/utils/get-possible-element-by-query-selector.test.mjs
@@ -0,0 +1,67 @@
+import assert from 'node:assert/strict'
+import { afterEach, test } from 'node:test'
+import { getPossibleElementByQuerySelector } from '../../../src/utils/get-possible-element-by-query-selector.mjs'
+
+const originalDocumentDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'document')
+
+const restoreDocument = () => {
+ if (originalDocumentDescriptor) {
+ Object.defineProperty(globalThis, 'document', originalDocumentDescriptor)
+ } else {
+ delete globalThis.document
+ }
+}
+
+const setDocument = (querySelector) => {
+ Object.defineProperty(globalThis, 'document', {
+ value: { querySelector },
+ configurable: true,
+ })
+}
+
+afterEach(() => {
+ restoreDocument()
+})
+
+test('getPossibleElementByQuerySelector returns the first matching selector', () => {
+ const firstMatch = { id: 'first' }
+ const calls = []
+ setDocument((selector) => {
+ calls.push(selector)
+ return selector === '.first' ? firstMatch : { id: 'later' }
+ })
+
+ const result = getPossibleElementByQuerySelector(['.first', '.later'])
+
+ assert.equal(result, firstMatch)
+ assert.deepEqual(calls, ['.first'])
+})
+
+test('getPossibleElementByQuerySelector falls through missing and invalid selectors', () => {
+ const fallbackMatch = { id: 'fallback' }
+ const calls = []
+ setDocument((selector) => {
+ calls.push(selector)
+ if (selector === '[') throw new DOMException('Invalid selector', 'SyntaxError')
+ if (selector === '.fallback') return fallbackMatch
+ return null
+ })
+
+ const result = getPossibleElementByQuerySelector(['.missing', '[', '.fallback'])
+
+ assert.equal(result, fallbackMatch)
+ assert.deepEqual(calls, ['.missing', '[', '.fallback'])
+})
+
+test('getPossibleElementByQuerySelector returns undefined without usable matches', () => {
+ const calls = []
+ setDocument((selector) => {
+ calls.push(selector)
+ return null
+ })
+
+ assert.equal(getPossibleElementByQuerySelector(null), undefined)
+ assert.equal(getPossibleElementByQuerySelector([]), undefined)
+ assert.equal(getPossibleElementByQuerySelector(['', null, '.missing']), undefined)
+ assert.deepEqual(calls, ['.missing'])
+})
diff --git a/tests/unit/utils/limited-fetch.test.mjs b/tests/unit/utils/limited-fetch.test.mjs
new file mode 100644
index 000000000..ec34143fc
--- /dev/null
+++ b/tests/unit/utils/limited-fetch.test.mjs
@@ -0,0 +1,166 @@
+import assert from 'node:assert/strict'
+import { afterEach, test } from 'node:test'
+import { limitedFetch } from '../../../src/utils/limited-fetch.mjs'
+
+const originalXMLHttpRequestDescriptor = Object.getOwnPropertyDescriptor(
+ globalThis,
+ 'XMLHttpRequest',
+)
+
+const restoreXMLHttpRequest = () => {
+ if (originalXMLHttpRequestDescriptor) {
+ Object.defineProperty(globalThis, 'XMLHttpRequest', originalXMLHttpRequestDescriptor)
+ } else {
+ delete globalThis.XMLHttpRequest
+ }
+}
+
+const installFakeXMLHttpRequest = ({ openError, sendError } = {}) => {
+ const requests = []
+
+ class FakeXMLHttpRequest {
+ constructor() {
+ this.aborted = false
+ requests.push(this)
+ }
+
+ open(method, url) {
+ if (openError) throw openError
+ this.method = method
+ this.url = url
+ }
+
+ send() {
+ if (sendError) throw sendError
+ this.sent = true
+ }
+
+ abort() {
+ this.aborted = true
+ }
+ }
+
+ Object.defineProperty(globalThis, 'XMLHttpRequest', {
+ value: FakeXMLHttpRequest,
+ configurable: true,
+ })
+
+ return requests
+}
+
+afterEach(() => {
+ restoreXMLHttpRequest()
+})
+
+test('limitedFetch keeps downloading while progress remains below the byte limit', async () => {
+ const requests = installFakeXMLHttpRequest()
+ const responsePromise = limitedFetch('https://example.com/data', 5)
+ let settled = false
+ const settlementPromise = responsePromise.then(
+ () => {
+ settled = true
+ },
+ () => {
+ settled = true
+ },
+ )
+ const [request] = requests
+
+ request.onprogress({
+ loaded: 4,
+ target: { responseText: 'data' },
+ })
+ await Promise.resolve()
+
+ assert.equal(request.aborted, false)
+ assert.equal(settled, false)
+
+ request.onload({
+ target: { responseText: 'data' },
+ })
+
+ assert.equal(await responsePromise, 'data')
+ await settlementPromise
+ assert.equal(request.method, 'GET')
+ assert.equal(request.url, 'https://example.com/data')
+ assert.equal(request.sent, true)
+})
+
+test('limitedFetch truncates and aborts when progress reaches the byte limit', async () => {
+ const requests = installFakeXMLHttpRequest()
+ const responsePromise = limitedFetch('https://example.com/data', 5)
+ const [request] = requests
+
+ request.onprogress({
+ loaded: 5,
+ target: { responseText: '123456789' },
+ })
+
+ assert.equal(await responsePromise, '12345')
+ assert.equal(request.aborted, true)
+})
+
+test('limitedFetch truncates a completed response without aborting it', async () => {
+ const requests = installFakeXMLHttpRequest()
+ const responsePromise = limitedFetch('https://example.com/data', 4)
+ const [request] = requests
+
+ request.onload({
+ target: { responseText: 'abcdefgh' },
+ })
+
+ assert.equal(await responsePromise, 'abcd')
+ assert.equal(request.aborted, false)
+})
+
+test('limitedFetch rejects with the XHR status when the request fails', async () => {
+ const requests = installFakeXMLHttpRequest()
+ const responsePromise = limitedFetch('https://example.com/data', 10)
+ const [request] = requests
+
+ request.onerror({
+ target: { status: 503 },
+ })
+
+ await assert.rejects(responsePromise, {
+ name: 'Error',
+ message: '503',
+ })
+})
+
+test('limitedFetch rejects when constructing XMLHttpRequest throws', async () => {
+ const constructorError = new Error('XMLHttpRequest unavailable')
+ Object.defineProperty(globalThis, 'XMLHttpRequest', {
+ value: class {
+ constructor() {
+ throw constructorError
+ }
+ },
+ configurable: true,
+ })
+
+ await assert.rejects(limitedFetch('https://example.com/data', 10), (error) => {
+ assert.equal(error, constructorError)
+ return true
+ })
+})
+
+test('limitedFetch rejects when opening the request throws', async () => {
+ const openError = new Error('Invalid URL')
+ installFakeXMLHttpRequest({ openError })
+
+ await assert.rejects(limitedFetch('not a url', 10), (error) => {
+ assert.equal(error, openError)
+ return true
+ })
+})
+
+test('limitedFetch rejects when sending the request throws', async () => {
+ const sendError = new Error('Request blocked')
+ installFakeXMLHttpRequest({ sendError })
+
+ await assert.rejects(limitedFetch('https://example.com/data', 10), (error) => {
+ assert.equal(error, sendError)
+ return true
+ })
+})