Files
TREK/client/src/components/Studio/printReady.test.ts
T
Maurice 30382dc366 feat(studio): a book saved by a newer Studio opens read-only instead of losing what it cannot read
- A book carries schemaRev. The reader that meets a newer one opens the book
  read-only with a reload banner and saves nothing, where it used to drop the
  elements it could not read on the next save.
- The server refuses a save, a restore or a tool edit that would write an older
  revision over a newer book or one newer than itself (409 book_schema_newer),
  on REST and through every book tool alike.
- normalizeBookDocument moves to book-normalize.ts.
- Print waits for photos eagerly and up to 15 s for an export of more than 40
  photos, and skips images without a source.
2026-10-23 14:15:01 +02:00

76 lines
3.0 KiB
TypeScript

/**
* FE-STUDIO-PRINT-001 to FE-STUDIO-PRINT-004
*
* When the print view of a book may be printed: every picture in, the SVG
* ones too wherever they sit (a map marker, a photo behind a clip path), and
* a ceiling that gives a book of many photographs longer before the Save
* button gives up waiting.
*/
import { afterEach, describe, expect, it, vi } from 'vitest';
import { printReadyCeilingMs, svgImagesToWait, whenReady } from './printReady';
const SVG = 'http://www.w3.org/2000/svg';
const XLINK = 'http://www.w3.org/1999/xlink';
/** A print document as the frame holds it, built from markup. */
function printDoc(markup: string): Document {
const doc = document.implementation.createHTMLDocument('print');
doc.body.innerHTML = markup;
return doc;
}
/** An iframe whose document and window are the given ones, as the print view's frame is once it loaded. */
function frameOf(doc: Document, win: Partial<Window> = {}): HTMLIFrameElement {
const iframe = document.createElement('iframe');
Object.defineProperty(iframe, 'contentDocument', { value: doc, configurable: true });
Object.defineProperty(iframe, 'contentWindow', { value: win, configurable: true });
return iframe;
}
afterEach(() => {
vi.useRealTimers();
});
describe('the pictures to wait for', () => {
it('FE-STUDIO-PRINT-001: finds an SVG image inside a clip path and in the defs, not only a marker photo', () => {
const doc = printDoc(
`<svg xmlns="${SVG}"><defs><clipPath id="c"><image href="/a.jpg"/></clipPath>` +
`<image id="d" href="/b.jpg"/></defs><image href="/c.jpg"/></svg>`
);
expect(svgImagesToWait(doc).map((el) => el.getAttribute('href'))).toEqual(['/a.jpg', '/b.jpg', '/c.jpg']);
});
it('FE-STUDIO-PRINT-002: takes an xlink source, and leaves out an image that names none and would never load', () => {
const doc = printDoc(`<svg xmlns="${SVG}"><image/></svg>`);
const linked = doc.createElementNS(SVG, 'image');
linked.setAttributeNS(XLINK, 'xlink:href', '/x.jpg');
doc.querySelector('svg')?.appendChild(linked);
expect(svgImagesToWait(doc)).toEqual([linked]);
});
});
describe('the ceiling', () => {
it('FE-STUDIO-PRINT-003: waits eight seconds for an ordinary book and fifteen for one of more than 40 photos', () => {
expect(printReadyCeilingMs(0)).toBe(8000);
expect(printReadyCeilingMs(40)).toBe(8000);
expect(printReadyCeilingMs(41)).toBe(15000);
});
it('FE-STUDIO-PRINT-004: holds a large book past eight seconds while an image is still loading', async () => {
vi.useFakeTimers();
const many = Array.from({ length: 41 }, (_, i) => `<image href="/p${i}.jpg"/>`).join('');
const doc = printDoc(`<svg xmlns="${SVG}">${many}</svg>`);
let ready = false;
// A window without its own timers: the wait falls back to the global one, which the fake timers drive.
void whenReady(frameOf(doc)).then(() => {
ready = true;
});
await vi.advanceTimersByTimeAsync(8100);
expect(ready).toBe(false);
await vi.advanceTimersByTimeAsync(7000);
expect(ready).toBe(true);
});
});