// @vitest-environment jsdom import { act, createElement, type ComponentType, type ReactNode } from 'react'; import { createRoot, type Root } from 'react-dom/client'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { SceneProvider, type SceneDataController } from '@/lib/contexts/scene-context'; import { PlaybackScreenCanvas } from '@/components/slide-renderer/Editor/ScreenCanvas'; import { SlideElementPickOverlay } from '@/components/canvas/slide-element-pick-overlay'; import type { Scene, SlideContent } from '@/lib/types/stage'; vi.mock('@/lib/hooks/use-i18n', () => ({ useI18n: () => ({ t: (key: string) => key }), })); const textElement = { id: 'text-1', type: 'text', left: 24, top: 32, width: 120, height: 48, rotate: 0, content: '

Production renderer

', defaultFontName: 'Arial', defaultColor: '#111111', } as const; const content: SlideContent = { type: 'slide', canvas: { id: 'slide-1', viewportSize: 1000, viewportRatio: 0.5625, background: { type: 'solid', color: '#ffffff' }, theme: { fontName: 'Arial', fontColor: '#111111', backgroundColor: '#ffffff', themeColors: ['#111111'], }, elements: [textElement], }, }; const scene = { id: 'scene-1', stageId: 'stage-1', title: 'Renderer differential', order: 0, type: 'slide', content, } as Extract; const controller: SceneDataController = { sceneId: scene.id, sceneType: 'slide', getSnapshot: () => content, updateSceneData: () => {}, }; const TestSceneProvider = SceneProvider as ComponentType<{ controller?: SceneDataController; children?: ReactNode; }>; function rect(left: number, top: number, width: number, height: number): DOMRect { return { x: left, y: top, left, top, width, height, right: left + width, bottom: top + height, toJSON: () => ({}), } as DOMRect; } class TestResizeObserver { observe() {} disconnect() {} unobserve() {} } describe.each([ ['Legacy', undefined], ['New', 'true'], ] as const)('SlideElementPickOverlay with the real %s playback renderer', (_name, flag) => { const rendererFlag = 'NEXT_PUBLIC_MAIC_PLAYBACK_RENDERER_ENABLED'; let originalFlag: string | undefined; let container: HTMLDivElement; let root: Root; let frames: FrameRequestCallback[]; beforeEach(() => { originalFlag = process.env[rendererFlag]; if (flag === undefined) delete process.env[rendererFlag]; else process.env[rendererFlag] = flag; vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); vi.stubGlobal('ResizeObserver', TestResizeObserver); frames = []; vi.stubGlobal( 'requestAnimationFrame', vi.fn((callback: FrameRequestCallback) => { frames.push(callback); return frames.length; }), ); vi.stubGlobal('cancelAnimationFrame', vi.fn()); vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(function ( this: HTMLElement, ) { if (this.dataset.testid === 'slide-element-pick-overlay') return rect(0, 0, 800, 450); if (this.classList.contains('base-element-text')) return rect(80, 60, 120, 48); if (this.classList.contains('slide-element')) return rect(0, 0, 800, 450); if (this.classList.contains('screen-element')) return rect(0, 0, 0, 0); return rect(0, 0, 800, 450); }); Object.defineProperty(document, 'elementsFromPoint', { configurable: true, value: vi.fn(() => { throw new Error('real renderer selection must use paint geometry'); }), }); container = document.createElement('div'); document.body.appendChild(container); root = createRoot(container); }); afterEach(() => { act(() => root.unmount()); container.remove(); if (originalFlag === undefined) delete process.env[rendererFlag]; else process.env[rendererFlag] = originalFlag; vi.restoreAllMocks(); vi.unstubAllGlobals(); }); it('selects from the renderer paint node and outlines that node geometry', () => { const onPick = vi.fn(); act(() => { root.render( createElement( TestSceneProvider, { controller }, createElement( 'div', null, createElement(PlaybackScreenCanvas), createElement(SlideElementPickOverlay, { scene, onPick, onCancel: vi.fn() }), ), ), ); }); const initialFrames = frames.splice(0); act(() => initialFrames.forEach((callback) => callback(0))); if (flag) { expect( container.querySelector('.slide-element-hit-target .base-element-text'), ).not.toBeNull(); expect( (document.getElementById('screen-element-text-1') as HTMLElement).style.pointerEvents, ).toBe('none'); } else { expect(container.querySelector('#screen-element-text-1 > .base-element-text')).not.toBeNull(); } const outline = container.querySelector('.border-violet-400\\/70') as HTMLElement; expect(outline.style.cssText).toContain('left: 80px'); expect(outline.style.cssText).toContain('top: 60px'); expect(outline.style.cssText).toContain('width: 120px'); act(() => { container.querySelector('[data-testid="slide-element-pick-overlay"]')!.dispatchEvent( new MouseEvent('click', { bubbles: true, cancelable: true, clientX: 100, clientY: 80, }), ); }); expect(onPick).toHaveBeenCalledWith(expect.objectContaining({ id: 'text-1' })); expect(document.elementsFromPoint).not.toHaveBeenCalled(); }); });