mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-24 16:03:02 +00:00
00018959c6
Every add in the Studio (a sticker, a shape, an icon, a text, a frame, a line, a travel element, a photo from the content panel or a drop), a text style and a grid of frames appended without looking at the page's cap of 90 elements, which paste and duplicate already kept. The save then failed with a 400 and every later save of the book failed the same way. Each of them now leaves a full page as it is and selects nothing, as a paste does. The opacity slider committed on every key let go on it, so the Tab that moved the focus there wrote the value it was last moved to, an empty undo step at best and another colour's opacity at worst. It commits once after it moved, on the key or the pointer let go or when the focus leaves. The Theme panel threw away colours, faces and motifs being tried whenever the book came back from a save by another editor or the assistant, because the same theme read anew is a new object. The draft now follows the book's theme only when the theme itself changed.
188 lines
11 KiB
TypeScript
188 lines
11 KiB
TypeScript
import { describe, it, expect, beforeEach } from 'vitest'
|
|
import type { BookDocument, BookElement, BookPageSetup, BookSpread } from '@trek/shared'
|
|
import {
|
|
MAX_BOOK_LAYOUTS, MAX_SPREAD_ELEMENTS, bookBadgeElementSchema, bookLayoutSchema, bookPageSetupSchema,
|
|
bookPhotoElementSchema, bookShapeElementSchema, bookTextElementSchema,
|
|
applySavedBookLayout as applySavedLayout, bookLayoutFromSpread as layoutFromSpread,
|
|
bookLayoutPreview as layoutPreview, bookLayoutRole as layoutRole, nextBookLayoutName as nextLayoutName,
|
|
} from '@trek/shared'
|
|
import { pastedElements } from '../../../src/components/Studio/studioClipboard'
|
|
import { useStudioStore } from '../../../src/store/studioStore'
|
|
|
|
/**
|
|
* Copying between pages and a book's own layouts (#2316).
|
|
*
|
|
* What matters is what lands in the document: fresh ids on every copy, because
|
|
* the book is edited by several people at once; nothing pasted off the paper;
|
|
* and a layout that keeps the design while the page it is applied to keeps its
|
|
* pictures, its words and its own day and coordinates.
|
|
*/
|
|
|
|
const page: BookPageSetup = bookPageSetupSchema.parse({ preset: 'square-210', pageWidth: 210, pageHeight: 210, bleed: 3, safe: 5 })
|
|
|
|
const text = (id: string, words: string, size = 10, over: Record<string, unknown> = {}): BookElement =>
|
|
bookTextElementSchema.parse({ id, kind: 'text', frame: { x: 20, y: 20, w: 80, h: 20 }, text: words, size, ...over }) as BookElement
|
|
const photo = (id: string, photoId: number | null, x = 10): BookElement =>
|
|
bookPhotoElementSchema.parse({ id, kind: 'photo', frame: { x, y: 60, w: 60, h: 60 }, photoId, frameStyle: 'polaroid' }) as BookElement
|
|
const shape = (id: string): BookElement =>
|
|
bookShapeElementSchema.parse({ id, kind: 'shape', frame: { x: 0, y: 150, w: 420, h: 60 } }) as BookElement
|
|
const badge = (id: string, variant: string, words: string): BookElement =>
|
|
bookBadgeElementSchema.parse({ id, kind: 'badge', variant, frame: { x: 5, y: 5, w: 30, h: 8 }, text: words }) as BookElement
|
|
|
|
const spread = (id: string, elements: BookElement[], role: BookSpread['role'] = 'inner'): BookSpread =>
|
|
({ id, role, background: null, elements, parked: [], entryId: null })
|
|
|
|
function load(doc: Partial<BookDocument>) {
|
|
useStudioStore.getState().load({ version: 1, title: 'T', page, spreads: [], ...doc } as BookDocument)
|
|
}
|
|
|
|
describe('pastedElements', () => {
|
|
it('FE-STUDIO-CLIP-001: every paste gets fresh ids and arrives unlocked where it was', () => {
|
|
const clip = { fromSpreadId: 'a', elements: [{ ...photo('p1', 5), locked: true } as BookElement] }
|
|
const first = pastedElements(clip, spread('b', []), page)
|
|
const second = pastedElements(clip, spread('b', []), page)
|
|
expect(first[0].id).not.toBe('p1')
|
|
expect(first[0].id).not.toBe(second[0].id)
|
|
expect(first[0]).toMatchObject({ locked: false, frame: { x: 10, y: 60, w: 60, h: 60 } })
|
|
})
|
|
|
|
it('FE-STUDIO-CLIP-002: back on its own page the copy steps aside, onto a single page it is pulled inside', () => {
|
|
const onRight = { ...photo('p1', 5, 300) } as BookElement
|
|
const same = pastedElements({ fromSpreadId: 'a', elements: [onRight] }, spread('a', []), page)
|
|
expect(same[0].frame).toMatchObject({ x: 304, y: 64 })
|
|
const cover = pastedElements({ fromSpreadId: 'a', elements: [onRight] }, spread('c', [], 'cover'), page)
|
|
expect(cover[0].frame).toMatchObject({ x: 150, w: 60 })
|
|
const wide = pastedElements({ fromSpreadId: 'a', elements: [shape('s1')] }, spread('c', [], 'cover'), page)
|
|
expect(wide[0].frame).toMatchObject({ x: 0, w: 210 })
|
|
})
|
|
})
|
|
|
|
describe('saved layouts', () => {
|
|
it('FE-STUDIO-LAYOUT-001: a kept layout is the design without the pictures, and reads through the contract', () => {
|
|
const layout = layoutFromSpread(spread('a', [photo('p1', 5), text('t1', 'Harbour', 22)]), page, 'Layout 1')
|
|
expect(layout.elements[0]).toMatchObject({ kind: 'photo', photoId: null, frameStyle: 'polaroid' })
|
|
expect(layout).toMatchObject({ role: 'inner', pageWidth: 210, pageHeight: 210, name: 'Layout 1' })
|
|
expect(bookLayoutSchema.safeParse(layout).success).toBe(true)
|
|
expect(layoutRole(spread('c', [], 'cover'))).toBe('single')
|
|
})
|
|
|
|
it('FE-STUDIO-LAYOUT-002: names count on past the ones taken', () => {
|
|
const l = (name: string) => ({ ...layoutFromSpread(spread('a', []), page, name) })
|
|
expect(nextLayoutName([], 'Layout')).toBe('Layout 1')
|
|
expect(nextLayoutName([l('Layout 2')], 'Layout')).toBe('Layout 3')
|
|
})
|
|
|
|
it('FE-STUDIO-LAYOUT-003: applying keeps the page\'s pictures, words and marks, in the layout\'s design', () => {
|
|
const drawn = spread('a', [
|
|
shape('s1'), photo('p1', 1), photo('p2', 2, 80),
|
|
text('t-title', 'Old title', 24), text('t-body', 'Old story', 10, { binding: { source: 'entry.story' } }),
|
|
text('t-label', 'CHAPTER', 8), badge('b1', 'day', 'DAY 1'),
|
|
])
|
|
const layout = layoutFromSpread(drawn, page, 'Layout 1')
|
|
const target = spread('b', [
|
|
photo('q1', 11), text('u-title', 'Reykjavík', 22), badge('c1', 'day', 'DAY 2'),
|
|
])
|
|
const out = applySavedLayout(target, layout, page)
|
|
const kinds = out.elements.map(e => e.kind)
|
|
expect(kinds).toEqual(['shape', 'photo', 'photo', 'text', 'text', 'badge'])
|
|
const photos = out.elements.filter(e => e.kind === 'photo') as Extract<BookElement, { kind: 'photo' }>[]
|
|
expect(photos.map(p => p.photoId)).toEqual([11, null])
|
|
expect(photos[0].frameStyle).toBe('polaroid')
|
|
const texts = out.elements.filter(e => e.kind === 'text') as Extract<BookElement, { kind: 'text' }>[]
|
|
// The title takes the largest box; the bound story box had nothing to take and is left out; the label stays.
|
|
expect(texts.map(t => [t.text, t.size])).toEqual([['Reykjavík', 24], ['CHAPTER', 8]])
|
|
const day = out.elements.find(e => e.kind === 'badge') as Extract<BookElement, { kind: 'badge' }>
|
|
expect(day.text).toBe('DAY 2')
|
|
expect(new Set(out.elements.map(e => e.id)).size).toBe(out.elements.length)
|
|
expect(out.elements.some(e => ['s1', 'p1', 't-title', 'b1'].includes(e.id))).toBe(false)
|
|
})
|
|
|
|
it('FE-STUDIO-LAYOUT-004: what the layout has no room for is parked, and a new trim scales the design', () => {
|
|
const layout = { ...layoutFromSpread(spread('a', [photo('p1', 1)]), page, 'L'), pageWidth: 105, pageHeight: 105 }
|
|
const out = applySavedLayout(spread('b', [photo('q1', 11), photo('q2', 12), text('u', 'Words')]), layout, page)
|
|
expect(out.elements[0].frame).toMatchObject({ x: 20, y: 120, w: 120, h: 120 })
|
|
expect(out.parked.map(e => (e.kind === 'photo' ? e.photoId : e.kind))).toEqual([12, 'text'])
|
|
})
|
|
|
|
it('FE-STUDIO-LAYOUT-005: the card draws the layout on this book\'s page, as a single page when it is one', () => {
|
|
const layout = { ...layoutFromSpread(spread('c', [photo('p1', 1)], 'cover'), page, 'L'), pageWidth: 105 }
|
|
const preview = layoutPreview(layout, page)
|
|
expect(preview.role).toBe('first')
|
|
expect(preview.elements[0].frame.x).toBe(20)
|
|
})
|
|
})
|
|
|
|
describe('studio store: clipboard and layouts', () => {
|
|
beforeEach(() => useStudioStore.setState({ clipboard: null }))
|
|
|
|
it('FE-STUDIO-STORE-CLIP-001: copy on one page, paste on another, as one undo step with the copy selected', () => {
|
|
load({ spreads: [spread('a', [photo('p1', 5), text('t1', 'Keep')]), spread('b', [])] })
|
|
const s = useStudioStore.getState()
|
|
s.copy(0, ['p1'])
|
|
s.paste(1)
|
|
const after = useStudioStore.getState()
|
|
const pasted = after.doc!.spreads[1].elements
|
|
expect(pasted).toHaveLength(1)
|
|
expect(pasted[0]).toMatchObject({ kind: 'photo', photoId: 5 })
|
|
expect(after.selection).toEqual([pasted[0].id])
|
|
after.undo()
|
|
expect(useStudioStore.getState().doc!.spreads[1].elements).toHaveLength(0)
|
|
})
|
|
|
|
it('FE-STUDIO-STORE-CLIP-002: nothing copied, nothing pasted; an unknown id copies nothing', () => {
|
|
load({ spreads: [spread('a', [photo('p1', 5)])] })
|
|
const s = useStudioStore.getState()
|
|
s.paste(0)
|
|
s.copy(0, ['missing'])
|
|
expect(useStudioStore.getState().clipboard).toBeNull()
|
|
expect(useStudioStore.getState().doc!.spreads[0].elements).toHaveLength(1)
|
|
})
|
|
|
|
it('FE-STUDIO-STORE-LAYOUT-001: save, apply to another page, delete', () => {
|
|
load({ spreads: [spread('a', [photo('p1', 1), text('t1', 'Title', 24)]), spread('b', [photo('q1', 9)])] })
|
|
expect(useStudioStore.getState().saveLayout(0, 'Layout 1')).toBe(true)
|
|
const layout = useStudioStore.getState().doc!.layouts![0]
|
|
useStudioStore.getState().applyLayout(1, layout.id)
|
|
const applied = useStudioStore.getState().doc!.spreads[1].elements
|
|
expect(applied.map(e => e.kind)).toEqual(['photo', 'text'])
|
|
expect(applied[0]).toMatchObject({ photoId: 9 })
|
|
useStudioStore.getState().removeLayout(layout.id)
|
|
expect(useStudioStore.getState().doc!.layouts).toEqual([])
|
|
})
|
|
|
|
it('FE-STUDIO-STORE-LAYOUT-003: what is locked stays where it is, at its place in the paint order', () => {
|
|
const panel = { ...shape('panel'), locked: true } as BookElement
|
|
const pinned = { ...photo('pin', 7, 300), locked: true } as BookElement
|
|
load({
|
|
spreads: [spread('a', [photo('p1', 1), text('t1', 'Title', 24)]), spread('b', [panel, photo('q1', 9), pinned])],
|
|
})
|
|
expect(useStudioStore.getState().saveLayout(0, 'Layout 1')).toBe(true)
|
|
const layout = useStudioStore.getState().doc?.layouts?.[0]
|
|
if (!layout) throw new Error('no layout kept')
|
|
useStudioStore.getState().applyLayout(1, layout.id)
|
|
const applied = useStudioStore.getState().doc?.spreads[1].elements ?? []
|
|
expect(applied[0]).toEqual(panel)
|
|
expect(applied.find(e => e.id === 'pin')).toEqual(pinned)
|
|
expect(applied.filter(e => e.kind === 'photo' && !e.locked).map(e => e.kind === 'photo' && e.photoId)).toEqual([9])
|
|
})
|
|
|
|
it('FE-STUDIO-STORE-ADD-001: a full page takes no new element, and nothing is selected for it', () => {
|
|
const full = Array.from({ length: MAX_SPREAD_ELEMENTS }, (_, i) => ({ ...shape('s'), id: `s${i}` }) as BookElement)
|
|
load({ spreads: [spread('a', full), spread('b', [])] })
|
|
const before = useStudioStore.getState().doc
|
|
expect(useStudioStore.getState().addElement(0, text('extra', 'One more'))).toBe(false)
|
|
expect(useStudioStore.getState().doc).toBe(before)
|
|
expect(useStudioStore.getState().selection).toEqual([])
|
|
expect(useStudioStore.getState().addElement(1, text('first', 'Fits'))).toBe(true)
|
|
expect(useStudioStore.getState().selection).toEqual(['first'])
|
|
})
|
|
|
|
it('FE-STUDIO-STORE-LAYOUT-002: an empty page is not a layout, and a full book says no', () => {
|
|
const full = Array.from({ length: MAX_BOOK_LAYOUTS }, (_, i) => layoutFromSpread(spread('x', []), page, `L${i}`))
|
|
load({ spreads: [spread('a', []), spread('b', [photo('p', 1)])], layouts: full })
|
|
expect(useStudioStore.getState().saveLayout(0, 'Empty')).toBe(false)
|
|
expect(useStudioStore.getState().saveLayout(1, 'One too many')).toBe(false)
|
|
expect(useStudioStore.getState().doc!.layouts).toHaveLength(MAX_BOOK_LAYOUTS)
|
|
})
|
|
})
|