| 1 | import { Window } from 'happy-dom' |
| 2 | import { describe, expect, it } from 'vitest' |
| 3 | import { buildEditModeInjectScript } from '@arcsin1/presentation-editor-runtime' |
| 4 | import { |
| 5 | buildIconElementHtml, |
| 6 | buildShapeElementHtml |
| 7 | } from '../../../src/renderer/src/components/session-detail/workspace/insert-shapes' |
| 8 | |
| 9 | type Rect = Pick<DOMRect, 'left' | 'top' | 'right' | 'bottom' | 'width' | 'height'> |
| 10 | |
| 11 | const rect = (left: number, top: number, width: number, height: number): Rect => ({ |
| 12 | left, |
| 13 | top, |
| 14 | right: left + width, |
| 15 | bottom: top + height, |
| 16 | width, |
| 17 | height |
| 18 | }) |
| 19 | |
| 20 | interface EditWindow extends Window { |
| 21 | __pptEditModeReadSnapshot: (selector: string) => { |
| 22 | kind: string |
| 23 | capabilities: string[] |
| 24 | blockId?: string |
| 25 | computed: { |
| 26 | svgPaintColor?: string |
| 27 | } |
| 28 | } | null |
| 29 | __pptEditModeApplyProperties: ( |
| 30 | selector: string, |
| 31 | patch: { style?: { backgroundColor?: string } } |
| 32 | ) => void |
| 33 | } |
| 34 | |
| 35 | function setupEditWindow(innerHtml: string): EditWindow { |
| 36 | const window = new Window({ url: 'http://localhost/page.html' }) as unknown as EditWindow & { |
| 37 | ResizeObserver: typeof ResizeObserver |
| 38 | eval: (code: string) => void |
| 39 | } |
| 40 | const { document } = window |
| 41 | |
| 42 | document.body.setAttribute('data-page-id', 'page') |
| 43 | document.body.innerHTML = innerHtml |
| 44 | |
| 45 | const root = document.querySelector('.ppt-page-root') as HTMLElement |
| 46 | window.HTMLElement.prototype.getBoundingClientRect = function () { |
| 47 | if (this === root) return rect(0, 0, 1000, 600) |
| 48 | // any child element gets a non-zero rect so it passes the >=2px gate |
| 49 | return rect(100, 100, 240, 120) |
| 50 | } |
| 51 | window.HTMLElement.prototype.getClientRects = function () { |
| 52 | return [this.getBoundingClientRect()] |
| 53 | } |
| 54 | window.ResizeObserver = class { |
| 55 | observe() {} |
| 56 | disconnect() {} |
| 57 | } as unknown as typeof ResizeObserver |
| 58 | |
| 59 | window.eval(buildEditModeInjectScript()) |
| 60 | return window |
| 61 | } |
| 62 | |
| 63 | const PAGE_SHELL = ` |
| 64 | <main class="ppt-page-root" data-ppt-guard-root="1" data-ppt-width="1000" data-ppt-height="600"> |
| 65 | <main data-block-id="content" data-role="content"></main> |
| 66 | </main> |
| 67 | ` |
| 68 | |
| 69 | describe('edit-mode classify for inserted shape/icon elements', () => { |
| 70 | it('classifies a shape div as kind=shape with appearance capabilities', () => { |
| 71 | const fragment = buildShapeElementHtml({ |
| 72 | blockId: 'select-arcsin1-shape0001', |
| 73 | left: 100, |
| 74 | top: 100, |
| 75 | width: 240, |
| 76 | height: 120, |
| 77 | zIndex: 12, |
| 78 | type: 'rounded-rect' |
| 79 | }) |
| 80 | const window = setupEditWindow(PAGE_SHELL.replace('</main>', `${fragment}</main>`)) |
| 81 | const snapshot = window.__pptEditModeReadSnapshot( |
| 82 | 'body[data-page-id="page"] [data-block-id="select-arcsin1-shape0001"]' |
| 83 | ) |
| 84 | expect(snapshot).not.toBeNull() |
| 85 | expect(snapshot!.kind).toBe('shape') |
| 86 | expect(snapshot!.capabilities).toEqual(expect.arrayContaining(['appearance', 'border'])) |
| 87 | expect(snapshot!.computed.svgPaintColor).toBe('#d4e4c1') |
| 88 | }) |
| 89 | |
| 90 | it('classifies an icon div as kind=shape (visual element) with appearance capabilities', () => { |
| 91 | const fragment = buildIconElementHtml({ |
| 92 | blockId: 'select-arcsin1-icon00001', |
| 93 | iconId: 'lightbulb', |
| 94 | left: 100, |
| 95 | top: 100, |
| 96 | width: 96, |
| 97 | height: 96, |
| 98 | zIndex: 12 |
| 99 | }) |
| 100 | const window = setupEditWindow(PAGE_SHELL.replace('</main>', `${fragment}</main>`)) |
| 101 | const snapshot = window.__pptEditModeReadSnapshot( |
| 102 | 'body[data-page-id="page"] [data-block-id="select-arcsin1-icon00001"]' |
| 103 | ) |
| 104 | expect(snapshot).not.toBeNull() |
| 105 | // icon is also a painted visual element; it must not fall back to "unknown" |
| 106 | expect(snapshot!.kind).not.toBe('unknown') |
| 107 | expect(snapshot!.capabilities).toEqual(expect.arrayContaining(['appearance'])) |
| 108 | }) |
| 109 | |
| 110 | it('applies appearance color changes to inserted shape svg paint', () => { |
| 111 | const fragment = buildShapeElementHtml({ |
| 112 | blockId: 'select-arcsin1-shape0002', |
| 113 | left: 100, |
| 114 | top: 100, |
| 115 | width: 240, |
| 116 | height: 120, |
| 117 | zIndex: 12, |
| 118 | type: 'rect' |
| 119 | }) |
| 120 | const selector = 'body[data-page-id="page"] [data-block-id="select-arcsin1-shape0002"]' |
| 121 | const window = setupEditWindow(PAGE_SHELL.replace('</main>', `${fragment}</main>`)) |
| 122 | |
| 123 | window.__pptEditModeApplyProperties(selector, { style: { backgroundColor: '#ff3366' } }) |
| 124 | |
| 125 | const rect = window.document.querySelector(`${selector} svg rect`) |
| 126 | expect(rect?.getAttribute('fill')).toBe('#ff3366') |
| 127 | expect(rect?.getAttribute('stroke')).toBe('#ff3366') |
| 128 | }) |
| 129 | |
| 130 | it('applies appearance color changes to inserted icon currentColor', () => { |
| 131 | const fragment = buildIconElementHtml({ |
| 132 | blockId: 'select-arcsin1-icon00002', |
| 133 | iconId: 'lightbulb', |
| 134 | left: 100, |
| 135 | top: 100, |
| 136 | width: 96, |
| 137 | height: 96, |
| 138 | zIndex: 12 |
| 139 | }) |
| 140 | const selector = 'body[data-page-id="page"] [data-block-id="select-arcsin1-icon00002"]' |
| 141 | const window = setupEditWindow(PAGE_SHELL.replace('</main>', `${fragment}</main>`)) |
| 142 | |
| 143 | window.__pptEditModeApplyProperties(selector, { style: { backgroundColor: '#3366ff' } }) |
| 144 | |
| 145 | const icon = window.document.querySelector(selector) as HTMLElement | null |
| 146 | expect(icon?.style.color).toBe('#3366ff') |
| 147 | }) |
| 148 | }) |
| 149 |