Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions tests/unit/utils/get-core-content-text.test.mjs
Original file line number Diff line number Diff line change
@@ -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(
`
<main id="search">Known site content</main>
<article>Generic article content</article>
`,
'https://www.google.com/search?q=test',
)

assert.equal(getCoreContentText(), 'Known site content')
})

test('getCoreContentText uses an article and normalizes its text', () => {
setDOM(`
<article> First paragraph

Second,, </article>
`)

assert.equal(getCoreContentText(), 'FirstparagraphSecond')
})

test('getCoreContentText falls back to a small document body', (t) => {
t.mock.method(console, 'log', () => {})
setDOM('<body>Body fallback content</body>')

assert.equal(getCoreContentText(), 'Body fallback content')
})
Comment thread
PeterDaveHello marked this conversation as resolved.

test('getCoreContentText falls back to the largest body child', (t) => {
t.mock.method(console, 'log', () => {})
setDOM(`
<body>
<main id="content">Largest content</main>
<aside>Smaller content</aside>
</body>
`)

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')
})
Comment thread
PeterDaveHello marked this conversation as resolved.
Comment thread
PeterDaveHello marked this conversation as resolved.

test('getCoreContentText uses a major nested content element', (t) => {
t.mock.method(console, 'log', () => {})
setDOM(`
<body>
<main id="content">
<div id="nested-content">Nested content</div>
<aside>Smaller content</aside>
</main>
</body>
`)

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')
})
67 changes: 67 additions & 0 deletions tests/unit/utils/get-possible-element-by-query-selector.test.mjs
Original file line number Diff line number Diff line change
@@ -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'])
})
166 changes: 166 additions & 0 deletions tests/unit/utils/limited-fetch.test.mjs
Original file line number Diff line number Diff line change
@@ -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
})
})