import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { flushPromises, mount } from '@vue/test-utils'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { defineComponent, h, onMounted, ref, type Component } from 'vue'; import DashcamFrame from '@/components/DashcamFrame.vue'; import DriveControls from '@/components/DriveControls.vue'; import { DASHCAM_LOAD_TIMEOUT_MS, DASHCAM_RETRY_MS, useDashcam } from '@/composables/useDashcam'; import { cancelDashcamCapture, readDashcam } from '@/journey/dashcam-feed'; import type { JourneySnapshot } from '@/journey/types'; import { validateProductionInputs } from '@/content/schemas'; import { render } from '@/entry-server'; import type { MotionPolicy } from '@/journey/motion-policy'; const fixture = (name: string): unknown => JSON.parse(readFileSync(resolve(process.cwd(), 'tests/fixtures', name), 'utf8')); const policy: MotionPolicy = { allowSceneRequest: true, allowAutomaticVideo: true, maximumTier: 'high', reason: 'full', }; describe('dashcam server HTML', () => { it('renders nothing of the dashcam on the server', async () => { const html = await render('/', validateProductionInputs( fixture('portfolio-content.valid.json'), fixture('asset-manifest.valid.json'), fixture('site-config.preview.json'), new URL('file:///prepared-assets/'), )); expect(html).not.toMatch(/dashcam/iu); }); }); describe('dashcam button', () => { const mountControls = (dashcam?: boolean) => mount(DriveControls, { props: { policy, maximumQuality: 'high', sceneStatus: 'running', audioState: { kind: 'idle' }, ...(dashcam === undefined ? {} : { dashcam }), }, }); it('is absent unless the shell offers it', () => { expect(mountControls().find('.drive-controls__dashcam').exists()).toBe(false); expect(mountControls(false).find('.drive-controls__dashcam').exists()).toBe(false); }); it('opens on click and prefetches the frame on hover and focus', async () => { const wrapper = mountControls(true); const button = wrapper.get('.drive-controls__dashcam'); expect(button.attributes('aria-label')).toBe('Dashcam'); expect(button.attributes('aria-keyshortcuts')).toBe('c'); expect(button.find('svg').exists()).toBe(true); await button.trigger('pointerenter'); await button.trigger('focus'); await button.trigger('click'); expect(wrapper.emitted('dashcamPrefetch')).toHaveLength(2); expect(wrapper.emitted('dashcam')).toHaveLength(1); }); }); describe('useDashcam', () => { let stage: HTMLElement; const allowed = ref(true); const running = ref(true); /** Stands in for DashcamFrame: announces its mount like the real one. */ const FrameStub = defineComponent({ emits: ['ready', 'close'], setup(_props, { emit }) { onMounted(() => emit('ready')); return () => h('div', { class: 'dashcam-frame' }, [h('button', { class: 'dashcam-close', onClick: () => emit('close') }, 'Close')]); }, }); let load: ReturnType Promise<{ default: Component }>>>; const host = () => mount(defineComponent({ setup() { const dashcam = useDashcam({ allowed, sceneRunning: running, drift: () => true, load }); return () => h('div', [ h('button', { class: 'drive-controls__dashcam', onClick: dashcam.show }), dashcam.engaged.value && dashcam.frame.value ? h(dashcam.frame.value, { onReady: dashcam.ready, onClose: dashcam.close }) : null, ]); }, }), { attachTo: document.body }); const press = (key: string, init: KeyboardEventInit = {}): KeyboardEvent => { const event = new KeyboardEvent('keydown', { key, cancelable: true, bubbles: true, ...init }); window.dispatchEvent(event); return event; }; /** Lets the import promise settle and the frame mount. */ const settle = async (): Promise => { for (let i = 0; i < 4; i++) await flushPromises(); }; beforeEach(() => { vi.useFakeTimers(); allowed.value = true; running.value = true; load = vi.fn(() => Promise.resolve({ default: FrameStub as Component })); stage = document.createElement('div'); stage.className = 'reading-stage'; stage.dataset.readingStage = 'live'; document.body.append(stage); }); afterEach(() => { stage.remove(); vi.useRealTimers(); vi.unstubAllGlobals(); delete document.documentElement.dataset.dashcam; }); it('folds the page only once the frame has mounted, and closes on c', async () => { const wrapper = host(); expect(press('c').defaultPrevented).toBe(true); // The chunk is still in flight: nothing has changed on the page yet. expect(document.documentElement.dataset.dashcam).toBeUndefined(); expect(readDashcam().open).toBe(false); await settle(); expect(wrapper.find('.dashcam-frame').exists()).toBe(true); expect(document.documentElement.dataset.dashcam).toBe('on'); expect(readDashcam()).toMatchObject({ open: true, drift: true }); expect(press('C', { shiftKey: true }).defaultPrevented).toBe(false); press('c'); await settle(); expect(wrapper.find('.dashcam-frame').exists()).toBe(false); expect(document.documentElement.dataset.dashcam).toBe('rise'); expect(readDashcam().open).toBe(false); vi.advanceTimersByTime(400); expect(document.documentElement.dataset.dashcam).toBeUndefined(); wrapper.unmount(); }); it('closes on Escape and from the frame\'s own Close control', async () => { const wrapper = host(); press('c'); await settle(); expect(press('Escape').defaultPrevented).toBe(true); expect(readDashcam().open).toBe(false); vi.advanceTimersByTime(400); await wrapper.get('.drive-controls__dashcam').trigger('click'); await settle(); await wrapper.get('.dashcam-close').trigger('click'); await settle(); expect(readDashcam().open).toBe(false); expect(wrapper.find('.dashcam-frame').exists()).toBe(false); wrapper.unmount(); }); it('retries a failed frame chunk once, then opens', async () => { load.mockRejectedValueOnce(new Error('chunk')); const wrapper = host(); press('c'); await settle(); expect(document.documentElement.dataset.dashcam).toBeUndefined(); vi.advanceTimersByTime(DASHCAM_RETRY_MS); await settle(); expect(load).toHaveBeenCalledTimes(2); expect(document.documentElement.dataset.dashcam).toBe('on'); wrapper.unmount(); }); it('abandons the open with nothing changed when the chunk keeps failing, and can try again', async () => { load.mockRejectedValueOnce(new Error('chunk')).mockRejectedValueOnce(new Error('chunk')); const wrapper = host(); press('c'); await settle(); vi.advanceTimersByTime(DASHCAM_RETRY_MS); await settle(); expect(document.documentElement.dataset.dashcam).toBeUndefined(); expect(readDashcam().open).toBe(false); expect(wrapper.find('.dashcam-frame').exists()).toBe(false); // Not stuck: the next press loads again and opens. press('c'); await settle(); expect(load).toHaveBeenCalledTimes(3); expect(document.documentElement.dataset.dashcam).toBe('on'); wrapper.unmount(); }); it('a slow chunk can be cancelled, and a late arrival opens nothing', async () => { let resolveLoad: (module: { default: Component }) => void = () => {}; load.mockImplementationOnce(() => new Promise((resolve) => { resolveLoad = resolve; })); const wrapper = host(); press('c'); expect(press('Escape').defaultPrevented).toBe(true); resolveLoad({ default: FrameStub as Component }); await settle(); expect(document.documentElement.dataset.dashcam).toBeUndefined(); expect(wrapper.find('.dashcam-frame').exists()).toBe(false); wrapper.unmount(); }); it('gives up on a chunk that never arrives', async () => { load.mockImplementation(() => new Promise(() => {})); const wrapper = host(); press('c'); vi.advanceTimersByTime(DASHCAM_LOAD_TIMEOUT_MS); await settle(); expect(document.documentElement.dataset.dashcam).toBeUndefined(); // c is a fresh request again, not a "close" of a phantom open. expect(press('c').defaultPrevented).toBe(true); wrapper.unmount(); }); it('ignores c with modifiers or inside an editable field', () => { const wrapper = host(); expect(press('c', { ctrlKey: true }).defaultPrevented).toBe(false); const input = document.createElement('input'); document.body.append(input); const event = new KeyboardEvent('keydown', { key: 'c', cancelable: true, bubbles: true }); input.dispatchEvent(event); expect(event.defaultPrevented).toBe(false); expect(document.documentElement.dataset.dashcam).toBeUndefined(); input.remove(); wrapper.unmount(); }); it.each([ ['motion is withheld', () => { allowed.value = false; }], ['the scene is not running', () => { running.value = false; }], ['the reading stage is not live', () => { stage.dataset.readingStage = 'still'; }], ])('does nothing when %s', async (_label, arrange) => { const wrapper = host(); arrange(); expect(press('c').defaultPrevented).toBe(false); await settle(); expect(document.documentElement.dataset.dashcam).toBeUndefined(); expect(readDashcam().open).toBe(false); wrapper.unmount(); }); it('does nothing on a phone-width viewport, where the button is hidden too', async () => { vi.stubGlobal('matchMedia', vi.fn((query: string) => ({ matches: query === '(max-width: 767px)' }) as MediaQueryList)); const wrapper = host(); expect(press('c').defaultPrevented).toBe(false); await settle(); expect(document.documentElement.dataset.dashcam).toBeUndefined(); expect(readDashcam().open).toBe(false); wrapper.unmount(); }); it.each([ ['motion is withdrawn', () => { allowed.value = false; }], ['the scene drops back to loading', () => { running.value = false; }], ])('closes when %s while open', async (_label, arrange) => { const wrapper = host(); press('c'); await settle(); expect(readDashcam().open).toBe(true); arrange(); await settle(); expect(readDashcam().open).toBe(false); expect(wrapper.find('.dashcam-frame').exists()).toBe(false); wrapper.unmount(); }); it('closes when the window is resized to phone width while open', async () => { let narrow = false; vi.stubGlobal('matchMedia', vi.fn((query: string) => ({ matches: narrow && query === '(max-width: 767px)' }) as MediaQueryList)); const wrapper = host(); press('c'); await settle(); expect(readDashcam().open).toBe(true); narrow = true; window.dispatchEvent(new Event('resize')); await settle(); expect(readDashcam().open).toBe(false); expect(document.documentElement.dataset.dashcam).toBe('rise'); expect(wrapper.find('.dashcam-frame').exists()).toBe(false); wrapper.unmount(); }); }); describe('DashcamFrame', () => { const journey = { activeSectionId: 'infrastructure', chapter: 'pre-dawn', routeProgress: 0.47, sectionActivation: { activeHeadingOffsetVh: -40, nextHeadingOffsetVh: 120 }, } as unknown as JourneySnapshot; const mountFrame = (sceneReady = true) => mount(DashcamFrame, { props: { journey, sectionLabel: 'Infrastructure', name: 'Luca Pîslaru', city: 'Bucharest', sceneReady }, attachTo: document.body, }); beforeEach(() => { vi.useFakeTimers(); }); afterEach(() => { cancelDashcamCapture(); vi.useRealTimers(); }); it('announces its mount and always renders a Close control', async () => { const wrapper = mountFrame(false); expect(wrapper.emitted('ready')).toHaveLength(1); const close = wrapper.get('.dashcam-frame__actions button:last-child'); expect(close.text()).toMatch(/close/iu); expect(close.attributes('disabled')).toBeUndefined(); expect(document.activeElement).toBe(close.element); await close.trigger('click'); expect(wrapper.emitted('close')).toHaveLength(1); wrapper.unmount(); }); it('recovers from a capture the scene never answers', async () => { const wrapper = mountFrame(); const save = wrapper.get('.dashcam-frame__actions button:first-child'); await save.trigger('click'); expect(save.attributes('disabled')).toBeDefined(); vi.advanceTimersByTime(1_200); await flushPromises(); expect(save.attributes('disabled')).toBeUndefined(); expect(wrapper.attributes('data-unavailable')).toBeUndefined(); expect(wrapper.text()).toMatch(/try again/iu); wrapper.unmount(); }); it('takes no postcard from a click on the empty frame', async () => { const wrapper = mountFrame(); await wrapper.trigger('click'); expect(wrapper.get('.dashcam-frame__actions button:first-child').attributes('disabled')).toBeUndefined(); expect(document.documentElement.dataset.dashcamShutter).toBeUndefined(); wrapper.unmount(); }); });