/** * The base of the picture and font libraries: a file asked for is fetched once, and the listeners * hear when it lands. The libraries are module singletons and outlive an editor that was signed out * of, so a listener can belong to a widget that is gone (#196). */ import { describe, expect, it, vi } from 'vitest'; import { FileLibrary } from '../../src/viewport/FileLibrary'; /** A library whose loads a test resolves by hand. */ class HandLibrary extends FileLibrary { readonly kept = new Map(); private readonly pending: (() => void)[] = []; constructor() { super('/files', 'test'); } ask(file: string): void { if (!this.kept.has(file)) this.startLoading(file); } land(): Promise { for (const resolve of this.pending.splice(0)) resolve(); return new Promise((resolve) => setTimeout(resolve, 0)); } protected fetchFile(url: string): Promise { return new Promise((resolve) => this.pending.push(() => resolve(url))); } protected keep(file: string, loaded: string): void { this.kept.set(file, loaded); } protected holds(file: string): boolean { return this.kept.has(file); } get waiting(): number { return this.pending.length; } } describe('the file library', () => { it('tells every listener when a file lands, even after one of them threw', async () => { const library = new HandLibrary(); const error = vi.spyOn(console, 'error').mockImplementation(() => {}); // First in line, as the canvas of an editor signed out of is: its graphics are destroyed. library.onChange(() => { throw new TypeError("Cannot read properties of null (reading 'clear')"); }); const later = vi.fn(); library.onChange(later); library.ask('a.png'); await library.land(); expect(library.kept.get('a.png')).toBe('/files/a.png'); expect(later).toHaveBeenCalledTimes(1); // Not swallowed: the broken listener still shows in the console. expect(error).toHaveBeenCalledWith('[test] a listener failed', expect.any(TypeError)); error.mockRestore(); }); it('forgets a listener once its unsubscribe has run', async () => { const library = new HandLibrary(); const gone = vi.fn(); const stop = library.onChange(gone); stop(); library.ask('a.png'); await library.land(); expect(gone).not.toHaveBeenCalled(); }); it('preloads every file once, before anything draws with it', async () => { const library = new HandLibrary(); library.preload(['a.png', 'b.png', 'a.png', '']); expect(library.waiting).toBe(2); // Asked again while on its way, and again once it is held: no second fetch either time. library.preload(['a.png', 'b.png']); expect(library.waiting).toBe(2); await library.land(); expect([...library.kept.keys()]).toEqual(['a.png', 'b.png']); library.preload(['a.png', 'b.png', 'c.png']); expect(library.waiting).toBe(1); }); });