Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion src/App.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import App from './App';
import { createInitialState } from './initialState';
import { createAppStore } from './store';

const mocks = vi.hoisted(() => ({ authenticated: true, options: {}, requests: [], hardNavigate: vi.fn() }));
const mocks = vi.hoisted(() => ({ authenticated: true, options: {}, requests: [], hardNavigate: vi.fn(), playerProps: null }));

vi.mock('@commaai/my-comma-auth', () => ({
default: {
Expand Down Expand Up @@ -41,6 +41,7 @@ vi.mock('react-map-gl', () => ({
}));
vi.mock('react-player/file', () => ({
default: React.forwardRef((_props, ref) => {
mocks.playerProps = _props;
React.useImperativeHandle(ref, () => ({
getCurrentTime: () => 0,
getDuration: () => 60,
Expand Down Expand Up @@ -154,6 +155,7 @@ describe('whole-app behavior', () => {
localStorage.clear();
sessionStorage.clear();
mocks.hardNavigate.mockClear();
mocks.playerProps = null;
});

test('root uses a valid stored device and keeps the selection', async () => {
Expand Down Expand Up @@ -271,4 +273,22 @@ describe('whole-app behavior', () => {
fireEvent.click(within(document.body).getByRole('button', { name: 'Close' }));
await waitFor(() => expect(history.location.pathname).toBe(`/${FIRST}`));
});

test('a failed video leaves timeline navigation owned by Redux', async () => {
const { store } = await renderApp(`/${FIRST}/${LOG}`);
const timeline = await screen.findByRole('slider', { name: 'Drive timeline' });

act(() => {
mocks.playerProps.onError('hlsError', {
fatal: true,
type: 'networkError',
response: { code: 404 },
});
});
expect(store.getState().videoStatus).toBe('failed');

fireEvent.pointerDown(timeline, { button: 0, clientX: 500, pageX: 500 });
fireEvent.pointerUp(timeline, { button: 0, clientX: 500, pageX: 500 });
expect(store.getState().offset).toBe(30000);
});
});
1 change: 1 addition & 0 deletions src/actions/cached.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as Sentry from '@sentry/react';

import * as Types from './types';
import { api } from '../api/backend';
import { reverseLookup } from '../utils/geocode';

const USE_LOCAL_COORDS_DATA = import.meta.env.VITE_APP_LOCAL_COORDS_DATA === 'true';
Expand Down
3 changes: 1 addition & 2 deletions src/actions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { athena as Athena, billing as Billing } from '../api';
import { api } from '../api/backend';

import * as Types from './types';
import { resetPlayback, selectLoop } from '../timeline/playback';
import { selectLoop } from '../timeline/playback';
import {hasRoutesData } from '../timeline/segments';
import { getDeviceFromState, deviceVersionAtLeast, deviceIsOnline } from '../utils';
import { webrtcConnectionManager } from '../utils/webrtc';
Expand Down Expand Up @@ -165,7 +165,6 @@ export function urlForState(dongleId, log_id, start, end, prime) {
function updateTimeline(state, dispatch, log_id, start, end, allowPathChange) {
if (!state.loop || !state.loop.startTime || !state.loop.duration || state.loop.startTime < start
|| state.loop.startTime + state.loop.duration > end || state.loop.duration < end - start) {
dispatch(resetPlayback());
dispatch(selectLoop(start, end));
}

Expand Down
3 changes: 3 additions & 0 deletions src/actions/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,12 @@ export const ACTION_PRIME_SUBSCRIBE_INFO = 'ACTION_PRIME_SUBSCRIBE_INFO';
export const ACTION_SEEK = 'ACTION_SEEK';
export const ACTION_PAUSE = 'ACTION_PAUSE';
export const ACTION_PLAY = 'ACTION_PLAY';
export const ACTION_PLAYBACK_SPEED = 'ACTION_PLAYBACK_SPEED';
export const ACTION_LOOP = 'ACTION_LOOP';
export const ACTION_BUFFER_VIDEO = 'ACTION_BUFFER_VIDEO';
export const ACTION_RESET = 'ACTION_RESET';
export const ACTION_HAS_AUDIO = 'ACTION_HAS_AUDIO';
export const ACTION_VIDEO_STATUS = 'ACTION_VIDEO_STATUS';

// segments
export const ACTION_UPDATE_SEGMENT_RANGE = 'ACTION_UPDATE_SEGMENT_RANGE';
Expand Down
25 changes: 14 additions & 11 deletions src/api/demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ const DEMO_PROFILE = {

const AFFECTED_SEGMENT = 1;

// One clone per test case. Each case mutates a fresh clone of the real public
// data on its way into the frontend.
const MISSING_DATA_CASES = [
{
title: 'Epoch date/time (no clock)',
Expand Down Expand Up @@ -102,16 +100,21 @@ const MISSING_DATA_CASES = [
},
];

// Keep two full-length routes for every case: one where the whole route is
// affected and one where only a single segment is affected.
const TEST_CASES = MISSING_DATA_CASES.flatMap((testCase) => [
testCase,

const TEST_CASES = [
{
...testCase,
title: `${testCase.title} (1 segment)`,
affectedSegment: AFFECTED_SEGMENT,
title: 'Public route (no issues)',
route() {},
},
]);
...MISSING_DATA_CASES.flatMap((testCase) => [
testCase,
{
...testCase,
title: `${testCase.title} (1 segment)`,
affectedSegment: AFFECTED_SEGMENT,
},
]),
];

function fileSegmentNumber(file) {
const pathParts = new URL(file).pathname.split('/');
Expand Down Expand Up @@ -168,7 +171,7 @@ export function createDemoBackend(realBackend) {
}

// Clone the cached public route into fresh demo routes on every call, each
// with a unique demo route ID and one mutation per test case.
// with a unique demo route ID and its test case's mutation, if any.
async function listDemoRoutes(routeStr) {
const publicRoute = await fetchPublicRoute();
const routes = TEST_CASES.map((testCase, index) => {
Expand Down
32 changes: 22 additions & 10 deletions src/components/DriveMap/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import { connect } from 'react-redux';
import ReactMapGL, { LinearInterpolator } from 'react-map-gl';

import { fetchDriveCoords } from '../../actions/cached';
import { currentOffset } from '../../timeline';
import { VideoStatus } from '../../timeline/playback';
import { getVideoPlayerCurrentTime } from '../../timeline/videoPlayer';
import { isIos } from '../../utils/browser';
import { DEFAULT_LOCATION, MAPBOX_STYLE, MAPBOX_TOKEN } from '../../utils/geocode';

const INTERACTION_TIMEOUT = 5000;
Expand Down Expand Up @@ -44,7 +46,7 @@ class DriveMap extends Component {
}

componentDidUpdate(prevProps) {
const { dispatch, currentRoute, startTime } = this.props;
const { dispatch, currentRoute } = this.props;

const prevRoute = prevProps.currentRoute?.fullname || null;
const route = currentRoute?.fullname || null;
Expand All @@ -55,10 +57,6 @@ class DriveMap extends Component {
}
}

if (prevProps.startTime && prevProps.startTime !== startTime) {
this.shouldFlyTo = true;
}

if (currentRoute && prevProps.currentRoute && currentRoute.driveCoords
&& prevProps.currentRoute.driveCoords !== currentRoute.driveCoords) {
this.shouldFlyTo = false;
Expand All @@ -73,6 +71,10 @@ class DriveMap extends Component {

componentWillUnmount() {
this.mounted = false;
if (this.rafId) {
cancelAnimationFrame(this.rafId);
this.rafId = null;
}
}

onInteraction(ev) {
Expand All @@ -97,7 +99,16 @@ class DriveMap extends Component {
const markerSource = this.map && this.map.getMap().getSource('seekPoint');
if (markerSource) {
if (this.props.currentRoute && this.props.currentRoute.driveCoords) {
const pos = this.posAtOffset(currentOffset());
let offset;
if (this.props.videoStatus === VideoStatus.FAILED || (this.props.hasAudio && isIos())) {
offset = this.props.offset;
} else {
offset = getVideoPlayerCurrentTime(this.props.currentRoute);
if (offset === null) {
offset = this.props.offset;
}
}
const pos = this.posAtOffset(offset);
if (pos && pos.some((coordinate, index) => coordinate != this.lastMapPos[index])) {
this.lastMapPos = pos;
markerSource.setData({
Expand All @@ -116,7 +127,7 @@ class DriveMap extends Component {
}
}

requestAnimationFrame(this.updateMarkerPos);
this.rafId = requestAnimationFrame(this.updateMarkerPos);
}

moveViewportTo(pos) {
Expand Down Expand Up @@ -207,7 +218,7 @@ class DriveMap extends Component {
}

initMap(mapComponent) {
if (!mapComponent) {
if (!mapComponent || typeof mapComponent.getMap !== 'function') {
this.map = null;
return;
}
Expand Down Expand Up @@ -308,7 +319,8 @@ class DriveMap extends Component {
const stateToProps = (state) => ({
offset: state.offset,
currentRoute: state.currentRoute,
startTime: state.startTime,
hasAudio: state.hasAudio,
videoStatus: state.videoStatus,
});

export default connect(stateToProps)(DriveMap);
Loading
Loading