| 1 | import { Window } from 'happy-dom' |
| 2 | import { describe, expect, it } from 'vitest' |
| 3 | import { |
| 4 | EDIT_MODE_CONSOLE_PREFIX, |
| 5 | buildEditModeInjectScript |
| 6 | } from '@arcsin1/presentation-editor-runtime' |
| 7 | |
| 8 | type Rect = Pick<DOMRect, 'left' | 'top' | 'right' | 'bottom' | 'width' | 'height'> |
| 9 | |
| 10 | const rect = (left: number, top: number, width: number, height: number): Rect => ({ |
| 11 | left, |
| 12 | top, |
| 13 | right: left + width, |
| 14 | bottom: top + height, |
| 15 | width, |
| 16 | height |
| 17 | }) |
| 18 | |
| 19 | describe('preview edit mode reference lines', () => { |
| 20 | it('supports element, permanent-guide, and grid snapping inside the webview guest', () => { |
| 21 | const window = new Window({ url: 'http://localhost/page.html' }) as unknown as Window & { |
| 22 | ResizeObserver: typeof ResizeObserver |
| 23 | eval: (code: string) => void |
| 24 | } |
| 25 | const { document } = window |
| 26 | const logs: string[] = [] |
| 27 | |
| 28 | document.body.setAttribute('data-page-id', 'page') |
| 29 | document.body.innerHTML = ` |
| 30 | <main class="ppt-page-root" data-ppt-guard-root="1" data-ppt-width="1000" data-ppt-height="600"> |
| 31 | <main data-block-id="content" data-role="content"> |
| 32 | <div data-block-id="drag">Drag</div> |
| 33 | <div data-block-id="target">Target</div> |
| 34 | </main> |
| 35 | </main> |
| 36 | ` |
| 37 | |
| 38 | const root = document.querySelector('.ppt-page-root') as HTMLElement |
| 39 | const content = document.querySelector('[data-block-id="content"]') as HTMLElement |
| 40 | const drag = document.querySelector('[data-block-id="drag"]') as HTMLElement |
| 41 | const target = document.querySelector('[data-block-id="target"]') as HTMLElement |
| 42 | |
| 43 | window.HTMLElement.prototype.getBoundingClientRect = function () { |
| 44 | if (this === root || this === content) return rect(0, 0, 1000, 600) |
| 45 | if (this === target) return rect(300, 100, 100, 100) |
| 46 | if (this === drag) { |
| 47 | const x = Number.parseFloat(drag.style.getPropertyValue('--ppt-drag-x')) || 0 |
| 48 | const y = Number.parseFloat(drag.style.getPropertyValue('--ppt-drag-y')) || 0 |
| 49 | return rect(100 + x, 100 + y, 100, 100) |
| 50 | } |
| 51 | return rect(0, 0, 0, 0) |
| 52 | } |
| 53 | window.HTMLElement.prototype.getClientRects = function () { |
| 54 | return [this.getBoundingClientRect()] |
| 55 | } |
| 56 | |
| 57 | document.elementFromPoint = () => drag |
| 58 | document.elementsFromPoint = () => [drag, content, root] |
| 59 | window.ResizeObserver = class { |
| 60 | observe() {} |
| 61 | disconnect() {} |
| 62 | } as unknown as typeof ResizeObserver |
| 63 | window.console.log = (value?: unknown) => logs.push(String(value)) |
| 64 | |
| 65 | window.eval(buildEditModeInjectScript()) |
| 66 | const editWindow = window as unknown as Window & { |
| 67 | __pptEditModeReadSnapPoints: () => { x: number[]; y: number[] } |
| 68 | __pptEditModeSetSnapSettings: (settings: unknown) => void |
| 69 | } |
| 70 | const dragFromTo = (startX: number, endX: number): void => { |
| 71 | drag.dispatchEvent( |
| 72 | new window.PointerEvent('pointerdown', { |
| 73 | bubbles: true, |
| 74 | cancelable: true, |
| 75 | button: 0, |
| 76 | clientX: startX, |
| 77 | clientY: 110, |
| 78 | pointerId: 1 |
| 79 | }) |
| 80 | ) |
| 81 | for (let index = 0; index < 2; index += 1) { |
| 82 | drag.dispatchEvent( |
| 83 | new window.PointerEvent('pointermove', { |
| 84 | bubbles: true, |
| 85 | cancelable: true, |
| 86 | clientX: endX, |
| 87 | clientY: 110, |
| 88 | pointerId: 1 |
| 89 | }) |
| 90 | ) |
| 91 | } |
| 92 | drag.dispatchEvent( |
| 93 | new window.PointerEvent('pointerup', { |
| 94 | bubbles: true, |
| 95 | cancelable: true, |
| 96 | button: 0, |
| 97 | clientX: endX, |
| 98 | clientY: 110, |
| 99 | pointerId: 1 |
| 100 | }) |
| 101 | ) |
| 102 | } |
| 103 | |
| 104 | expect(editWindow.__pptEditModeReadSnapPoints().x).toEqual( |
| 105 | expect.arrayContaining([300, 350, 400]) |
| 106 | ) |
| 107 | dragFromTo(110, 306) |
| 108 | |
| 109 | expect(drag.style.getPropertyValue('--ppt-drag-x')).toBe('200.0px') |
| 110 | expect(drag.classList.contains('arcsin1-presentation-editor-selected')).toBe(false) |
| 111 | expect(document.querySelector('[data-arcsin1-presentation-editor-guide="vertical"]')).not.toBeNull() |
| 112 | expect( |
| 113 | (document.querySelector('[data-arcsin1-presentation-editor-guide="vertical"]') as HTMLElement).style.display |
| 114 | ).toBe('none') |
| 115 | |
| 116 | const movedLog = logs.findLast((item) => |
| 117 | item.startsWith(`${EDIT_MODE_CONSOLE_PREFIX}{"type":"moved"`) |
| 118 | ) |
| 119 | expect(movedLog).toBeTruthy() |
| 120 | expect(JSON.parse(String(movedLog).slice(EDIT_MODE_CONSOLE_PREFIX.length))).toMatchObject({ |
| 121 | selector: 'body[data-page-id="page"] [data-block-id="drag"]', |
| 122 | x: 200, |
| 123 | deltaX: 200, |
| 124 | visualX: 300 |
| 125 | }) |
| 126 | |
| 127 | editWindow.__pptEditModeSetSnapSettings({ |
| 128 | enabled: true, |
| 129 | guides: { vertical: [500], horizontal: [] }, |
| 130 | grid: { enabled: false, size: 20 } |
| 131 | }) |
| 132 | dragFromTo(310, 506) |
| 133 | expect(drag.style.getPropertyValue('--ppt-drag-x')).toBe('400.0px') |
| 134 | |
| 135 | editWindow.__pptEditModeSetSnapSettings({ |
| 136 | enabled: true, |
| 137 | guides: { vertical: [], horizontal: [] }, |
| 138 | grid: { enabled: true, size: 100 } |
| 139 | }) |
| 140 | dragFromTo(510, 607) |
| 141 | expect(drag.style.getPropertyValue('--ppt-drag-x')).toBe('500.0px') |
| 142 | }) |
| 143 | |
| 144 | it('fails loudly when the page root is missing slide size metadata', () => { |
| 145 | const window = new Window({ url: 'http://localhost/page.html' }) as unknown as Window & { |
| 146 | ResizeObserver: typeof ResizeObserver |
| 147 | eval: (code: string) => void |
| 148 | } |
| 149 | const { document } = window |
| 150 | |
| 151 | document.body.setAttribute('data-page-id', 'page') |
| 152 | document.body.innerHTML = ` |
| 153 | <main class="ppt-page-root" data-ppt-guard-root="1"> |
| 154 | <main data-block-id="content" data-role="content"> |
| 155 | <div data-block-id="target">Target</div> |
| 156 | </main> |
| 157 | </main> |
| 158 | ` |
| 159 | |
| 160 | window.HTMLElement.prototype.getBoundingClientRect = function () { |
| 161 | return rect(0, 0, 1000, 600) |
| 162 | } |
| 163 | window.HTMLElement.prototype.getClientRects = function () { |
| 164 | return [this.getBoundingClientRect()] |
| 165 | } |
| 166 | window.ResizeObserver = class { |
| 167 | observe() {} |
| 168 | disconnect() {} |
| 169 | } as unknown as typeof ResizeObserver |
| 170 | |
| 171 | window.eval(buildEditModeInjectScript()) |
| 172 | const editWindow = window as unknown as Window & { |
| 173 | __pptEditModeReadSnapPoints: () => { x: number[]; y: number[] } |
| 174 | } |
| 175 | |
| 176 | expect(() => editWindow.__pptEditModeReadSnapPoints()).toThrow( |
| 177 | 'missing page root slide size metadata' |
| 178 | ) |
| 179 | }) |
| 180 | }) |
| 181 |