import { PageRecordType, TLDeepLink, createDeepLinkString, createShapeId } from '@tldraw/editor' import { vi } from 'vitest' import { TestEditor } from './TestEditor' vi.useFakeTimers() let editor: TestEditor afterEach(() => { editor?.dispose() }) beforeEach(() => { editor = new TestEditor() editor.createShapes([]) }) const makeUrl = (link: TLDeepLink, name = 'd') => { return `http://localhost/?${name}=${createDeepLinkString(link)}` } describe('type: viewport', () => { it('handles linking to a viewport of the same dimensions', () => { const bounds = { x: -500, y: 2342, w: editor.bounds.width, h: editor.bounds.height } expect(editor.getViewportPageBounds()).not.toMatchObject(bounds) const link = makeUrl({ type: 'viewport', bounds }) window.history.pushState({}, '', link) editor.navigateToDeepLink() expect(editor.getViewportPageBounds()).toMatchObject(bounds) }) it('handles linking to a viewport of the same dimensions (explicit url)', () => { const bounds = { x: -500, y: 2342, w: editor.bounds.width, h: editor.bounds.height } expect(editor.getViewportPageBounds()).not.toMatchObject(bounds) const url = makeUrl({ type: 'viewport', bounds }) editor.navigateToDeepLink({ url }) expect(editor.getViewportPageBounds()).toMatchObject(bounds) }) it('tries to contain the viewport if the dimensions are different (horizontal)', () => { const bounds = { x: -500, y: 2342, w: editor.bounds.width - 200, h: editor.bounds.height } // the given viewport is 200px smaller than the editor bounds, so it will shift the viewport to the left 100px // to center it horizontally const expected = { x: -600, y: 2342, w: editor.bounds.width, h: editor.bounds.height } expect(editor.getViewportPageBounds()).not.toMatchObject(expected) const url = makeUrl({ type: 'viewport', bounds }) editor.navigateToDeepLink({ url }) expect(editor.getViewportPageBounds()).toMatchObject(expected) }) it('tries to contain the viewport if the dimensions are different (vertical)', () => { const bounds = { x: -500, y: 2342, w: editor.bounds.width, h: editor.bounds.height - 200 } // the given viewport is 200px smaller than the editor bounds, so it will shift the viewport up 100px // to center it vertically const expected = { x: -500, y: 2242, w: editor.bounds.width, h: editor.bounds.height } expect(editor.getViewportPageBounds()).not.toMatchObject(expected) const url = makeUrl({ type: 'viewport', bounds }) editor.navigateToDeepLink({ url }) expect(editor.getViewportPageBounds()).toMatchObject(expected) }) it('handles linking to a viewport on a specific page', () => { const pageId = PageRecordType.createId('foo') editor.createPage({ id: pageId }) expect(editor.getCurrentPageId()).not.toBe(pageId) const bounds = { x: -500, y: 2342, w: editor.bounds.width, h: editor.bounds.height } const url = makeUrl({ type: 'viewport', bounds, pageId }) editor.navigateToDeepLink({ url }) expect(editor.getCurrentPageId()).toBe(pageId) expect(editor.getViewportPageBounds()).toMatchObject(bounds) }) it('will not change the viewport if the specific page does not exist', () => { const pageId = PageRecordType.createId('foo') const bounds = { x: -500, y: 2342, w: editor.bounds.width, h: editor.bounds.height } const url = makeUrl({ type: 'viewport', bounds, pageId }) editor.navigateToDeepLink({ url }) expect(editor.getCurrentPageId()).not.toBe(pageId) expect(editor.getViewportPageBounds()).not.toMatchObject(bounds) }) }) describe('type: page', () => { it('handles linking to a page only, and will center the content on the page', () => { const initialPageId = editor.getCurrentPageId() const pageId = PageRecordType.createId('foo') editor.createPage({ id: pageId }) editor.setCurrentPage(pageId) editor.createShapes([ { id: createShapeId(), type: 'geo', x: 200, y: 200, props: { w: 100, h: 100 } }, ]) editor.setCurrentPage(initialPageId) const url = makeUrl({ type: 'page', pageId }) editor.navigateToDeepLink({ url }) expect(editor.getCurrentPageId()).toBe(pageId) expect(editor.getViewportPageBounds()).toMatchObject({ x: 250 - editor.bounds.width / 2, y: 250 - editor.bounds.height / 2, }) }) it('wont switch page if the page does not exist, but will still center the content', () => { const initialPageId = editor.getCurrentPageId() const pageId = PageRecordType.createId('foo') editor.createShapes([ { id: createShapeId(), type: 'geo', x: 200, y: 200, props: { w: 100, h: 100 } }, ]) const url = makeUrl({ type: 'page', pageId }) editor.navigateToDeepLink({ url }) expect(editor.getCurrentPageId()).toBe(initialPageId) expect(editor.getViewportPageBounds()).toMatchObject({ x: 250 - editor.bounds.width / 2, y: 250 - editor.bounds.height / 2, }) }) }) describe('type: shapes', () => { it('keeps it a 100% if they fit within the viewport', () => { const boxA = createShapeId() const boxB = createShapeId() const boxC = createShapeId() editor.createShapes([ { id: boxA, type: 'geo', x: 100, y: 100, props: { w: 100, h: 100 } }, { id: boxB, type: 'geo', x: -200, y: -200, props: { w: 100, h: 100 } }, { id: boxC, type: 'geo', x: 300, y: 300, props: { w: 100, h: 100 } }, ]) const url = makeUrl({ type: 'shapes', shapeIds: [boxA, boxB] }) editor.navigateToDeepLink({ url }) const viewport = editor.getViewportPageBounds() expect(viewport.contains(editor.getShapePageBounds(boxA)!)).toBe(true) expect(viewport.contains(editor.getShapePageBounds(boxB)!)).toBe(true) expect(viewport.contains(editor.getShapePageBounds(boxC)!)).toBe(false) expect(viewport).toMatchObject({ w: editor.bounds.width, h: editor.bounds.height }) expect(editor.getZoomLevel()).toBe(1) }) it('zooms out if the shapes do not quite fit', () => { const boxA = createShapeId() const boxB = createShapeId() const boxC = createShapeId() editor.createShapes([ { id: boxA, type: 'geo', x: 500, y: 500, props: { w: 100, h: 100 } }, { id: boxB, type: 'geo', x: -500, y: -500, props: { w: 100, h: 100 } }, { id: boxC, type: 'geo', x: 1300, y: 1300, props: { w: 100, h: 100 } }, ]) const url = makeUrl({ type: 'shapes', shapeIds: [boxA, boxB] }) editor.navigateToDeepLink({ url }) const viewport = editor.getViewportPageBounds() expect(viewport.contains(editor.getShapePageBounds(boxA)!)).toBe(true) expect(viewport.contains(editor.getShapePageBounds(boxB)!)).toBe(true) expect(viewport.contains(editor.getShapePageBounds(boxC)!)).toBe(false) expect(viewport).not.toMatchObject({ w: editor.bounds.width, h: editor.bounds.height }) expect(editor.getZoomLevel()).toBeLessThan(1) }) it('switches to the page that most of the shapes are on', () => { const initialPageId = editor.getCurrentPageId() const otherPageId = PageRecordType.createId('foo') const boxA = createShapeId() const boxB = createShapeId() const boxC = createShapeId() editor.createShapes([{ id: boxA, type: 'geo', x: 500, y: 500, props: { w: 100, h: 100 } }]) editor.createPage({ id: otherPageId }) editor.setCurrentPage(otherPageId) editor.createShapes([ { id: boxB, type: 'geo', x: -500, y: -500, props: { w: 100, h: 100 } }, { id: boxC, type: 'geo', x: 1300, y: 1300, props: { w: 100, h: 100 } }, ]) editor.setCurrentPage(initialPageId) const url = makeUrl({ type: 'shapes', shapeIds: [boxA, boxB, boxC] }) editor.navigateToDeepLink({ url }) expect(editor.getCurrentPageId()).toBe(otherPageId) const viewport = editor.getViewportPageBounds() expect(viewport.contains(editor.getShapePageBounds(boxB)!)).toBe(true) expect(viewport.contains(editor.getShapePageBounds(boxC)!)).toBe(true) }) })