feat: introduce the compact vehicle HUD
This commit is contained in:
@@ -4,6 +4,7 @@ import { nextTick } from 'vue';
|
||||
import SceneCanvas from '@/components/SceneCanvas.vue';
|
||||
import DriveControls from '@/components/DriveControls.vue';
|
||||
import JourneyHud from '@/components/JourneyHud.vue';
|
||||
import type { AudioUiState } from '@/audio/types';
|
||||
import type { MotionPolicy, QualityTier } from '@/journey/motion-policy';
|
||||
import type { JourneySnapshot } from '@/journey/types';
|
||||
|
||||
@@ -191,43 +192,85 @@ describe('SceneCanvas', () => {
|
||||
});
|
||||
|
||||
describe('DriveControls', () => {
|
||||
const mountControls = (motionPolicy: MotionPolicy, maximum: QualityTier = 'high') => mount(DriveControls, {
|
||||
props: { policy: motionPolicy, maximumQuality: maximum, sceneStatus: 'running' },
|
||||
const mountControls = (
|
||||
motionPolicy: MotionPolicy,
|
||||
maximum: QualityTier = 'high',
|
||||
audioState: AudioUiState = { kind: 'idle' },
|
||||
) => mount(DriveControls, {
|
||||
attachTo: document.body,
|
||||
props: { policy: motionPolicy, maximumQuality: maximum, sceneStatus: 'running', audioState },
|
||||
});
|
||||
|
||||
it('uses exact native control names and a visible non-color status', async () => {
|
||||
it('presents one explicit drive action before audio activation', async () => {
|
||||
const wrapper = mountControls(policy());
|
||||
|
||||
expect(wrapper.get('button').text()).toBe('Pause motion');
|
||||
expect(wrapper.get('select').attributes('aria-label')).toBe('Quality control');
|
||||
expect(wrapper.text()).toContain('Motion: active');
|
||||
await wrapper.get('button').trigger('click');
|
||||
expect(wrapper.emitted('pause')).toHaveLength(1);
|
||||
expect(wrapper.get('[data-drive-state="idle"]').text()).toContain('START THE DRIVE');
|
||||
expect(wrapper.find('[aria-label="Mute sound"]').exists()).toBe(false);
|
||||
await wrapper.get('[aria-label="Start the drive"]').trigger('click');
|
||||
expect(wrapper.emitted('audioStart')).toHaveLength(1);
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it('collapses the running state to named vehicle controls and an upward settings panel', async () => {
|
||||
const wrapper = mountControls(policy(), 'high', { kind: 'running', muted: false, volume: 0.4 });
|
||||
|
||||
expect(wrapper.get('[aria-label="Mute sound"]')).toBeTruthy();
|
||||
expect(wrapper.get('[aria-label="Sound volume"]')).toBeTruthy();
|
||||
expect(wrapper.get('[aria-label="Pause motion"]')).toBeTruthy();
|
||||
const settings = wrapper.get('[aria-label="Drive settings"]');
|
||||
await settings.trigger('click');
|
||||
|
||||
const panel = wrapper.get('[role="dialog"][aria-label="Drive settings"]');
|
||||
expect(panel.text()).not.toContain('Motion: active');
|
||||
expect(panel.find('[role="status"]').exists()).toBe(false);
|
||||
expect(panel.get('select').attributes('aria-label')).toBe('Quality control');
|
||||
expect(settings.attributes('aria-expanded')).toBe('true');
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it('dismisses settings with Escape and outside interaction', async () => {
|
||||
const wrapper = mountControls(policy(), 'high', { kind: 'running', muted: false, volume: 0.4 });
|
||||
const settings = wrapper.get('[aria-label="Drive settings"]');
|
||||
await settings.trigger('click');
|
||||
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
|
||||
await nextTick();
|
||||
expect(wrapper.find('[role="dialog"][aria-label="Drive settings"]').exists()).toBe(false);
|
||||
expect(document.activeElement).toBe(settings.element);
|
||||
|
||||
await settings.trigger('click');
|
||||
document.body.dispatchEvent(new MouseEvent('pointerdown', { bubbles: true }));
|
||||
await nextTick();
|
||||
expect(wrapper.find('[role="dialog"][aria-label="Drive settings"]').exists()).toBe(false);
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it.each([
|
||||
['visitor-pause', 'Resume motion'],
|
||||
['saved-static', 'Enable motion'],
|
||||
] as const)('offers the correct recovery action for %s', (reason, label) => {
|
||||
const wrapper = mountControls(policy({ allowSceneRequest: false, maximumTier: 'static', reason }));
|
||||
expect(wrapper.get('button').text()).toBe(label);
|
||||
const wrapper = mountControls(
|
||||
policy({ allowSceneRequest: false, maximumTier: 'static', reason }),
|
||||
'static',
|
||||
{ kind: 'running', muted: false, volume: 0.4 },
|
||||
);
|
||||
expect(wrapper.get(`[aria-label="${label}"]`)).toBeTruthy();
|
||||
wrapper.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
describe('JourneyHud', () => {
|
||||
it('renders route percent, current section, quality and arrival from one snapshot', () => {
|
||||
it('renders only route percent, current section, and arrival from one snapshot', () => {
|
||||
const wrapper = mount(JourneyHud, {
|
||||
props: {
|
||||
journey: journey({ routeProgress: 1, activeSectionId: 'contact', arrived: true }),
|
||||
sectionLabel: 'Contact',
|
||||
quality: 'low',
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toContain('100%');
|
||||
expect(wrapper.text()).toContain('Contact');
|
||||
expect(wrapper.text()).toContain('Low');
|
||||
expect(wrapper.text()).toContain('ARRIVED');
|
||||
expect(wrapper.text()).not.toContain('Low');
|
||||
expect(wrapper.attributes('aria-hidden')).toBe('true');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user