/** * 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); } } 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(); }); });