feat: dashcam photo mode with telemetry frame, lens drift and postcard capture
This commit is contained in:
169
tests/unit/dashcam-controls.test.ts
Normal file
169
tests/unit/dashcam-controls.test.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { defineComponent, h, ref } from 'vue';
|
||||
import DriveControls from '@/components/DriveControls.vue';
|
||||
import { useDashcam } from '@/composables/useDashcam';
|
||||
import { readDashcam } from '@/journey/dashcam-feed';
|
||||
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);
|
||||
|
||||
const host = () => mount(defineComponent({
|
||||
setup() {
|
||||
const dashcam = useDashcam({ allowed, sceneRunning: running, drift: () => true });
|
||||
return () => h('button', { class: 'drive-controls__dashcam', onClick: dashcam.show });
|
||||
},
|
||||
}), { 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;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
allowed.value = true;
|
||||
running.value = true;
|
||||
stage = document.createElement('div');
|
||||
stage.className = 'reading-stage';
|
||||
stage.dataset.readingStage = 'live';
|
||||
document.body.append(stage);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
stage.remove();
|
||||
vi.useRealTimers();
|
||||
delete document.documentElement.dataset.dashcam;
|
||||
});
|
||||
|
||||
it('opens and closes on c, then lifts the rise state after 400 ms', () => {
|
||||
const wrapper = host();
|
||||
expect(press('c').defaultPrevented).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');
|
||||
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', () => {
|
||||
const wrapper = host();
|
||||
press('c');
|
||||
expect(press('Escape').defaultPrevented).toBe(true);
|
||||
expect(readDashcam().open).toBe(false);
|
||||
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', (_label, arrange) => {
|
||||
const wrapper = host();
|
||||
arrange();
|
||||
expect(press('c').defaultPrevented).toBe(false);
|
||||
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', () => {
|
||||
const matchMedia = vi.fn((query: string) => ({ matches: query === '(max-width: 767px)' }) as MediaQueryList);
|
||||
vi.stubGlobal('matchMedia', matchMedia);
|
||||
try {
|
||||
const wrapper = host();
|
||||
expect(press('c').defaultPrevented).toBe(false);
|
||||
expect(document.documentElement.dataset.dashcam).toBeUndefined();
|
||||
expect(readDashcam().open).toBe(false);
|
||||
wrapper.unmount();
|
||||
} finally {
|
||||
vi.unstubAllGlobals();
|
||||
}
|
||||
});
|
||||
|
||||
it('closes when motion is withdrawn while open', async () => {
|
||||
const wrapper = host();
|
||||
press('c');
|
||||
allowed.value = false;
|
||||
await vi.runOnlyPendingTimersAsync();
|
||||
expect(readDashcam().open).toBe(false);
|
||||
wrapper.unmount();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user