import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { inlineSingleBackgroundEntry } from '../src/utils/image.js'
import { snapFetch } from '../src/modules/snapFetch.js'
import { safeEncodeURI, resolveURL } from '../src/utils/helpers.js'
import { cache } from '../src/core/cache.js'
if (typeof window !== 'undefined') {
window.addEventListener('unhandledrejection', (e) => {
const msg = String(e?.reason?.message || '')
if (
msg.includes('[SnapDOM - snapFetch] Fetch failed and no proxy provided') ||
msg.includes('[SnapDOM - snapFetch] Recently failed (cooldown).') ||
msg.includes('Image load timed out')
) {
e.preventDefault()
}
})
}
function clearCaches() {
cache.image?.clear?.()
cache.background?.clear?.()
cache.resource?.clear?.()
}
let OrigImage
let OrigFetch
beforeEach(() => {
vi.restoreAllMocks()
clearCaches()
OrigImage = globalThis.Image
OrigFetch = globalThis.fetch
globalThis.fetch = vi.fn(async () => ({
ok: true,
status: 200,
blob: async () =>
new Blob([new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10])], { type: 'image/png' }),
text: async () => '<svg xmlns="http://www.w3.org/2000/svg"><rect width="1" height="1"/></svg>',
}))
})
afterEach(() => {
globalThis.Image = OrigImage
globalThis.fetch = OrigFetch
})
describe('inlineSingleBackgroundEntry', () => {
it('returns gradients and "none" unchanged', async () => {
await expect(inlineSingleBackgroundEntry('linear-gradient(white, black)')).resolves.toBe('linear-gradient(white, black)')
await expect(inlineSingleBackgroundEntry('none')).resolves.toBe('none')
})
it('returns non-url entries unchanged', async () => {
await expect(inlineSingleBackgroundEntry('foo bar baz')).resolves.toBe('foo bar baz')
})
it('returns cached data URL when present in cache.background', async () => {
const url = 'https://example.com/img.png'
const data = 'data:image/png;base64,AAA'
cache.background.set(`|${url}`, data)
const out = await inlineSingleBackgroundEntry(`url("${url}")`)
expect(out).toBe(`url("${data}")`)
})
it('a no-proxy failure does not poison a later capture using a proxy (#8)', async () => {
const url = 'https://cors.example.com/img.png'
const encoded = safeEncodeURI(resolveURL(url))
cache.background.set(`|${encoded}`, null)
expect(await inlineSingleBackgroundEntry(`url("${url}")`)).toBe('none')
globalThis.fetch = vi.fn(async () => ({
ok: true,
status: 200,
blob: async () => new Blob([new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10])], { type: 'image/png' }),
}))
const out = await inlineSingleBackgroundEntry(`url("${url}")`, { useProxy: 'https://proxy/?u=' })
expect(out).not.toBe('none')
expect(out).toMatch(/^url\("data:image\/png/)
})
it('inlines via snapFetch on success (raster path → Image onload)', async () => {
globalThis.Image = class {
constructor() { setTimeout(() => this.onload && this.onload(), 0) }
set src(_) {}
decode() { return Promise.resolve() }
get naturalWidth() { return 2 }
get naturalHeight() { return 2 }
get width() { return 2 }
get height() { return 2 }
set crossOrigin(_) {}
set onload(_) {}
set onerror(_) {}
}
const out = await inlineSingleBackgroundEntry('url("https://assets.example.com/a.png")')
expect(out).toMatch(/^url\("data:image\/png;base64,/)
})
it('degrades to "none" if inlining fails (no proxy)', async () => {
globalThis.Image = class {
constructor() { setTimeout(() => this.onerror && this.onerror(), 0) }
set src(_) {}
set crossOrigin(_) {}
set onload(_) {}
set onerror(_) {}
}
const mockFetch = (globalThis.fetch)
mockFetch.mockResolvedValueOnce({ ok: false, status: 500, blob: async () => new Blob([], { type: 'image/png' }) })
const out = await inlineSingleBackgroundEntry('url("https://bad.example.com/x.png")')
expect(out).toBe('none')
})
describe('image-set() resolution matching', () => {
let originalDpr
beforeEach(() => {
originalDpr = Object.getOwnPropertyDescriptor(window, 'devicePixelRatio')
})
afterEach(() => {
if (originalDpr) Object.defineProperty(window, 'devicePixelRatio', originalDpr)
})
it('picks the 2x candidate on a 2x display, not whichever url() comes first', async () => {
Object.defineProperty(window, 'devicePixelRatio', { value: 2, configurable: true })
const oneX = 'https://example.com/photo-1x.png'
const twoX = 'https://example.com/photo-2x.png'
cache.background.set(`|${oneX}`, 'data:image/png;base64,ONEX')
cache.background.set(`|${twoX}`, 'data:image/png;base64,TWOX')
const out = await inlineSingleBackgroundEntry(
`image-set(url("${oneX}") 1x, url("${twoX}") 2x)`
)
expect(out).toBe('url("data:image/png;base64,TWOX")')
})
it('picks the 1x candidate on a 1x display', async () => {
Object.defineProperty(window, 'devicePixelRatio', { value: 1, configurable: true })
const oneX = 'https://example.com/photo-1x-b.png'
const twoX = 'https://example.com/photo-2x-b.png'
cache.background.set(`|${oneX}`, 'data:image/png;base64,ONEXB')
cache.background.set(`|${twoX}`, 'data:image/png;base64,TWOXB')
const out = await inlineSingleBackgroundEntry(
`image-set(url("${oneX}") 1x, url("${twoX}") 2x)`
)
expect(out).toBe('url("data:image/png;base64,ONEXB")')
})
it('works with the -webkit- prefixed form', async () => {
Object.defineProperty(window, 'devicePixelRatio', { value: 2, configurable: true })
const oneX = 'https://example.com/wk-1x.png'
const twoX = 'https://example.com/wk-2x.png'
cache.background.set(`|${oneX}`, 'data:image/png;base64,WKONE')
cache.background.set(`|${twoX}`, 'data:image/png;base64,WKTWO')
const out = await inlineSingleBackgroundEntry(
`-webkit-image-set(url("${oneX}") 1x, url("${twoX}") 2x)`
)
expect(out).toBe('url("data:image/png;base64,WKTWO")')
})
})
})
* @param {import('../src/modules/snapFetch.js').SnapFetchResult} r
*/
function expectOkDataURL(r) {
expect(r.ok).toBe(true)
expect(typeof r.data).toBe('string')
expect(r.data).toMatch(/^data:/)
}
* @param {import('../src/modules/snapFetch.js').SnapFetchResult} r
*/
function expectOkText(r) {
expect(r.ok).toBe(true)
expect(typeof r.data).toBe('string')
}
describe('snapFetch (raster path)', () => {
it('resolves a DataURL when the image loads and decode succeeds', async () => {
globalThis.Image = class {
constructor() { setTimeout(() => this.onload && this.onload(), 0) }
set src(_) {}
decode() { return Promise.resolve() }
get naturalWidth() { return 3 }
get naturalHeight() { return 4 }
set crossOrigin(_) {}
set onload(_) {}
set onerror(_) {}
}
const r = await snapFetch('https://cdn.example.com/photo.jpg', { timeout: 100, as: 'dataURL' })
expectOkDataURL(r)
expect(r.mime).toMatch(/image\/png|image\/jpeg|image\/jpg/i)
})
it('uses credentials: "include" for same-origin URLs', async () => {
const spy = vi.spyOn(globalThis, 'fetch')
await snapFetch('/local.png', { timeout: 100, as: 'blob' })
expect(spy).toHaveBeenCalledTimes(1)
const [, opts] = spy.mock.calls[0]
expect(opts.credentials).toBe('include')
spy.mockRestore()
})
})
describe('snapFetch (svg path)', () => {
it('inlines SVG via direct text fetch when as:"text"', async () => {
const r = await snapFetch('https://example.com/icon.svg', { as: 'text' })
expectOkText(r)
expect(String(r.data)).toMatch(/^<svg[\s>]/)
})
it('deduplicates in-flight fetches (single network call for concurrent requests)', async () => {
const fetchSpy = vi.spyOn(globalThis, 'fetch')
fetchSpy.mockImplementationOnce(async () => {
await new Promise(r => setTimeout(r, 50))
return {
ok: true,
status: 200,
text: async () => '<svg xmlns="http://www.w3.org/2000/svg"></svg>',
blob: async () => new Blob([new Uint8Array([137,80,78,71])], { type: 'image/png' }),
headers: new Headers({ 'content-type': 'image/svg+xml' }),
}
})
const p1 = snapFetch('https://slow.example.com/a.svg', { as: 'text' })
const p2 = snapFetch('https://slow.example.com/a.svg', { as: 'text' })
const [a, b] = await Promise.all([p1, p2])
expectOkText(a)
expectOkText(b)
expect(fetchSpy).toHaveBeenCalledTimes(1)
fetchSpy.mockRestore()
})
it('sets cooldown after failure and resolves ok:false; subsequent calls hit fromCache quickly', async () => {
const fetchSpy = vi.spyOn(globalThis, 'fetch')
fetchSpy.mockRejectedValueOnce(new Error('boom'))
const opts = { errorTTL: 5000, as: 'text' }
const r1 = await snapFetch('https://fail.example.com/x.svg', opts)
expect(r1.ok).toBe(false)
expect(['network', 'timeout', 'abort', 'http_error']).toContain(r1.reason)
fetchSpy.mockClear()
const r2 = await snapFetch('https://fail.example.com/x.svg', opts)
expect(r2.ok).toBe(false)
expect(r2.fromCache).toBe(true)
expect(fetchSpy).not.toHaveBeenCalled()
fetchSpy.mockRestore()
})
it('uses proxy for cross-origin when provided and returns DataURL if requested', async () => {
const f = (globalThis.fetch)
f.mockResolvedValueOnce({
ok: true,
status: 200,
blob: async () => new Blob([new Uint8Array([137,80,78,71,13,10,26,10])], { type: 'image/png' }),
text: async () => '<svg/>',
headers: new Headers({ 'content-type': 'image/png' }),
})
const proxy = 'https://proxy.test/?'
const url = 'https://blocked.example.com/asset.svg'
const r = await snapFetch(url, { useProxy: proxy, as: 'dataURL' })
expectOkDataURL(r)
expect(r.url.startsWith(proxy)).toBe(true)
})
})