diff --git a/src/components/root/app-router.test.tsx b/src/components/root/app-router.test.tsx index 4824ea7c8..8cc1fd715 100644 --- a/src/components/root/app-router.test.tsx +++ b/src/components/root/app-router.test.tsx @@ -2,8 +2,9 @@ import React from 'react'; import { render } from '../../test-utils'; import AppRouter from './app-router'; import rootReducer from '../../redux'; -import { createMockAppStore } from '../../setupTests'; +import { createMockAppStore, createParamInLocalStorage } from '../../setupTests'; import { screen } from '@testing-library/react'; +import rmgRuntime, { RmgEnv } from '@railmapgen/rmg-runtime'; jest.mock('./app-view', () => { return () => ( @@ -27,36 +28,140 @@ const mockStore = createMockAppStore({ }); describe('AppRouter', () => { - afterEach(() => { - mockStore.clearActions(); - }); + describe('AppRouter - UAT toggle on', () => { + beforeEach(() => { + jest.spyOn(rmgRuntime, 'getEnv').mockReturnValue(RmgEnv.UAT); + }); - it('Can render param selector view if param id is not specified', () => { - render(, { store: mockStore, route: '/' }); + afterEach(() => { + mockStore.clearActions(); + }); - const actions = mockStore.getActions(); - expect(actions).toHaveLength(0); + it('Can render param selector view if param id is not specified', () => { + render(, { store: mockStore, route: '/' }); - expect(screen.getByRole('presentation', { name: 'Mock Param Selector View' })).toBeInTheDocument(); - }); + const actions = mockStore.getActions(); + expect(actions).toHaveLength(0); + + expect(screen.getByRole('presentation', { name: 'Mock Param Selector View' })).toBeInTheDocument(); + }); + + it('Can read param from localStorage and render app view if param is not loaded', () => { + createParamInLocalStorage('test-id'); + render(, { store: mockStore, route: '/?project=test-id' }); + + const actions = mockStore.getActions(); + expect(actions).toContainEqual({ type: 'app/setCurrentParamId', payload: 'test-id' }); + expect(actions).toContainEqual(expect.objectContaining({ type: 'SET_FULL_PARAM' })); + + expect(screen.getByRole('presentation', { name: 'Mock App View' })).toBeInTheDocument(); + }); + + it('Can render param selector view if no param in localStorage matches URL param ID', () => { + render(, { store: mockStore, route: '/?project=test-id-2' }); - it('Can read param from localStorage and render app view if param is not loaded', () => { - render(, { store: mockStore, route: '/?project=test-id' }); + // TODO + // expect(screen.getByRole('presentation', { name: 'Mock Param Selector View' })).toBeInTheDocument(); + }); - const actions = mockStore.getActions(); - expect(actions).toContainEqual({ type: 'app/setCurrentParamId', payload: 'test-id' }); - expect(actions).toContainEqual(expect.objectContaining({ type: 'SET_FULL_PARAM' })); + it('Can render app view if param is loaded', async () => { + const mockStore = createMockAppStore({ + ...realStore, + app: { ...realStore.app, currentParamId: 'test-id' }, + }); + render(, { store: mockStore, route: '/?project=test-id' }); - expect(screen.getByRole('presentation', { name: 'Mock App View' })).toBeInTheDocument(); + const actions = mockStore.getActions(); + expect(actions).toHaveLength(0); + + await screen.findByRole('presentation', { name: 'Mock App View' }); + }); }); - it('Can render app view if param is loaded', async () => { - const mockStore = createMockAppStore({ ...realStore, app: { ...realStore.app, currentParamId: 'test-id' } }); - render(, { store: mockStore, route: '/?project=test-id' }); + describe('AppRouter - PRD toggle off', () => { + beforeEach(() => { + jest.spyOn(rmgRuntime, 'getEnv').mockReturnValue(RmgEnv.PRD); + }); + + afterEach(() => { + mockStore.clearActions(); + window.localStorage.clear(); + }); + + it('Can generate param and render app view if param id is not specified and no param in localStorage', () => { + render(, { store: mockStore, route: '/' }); + + // update redux store + const actions = mockStore.getActions(); + expect(actions).toContainEqual(expect.objectContaining({ type: 'app/setCurrentParamId' })); + expect(actions).toContainEqual(expect.objectContaining({ type: 'SET_FULL_PARAM' })); + + // route to new param id + const currentParamId = actions.find(action => action.type === 'app/setCurrentParamId').payload; + // TODO: URL search param? + + expect(screen.getByRole('presentation', { name: 'Mock App View' })).toBeInTheDocument(); + }); + + it('Can read first param from localStorage and render app view if param id is not specified', () => { + createParamInLocalStorage('test-id'); + render(, { store: mockStore, route: '/' }); + + // update redux store + const actions = mockStore.getActions(); + expect(actions).toContainEqual({ type: 'app/setCurrentParamId', payload: 'test-id' }); + expect(actions).toContainEqual({ + type: 'SET_FULL_PARAM', + fullParam: expect.objectContaining({ line_num: 'test-id' }), + }); + + // route to new param id + // TODO: URL search param? + + expect(screen.getByRole('presentation', { name: 'Mock App View' })).toBeInTheDocument(); + }); + + it('Can read param from localStorage and render app view if param is not loaded', () => { + createParamInLocalStorage('test-id'); + render(, { store: mockStore, route: '/?project=test-id' }); + + const actions = mockStore.getActions(); + expect(actions).toContainEqual({ type: 'app/setCurrentParamId', payload: 'test-id' }); + expect(actions).toContainEqual(expect.objectContaining({ type: 'SET_FULL_PARAM' })); + + // TODO: URL search param? + + expect(screen.getByRole('presentation', { name: 'Mock App View' })).toBeInTheDocument(); + }); + + it('Can read first param from localStorage and render app selector view if no param in localStorage matches URL param ID', () => { + createParamInLocalStorage('test-id-1'); + render(, { store: mockStore, route: '/?project=test-id-2' }); + + // update redux store + const actions = mockStore.getActions(); + expect(actions).toContainEqual({ type: 'app/setCurrentParamId', payload: 'test-id-1' }); + expect(actions).toContainEqual({ + type: 'SET_FULL_PARAM', + fullParam: expect.objectContaining({ line_num: 'test-id-1' }), + }); + + // TODO: URL search param? + + expect(screen.getByRole('presentation', { name: 'Mock App View' })).toBeInTheDocument(); + }); + + it('Can render app view if param is loaded', async () => { + const mockStore = createMockAppStore({ + ...realStore, + app: { ...realStore.app, currentParamId: 'test-id' }, + }); + render(, { store: mockStore, route: '/?project=test-id' }); - const actions = mockStore.getActions(); - expect(actions).toHaveLength(0); + const actions = mockStore.getActions(); + expect(actions).toHaveLength(0); - await screen.findByRole('presentation', { name: 'Mock App View' }); + await screen.findByRole('presentation', { name: 'Mock App View' }); + }); }); }); diff --git a/src/components/root/app-router.tsx b/src/components/root/app-router.tsx index a01ee29a5..82e343f01 100644 --- a/src/components/root/app-router.tsx +++ b/src/components/root/app-router.tsx @@ -2,16 +2,18 @@ import React, { useEffect, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; import AppView from './app-view'; import { useRootDispatch, useRootSelector } from '../../redux'; -import rmgRuntime from '@railmapgen/rmg-runtime'; +import rmgRuntime, { RmgEnv } from '@railmapgen/rmg-runtime'; import { LanguageCode } from '@railmapgen/rmg-translate'; import ParamSelectorView from './param-selector-view'; import { readParam } from '../../redux/app/action'; +import { getParamMap } from '../../util/param-manager-utils'; +import { nanoid } from 'nanoid'; export default function AppRouter() { const dispatch = useRootDispatch(); const { currentParamId } = useRootSelector(state => state.app); - const [searchParams] = useSearchParams(); + const [searchParams, setSearchParams] = useSearchParams(); const paramId = searchParams.get('project'); const [isLoaded, setIsLoaded] = useState(false); @@ -20,16 +22,34 @@ export default function AppRouter() { console.log('searchParam: project=' + paramId); if (paramId) { if (paramId === currentParamId) { + console.log('AppRouter:: Store param ID matches URL param ID. Rendering app view...'); setIsLoaded(true); } else { - console.warn( - 'AppRouter:: URL param ID does not match store param ID. Reading param with ID=' + paramId - ); + if (rmgRuntime.getEnv() === RmgEnv.PRD) { + const paramMap = getParamMap(); + const paramIds = Object.keys(paramMap); + if (paramIds.length && !paramIds.includes(paramId)) { + console.warn( + 'AppRouter:: URL param ID does not exist in localStorage. Reset to first param in localStorage in PRD env.' + ); + const paramMap = getParamMap(); + setSearchParams({ project: Object.keys(paramMap)[0] }); + return; + } + } + + console.log(`AppRouter:: Reading param (ID=${paramId}) from localStorage`); dispatch(readParam(paramId, rmgRuntime.getLanguage() as LanguageCode)); setIsLoaded(true); } } else { - console.log('AppRouter:: Render param selector'); + if (rmgRuntime.getEnv() === RmgEnv.PRD) { + console.log('AppRouter:: Redirect to first param in localStorage in PRD env'); + const paramMap = getParamMap(); + setSearchParams({ project: Object.keys(paramMap)[0] ?? nanoid() }); + } else { + console.log('AppRouter:: No URL param ID provided. Rendering param selector view...'); + } } }, [paramId]); diff --git a/src/components/root/param-selector-view.test.tsx b/src/components/root/param-selector-view.test.tsx index cc3bb3034..9af8dc18e 100644 --- a/src/components/root/param-selector-view.test.tsx +++ b/src/components/root/param-selector-view.test.tsx @@ -2,25 +2,16 @@ import React from 'react'; import { render } from '../../test-utils'; import ParamSelectorView from './param-selector-view'; import rootReducer from '../../redux'; -import { createMockAppStore } from '../../setupTests'; -import { initParam } from '../../redux/param/util'; -import { LocalStorageKey, RmgStyle } from '../../constants/constants'; -import { LanguageCode } from '@railmapgen/rmg-translate'; +import { createMockAppStore, createParamInLocalStorage } from '../../setupTests'; import { fireEvent, screen } from '@testing-library/react'; const realStore = rootReducer.getState(); const mockStore = createMockAppStore({ ...realStore }); -const generateParamInLocalStorage = (id: string) => { - const rmgParam = initParam(RmgStyle.MTR, LanguageCode.English); - rmgParam.line_num = id; - window.localStorage.setItem(LocalStorageKey.PARAM_BY_ID + id, JSON.stringify(rmgParam)); -}; - describe('ParamSelectorView', () => { it('Can render view with list of projects as expected', () => { - generateParamInLocalStorage('test-1'); - generateParamInLocalStorage('test-2'); + createParamInLocalStorage('test-1'); + createParamInLocalStorage('test-2'); render(, { store: mockStore, route: '/' }); diff --git a/src/setupProxy.js b/src/setupProxy.js index 922afb52a..9fb65fece 100644 --- a/src/setupProxy.js +++ b/src/setupProxy.js @@ -3,7 +3,7 @@ module.exports = app => { res.send({ component: 'rmg', version: '9.9.9', - environment: 'DEV', + environment: 'PRD', instance: 'localhost', }); }); diff --git a/src/setupTests.ts b/src/setupTests.ts index d38e341e1..e2d0bc420 100644 --- a/src/setupTests.ts +++ b/src/setupTests.ts @@ -1,7 +1,9 @@ import createMockStore from 'redux-mock-store'; -import { BranchStyle, StationDict } from './constants/constants'; +import { BranchStyle, LocalStorageKey, RmgStyle, StationDict } from './constants/constants'; import rootReducer, { RootState } from './redux'; import { getDefaultMiddleware, ThunkDispatch } from '@reduxjs/toolkit'; +import { initParam } from './redux/param/util'; +import { LanguageCode } from '@railmapgen/rmg-translate'; // FIXME: any -> AnyAction? type DispatchExts = ThunkDispatch; @@ -89,3 +91,9 @@ global.fetch = (...args) => { return originalFetch(...args); } }; + +export const createParamInLocalStorage = (id: string) => { + const rmgParam = initParam(RmgStyle.MTR, LanguageCode.English); + rmgParam.line_num = id; + window.localStorage.setItem(LocalStorageKey.PARAM_BY_ID + id, JSON.stringify(rmgParam)); +};