mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-24 16:03:02 +00:00
cbf9325e7e
- The element, spread, document, page operation and card unions carry a name of their own, so the declarations of every schema that holds them refer to them instead of spelling them out again. The shared declarations shrink from 9 to 5.5 MB and the server's typecheck from 4.4 to 2.9 GB, so the build fits the image's 4 GB again. - edit_book_page and revise_book_design take add by what the element is, with an id of its own; its look follows with style by that id in the same call, every look field described once. A change an assistant card kept with its look on add still reads and runs. - restore_book_page states what every element has once, then each kind's own fields and the type the five travel kinds share, without defaults; the page is read as a spread again before it goes back. - The theme links are explained once in the page edit's description, and style points a text's face, a shape's paint and outline and a photo's crop look at their one full description. - The lockup roles live in a module of their own, so the operation schemas no longer import the lockup engine, whose cycle left the operations unset in the shared tests. - The theme switches no longer fall through a comment, the newer document test follows the current revision, the line inspector names its stroke keys, a text's anchor reads no physical side, two shapes record the proportions they are drawn at, and the photo look and line tests follow the slider and the folded ornaments.
205 lines
9.6 KiB
TypeScript
205 lines
9.6 KiB
TypeScript
import {
|
|
bookLineElementSchema,
|
|
bookLinePoints,
|
|
bookPageSetupSchema,
|
|
bookPhotoElementSchema,
|
|
bookShapeElementSchema,
|
|
type BookDocument,
|
|
type BookElement,
|
|
type BookLineElement,
|
|
type BookSpread,
|
|
} from '@trek/shared';
|
|
import { beforeEach, describe, expect, it } from 'vitest';
|
|
|
|
import { LineHandles } from '../../../src/components/Studio/LineHandles';
|
|
import { DIVIDER_PRESETS, LINE_PRESETS } from '../../../src/components/Studio/linePresets';
|
|
import { constrainPoint } from '../../../src/components/Studio/lineSnap';
|
|
import { SpreadView } from '../../../src/components/Studio/SpreadView';
|
|
import { EdgeSection } from '../../../src/components/Studio/StudioEdgeControls';
|
|
import { StudioInspector } from '../../../src/components/Studio/StudioInspector';
|
|
import { StudioLineShelf } from '../../../src/components/Studio/StudioLineShelf';
|
|
import { useStudioStore } from '../../../src/store/studioStore';
|
|
import { fireEvent, render } from '../../helpers/render';
|
|
|
|
/**
|
|
* The drawn line and the cut edge in the Studio: drawn on the page, placed
|
|
* from the shelf, styled in the inspector and shaped by its handles.
|
|
*/
|
|
|
|
const page = bookPageSetupSchema.parse({});
|
|
const t = (k: string) => k;
|
|
|
|
const line = (over: Partial<BookLineElement> = {}) =>
|
|
bookLineElementSchema.parse({ id: 'l1', kind: 'line', frame: { x: 230, y: 100, w: 100, h: 4 }, ...over });
|
|
|
|
function spread(elements: BookElement[]): BookSpread {
|
|
return { id: 'sp1', role: 'inner', background: null, elements, parked: [], entryId: null };
|
|
}
|
|
|
|
function load(elements: BookElement[], selection = elements.slice(0, 1).map((e) => e.id)) {
|
|
useStudioStore.getState().load({ version: 1, title: 'T', page, spreads: [spread(elements)] } as BookDocument);
|
|
useStudioStore.getState().select(selection);
|
|
}
|
|
|
|
const els = () => useStudioStore.getState().doc?.spreads[0].elements ?? [];
|
|
const first = () => els()[0] as BookLineElement;
|
|
|
|
beforeEach(() => load([]));
|
|
|
|
describe('on the page', () => {
|
|
it('FE-STUDIO-LINEEL-001: draws a line as a stroked body with its heads filled', () => {
|
|
const { container } = render(
|
|
<SpreadView spread={spread([line({ endCap: 'arrow', dash: 'dashed' })])} page={page} />
|
|
);
|
|
const paths = [...container.querySelectorAll('svg path')];
|
|
expect(paths).toHaveLength(2);
|
|
expect(paths[0].getAttribute('stroke')).toBe('#141414');
|
|
expect(paths[0].getAttribute('stroke-dasharray')).toBe('1.5 1');
|
|
expect(paths[1].getAttribute('fill')).toBe('#141414');
|
|
});
|
|
|
|
it('FE-STUDIO-LINEEL-002: cuts a rectangle with an edge as a path, and clips a photo to its edge', () => {
|
|
const frame = { x: 10, y: 10, w: 60, h: 40 };
|
|
const rect = bookShapeElementSchema.parse({ id: 's1', kind: 'shape', frame, edge: 'zigzag', edgeSize: 3 });
|
|
const plain = bookShapeElementSchema.parse({ id: 's2', kind: 'shape', frame });
|
|
const photo = bookPhotoElementSchema.parse({ id: 'p1', kind: 'photo', frame, photoId: 4, edge: 'perforated' });
|
|
const { container } = render(<SpreadView spread={spread([rect, plain, photo])} page={page} />);
|
|
const cut = container.querySelectorAll('svg path')[0];
|
|
expect(cut.getAttribute('d')?.split('L').length).toBeGreaterThan(40);
|
|
// The clip's id is the photo's with a suffix of its own, so a spread drawn twice keeps them apart.
|
|
expect(container.querySelector('clipPath[id^="c-p1-"] path')?.getAttribute('d')).toMatch(/^M0 0L/);
|
|
});
|
|
});
|
|
|
|
describe('the shelf', () => {
|
|
it('FE-STUDIO-LINEEL-003: offers every line and divider, each labelled, and places it on the right-hand page', () => {
|
|
const added: BookElement[] = [];
|
|
const { getByLabelText, container } = render(
|
|
<StudioLineShelf page={page} single={false} onAdd={(el) => added.push(el)} t={t} />
|
|
);
|
|
expect(container.querySelectorAll('.st-shape-btn')).toHaveLength(LINE_PRESETS.length + DIVIDER_PRESETS.length);
|
|
fireEvent.click(getByLabelText('journey.studio.linePreset.route'));
|
|
fireEvent.click(getByLabelText('journey.studio.linePreset.divider-sun'));
|
|
const [route, divider] = added as BookLineElement[];
|
|
expect(route).toMatchObject({ kind: 'line', dash: 'dotted', curve: 'smooth', startCap: 'dot', endCap: 'arrow' });
|
|
expect(route.points).toHaveLength(3);
|
|
expect(bookLinePoints(route)[0][0]).toBeCloseTo(262.5, 1);
|
|
expect(divider).toMatchObject({ ornament: 'sun', strokeWidth: 0.3 });
|
|
});
|
|
|
|
it('FE-STUDIO-LINEEL-004: places a line on a single page from its own left edge', () => {
|
|
const added: BookElement[] = [];
|
|
const { getByLabelText } = render(<StudioLineShelf page={page} single onAdd={(el) => added.push(el)} t={t} />);
|
|
fireEvent.click(getByLabelText('journey.studio.linePreset.straight'));
|
|
expect(bookLinePoints(added[0] as BookLineElement)[0][0]).toBeCloseTo(52.5, 1);
|
|
});
|
|
});
|
|
|
|
describe('the inspector', () => {
|
|
const draw = () =>
|
|
render(
|
|
<StudioInspector
|
|
spreadIndex={0}
|
|
page={page}
|
|
stats={null}
|
|
path={[]}
|
|
source={{ entries: [], photos: [], photoEntries: {} }}
|
|
setPageNumbers={() => {}}
|
|
t={t}
|
|
locale="en"
|
|
/>
|
|
);
|
|
|
|
it('FE-STUDIO-LINEEL-005: styles a line, its ends and its ornament, refitting the frame for a head', () => {
|
|
load([line()]);
|
|
const { getByText, getByTitle } = draw();
|
|
fireEvent.click(getByText('journey.studio.strokeDashed'));
|
|
expect(first().dash).toBe('dashed');
|
|
fireEvent.click(getByText('journey.studio.lineCourse.smooth'));
|
|
fireEvent.click(getByText('journey.studio.lineBody.wave'));
|
|
expect(first()).toMatchObject({ curve: 'smooth', squiggle: 'wave' });
|
|
const before = first().frame.h;
|
|
fireEvent.click(document.querySelectorAll('[title="journey.studio.cap.arrow"]')[1] as HTMLElement);
|
|
expect(first().endCap).toBe('arrow');
|
|
expect(first().frame.h).toBeGreaterThan(before);
|
|
fireEvent.click(getByText('journey.studio.swapEnds'));
|
|
expect(first()).toMatchObject({ startCap: 'arrow', endCap: 'none' });
|
|
// The ornaments stay folded away until a line has one.
|
|
const ornaments = [...document.querySelectorAll('.st-section-head')].find(
|
|
(b) => b.textContent === 'journey.studio.ornamentTitle'
|
|
) as HTMLElement;
|
|
fireEvent.click(ornaments);
|
|
fireEvent.click(getByTitle('journey.studio.ornament.heart'));
|
|
expect(first().ornament).toBe('heart');
|
|
});
|
|
|
|
it('FE-STUDIO-LINEEL-006: cuts an edge on a shape from its tiles and sets how deep it bites', () => {
|
|
const frame = { x: 10, y: 10, w: 60, h: 40 };
|
|
load([bookShapeElementSchema.parse({ id: 's1', kind: 'shape', frame })]);
|
|
const { getByLabelText, container } = draw();
|
|
const head = [...container.querySelectorAll('.st-section-head')].find(
|
|
(b) => b.textContent === 'journey.studio.edgeStyle'
|
|
) as HTMLElement;
|
|
fireEvent.click(head);
|
|
fireEvent.click(getByLabelText('journey.studio.edge.perforated'));
|
|
expect(els()[0]).toMatchObject({ edge: 'perforated' });
|
|
});
|
|
});
|
|
|
|
describe('the edge controls', () => {
|
|
it('FE-STUDIO-LINEEL-007: stay away from a shape that takes no edge, and say so where one is set anyway', () => {
|
|
const set: unknown[] = [];
|
|
const none = render(
|
|
<EdgeSection edge={undefined} edgeSize={undefined} outline="heart" onSet={(p) => set.push(p)} t={t} />
|
|
);
|
|
expect(none.container.innerHTML).toBe('');
|
|
const stray = render(<EdgeSection edge="torn" edgeSize={2} outline="heart" onSet={(p) => set.push(p)} t={t} />);
|
|
expect(stray.getByText('journey.studio.edgeUnavailable')).toBeTruthy();
|
|
fireEvent.click(stray.getByLabelText('journey.studio.edge.straight'));
|
|
expect(set).toEqual([{ edge: 'straight' }]);
|
|
const photo = render(<EdgeSection edge="scallop" edgeSize={4} outline={null} onSet={(p) => set.push(p)} t={t} />);
|
|
const depth = photo.container.querySelector('.st-num input') as HTMLInputElement;
|
|
expect(depth.value).toBe('4');
|
|
});
|
|
});
|
|
|
|
describe('the handles', () => {
|
|
it('FE-STUDIO-LINEEL-008: holds a dragged end level or plumb near it, and to steps with shift', () => {
|
|
const level = constrainPoint([100, 1], [0, 0], false);
|
|
expect(level[1]).toBe(0);
|
|
expect(level[0]).toBeCloseTo(100, 1);
|
|
expect(constrainPoint([100, 30], [0, 0], false)).toEqual([100, 30]);
|
|
const stepped = constrainPoint([100, 30], [0, 0], true);
|
|
expect((Math.atan2(stepped[1], stepped[0]) * 180) / Math.PI).toBeCloseTo(15, 5);
|
|
expect(constrainPoint([5, 5], undefined, true)).toEqual([5, 5]);
|
|
});
|
|
|
|
it('FE-STUDIO-LINEEL-009: drags an end on the spread, adds a point from the middle and removes it again', () => {
|
|
load([line()]);
|
|
const draw = () => render(<LineHandles el={first()} spreadIndex={0} scaled={2} t={t} />);
|
|
const { container, unmount } = draw();
|
|
const ends = container.querySelectorAll('[title="journey.studio.addPoint"]');
|
|
expect(ends).toHaveLength(1);
|
|
const handles = container.querySelectorAll('.st-handle');
|
|
const end = handles[handles.length - 1] as HTMLElement;
|
|
fireEvent.pointerDown(end, { clientX: 0, clientY: 0, pointerId: 1 });
|
|
fireEvent.pointerMove(end, { clientX: 20, clientY: 40, pointerId: 1 });
|
|
fireEvent.pointerUp(end, { pointerId: 1 });
|
|
const moved = bookLinePoints(first());
|
|
expect(moved[1][0]).toBeCloseTo(340, 1);
|
|
expect(moved[1][1]).toBeCloseTo(122, 1);
|
|
unmount();
|
|
|
|
const again = draw();
|
|
const mid = again.container.querySelector('[title="journey.studio.addPoint"]') as HTMLElement;
|
|
fireEvent.pointerDown(mid, { clientX: 0, clientY: 0, pointerId: 2 });
|
|
fireEvent.pointerUp(mid, { pointerId: 2 });
|
|
expect(first().points).toHaveLength(3);
|
|
again.unmount();
|
|
|
|
const third = draw();
|
|
fireEvent.doubleClick(third.container.querySelector('[title="journey.studio.removePoint"]') as HTMLElement);
|
|
expect(first().points).toHaveLength(2);
|
|
});
|
|
});
|