| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516 |
- // @vitest-environment jsdom
- import { Context } from '@deepseek-ai/cordis'
- import Loader from '@deepseek-ai/cordis-plugin-loader'
- import { afterEach, describe, expect, it, vi } from 'vitest'
- import { apply as provideModules, createClientModuleSystem } from '../src/client/index.ts'
- import type { ClientBundleRegistration, ClientModuleLoaderTarget, WebBootEntry, WebBootGraph } from '../src/client/index.ts'
- const contexts: Context[] = []
- afterEach(async () => {
- for (const ctx of contexts.splice(0)) {
- await ctx.fiber.dispose()
- await ctx.fiber.await()
- }
- document.head.querySelectorAll('style').forEach((el) => { el.remove() })
- document.body.replaceChildren()
- vi.restoreAllMocks()
- })
- const row = (id: string, rev = 'r0', extra: Partial<WebBootEntry> = {}): WebBootEntry => ({
- id, rev, url: `/plugins/??${id}/client.js&rev=${rev}`, ...extra,
- })
- const graph = (...entries: WebBootEntry[]): WebBootGraph => ({
- rev: JSON.stringify(entries), entries,
- batches: entries.length === 0 ? [] : [{ phase: 'application', url: '/batch', rev: 'batch', entries: entries.map(row => row.id) }],
- })
- const deferred = () => {
- let resolve!: () => void
- const promise = new Promise<void>((done) => { resolve = done })
- return { promise, resolve }
- }
- async function bench(initial: WebBootGraph, factories: Record<string, ClientBundleRegistration['factory']>, start = true) {
- const ctx = new Context()
- contexts.push(ctx)
- const fetched: string[] = []
- const target: ClientModuleLoaderTarget = {
- mode: 'queue', pendingQueue: [], load: () => {},
- create: options => createClientModuleSystem(target, { id: 'bootstrap', exports: { inject: ['loader'], apply: provideModules } }, options),
- }
- let arrival: (url: string) => Promise<void> = async () => {}
- const modules = target.create({
- boot: initial, staticModules: {},
- loadBundle: async (url) => {
- fetched.push(url)
- const ids = url === '/batch' ? initial.entries.map(row => row.id).filter(id => id !== 'bootstrap') : [url.split('??')[1]!.split('/client.js')[0]!]
- const registrations = ids.map(id => ({ id, factory: factories[id]! }))
- await arrival(url)
- for (const registration of registrations) target.load(registration)
- },
- })
- await ctx.plugin(Loader)
- ctx.loader.internal = modules as never
- if (start) await modules.entries.start(ctx.loader, modules.manifest)
- return { ctx, modules, fetched, target, arrival: (fn: typeof arrival) => { arrival = fn } }
- }
- /** A visible plugin whose style and listener are owned by its factory and fiber respectively. */
- function visible(id: string, effects: { mounted: number; disposed: number; hits: number }, cleanup?: () => Promise<void>) {
- return () => {
- const style = document.createElement('style')
- style.dataset.plugin = id
- style.textContent = `[data-live="${id}"] { color: rgb(12, 34, 56); }`
- document.head.append(style)
- return { apply(ctx: Context) {
- ctx.effect(() => {
- effects.mounted++
- const el = document.createElement('div')
- el.dataset.live = id
- document.body.append(el)
- const listener = () => { effects.hits++ }
- window.addEventListener('live-test', listener)
- return async () => {
- window.removeEventListener('live-test', listener)
- el.remove()
- await cleanup?.()
- effects.disposed++
- }
- })
- } }
- }
- }
- describe('client manifest entries', () => {
- it('adds, drains removal and re-enables one instance with styles; unrelated entries survive', async () => {
- const effects = { mounted: 0, disposed: 0, hits: 0 }
- const cleanup = deferred()
- const b = await bench(graph(), { pet: visible('pet', effects, () => cleanup.promise) })
- b.target.load({ id: 'local', factory: () => ({ apply() {} }) })
- const localId = await b.ctx.loader.create({ name: 'local' })
- await b.modules.entries.sync(graph(row('pet')))
- expect(b.fetched).toEqual([row('pet').url])
- expect(document.querySelectorAll('[data-live=pet]')).toHaveLength(1)
- expect(document.querySelectorAll('style[data-plugin=pet]')).toHaveLength(1)
- window.dispatchEvent(new Event('live-test'))
- expect(effects.hits).toBe(1)
- const removing = b.modules.entries.sync(graph())
- await vi.waitFor(() => { expect(document.querySelector('[data-live=pet]')).toBeNull() })
- const readding = b.modules.entries.sync(graph(row('pet')))
- expect(effects.disposed).toBe(0)
- expect(effects.mounted).toBe(1)
- cleanup.resolve()
- await Promise.all([removing, readding])
- expect(effects).toEqual({ mounted: 2, disposed: 1, hits: 1 })
- expect(document.querySelectorAll('style[data-plugin=pet]')).toHaveLength(1)
- expect(b.ctx.loader.resolve(localId).fiber?.state).toBe(2)
- await b.modules.entries.sync(graph())
- window.dispatchEvent(new Event('live-test'))
- expect(effects).toEqual({ mounted: 2, disposed: 2, hits: 1 })
- expect(document.querySelectorAll('style[data-plugin=pet]')).toHaveLength(0)
- expect(b.modules.loadCache.has('pet')).toBe(false)
- expect(b.modules.entries.state.getSnapshot()).toEqual({ syncing: false, failures: [] })
- })
- it('does not mount an obsolete download and can load it again later', async () => {
- const effects = { mounted: 0, disposed: 0, hits: 0 }
- const b = await bench(graph(), { pet: visible('pet', effects) })
- const arrival = deferred()
- const started = deferred()
- b.arrival(async () => { started.resolve(); await arrival.promise })
- const enabling = b.modules.entries.sync(graph(row('pet')))
- await started.promise
- const disabling = b.modules.entries.sync(graph())
- arrival.resolve()
- await Promise.all([enabling, disabling])
- expect(effects.mounted).toBe(0)
- expect(b.modules.loadCache.has('pet')).toBe(false)
- await b.modules.entries.sync(graph(row('pet')))
- expect(effects.mounted).toBe(1)
- expect(b.fetched).toEqual([row('pet').url, row('pet').url])
- })
- it('registers dynamic dependencies first and retains one still required by an unmanaged entry', async () => {
- const materialized = vi.fn<(id: string) => void>()
- const b = await bench(graph(row('existing')), {
- existing: () => { materialized('existing'); return { apply() {} } },
- dependency: () => { materialized('dependency'); return { apply() {}, value: 42 } },
- consumer: (require) => {
- materialized('consumer')
- expect(require('dependency/client')).toHaveProperty('value', 42)
- return { apply() {} }
- },
- })
- await b.modules.entries.sync(graph(row('existing'), row('consumer', 'r0', { external: ['dependency/client'] }), row('dependency')))
- expect(b.fetched.slice(1)).toEqual([row('dependency').url, row('consumer').url])
- expect(materialized.mock.calls.map(([id]) => id)).toEqual(['existing', 'consumer', 'dependency'])
- b.target.load({ id: 'local', factory: require => ({ apply() {}, dep: require('dependency/client') }) })
- const localId = await b.ctx.loader.create({ name: 'local' })
- await b.modules.entries.sync(graph(row('existing')))
- expect(b.modules.loadCache.has('consumer')).toBe(false)
- expect(b.modules.loadCache.has('dependency')).toBe(true)
- b.ctx.loader.remove(localId)
- await b.modules.entries.retry()
- expect(b.modules.loadCache.has('dependency')).toBe(false)
- expect(materialized.mock.calls.filter(([id]) => id === 'existing')).toHaveLength(1)
- })
- it('diagnoses download and apply failures, retries an identical graph and leaves healthy plugins running', async () => {
- const effects = { mounted: 0, disposed: 0, hits: 0 }
- let broken = true
- const b = await bench(graph(row('healthy')), {
- healthy: visible('healthy', effects),
- bad: () => ({ apply() { if (broken) throw new Error('apply unavailable') } }),
- })
- b.arrival(async () => { throw new Error('download unavailable') })
- await b.modules.entries.sync(graph(row('healthy'), row('bad')))
- expect(b.modules.entries.state.getSnapshot().failures[0]?.message).toContain('download unavailable')
- b.arrival(async () => {})
- await b.modules.entries.retry()
- expect(b.modules.entries.state.getSnapshot().failures[0]?.message).toContain('apply unavailable')
- broken = false
- await b.modules.entries.retry()
- expect(b.modules.entries.state.getSnapshot().failures).toEqual([])
- expect(effects.mounted).toBe(1)
- expect(effects.disposed).toBe(0)
- })
- it('deduplicates rebuilt and graph revisions and retries after failed code arrival', async () => {
- const effects = { mounted: 0, disposed: 0, hits: 0 }
- const b = await bench(graph(row('pet')), { pet: visible('pet', effects) })
- await b.modules.entries.reload('pet', 'r1')
- await b.modules.entries.sync(graph(row('pet', 'r1')))
- expect(effects).toEqual({ mounted: 2, disposed: 1, hits: 0 })
- b.arrival(async () => { throw new Error('offline') })
- await expect(b.modules.entries.reload('pet', 'r2')).rejects.toThrow('offline')
- expect(effects.mounted).toBe(2)
- b.arrival(async () => {})
- await b.modules.entries.retry()
- expect(effects).toEqual({ mounted: 3, disposed: 2, hits: 0 })
- expect(document.querySelectorAll('style[data-plugin=pet]')).toHaveLength(1)
- })
- it('rejects malformed graphs before mutating active entries', async () => {
- const b = await bench(graph(row('a')), { a: () => ({ apply() {} }) })
- expect(() => b.modules.entries.sync({ rev: 'bad', entries: [{ id: 'a' }], batches: [] })).toThrow('string id/url/rev')
- expect([...b.ctx.loader.entries()].map(entry => entry.options.name)).toEqual(['a'])
- expect(() => b.modules.entries.start(b.ctx.loader, b.modules.manifest)).toThrow('already started')
- })
- })
- it('publishes stable local snapshots and contains a failing subscriber', async () => {
- const b = await bench(graph(), {})
- const error = vi.spyOn(console, 'error').mockImplementation(() => {})
- const listener = vi.fn()
- const removeBad = b.modules.entries.state.subscribe(() => { throw new Error('subscriber') })
- const remove = b.modules.entries.state.subscribe(listener)
- expect(b.modules.entries.state.getSnapshot()).toBe(b.modules.entries.state.getSnapshot())
- await b.modules.entries.retry()
- expect(listener).toHaveBeenCalledTimes(2)
- expect(error).toHaveBeenCalled()
- removeBad()
- remove()
- await b.modules.entries.retry()
- expect(listener).toHaveBeenCalledTimes(2)
- })
- it('ignores superseded queued snapshots and rebuilds of absent or unchanged entries', async () => {
- const b = await bench(graph(), { a: () => ({ apply() {} }) })
- const first = b.modules.entries.sync(graph(row('a')))
- const second = b.modules.entries.sync(graph())
- await Promise.all([first, second])
- expect(b.fetched).toEqual([])
- await b.modules.entries.reload('a', 'r1')
- await b.modules.entries.sync(graph(row('a')))
- await b.modules.entries.reload('a', 'r0')
- await b.ctx.fiber.dispose()
- await b.modules.entries.reload('a', 'r2')
- await b.modules.entries.sync(graph())
- expect(b.fetched).toHaveLength(1)
- })
- it('reports reconciliation before startup and missing service activation without losing later retries', async () => {
- const b = await bench(graph(), { pending: () => ({ inject: ['missing'], apply() {} }) }, false)
- await expect(b.modules.entries.sync(graph())).rejects.toThrow('have not started')
- await b.modules.entries.start(b.ctx.loader, b.modules.manifest)
- await b.modules.entries.sync(graph(row('pending')))
- expect(b.modules.entries.state.getSnapshot().failures[0]?.message).toContain('waiting for activation')
- b.ctx.provide('missing', {})
- await b.modules.entries.retry()
- expect(b.modules.entries.state.getSnapshot().failures).toEqual([])
- })
- it('retries a materialization failure with the same graph and cleans its partial styles', async () => {
- let broken = true
- const b = await bench(graph(), { a: () => {
- const style = document.createElement('style')
- style.dataset.plugin = 'a'
- document.head.append(style)
- if (broken) throw new Error('factory failed')
- return { apply() {} }
- } })
- await b.modules.entries.sync(graph(row('a')))
- expect(b.modules.entries.state.getSnapshot().failures[0]?.message).toContain('factory failed')
- expect(document.querySelectorAll('style[data-plugin=a]')).toHaveLength(0)
- await b.modules.entries.retry()
- expect(b.modules.entries.state.getSnapshot().failures[0]?.message).toContain('factory failed')
- await b.modules.entries.reload('a', 'r1').catch(() => {})
- expect(b.modules.entries.state.getSnapshot().failures[0]?.message).toContain('factory failed')
- broken = false
- await b.modules.entries.retry()
- expect(b.modules.entries.state.getSnapshot().failures).toEqual([])
- expect(document.querySelectorAll('style[data-plugin=a]')).toHaveLength(1)
- })
- it('does not finish a stale code replacement after download or asynchronous teardown', async () => {
- const effects = { mounted: 0, disposed: 0, hits: 0 }
- const cleanup = deferred()
- const b = await bench(graph(row('a')), { a: visible('a', effects, () => cleanup.promise) })
- const download = deferred()
- const started = deferred()
- b.arrival(async () => { started.resolve(); await download.promise })
- const rebuilding = b.modules.entries.reload('a', 'r1')
- await started.promise
- const snapshot = b.modules.entries.sync(graph(row('a')))
- download.resolve()
- await Promise.all([rebuilding, snapshot])
- expect(effects.mounted).toBe(1)
- const swapping = b.modules.entries.reload('a', 'r2')
- await vi.waitFor(() => { expect(document.querySelector('[data-live=a]')).toBeNull() })
- const disabling = b.modules.entries.sync(graph())
- cleanup.resolve()
- await Promise.all([swapping, disabling])
- expect(effects.mounted).toBe(1)
- expect(b.modules.loadCache.has('a')).toBe(false)
- })
- it('stops an obsolete multi-entry application after awaiting removal', async () => {
- const cleanup = deferred()
- const effects = { mounted: 0, disposed: 0, hits: 0 }
- const b = await bench(graph(row('a')), { a: visible('a', effects, () => cleanup.promise), b: () => ({ apply() {} }) })
- const first = b.modules.entries.sync(graph(row('b')))
- await vi.waitFor(() => { expect(document.querySelector('[data-live=a]')).toBeNull() })
- const latest = b.modules.entries.sync(graph())
- cleanup.resolve()
- await Promise.all([first, latest])
- expect(b.fetched).toEqual(['/batch'])
- })
- it('keeps bootstrap ownership explicit and diagnoses removal without changing entries', async () => {
- const b = await bench(graph(row('bootstrap')), {})
- await expect(b.modules.entries.sync(graph())).rejects.toThrow('removing bootstrap module')
- expect([...b.ctx.loader.entries()].map(entry => entry.options.name)).toEqual(['bootstrap'])
- expect(b.modules.entries.state.getSnapshot().syncing).toBe(false)
- await b.modules.entries.sync(graph(row('bootstrap')))
- expect(b.modules.entries.state.getSnapshot().failures).toEqual([])
- })
- it('retains unrelated style tags while reporting a failed replacement', async () => {
- let broken = false
- const effects = { mounted: 0, disposed: 0, hits: 0 }
- const b = await bench(graph(row('a')), { a: () => {
- if (broken) throw new Error('changed factory failed')
- return visible('a', effects)()
- } })
- const unrelated = document.createElement('style')
- unrelated.dataset.plugin = 'unrelated'
- document.head.append(unrelated)
- broken = true
- await b.modules.entries.sync(graph(row('a', 'r1')))
- expect(b.modules.entries.state.getSnapshot().failures[0]?.message).toContain('changed factory failed')
- expect(unrelated.isConnected).toBe(true)
- })
- it('keeps unrelated page failures visible when a rebuilt download fails', async () => {
- const b = await bench(graph(row('a'), row('bad')), {
- a: () => ({ apply() {} }),
- bad: () => ({ apply() { throw new Error('bad apply') } }),
- })
- await b.modules.entries.retry()
- b.arrival(async () => { throw new Error('a download') })
- await expect(b.modules.entries.reload('a', 'r1')).rejects.toThrow('a download')
- expect(b.modules.entries.state.getSnapshot().failures.map(failure => failure.id)).toEqual(['bad', 'a'])
- })
- it('retains a rejected Loader entry for retry instead of creating an orphan sibling', async () => {
- let broken = true
- const b = await bench(graph(), { a: () => broken ? { default: 'invalid' } : { apply() {} } })
- await b.modules.entries.sync(graph(row('a')))
- const entry = [...b.ctx.loader.entries()][0]!
- expect(b.modules.entries.state.getSnapshot().failures[0]?.message).toContain('invalid plugin')
- expect([...b.ctx.loader.entries()]).toHaveLength(1)
- broken = false
- await b.modules.entries.retry()
- expect([...b.ctx.loader.entries()]).toEqual([entry])
- expect(entry.fiber?.state).toBe(2)
- })
- it('reports Loader import failures and recovers the same entry after the importer recovers', async () => {
- const b = await bench(graph(), { a: () => ({ apply() {} }) })
- let broken = true
- b.ctx.loader.internal = {
- version: 'client', import: async (id: string) => {
- if (broken) throw new Error('Loader import failed')
- return b.modules.import(id)
- },
- } as never
- await b.modules.entries.sync(graph(row('a')))
- expect(b.modules.entries.state.getSnapshot().failures[0]?.message).toContain('import failed')
- const entry = [...b.ctx.loader.entries()][0]!
- broken = false
- await b.modules.entries.retry()
- expect(entry.fiber?.state).toBe(2)
- broken = true
- await expect(b.modules.entries.reload('a', 'r1')).rejects.toThrow('import failed')
- expect(b.modules.entries.state.getSnapshot().failures[0]?.id).toBe('a')
- })
- it.each([false, true])('does not mount a materialized factory after a newer removal (rebuild: %s)', async (rebuild) => {
- let stop: Promise<void> | undefined
- let materializations = 0
- let mounted = 0
- const factories = { a: () => {
- materializations++
- if (materializations === (rebuild ? 2 : 1)) {
- queueMicrotask(() => { stop = b.modules.entries.sync(graph()) })
- }
- return { apply() { mounted++ } }
- } }
- const b = await bench(graph(), factories)
- if (rebuild) {
- // The first materialization remains live until the replacement reaches the import barrier.
- materializations = -1
- await b.modules.entries.sync(graph(row('a')))
- materializations = 1
- await b.modules.entries.reload('a', 'r1')
- } else {
- await b.modules.entries.sync(graph(row('a')))
- }
- await stop
- expect(mounted).toBe(rebuild ? 1 : 0)
- expect([...b.ctx.loader.entries()]).toHaveLength(0)
- })
- it('coalesces an overlapping graph snapshot with the same rebuilt artifact', async () => {
- const effects = { mounted: 0, disposed: 0, hits: 0 }
- const b = await bench(graph(row('a')), { a: visible('a', effects) })
- const download = deferred()
- const started = deferred()
- b.arrival(async () => { started.resolve(); await download.promise })
- const rebuilding = b.modules.entries.reload('a', 'r1')
- await started.promise
- const syncing = b.modules.entries.sync(graph(row('a', 'r1')))
- download.resolve()
- await Promise.all([rebuilding, syncing])
- expect(b.fetched).toEqual(['/batch', row('a', 'r1').url])
- expect(effects).toEqual({ mounted: 2, disposed: 1, hits: 0 })
- })
- it.each(['graph', 'rebuilt'])('replaces a failed factory before entry creation on a new %s revision', async (source) => {
- const effects = { mounted: 0, disposed: 0, hits: 0 }
- const factories: Record<string, ClientBundleRegistration['factory']> = { a: () => { throw new Error('broken r0 factory') } }
- const b = await bench(graph(), factories)
- await b.modules.entries.sync(graph(row('a')))
- expect([...b.ctx.loader.entries()]).toHaveLength(0)
- expect(b.modules.entries.state.getSnapshot().failures[0]?.message).toContain('broken r0 factory')
- factories.a = () => ({ ...visible('a', effects)(), revision: 'r1' })
- if (source === 'graph') await b.modules.entries.sync(graph(row('a', 'r1')))
- else await b.modules.entries.reload('a', 'r1')
- await b.modules.entries.retry()
- expect(b.fetched).toEqual([row('a').url, row('a', 'r1').url])
- expect(await b.modules.import('a')).toHaveProperty('revision', 'r1')
- expect(document.querySelectorAll('[data-live=a]')).toHaveLength(1)
- expect(effects.mounted).toBe(1)
- expect(b.modules.entries.state.getSnapshot().failures).toEqual([])
- })
- it('replaces a superseded arrival and its cached dependency before mounting the latest code', async () => {
- const dependency = row('dependency')
- const consumer = row('consumer', 'r0', { external: ['dependency/client'] })
- const factories: Record<string, ClientBundleRegistration['factory']> = {
- dependency: () => ({ apply() {}, revision: 'r0' }),
- consumer: require => ({ apply() {}, dependency: require('dependency/client'), revision: 'r0' }),
- }
- const b = await bench(graph(), factories)
- const started = deferred()
- const download = deferred()
- b.arrival(async (url) => { if (url === consumer.url) { started.resolve(); await download.promise } })
- const old = b.modules.entries.sync(graph(consumer, dependency))
- try {
- await started.promise
- factories.dependency = () => ({ apply() {}, revision: 'r1' })
- factories.consumer = require => ({ apply() {}, dependency: require('dependency/client'), revision: 'r1' })
- const latest = b.modules.entries.sync(graph(row('consumer', 'r1', { external: ['dependency/client'] }), row('dependency', 'r1')))
- download.resolve()
- await Promise.all([old, latest])
- expect(await b.modules.import('consumer')).toMatchObject({ revision: 'r1', dependency: { revision: 'r1' } })
- expect(b.fetched).toEqual([dependency.url, consumer.url, row('dependency', 'r1').url, row('consumer', 'r1').url])
- expect(b.modules.entries.state.getSnapshot().failures).toEqual([])
- } finally {
- download.resolve()
- await old
- }
- })
- it.each(['graph', 'rebuilt'])('preserves bootstrap and dependent fibers when a %s requests new bootstrap code', async (source) => {
- const effects = { mounted: 0, disposed: 0, hits: 0 }
- const b = await bench(graph(row('bootstrap'), row('consumer')), {
- consumer: () => ({ ...visible('consumer', effects)(), inject: ['modules'] }),
- })
- const fibers = [...b.ctx.loader.entries()].map(entry => entry.fiber)
- const exports = await b.modules.import('bootstrap')
- if (source === 'graph') await b.modules.entries.sync(graph(row('bootstrap', 'r1'), row('consumer')))
- else await expect(b.modules.entries.reload('bootstrap', 'r1')).rejects.toThrow('requires a page reload')
- for (let retry = 0; retry < 2; retry++) {
- await b.modules.entries.retry()
- expect(b.modules.entries.state.getSnapshot().failures).toEqual([
- { id: 'bootstrap', message: 'Error: client-modules: replacing bootstrap module bootstrap requires a page reload' },
- ])
- }
- expect([...b.ctx.loader.entries()].map(entry => entry.fiber)).toEqual(fibers)
- expect(await b.modules.import('bootstrap')).toBe(exports)
- expect(effects).toEqual({ mounted: 1, disposed: 0, hits: 0 })
- expect(b.fetched).toEqual(['/batch'])
- })
- it('discards a failed arrival target when an uncreated entry receives a newer graph', async () => {
- const b = await bench(graph(), { a: () => { throw new Error('r0 factory') } })
- await b.modules.entries.sync(graph(row('a')))
- b.arrival(async () => { throw new Error('offline r1') })
- await b.modules.entries.reload('a', 'r1')
- expect(b.modules.entries.state.getSnapshot().failures[0]?.message).toContain('offline r1')
- b.arrival(async () => {})
- await b.modules.entries.sync(graph(row('a', 'r2')))
- expect(b.fetched).toEqual([row('a').url, row('a', 'r1').url, row('a', 'r2').url])
- })
- it('uses the latest desired revision when a rebuild queues before entry creation', async () => {
- const b = await bench(graph(), { a: () => ({ apply() {} }) })
- const adding = b.modules.entries.sync(graph(row('a')))
- const rebuilding = b.modules.entries.reload('a', 'r1')
- const latest = b.modules.entries.sync(graph(row('a', 'r2')))
- await Promise.all([adding, rebuilding, latest])
- expect(b.fetched).toEqual([row('a', 'r2').url])
- expect([...b.ctx.loader.entries()]).toHaveLength(1)
- })
- it('cleans styles from a materialized factory superseded before its entry is created', async () => {
- const effects = { mounted: 0, disposed: 0, hits: 0 }
- let latest: Promise<void> | undefined
- const factories: Record<string, ClientBundleRegistration['factory']> = { a: () => {
- const old = visible('a', effects)()
- queueMicrotask(() => {
- factories.a = visible('a', effects)
- latest = b.modules.entries.sync(graph(row('a', 'r1')))
- })
- return old
- } }
- const b = await bench(graph(), factories)
- await b.modules.entries.sync(graph(row('a')))
- await latest
- expect(b.fetched).toEqual([row('a').url, row('a', 'r1').url])
- expect(effects).toEqual({ mounted: 1, disposed: 0, hits: 0 })
- expect(document.querySelectorAll('style[data-plugin=a]')).toHaveLength(1)
- })
|