From 1a9d593fd3f897133c63173e83dd9941fcb008fe Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Tue, 3 Dec 2024 09:31:10 +0800 Subject: [PATCH] Increase testing (#1036) * beacon-table Signed-off-by: Aaron Chong * Ignore .stories.tsx and .js files Signed-off-by: Aaron Chong * role-list-page Signed-off-by: Aaron Chong * user-list-page Signed-off-by: Aaron Chong * user-profile-page Signed-off-by: Aaron Chong * Ignore index.ts files Signed-off-by: Aaron Chong * door-summary, tests, clean up unused opmode Signed-off-by: Aaron Chong * doors-table, door-utils Signed-off-by: Aaron Chong * lift-summary, removed lift-card Signed-off-by: Aaron Chong * lifts-table Signed-off-by: Aaron Chong * doors-table, lifts-table Signed-off-by: Aaron Chong * remove delivery-alert-store Signed-off-by: Aaron Chong * map, basic empty render test, cleanup map directory Signed-off-by: Aaron Chong * map, test with office map Signed-off-by: Aaron Chong * camera-control Signed-off-by: Aaron Chong * Revert resize observer import, using a mock resize observer for testing, address comments Signed-off-by: Aaron Chong --------- Signed-off-by: Aaron Chong --- codecov.yml | 4 + .../components/admin/role-list-page.test.tsx | 44 ++ .../components/admin/user-list-page.test.tsx | 44 ++ .../admin/user-profile-page.test.tsx | 42 ++ .../beacons/beacon-table-datagrid.tsx | 4 +- .../components/beacons/beacon-table.test.tsx | 36 + .../src/components/delivery-alert-store.tsx | 627 ------------------ .../components/doors/door-card.stories.tsx | 35 - .../src/components/doors/door-card.tsx | 73 -- .../src/components/doors/door-controls.tsx | 25 - .../components/doors/door-summary.test.tsx | 66 ++ .../src/components/doors/door-summary.tsx | 27 +- .../doors/door-table-datagrid.test.tsx | 14 +- .../src/components/doors/door-utils.test.ts | 24 + .../src/components/doors/door-utils.ts | 2 +- .../src/components/doors/doors-table.test.tsx | 102 +++ .../components/doors/{index.tsx => index.ts} | 1 - .../src/components/lifts/index.ts | 1 - .../components/lifts/lift-card.stories.tsx | 35 - .../src/components/lifts/lift-card.tsx | 105 --- .../components/lifts/lift-summary.test.tsx | 57 ++ .../src/components/lifts/lift-summary.tsx | 8 +- .../components/lifts/lift-table-datagrid.tsx | 4 +- .../src/components/lifts/lifts-table.test.tsx | 127 ++++ .../components/map/camera-control.test.tsx | 23 + .../src/components/map/index.ts | 1 + ...ol.test.tsx => layers-controller.test.tsx} | 0 .../src/components/map/map.test.tsx | 48 ++ .../src/components/map/{index.tsx => map.tsx} | 4 +- .../src/components/rmf-dashboard.tsx | 2 - .../src/components/robots/robots-table.tsx | 2 +- .../src/components/workcells/utils.ts | 2 +- .../workcells/workcell-table.test.tsx | 2 +- .../src/micro-apps/map-app.ts | 4 +- pnpm-lock.yaml | 2 +- 35 files changed, 658 insertions(+), 939 deletions(-) create mode 100644 packages/rmf-dashboard-framework/src/components/admin/role-list-page.test.tsx create mode 100644 packages/rmf-dashboard-framework/src/components/admin/user-list-page.test.tsx create mode 100644 packages/rmf-dashboard-framework/src/components/admin/user-profile-page.test.tsx create mode 100644 packages/rmf-dashboard-framework/src/components/beacons/beacon-table.test.tsx delete mode 100644 packages/rmf-dashboard-framework/src/components/delivery-alert-store.tsx delete mode 100644 packages/rmf-dashboard-framework/src/components/doors/door-card.stories.tsx delete mode 100644 packages/rmf-dashboard-framework/src/components/doors/door-card.tsx delete mode 100644 packages/rmf-dashboard-framework/src/components/doors/door-controls.tsx create mode 100644 packages/rmf-dashboard-framework/src/components/doors/door-summary.test.tsx create mode 100644 packages/rmf-dashboard-framework/src/components/doors/door-utils.test.ts create mode 100644 packages/rmf-dashboard-framework/src/components/doors/doors-table.test.tsx rename packages/rmf-dashboard-framework/src/components/doors/{index.tsx => index.ts} (68%) delete mode 100644 packages/rmf-dashboard-framework/src/components/lifts/lift-card.stories.tsx delete mode 100644 packages/rmf-dashboard-framework/src/components/lifts/lift-card.tsx create mode 100644 packages/rmf-dashboard-framework/src/components/lifts/lift-summary.test.tsx create mode 100644 packages/rmf-dashboard-framework/src/components/lifts/lifts-table.test.tsx create mode 100644 packages/rmf-dashboard-framework/src/components/map/camera-control.test.tsx create mode 100644 packages/rmf-dashboard-framework/src/components/map/index.ts rename packages/rmf-dashboard-framework/src/components/map/{layer-control.test.tsx => layers-controller.test.tsx} (100%) create mode 100644 packages/rmf-dashboard-framework/src/components/map/map.test.tsx rename packages/rmf-dashboard-framework/src/components/map/{index.tsx => map.tsx} (99%) diff --git a/codecov.yml b/codecov.yml index 51b74ac10..f310ab856 100644 --- a/codecov.yml +++ b/codecov.yml @@ -2,6 +2,10 @@ coverage: status: project: false patch: false +ignore: + - packages/rmf-dashboard-framework/examples + - "**/*.js" + - "**/*.stories.tsx" flags: dashboard: paths: diff --git a/packages/rmf-dashboard-framework/src/components/admin/role-list-page.test.tsx b/packages/rmf-dashboard-framework/src/components/admin/role-list-page.test.tsx new file mode 100644 index 000000000..dd728aa8f --- /dev/null +++ b/packages/rmf-dashboard-framework/src/components/admin/role-list-page.test.tsx @@ -0,0 +1,44 @@ +import { waitFor } from '@testing-library/react'; +import React from 'react'; +import { describe, expect, it } from 'vitest'; + +import { RmfApiProvider } from '../../hooks'; +import { RmfApi } from '../../services'; +import { MockRmfApi, render, TestProviders } from '../../utils/test-utils.test'; +import { RoleListPage } from './role-list-page'; + +describe('Role List Page', () => { + const Base = (props: React.PropsWithChildren<{}>) => { + const rmfApi = React.useMemo(() => { + const mockRmfApi = new MockRmfApi(); + // mock out some api calls so they never resolves + mockRmfApi.adminApi.getRolesAdminRolesGet = () => new Promise(() => {}); + mockRmfApi.adminApi.createRoleAdminRolesPost = () => new Promise(() => {}); + mockRmfApi.adminApi.deleteRoleAdminRolesRoleDelete = () => new Promise(() => {}); + mockRmfApi.adminApi.getRolePermissionsAdminRolesRolePermissionsGet = () => + new Promise(() => {}); + mockRmfApi.adminApi.addRolePermissionAdminRolesRolePermissionsPost = () => + new Promise(() => {}); + mockRmfApi.adminApi.removeRolePermissionAdminRolesRolePermissionsRemovePost = () => + new Promise(() => {}); + return mockRmfApi; + }, []); + return ( + + {props.children} + + ); + }; + + it('renders role list page', async () => { + await expect( + waitFor(() => + render( + + + , + ), + ), + ).resolves.not.toThrow(); + }); +}); diff --git a/packages/rmf-dashboard-framework/src/components/admin/user-list-page.test.tsx b/packages/rmf-dashboard-framework/src/components/admin/user-list-page.test.tsx new file mode 100644 index 000000000..4aa72e371 --- /dev/null +++ b/packages/rmf-dashboard-framework/src/components/admin/user-list-page.test.tsx @@ -0,0 +1,44 @@ +import { render as render_, waitFor } from '@testing-library/react'; +import React from 'react'; +import { MemoryRouter } from 'react-router'; +import { describe, expect, it } from 'vitest'; + +import { AppControllerProvider, RmfApiProvider } from '../../hooks'; +import { RmfApi } from '../../services'; +import { makeMockAppController, MockRmfApi, TestProviders } from '../../utils/test-utils.test'; +import { UserListPage } from './user-list-page'; + +const render = (ui: React.ReactNode) => + render_({ui}); + +describe('UserListPage', () => { + const Base = (props: React.PropsWithChildren<{}>) => { + const rmfApi = React.useMemo(() => { + const mockRmfApi = new MockRmfApi(); + // mock out some api calls so they never resolves + mockRmfApi.adminApi.getUsersAdminUsersGet = () => new Promise(() => {}); + mockRmfApi.adminApi.deleteUserAdminUsersUsernameDelete = () => new Promise(() => {}); + mockRmfApi.adminApi.createUserAdminUsersPost = () => new Promise(() => {}); + return mockRmfApi; + }, []); + return ( + + {props.children} + + ); + }; + + it('renders user list page', async () => { + await expect( + waitFor(() => + render( + + + + + , + ), + ), + ).resolves.not.toThrow(); + }); +}); diff --git a/packages/rmf-dashboard-framework/src/components/admin/user-profile-page.test.tsx b/packages/rmf-dashboard-framework/src/components/admin/user-profile-page.test.tsx new file mode 100644 index 000000000..6a85ea854 --- /dev/null +++ b/packages/rmf-dashboard-framework/src/components/admin/user-profile-page.test.tsx @@ -0,0 +1,42 @@ +import { render as render_, waitFor } from '@testing-library/react'; +import React from 'react'; +import { describe, expect, it } from 'vitest'; + +import { AppControllerProvider, RmfApiProvider } from '../../hooks'; +import { RmfApi } from '../../services'; +import { makeMockAppController, MockRmfApi, TestProviders } from '../../utils/test-utils.test'; +import { UserProfilePage } from './user-profile-page'; + +const render = (ui: React.ReactNode) => + render_({ui}); + +describe('UserProfilePage', () => { + const Base = (props: React.PropsWithChildren<{}>) => { + const rmfApi = React.useMemo(() => { + const mockRmfApi = new MockRmfApi(); + // mock out some api calls so they never resolves + mockRmfApi.adminApi.getUserAdminUsersUsernameGet = () => new Promise(() => {}); + mockRmfApi.adminApi.makeAdminAdminUsersUsernameMakeAdminPost = () => new Promise(() => {}); + mockRmfApi.adminApi.getRolesAdminRolesGet = () => new Promise(() => {}); + mockRmfApi.adminApi.setUserRolesAdminUsersUsernameRolesPut = () => new Promise(() => {}); + return mockRmfApi; + }, []); + return ( + + {props.children} + + ); + }; + + it('renders user profile page', async () => { + await expect( + waitFor(() => + render( + + + , + ), + ), + ).resolves.not.toThrow(); + }); +}); diff --git a/packages/rmf-dashboard-framework/src/components/beacons/beacon-table-datagrid.tsx b/packages/rmf-dashboard-framework/src/components/beacons/beacon-table-datagrid.tsx index 8af6b251e..9e5bdf7c0 100644 --- a/packages/rmf-dashboard-framework/src/components/beacons/beacon-table-datagrid.tsx +++ b/packages/rmf-dashboard-framework/src/components/beacons/beacon-table-datagrid.tsx @@ -90,7 +90,7 @@ export function BeaconDataGridTable({ beacons }: BeaconDataGridTableProps): JSX. headerName: 'Level', width: 150, editable: false, - valueGetter: (params: GridValueGetterParams) => params.row.level ?? 'N/A', + valueGetter: (params: GridValueGetterParams) => params.row.level ?? 'n/a', flex: 1, filterable: true, }, @@ -99,7 +99,7 @@ export function BeaconDataGridTable({ beacons }: BeaconDataGridTableProps): JSX. headerName: 'Type', width: 150, editable: false, - valueGetter: (params: GridValueGetterParams) => params.row.category ?? 'N/A', + valueGetter: (params: GridValueGetterParams) => params.row.category ?? 'n/a', flex: 1, filterable: true, }, diff --git a/packages/rmf-dashboard-framework/src/components/beacons/beacon-table.test.tsx b/packages/rmf-dashboard-framework/src/components/beacons/beacon-table.test.tsx new file mode 100644 index 000000000..0f3c0e048 --- /dev/null +++ b/packages/rmf-dashboard-framework/src/components/beacons/beacon-table.test.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { describe, expect, it } from 'vitest'; + +import { RmfApiProvider } from '../../hooks'; +import { RmfApi } from '../../services'; +import { MockRmfApi, render, TestProviders } from '../../utils/test-utils.test'; +import { BeaconsTable } from './beacons-table'; + +describe('BeaconsTable', () => { + const Base = (props: React.PropsWithChildren<{}>) => { + const rmfApi = React.useMemo(() => { + const mockRmfApi = new MockRmfApi(); + // mock out some api calls so they never resolves + mockRmfApi.beaconsApi.getBeaconsBeaconsGet = () => new Promise(() => {}); + return mockRmfApi; + }, []); + return ( + + {props.children} + + ); + }; + + it('renders with beacons table', () => { + const root = render( + + + , + ); + expect(root.getByText('Name')).toBeTruthy(); + expect(root.getByText('Op. Mode')).toBeTruthy(); + expect(root.getByText('Level')).toBeTruthy(); + expect(root.getByText('Type')).toBeTruthy(); + expect(root.getByText('Beacon State')).toBeTruthy(); + }); +}); diff --git a/packages/rmf-dashboard-framework/src/components/delivery-alert-store.tsx b/packages/rmf-dashboard-framework/src/components/delivery-alert-store.tsx deleted file mode 100644 index 9583106d1..000000000 --- a/packages/rmf-dashboard-framework/src/components/delivery-alert-store.tsx +++ /dev/null @@ -1,627 +0,0 @@ -import { Button, Divider, TextField, Tooltip, useMediaQuery, useTheme } from '@mui/material'; -import Dialog from '@mui/material/Dialog'; -import DialogActions from '@mui/material/DialogActions'; -import DialogContent from '@mui/material/DialogContent'; -import DialogTitle from '@mui/material/DialogTitle'; -import { - Action as DeliveryAlertAction, - ApiServerModelsDeliveryAlertsDeliveryAlertCategory as DeliveryAlertCategory, - ApiServerModelsDeliveryAlertsDeliveryAlertTier as DeliveryAlertTier, - DeliveryAlert, - TaskStateOutput as TaskState, -} from 'api-client'; -import React from 'react'; - -import { useAppController, useRmfApi } from '../hooks'; -import { TaskCancelButton } from './tasks/task-cancellation'; -import { TaskInspector } from './tasks/task-inspector'; - -const categoryToText = (category: DeliveryAlertCategory): string => { - switch (category) { - case DeliveryAlertCategory.Missing: { - return 'No cart detected'; - } - case DeliveryAlertCategory.Wrong: { - return 'Wrong cart detected'; - } - case DeliveryAlertCategory.Obstructed: { - return 'Goal is obstructed'; - } - case DeliveryAlertCategory.Cancelled: { - return 'Task is cancelled'; - } - default: { - return ''; - } - } -}; - -interface DeliveryWarningDialogProps { - deliveryAlert: DeliveryAlert; - taskState?: TaskState; - onOverride?: (deliveryAlert: DeliveryAlert) => Promise; - onResume?: (deliveryAlert: DeliveryAlert) => Promise; - onClose: () => void; -} - -const DeliveryWarningDialog = React.memo((props: DeliveryWarningDialogProps) => { - const { deliveryAlert, taskState, onOverride, onResume, onClose } = props; - const [isOpen, setIsOpen] = React.useState(true); - const [actionTaken, setActionTaken] = React.useState(!onOverride && !onResume); - const [newTaskState, setNewTaskState] = React.useState(null); - const [openTaskInspector, setOpenTaskInspector] = React.useState(false); - const appController = useAppController(); - const rmfApi = useRmfApi(); - const isScreenHeightLessThan800 = useMediaQuery('(max-height:800px)'); - - React.useEffect(() => { - if (deliveryAlert.action !== DeliveryAlertAction.Waiting) { - setActionTaken(true); - } - }, [deliveryAlert]); - - React.useEffect(() => { - if (!taskState) { - setNewTaskState(null); - return; - } - const sub = rmfApi.getTaskStateObs(taskState.booking.id).subscribe((taskStateUpdate) => { - setNewTaskState(taskStateUpdate); - if ( - deliveryAlert.action === DeliveryAlertAction.Waiting && - taskStateUpdate.status && - taskStateUpdate.status === 'canceled' - ) { - (async () => { - try { - await rmfApi.deliveryAlertsApi.respondToDeliveryAlertDeliveryAlertsDeliveryAlertIdResponsePost( - deliveryAlert.id, - deliveryAlert.category, - deliveryAlert.tier, - deliveryAlert.task_id ?? '', - DeliveryAlertAction.Cancel, - deliveryAlert.message ?? '', - ); - } catch (e) { - appController.showAlert( - 'error', - `Failed to cancel delivery alert ${deliveryAlert.id}: ${(e as Error).message}`, - ); - } - setActionTaken(true); - })(); - } - }); - return () => sub.unsubscribe(); - }, [rmfApi, deliveryAlert, taskState, appController]); - - const titleUpdateText = (action: DeliveryAlertAction) => { - switch (action) { - case DeliveryAlertAction.Override: { - return ' - [Overridden]'; - } - case DeliveryAlertAction.Resume: { - return ' - [Resumed]'; - } - case DeliveryAlertAction.Cancel: { - return ' - [Cancelled]'; - } - case DeliveryAlertAction.Waiting: - default: { - return ''; - } - } - }; - - const theme = useTheme(); - - return ( - <> - - - Delivery - warning!{titleUpdateText(deliveryAlert.action)} - - - - 0 - ? deliveryAlert.task_id - : 'n/a' - } - /> - - - - - {(newTaskState && newTaskState.status && newTaskState.status === 'canceled') || - deliveryAlert.category === DeliveryAlertCategory.Cancelled ? ( - - ) : deliveryAlert.message && deliveryAlert.message.includes(' latch ') ? ( - - ) : newTaskState ? ( - - - - ) : ( - - )} - - - - - - - - - - {taskState && openTaskInspector && ( - setOpenTaskInspector(false)} /> - )} - - ); -}); - -interface DeliveryErrorDialogProps { - deliveryAlert: DeliveryAlert; - taskState?: TaskState; - onClose: () => void; -} - -const DeliveryErrorDialog = React.memo((props: DeliveryErrorDialogProps) => { - const { deliveryAlert, taskState, onClose } = props; - const [isOpen, setIsOpen] = React.useState(true); - const [openTaskInspector, setOpenTaskInspector] = React.useState(false); - const isScreenHeightLessThan800 = useMediaQuery('(max-height:800px)'); - - const theme = useTheme(); - - return ( - <> - - Delivery - error! - - - - - - - - {taskState ? ( - - - - ) : null} - - - - {taskState && openTaskInspector && ( - setOpenTaskInspector(false)} /> - )} - - ); -}); - -interface DeliveryAlertData { - deliveryAlert: DeliveryAlert; - taskState?: TaskState; -} - -export const DeliveryAlertStore = React.memo(() => { - const rmfApi = useRmfApi(); - const [alerts, setAlerts] = React.useState>({}); - const appController = useAppController(); - - const filterAndPushDeliveryAlert = (deliveryAlert: DeliveryAlert, taskState?: TaskState) => { - // Check if a delivery alert for a task is already open, if so, replace it - // with this new incoming deliveryAlert. - setAlerts((prev) => { - if (!deliveryAlert.task_id) { - return { - ...prev, - [deliveryAlert.id]: { deliveryAlert, taskState }, - }; - } - - // TODO(ac): set action to cancelled for delivery alerts that have been - // updated. - const filteredAlerts = Object.fromEntries( - Object.entries(prev).filter( - ([_, alertData]) => - !alertData.deliveryAlert.task_id || - alertData.deliveryAlert.task_id !== deliveryAlert.task_id, - ), - ); - - if (deliveryAlert.action === DeliveryAlertAction.Waiting) { - filteredAlerts[deliveryAlert.id] = { - deliveryAlert, - taskState, - }; - } - return filteredAlerts; - }); - }; - - React.useEffect(() => { - const sub = rmfApi.deliveryAlertObsStore.subscribe(async (deliveryAlert) => { - // DEBUG - console.log( - `Got a delivery alert [${deliveryAlert.id}] with action [${deliveryAlert.action}]`, - ); - - let state: TaskState | undefined = undefined; - if (deliveryAlert.task_id) { - try { - state = (await rmfApi.tasksApi.getTaskStateTasksTaskIdStateGet(deliveryAlert.task_id)) - .data; - } catch { - console.error(`Failed to fetch task state for ${deliveryAlert.task_id}`); - } - } - filterAndPushDeliveryAlert(deliveryAlert, state); - }); - return () => sub.unsubscribe(); - }, [rmfApi]); - - const onOverride = React.useCallback['onOverride']>( - async (delivery_alert) => { - try { - await rmfApi.deliveryAlertsApi.respondToDeliveryAlertDeliveryAlertsDeliveryAlertIdResponsePost( - delivery_alert.id, - delivery_alert.category, - delivery_alert.tier, - delivery_alert.task_id ?? '', - DeliveryAlertAction.Override, - delivery_alert.message ?? '', - ); - const taskReferenceText = delivery_alert.task_id - ? `, continuing with task ${delivery_alert.task_id}` - : ''; - appController.showAlert( - 'success', - `Overriding delivery alert ${delivery_alert.id}${taskReferenceText}`, - ); - removeDeliveryAlertDialog(delivery_alert.id); - } catch (e) { - const taskReferenceText = delivery_alert.task_id - ? ` and continue with task ${delivery_alert.task_id}` - : ''; - appController.showAlert( - 'error', - `Failed to override delivery alert ${delivery_alert.id}${taskReferenceText}: ${ - (e as Error).message - }`, - ); - } - }, - [rmfApi, appController], - ); - - const removeDeliveryAlertDialog = (id: string) => { - setAlerts((prev) => Object.fromEntries(Object.entries(prev).filter(([key]) => key !== id))); - }; - - const onResume = React.useCallback['onResume']>( - async (delivery_alert) => { - try { - await rmfApi.deliveryAlertsApi.respondToDeliveryAlertDeliveryAlertsDeliveryAlertIdResponsePost( - delivery_alert.id, - delivery_alert.category, - delivery_alert.tier, - delivery_alert.task_id ?? '', - DeliveryAlertAction.Resume, - delivery_alert.message ?? '', - ); - const taskReferenceText = delivery_alert.task_id - ? `, continuing with task ${delivery_alert.task_id}` - : ''; - appController.showAlert( - 'success', - `Resuming after delivery alert ${delivery_alert.id}${taskReferenceText}`, - ); - removeDeliveryAlertDialog(delivery_alert.id); - } catch (e) { - const taskReferenceText = delivery_alert.task_id ? ` ${delivery_alert.task_id}` : ''; - appController.showAlert( - 'error', - `Failed to resume task${taskReferenceText}: ${(e as Error).message}`, - ); - } - }, - [rmfApi, appController], - ); - - return ( - <> - {Object.values(alerts).map((alert) => { - if (alert.deliveryAlert.tier === DeliveryAlertTier.Error) { - return ( - removeDeliveryAlertDialog(alert.deliveryAlert.id)} - key={alert.deliveryAlert.id} - /> - ); - } - - if (alert.deliveryAlert.category === DeliveryAlertCategory.Cancelled) { - console.warn( - 'Delivery alert with category [cancelled] submitted as a warning, this might be a mistake, alert promoted to an error.', - ); - return ( - removeDeliveryAlertDialog(alert.deliveryAlert.id)} - key={alert.deliveryAlert.id} - /> - ); - } - - // Allow resume if the obstruction is related to a latching problem. - return ( - removeDeliveryAlertDialog(alert.deliveryAlert.id)} - key={alert.deliveryAlert.id} - /> - ); - })} - - ); -}); diff --git a/packages/rmf-dashboard-framework/src/components/doors/door-card.stories.tsx b/packages/rmf-dashboard-framework/src/components/doors/door-card.stories.tsx deleted file mode 100644 index de399883d..000000000 --- a/packages/rmf-dashboard-framework/src/components/doors/door-card.stories.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { CardActions } from '@mui/material'; -import { Meta, StoryObj } from '@storybook/react'; -import { Door } from 'rmf-models/ros/rmf_building_map_msgs/msg'; -import { DoorMode } from 'rmf-models/ros/rmf_door_msgs/msg'; - -import { DoorCard } from './door-card'; -import { DoorControls } from './door-controls'; - -export default { - title: 'Door Card', - component: DoorCard, -} satisfies Meta; - -type Story = StoryObj; - -export const Default: Story = { - args: { - name: 'main_door', - level: 'L1', - mode: DoorMode.MODE_OPEN, - type: Door.DOOR_TYPE_SINGLE_SWING, - }, - render: (args) => , -}; - -export const WithControls: Story = { - args: Default.args, - render: (args) => ( - - - - - - ), -}; diff --git a/packages/rmf-dashboard-framework/src/components/doors/door-card.tsx b/packages/rmf-dashboard-framework/src/components/doors/door-card.tsx deleted file mode 100644 index 1c4a91344..000000000 --- a/packages/rmf-dashboard-framework/src/components/doors/door-card.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { Card, CardContent, CardProps, Grid, SxProps, Typography, useTheme } from '@mui/material'; -import React from 'react'; -import { DoorMode } from 'rmf-models/ros/rmf_door_msgs/msg'; - -import { doorModeToString, doorTypeToString } from './door-utils'; - -export interface DoorCardProps extends CardProps { - name: string; - level: string; - mode: number; - type: number; -} - -export function DoorCard({ - name, - level, - mode, - type, - children, - ...cardProps -}: DoorCardProps): JSX.Element { - const theme = useTheme(); - const labelStyle = React.useMemo(() => { - switch (mode) { - case DoorMode.MODE_OPEN: - return { - backgroundColor: theme.palette.success.main, - color: theme.palette.success.contrastText, - }; - case DoorMode.MODE_CLOSED: - return { - backgroundColor: theme.palette.error.main, - color: theme.palette.error.contrastText, - }; - case DoorMode.MODE_MOVING: - return { - backgroundColor: theme.palette.warning.main, - color: theme.palette.warning.contrastText, - }; - default: - return { - backgroundColor: theme.palette.action.disabledBackground, - color: theme.palette.action.disabled, - }; - } - }, [theme, mode]); - - return ( - - - - {name} - - - - - {level} - - - - - {doorModeToString(mode)} - - - - - {doorTypeToString(type)} - - - {children} - - ); -} diff --git a/packages/rmf-dashboard-framework/src/components/doors/door-controls.tsx b/packages/rmf-dashboard-framework/src/components/doors/door-controls.tsx deleted file mode 100644 index 488b9f967..000000000 --- a/packages/rmf-dashboard-framework/src/components/doors/door-controls.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Button, ButtonGroup } from '@mui/material'; -import React from 'react'; - -export interface DoorControlsProps { - doorName?: string; - onOpenClick?(event: React.MouseEvent): void; - onCloseClick?(event: React.MouseEvent): void; -} - -export function DoorControls({ - doorName, - onOpenClick, - onCloseClick, -}: DoorControlsProps): JSX.Element { - return ( - - - - - ); -} diff --git a/packages/rmf-dashboard-framework/src/components/doors/door-summary.test.tsx b/packages/rmf-dashboard-framework/src/components/doors/door-summary.test.tsx new file mode 100644 index 000000000..910cfcf35 --- /dev/null +++ b/packages/rmf-dashboard-framework/src/components/doors/door-summary.test.tsx @@ -0,0 +1,66 @@ +import { waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { DoorState } from 'api-client'; +import React, { act } from 'react'; +import { Door as RmfDoor } from 'rmf-models/ros/rmf_building_map_msgs/msg'; +import { DoorMode as RmfDoorMode } from 'rmf-models/ros/rmf_door_msgs/msg'; +import { describe, expect, it, vi } from 'vitest'; + +import { RmfApiProvider } from '../../hooks'; +import { MockRmfApi, render, TestProviders } from '../../utils/test-utils.test'; +import { DoorSummary } from './door-summary'; +import { makeDoor } from './test-utils.test'; + +describe('DoorSummary', () => { + const mockDoor: RmfDoor = makeDoor({ name: 'test_door' }); + + const rmfApi = new MockRmfApi(); + const Base = (props: React.PropsWithChildren<{}>) => { + return ( + + {props.children} + + ); + }; + + it('renders door summary correctly', async () => { + const onCloseMock = vi.fn(); + const root = render( + + + , + ); + + // Create the subject for the door + const doorStateObs = rmfApi.getDoorStateObs('test_door'); + let emittedDoorState: DoorState | undefined; + doorStateObs.subscribe((doorState) => { + emittedDoorState = doorState; + }); + + const mockDoorState: DoorState = { + door_time: { sec: 0, nanosec: 0 }, + door_name: 'test_door', + current_mode: { value: RmfDoorMode.MODE_OPEN }, + }; + act(() => { + rmfApi.doorStateObsStore['test_door'].next(mockDoorState); + }); + + expect(emittedDoorState).toEqual(mockDoorState); + expect(emittedDoorState?.current_mode.value).toEqual(RmfDoorMode.MODE_OPEN); + + expect(root.getByText('Name')).toBeTruthy(); + expect(root.getByText('Current Floor')).toBeTruthy(); + expect(root.getByText('Type')).toBeTruthy(); + expect(root.getByText('State')).toBeTruthy(); + + expect(root.getByText('test_door')).toBeTruthy(); + expect(root.getByText('L1')).toBeTruthy(); + expect(root.getByText('Single Swing')).toBeTruthy(); + expect(root.getByText('OPEN')).toBeTruthy(); + + userEvent.keyboard('{Escape}'); + await waitFor(() => expect(onCloseMock).toHaveBeenCalledTimes(1)); + }); +}); diff --git a/packages/rmf-dashboard-framework/src/components/doors/door-summary.tsx b/packages/rmf-dashboard-framework/src/components/doors/door-summary.tsx index f50e7a231..2adde3b88 100644 --- a/packages/rmf-dashboard-framework/src/components/doors/door-summary.tsx +++ b/packages/rmf-dashboard-framework/src/components/doors/door-summary.tsx @@ -1,26 +1,25 @@ import { Dialog, DialogContent, DialogTitle, Divider, TextField, useTheme } from '@mui/material'; -import { Level } from 'api-client'; import React from 'react'; import { Door as DoorModel } from 'rmf-models/ros/rmf_building_map_msgs/msg'; import { useRmfApi } from '../../hooks'; import { getApiErrorMessage } from '../../utils/api'; -import { doorModeToOpModeString, DoorTableData } from './door-table-datagrid'; +import { DoorTableData } from './door-table-datagrid'; import { doorModeToString, doorTypeToString } from './door-utils'; interface DoorSummaryProps { onClose: () => void; door: DoorModel; - level: Level; + doorLevelName: string; } -export const DoorSummary = ({ onClose, door, level }: DoorSummaryProps): JSX.Element => { +export const DoorSummary = ({ onClose, door, doorLevelName }: DoorSummaryProps): JSX.Element => { const rmfApi = useRmfApi(); const [doorData, setDoorData] = React.useState({ index: 0, - doorName: '', - levelName: '', - doorType: 0, + doorName: door.name, + levelName: doorLevelName, + doorType: door.door_type, doorState: undefined, }); @@ -31,7 +30,7 @@ export const DoorSummary = ({ onClose, door, level }: DoorSummaryProps): JSX.Ele setDoorData({ index: 0, doorName: door.name, - levelName: level.name, + levelName: doorLevelName, doorType: door.door_type, doorState: doorState, }); @@ -43,7 +42,7 @@ export const DoorSummary = ({ onClose, door, level }: DoorSummaryProps): JSX.Ele }; fetchDataForDoor(); - }, [rmfApi, level, door]); + }, [rmfApi, doorLevelName, door]); const [isOpen, setIsOpen] = React.useState(true); @@ -70,7 +69,7 @@ export const DoorSummary = ({ onClose, door, level }: DoorSummaryProps): JSX.Ele {Object.entries(doorData).map(([key, value]) => { if (key === 'index') { - return <>; + return
; } let displayValue = value; let displayLabel = key; @@ -78,10 +77,6 @@ export const DoorSummary = ({ onClose, door, level }: DoorSummaryProps): JSX.Ele case 'doorName': displayLabel = 'Name'; break; - case 'opMode': - displayValue = doorModeToOpModeString(value.current_mode); - displayLabel = 'Op. Mode'; - break; case 'levelName': displayLabel = 'Current Floor'; break; @@ -90,7 +85,9 @@ export const DoorSummary = ({ onClose, door, level }: DoorSummaryProps): JSX.Ele displayLabel = 'Type'; break; case 'doorState': - displayValue = value ? doorModeToString(value.current_mode.value) : -1; + displayValue = value + ? doorModeToString(value.current_mode.value) + : doorModeToString(undefined); displayLabel = 'State'; break; default: diff --git a/packages/rmf-dashboard-framework/src/components/doors/door-table-datagrid.test.tsx b/packages/rmf-dashboard-framework/src/components/doors/door-table-datagrid.test.tsx index 472ec6eb1..67f769295 100644 --- a/packages/rmf-dashboard-framework/src/components/doors/door-table-datagrid.test.tsx +++ b/packages/rmf-dashboard-framework/src/components/doors/door-table-datagrid.test.tsx @@ -1,7 +1,9 @@ import { render } from '@testing-library/react'; +import { waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { Door as RmfDoor } from 'rmf-models/ros/rmf_building_map_msgs/msg'; import { DoorMode as RmfDoorMode } from 'rmf-models/ros/rmf_door_msgs/msg'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { DoorDataGridTable, DoorTableData } from './door-table-datagrid'; import { makeDoorState } from './test-utils.test'; @@ -46,4 +48,14 @@ describe('DoorDataGridTable', () => { expect(root.queryByText('Type')).toBeTruthy(); expect(root.queryByText('Door State')).toBeTruthy(); }); + + it('door clicks triggered', async () => { + const onDoorClicked = vi.fn(); + const root = render(); + + userEvent.click(root.getByText('Open')); + await waitFor(() => expect(onDoorClicked).toHaveBeenCalledTimes(1)); + userEvent.click(root.getByText('Close')); + await waitFor(() => expect(onDoorClicked).toHaveBeenCalledTimes(2)); + }); }); diff --git a/packages/rmf-dashboard-framework/src/components/doors/door-utils.test.ts b/packages/rmf-dashboard-framework/src/components/doors/door-utils.test.ts new file mode 100644 index 000000000..329a7db32 --- /dev/null +++ b/packages/rmf-dashboard-framework/src/components/doors/door-utils.test.ts @@ -0,0 +1,24 @@ +import { Door as RmfDoor } from 'rmf-models/ros/rmf_building_map_msgs/msg'; +import { DoorMode as RmfDoorMode } from 'rmf-models/ros/rmf_door_msgs/msg'; +import { describe, expect, it } from 'vitest'; + +import { doorModeToString, doorTypeToString } from './door-utils'; + +describe('door utils', () => { + it('doorModeToString', () => { + expect(doorModeToString(RmfDoorMode.MODE_OPEN)).toEqual('OPEN'); + expect(doorModeToString(RmfDoorMode.MODE_CLOSED)).toEqual('CLOSED'); + expect(doorModeToString(RmfDoorMode.MODE_MOVING)).toEqual('MOVING'); + expect(doorModeToString(-1)).toEqual('UNKNOWN'); + }); + + it('doorTypeToString', () => { + expect(doorTypeToString(RmfDoor.DOOR_TYPE_DOUBLE_SLIDING)).toEqual('Double Sliding'); + expect(doorTypeToString(RmfDoor.DOOR_TYPE_DOUBLE_SWING)).toEqual('Double Swing'); + expect(doorTypeToString(RmfDoor.DOOR_TYPE_DOUBLE_TELESCOPE)).toEqual('Double Telescope'); + expect(doorTypeToString(RmfDoor.DOOR_TYPE_SINGLE_SLIDING)).toEqual('Single Sliding'); + expect(doorTypeToString(RmfDoor.DOOR_TYPE_SINGLE_SWING)).toEqual('Single Swing'); + expect(doorTypeToString(RmfDoor.DOOR_TYPE_SINGLE_TELESCOPE)).toEqual('Single Telescope'); + expect(doorTypeToString(-1)).toEqual('Unknown (-1)'); + }); +}); diff --git a/packages/rmf-dashboard-framework/src/components/doors/door-utils.ts b/packages/rmf-dashboard-framework/src/components/doors/door-utils.ts index 5f85a366a..7a0ecc544 100644 --- a/packages/rmf-dashboard-framework/src/components/doors/door-utils.ts +++ b/packages/rmf-dashboard-framework/src/components/doors/door-utils.ts @@ -28,7 +28,7 @@ export interface DoorData { export function doorModeToString(doorMode?: number): string { if (doorMode === undefined) { - return 'N/A'; + return 'n/a'; } switch (doorMode) { case RmfDoorMode.MODE_OPEN: diff --git a/packages/rmf-dashboard-framework/src/components/doors/doors-table.test.tsx b/packages/rmf-dashboard-framework/src/components/doors/doors-table.test.tsx new file mode 100644 index 000000000..94dcdb124 --- /dev/null +++ b/packages/rmf-dashboard-framework/src/components/doors/doors-table.test.tsx @@ -0,0 +1,102 @@ +import { BuildingMap, DoorState } from 'api-client'; +import React, { act } from 'react'; +import { Door as RmfDoor } from 'rmf-models/ros/rmf_building_map_msgs/msg'; +import { DoorMode as RmfDoorMode } from 'rmf-models/ros/rmf_door_msgs/msg'; +import { describe, expect, it } from 'vitest'; + +import { RmfApiProvider } from '../../hooks'; +import { MockRmfApi, render, TestProviders } from '../../utils/test-utils.test'; +import { DoorsTable } from './doors-table'; + +describe('DoorsTable', () => { + const rmfApi = new MockRmfApi(); + // mock out some api calls so they never resolves + rmfApi.doorsApi.postDoorRequestDoorsDoorNameRequestPost = () => new Promise(() => {}); + const Base = (props: React.PropsWithChildren<{}>) => { + return ( + + {props.children} + + ); + }; + + it('renders with empty doors table', () => { + const root = render( + + + , + ); + expect(root.getByText('Name')).toBeTruthy(); + expect(root.getByText('Op. Mode')).toBeTruthy(); + expect(root.getByText('Current Floor')).toBeTruthy(); + expect(root.getByText('Type')).toBeTruthy(); + expect(root.getByText('Door State')).toBeTruthy(); + }); + + it('renders with mock door', async () => { + const root = render( + + + , + ); + + const mockBuildingMap: BuildingMap = { + name: 'test_map', + levels: [ + { + name: 'L2', + elevation: 10, + images: [], + places: [], + doors: [ + { + name: 'test_door2', + v1_x: 0, + v1_y: 0, + v2_x: 0, + v2_y: 0, + door_type: RmfDoor.DOOR_TYPE_DOUBLE_SWING, + motion_range: 0, + motion_direction: 0, + }, + ], + nav_graphs: [], + wall_graph: { + name: 'test_graph', + vertices: [], + edges: [], + params: [], + }, + }, + ], + lifts: [], + }; + + act(() => { + rmfApi.buildingMapObs.next(mockBuildingMap); + }); + + // Create the subject for the door + rmfApi.getDoorStateObs('test_door2'); + const mockDoorState: DoorState = { + door_time: { sec: 0, nanosec: 0 }, + door_name: 'test_door2', + current_mode: { value: RmfDoorMode.MODE_CLOSED }, + }; + act(() => { + rmfApi.doorStateObsStore['test_door2'].next(mockDoorState); + }); + + expect(root.getByText('Name')).toBeTruthy(); + expect(root.getByText('Op. Mode')).toBeTruthy(); + expect(root.getByText('Current Floor')).toBeTruthy(); + expect(root.getByText('Type')).toBeTruthy(); + expect(root.getByText('Door State')).toBeTruthy(); + + expect(root.getByText('test_door2')).toBeTruthy(); + expect(root.getByText('ONLINE')).toBeTruthy(); + expect(root.getByText('L2')).toBeTruthy(); + expect(root.getByText('Double Swing')).toBeTruthy(); + expect(root.getByText('CLOSED')).toBeTruthy(); + }); +}); diff --git a/packages/rmf-dashboard-framework/src/components/doors/index.tsx b/packages/rmf-dashboard-framework/src/components/doors/index.ts similarity index 68% rename from packages/rmf-dashboard-framework/src/components/doors/index.tsx rename to packages/rmf-dashboard-framework/src/components/doors/index.ts index 9a56e4a6b..732195a65 100644 --- a/packages/rmf-dashboard-framework/src/components/doors/index.tsx +++ b/packages/rmf-dashboard-framework/src/components/doors/index.ts @@ -1,3 +1,2 @@ -export * from './door-card'; export * from './door-summary'; export * from './doors-table'; diff --git a/packages/rmf-dashboard-framework/src/components/lifts/index.ts b/packages/rmf-dashboard-framework/src/components/lifts/index.ts index 69b6a0f2a..0d6bfc49d 100644 --- a/packages/rmf-dashboard-framework/src/components/lifts/index.ts +++ b/packages/rmf-dashboard-framework/src/components/lifts/index.ts @@ -1,4 +1,3 @@ -export * from './lift-card'; export * from './lift-request-dialog'; export * from './lift-summary'; export * from './lifts-table'; diff --git a/packages/rmf-dashboard-framework/src/components/lifts/lift-card.stories.tsx b/packages/rmf-dashboard-framework/src/components/lifts/lift-card.stories.tsx deleted file mode 100644 index ebb3388de..000000000 --- a/packages/rmf-dashboard-framework/src/components/lifts/lift-card.stories.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { CardActions } from '@mui/material'; -import { Meta, StoryObj } from '@storybook/react'; -import { LiftState } from 'rmf-models/ros/rmf_lift_msgs/msg'; - -import { LiftCard } from './lift-card'; -import { LiftControls } from './lift-controls'; - -export default { - title: 'Lift Card', - component: LiftCard, -} satisfies Meta; - -type Story = StoryObj; - -export const Default: Story = { - args: { - name: 'main_lift', - motionState: LiftState.MOTION_UP, - currentFloor: 'L1', - destinationFloor: 'L2', - doorState: LiftState.DOOR_CLOSED, - }, - render: (args) => , -}; - -export const WithControls: Story = { - args: Default.args, - render: (args) => ( - - - - - - ), -}; diff --git a/packages/rmf-dashboard-framework/src/components/lifts/lift-card.tsx b/packages/rmf-dashboard-framework/src/components/lifts/lift-card.tsx deleted file mode 100644 index 130261dee..000000000 --- a/packages/rmf-dashboard-framework/src/components/lifts/lift-card.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward'; -import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; -import { - Box, - Card, - CardContent, - CardProps, - Grid, - SxProps, - Typography, - useTheme, -} from '@mui/material'; -import { LiftState } from 'rmf-models/ros/rmf_lift_msgs/msg'; - -import { doorStateToString, motionStateToString } from './lift-utils'; - -export interface LiftCardProps extends CardProps { - name: string; - motionState?: number; - doorState?: number; - currentFloor?: string; - destinationFloor?: string; -} - -export function LiftCard({ - name, - motionState, - doorState, - currentFloor, - destinationFloor, - children, - ...cardProps -}: LiftCardProps): JSX.Element { - const theme = useTheme(); - const currMotion = motionStateToString(motionState); - const motionArrowActiveStyle: SxProps = { - color: theme.palette.primary.main, - }; - const motionArrowIdleStyle: SxProps = { - color: theme.palette.action.disabled, - opacity: theme.palette.action.disabledOpacity, - }; - const currDoorMotion = doorStateToString(doorState); - - const doorStateLabelStyle: SxProps = (() => { - switch (doorState) { - case LiftState.DOOR_OPEN: - return { - backgroundColor: theme.palette.success.main, - color: theme.palette.success.contrastText, - }; - case LiftState.DOOR_CLOSED: - return { - backgroundColor: theme.palette.error.main, - color: theme.palette.error.contrastText, - }; - case LiftState.DOOR_MOVING: - return { - backgroundColor: theme.palette.warning.main, - color: theme.palette.warning.contrastText, - }; - default: - return { - backgroundColor: theme.palette.action.disabledBackground, - color: theme.palette.action.disabled, - }; - } - })(); - - return ( - - - - - - {name} - - - {destinationFloor || 'Unknown'} - - - {currDoorMotion} - - - - - - - {currentFloor || '?'} - - - - - - - {children} - - ); -} diff --git a/packages/rmf-dashboard-framework/src/components/lifts/lift-summary.test.tsx b/packages/rmf-dashboard-framework/src/components/lifts/lift-summary.test.tsx new file mode 100644 index 000000000..bcad7040d --- /dev/null +++ b/packages/rmf-dashboard-framework/src/components/lifts/lift-summary.test.tsx @@ -0,0 +1,57 @@ +import { waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import type { Lift, LiftState } from 'api-client'; +import React, { act } from 'react'; +import { describe, expect, it, vi } from 'vitest'; + +import { RmfApiProvider } from '../../hooks'; +import { MockRmfApi, render, TestProviders } from '../../utils/test-utils.test'; +import { LiftSummary } from './lift-summary'; +import { makeLift, makeLiftState } from './test-utils.test'; + +describe('LiftSummary', () => { + const mockLift: Lift = makeLift({ name: 'test_lift' }); + + const rmfApi = new MockRmfApi(); + const Base = (props: React.PropsWithChildren<{}>) => { + return ( + + {props.children} + + ); + }; + + it('renders lift summary correctly', async () => { + const onCloseMock = vi.fn(); + const root = render( + + + , + ); + + // Create the subject for the lift + rmfApi.getLiftStateObs('test_lift'); + const mockLiftState: LiftState = makeLiftState({ + lift_name: 'test_lift', + destination_floor: 'L2', + }); + act(() => { + rmfApi.liftStateObsStore['test_lift'].next(mockLiftState); + }); + + expect(root.getByText('Name')).toBeTruthy(); + expect(root.getByText('Current Floor')).toBeTruthy(); + expect(root.getByText('Destination Floor')).toBeTruthy(); + expect(root.getByText('State')).toBeTruthy(); + expect(root.getByText('Session ID')).toBeTruthy(); + + expect(root.getByText('test_lift')).toBeTruthy(); + expect(root.getByText('L1')).toBeTruthy(); + expect(root.getByText('L2')).toBeTruthy(); + expect(root.getByText('CLOSED')).toBeTruthy(); + expect(root.getByText('test_session')).toBeTruthy(); + + userEvent.keyboard('{Escape}'); + await waitFor(() => expect(onCloseMock).toHaveBeenCalledTimes(1)); + }); +}); diff --git a/packages/rmf-dashboard-framework/src/components/lifts/lift-summary.tsx b/packages/rmf-dashboard-framework/src/components/lifts/lift-summary.tsx index 6df11d858..814d70deb 100644 --- a/packages/rmf-dashboard-framework/src/components/lifts/lift-summary.tsx +++ b/packages/rmf-dashboard-framework/src/components/lifts/lift-summary.tsx @@ -13,7 +13,7 @@ import React from 'react'; import { useRmfApi } from '../../hooks'; import { getApiErrorMessage } from '../../utils/api'; import { LiftTableData } from './lift-table-datagrid'; -import { doorStateToString, liftModeToString } from './lift-utils'; +import { doorStateToString } from './lift-utils'; interface LiftSummaryProps { onClose: () => void; @@ -89,7 +89,7 @@ export const LiftSummary = ({ onClose, lift }: LiftSummaryProps): JSX.Element => {Object.entries(liftData).map(([key, value]) => { if (key === 'index' || key === 'motionState' || key === 'lift') { - return <>; + return
; } let displayValue = value; let displayLabel = key; @@ -97,10 +97,6 @@ export const LiftSummary = ({ onClose, lift }: LiftSummaryProps): JSX.Element => case 'name': displayLabel = 'Name'; break; - case 'mode': - displayValue = liftModeToString(value); - displayLabel = 'Mode'; - break; case 'currentFloor': displayLabel = 'Current Floor'; break; diff --git a/packages/rmf-dashboard-framework/src/components/lifts/lift-table-datagrid.tsx b/packages/rmf-dashboard-framework/src/components/lifts/lift-table-datagrid.tsx index 496900292..8d035cf2b 100644 --- a/packages/rmf-dashboard-framework/src/components/lifts/lift-table-datagrid.tsx +++ b/packages/rmf-dashboard-framework/src/components/lifts/lift-table-datagrid.tsx @@ -200,7 +200,7 @@ export function LiftDataGridTable({ lifts, onLiftClick }: LiftDataGridTableProps width: 150, editable: false, valueGetter: (params: GridValueGetterParams) => - params.row.currentFloor ? params.row.currentFloor : 'N/A', + params.row.currentFloor ? params.row.currentFloor : 'n/a', flex: 1, filterable: true, }, @@ -210,7 +210,7 @@ export function LiftDataGridTable({ lifts, onLiftClick }: LiftDataGridTableProps width: 150, editable: false, valueGetter: (params: GridValueGetterParams) => - params.row.destinationFloor ? params.row.destinationFloor : 'N/A', + params.row.destinationFloor ? params.row.destinationFloor : 'n/a', flex: 1, filterable: true, }, diff --git a/packages/rmf-dashboard-framework/src/components/lifts/lifts-table.test.tsx b/packages/rmf-dashboard-framework/src/components/lifts/lifts-table.test.tsx new file mode 100644 index 000000000..1f484c619 --- /dev/null +++ b/packages/rmf-dashboard-framework/src/components/lifts/lifts-table.test.tsx @@ -0,0 +1,127 @@ +import { BuildingMap, LiftState } from 'api-client'; +import React, { act } from 'react'; +import { LiftState as RmfLiftState } from 'rmf-models/ros/rmf_lift_msgs/msg'; +import { describe, expect, it } from 'vitest'; + +import { RmfApiProvider } from '../../hooks'; +import { MockRmfApi, render, TestProviders } from '../../utils/test-utils.test'; +import { LiftsTable } from './lifts-table'; +import { makeLift, makeLiftState } from './test-utils.test'; + +describe('LiftsTable', () => { + const rmfApi = new MockRmfApi(); + // mock out some api calls so they never resolves + rmfApi.fleetsApi.getFleetsFleetsGet = () => new Promise(() => {}); + rmfApi.liftsApi.postLiftRequestLiftsLiftNameRequestPost = () => new Promise(() => {}); + const Base = (props: React.PropsWithChildren<{}>) => { + return ( + + {props.children} + + ); + }; + + it('renders with lifts table', () => { + const root = render( + + + , + ); + expect(root.getByText('Name')).toBeTruthy(); + expect(root.getByText('Op. Mode')).toBeTruthy(); + expect(root.getByText('Current Floor')).toBeTruthy(); + expect(root.getByText('Destination Floor')).toBeTruthy(); + expect(root.getByText('Lift State')).toBeTruthy(); + }); + + it('renders with mock lift in AGV', async () => { + const root = render( + + + , + ); + + const mockBuildingMap: BuildingMap = { + name: 'test_map', + levels: [], + lifts: [ + makeLift({ + name: 'test_lift2', + levels: ['L1', 'L2', 'L3'], + }), + ], + }; + + act(() => { + rmfApi.buildingMapObs.next(mockBuildingMap); + }); + + // Create the subject for the lift + rmfApi.getLiftStateObs('test_lift2'); + const mockLiftState: LiftState = makeLiftState({ + lift_name: 'test_lift2', + current_floor: 'L2', + destination_floor: 'L1', + }); + act(() => { + rmfApi.liftStateObsStore['test_lift2'].next(mockLiftState); + }); + + expect(root.getByText('Name')).toBeTruthy(); + expect(root.getByText('Op. Mode')).toBeTruthy(); + expect(root.getByText('Current Floor')).toBeTruthy(); + expect(root.getByText('Destination Floor')).toBeTruthy(); + expect(root.getByText('Lift State')).toBeTruthy(); + + expect(root.getByText('test_lift2')).toBeTruthy(); + expect(root.getByText('AGV')).toBeTruthy(); + expect(root.getByText('L2')).toBeTruthy(); + expect(root.getByText('L1')).toBeTruthy(); + }); + + it('renders with mock lift in HUMAN', async () => { + const root = render( + + + , + ); + + const mockBuildingMap: BuildingMap = { + name: 'test_map', + levels: [], + lifts: [ + makeLift({ + name: 'test_lift2', + levels: ['L1', 'L2', 'L3'], + }), + ], + }; + + act(() => { + rmfApi.buildingMapObs.next(mockBuildingMap); + }); + + // Create the subject for the lift + rmfApi.getLiftStateObs('test_lift2'); + const mockLiftState2: LiftState = makeLiftState({ + lift_name: 'test_lift2', + current_floor: 'L1', + destination_floor: 'L3', + current_mode: RmfLiftState.MODE_HUMAN, + }); + act(() => { + rmfApi.liftStateObsStore['test_lift2'].next(mockLiftState2); + }); + + expect(root.getByText('Name')).toBeTruthy(); + expect(root.getByText('Op. Mode')).toBeTruthy(); + expect(root.getByText('Current Floor')).toBeTruthy(); + expect(root.getByText('Destination Floor')).toBeTruthy(); + expect(root.getByText('Lift State')).toBeTruthy(); + + expect(root.getByText('test_lift2')).toBeTruthy(); + expect(root.getByText('HUMAN')).toBeTruthy(); + expect(root.getByText('L1')).toBeTruthy(); + expect(root.getByText('L3')).toBeTruthy(); + }); +}); diff --git a/packages/rmf-dashboard-framework/src/components/map/camera-control.test.tsx b/packages/rmf-dashboard-framework/src/components/map/camera-control.test.tsx new file mode 100644 index 000000000..561c278ee --- /dev/null +++ b/packages/rmf-dashboard-framework/src/components/map/camera-control.test.tsx @@ -0,0 +1,23 @@ +import { Canvas } from '@react-three/fiber'; +import { render } from '@testing-library/react'; +import React from 'react'; +import { describe, it } from 'vitest'; + +import { TestProviders } from '../../utils/test-utils.test'; +import { CameraControl } from './camera-control'; + +describe('CameraControl', () => { + const Base = (_: React.PropsWithChildren<{}>) => { + return ; + }; + + it('should render without crashing', () => { + render( + + + + + , + ); + }); +}); diff --git a/packages/rmf-dashboard-framework/src/components/map/index.ts b/packages/rmf-dashboard-framework/src/components/map/index.ts new file mode 100644 index 000000000..111a393c6 --- /dev/null +++ b/packages/rmf-dashboard-framework/src/components/map/index.ts @@ -0,0 +1 @@ +export * from './map'; diff --git a/packages/rmf-dashboard-framework/src/components/map/layer-control.test.tsx b/packages/rmf-dashboard-framework/src/components/map/layers-controller.test.tsx similarity index 100% rename from packages/rmf-dashboard-framework/src/components/map/layer-control.test.tsx rename to packages/rmf-dashboard-framework/src/components/map/layers-controller.test.tsx diff --git a/packages/rmf-dashboard-framework/src/components/map/map.test.tsx b/packages/rmf-dashboard-framework/src/components/map/map.test.tsx new file mode 100644 index 000000000..b06a85a5e --- /dev/null +++ b/packages/rmf-dashboard-framework/src/components/map/map.test.tsx @@ -0,0 +1,48 @@ +import { render, waitFor } from '@testing-library/react'; +import React, { act } from 'react'; +import { describe, expect, it } from 'vitest'; + +import { RmfApiProvider } from '../../hooks'; +import { MockRmfApi, TestProviders } from '../../utils/test-utils.test'; +import Map from './map'; +import { officeMap } from './test-utils.test'; + +// Obtained from https://github.com/ZeeCoder/use-resize-observer/issues/40 +// to resolve ResizeObserver related errors during testing. +class ResizeObserver { + observe() {} + unobserve() {} + disconnect() {} +} + +describe('Map', () => { + window.ResizeObserver = ResizeObserver; + const rmfApi = new MockRmfApi(); + rmfApi.buildingApi.getBuildingMapBuildingMapGet = () => new Promise(() => {}); + const Base = (props: React.PropsWithChildren<{}>) => { + return ( + + {props.children} + + ); + }; + + it('renders map with office BuildingMap', async () => { + const root = render( + + + , + ); + + act(() => { + rmfApi.buildingMapObs.next(officeMap); + }); + + await expect(waitFor(() => root.getByText('test_attribution_prefix'))).resolves.toBeTruthy(); + }); +}); diff --git a/packages/rmf-dashboard-framework/src/components/map/index.tsx b/packages/rmf-dashboard-framework/src/components/map/map.tsx similarity index 99% rename from packages/rmf-dashboard-framework/src/components/map/index.tsx rename to packages/rmf-dashboard-framework/src/components/map/map.tsx index 730892a06..676fb8c41 100644 --- a/packages/rmf-dashboard-framework/src/components/map/index.tsx +++ b/packages/rmf-dashboard-framework/src/components/map/map.tsx @@ -483,7 +483,7 @@ export const Map = styled((props: MapProps) => { setDistance(Math.max(size.x, size.y, size.z) * 0.7); }, [sceneBoundingBox]); - return buildingMap && currentLevel && robotLocations ? ( + return buildingMap && currentLevel ? ( { setOpenDoorSummary(false)} door={selectedDoor} - level={currentLevel} + doorLevelName={currentLevel.name} /> )} diff --git a/packages/rmf-dashboard-framework/src/components/rmf-dashboard.tsx b/packages/rmf-dashboard-framework/src/components/rmf-dashboard.tsx index 93625d431..bfab7e512 100644 --- a/packages/rmf-dashboard-framework/src/components/rmf-dashboard.tsx +++ b/packages/rmf-dashboard-framework/src/components/rmf-dashboard.tsx @@ -35,7 +35,6 @@ import { } from '../services'; import { AlertManager } from './alert-manager'; import AppBar, { APP_BAR_HEIGHT } from './appbar'; -import { DeliveryAlertStore } from './delivery-alert-store'; import LocalizationProvider from './locale'; import { getDefaultTaskDefinition } from './tasks/types'; import { DashboardThemes } from './theme'; @@ -249,7 +248,6 @@ export function RmfDashboard(props: RmfDashboardProps) { - {/* TODO: Support stacking of alerts */} diff --git a/packages/rmf-dashboard-framework/src/components/robots/robots-table.tsx b/packages/rmf-dashboard-framework/src/components/robots/robots-table.tsx index 29598ed6c..4e5699f45 100644 --- a/packages/rmf-dashboard-framework/src/components/robots/robots-table.tsx +++ b/packages/rmf-dashboard-framework/src/components/robots/robots-table.tsx @@ -59,7 +59,7 @@ export const RobotsTable = () => { status: robot.status || undefined, estFinishTime: estFinishTime || undefined, lastUpdateTime: robot.unix_millis_time ? robot.unix_millis_time : undefined, - level: robot.location?.map || 'N/A', + level: robot.location?.map || 'n/a', commission: robot.commission || undefined, }; }) diff --git a/packages/rmf-dashboard-framework/src/components/workcells/utils.ts b/packages/rmf-dashboard-framework/src/components/workcells/utils.ts index 1cc110722..6ffec127d 100644 --- a/packages/rmf-dashboard-framework/src/components/workcells/utils.ts +++ b/packages/rmf-dashboard-framework/src/components/workcells/utils.ts @@ -9,6 +9,6 @@ export function dispenserModeToString(mode: number): string { case RmfDispenserState.OFFLINE: return 'OFFLINE'; default: - return 'N/A'; + return 'n/a'; } } diff --git a/packages/rmf-dashboard-framework/src/components/workcells/workcell-table.test.tsx b/packages/rmf-dashboard-framework/src/components/workcells/workcell-table.test.tsx index b52667d32..900d333f1 100644 --- a/packages/rmf-dashboard-framework/src/components/workcells/workcell-table.test.tsx +++ b/packages/rmf-dashboard-framework/src/components/workcells/workcell-table.test.tsx @@ -28,6 +28,6 @@ describe('Workcell table', () => { expect(root.getByLabelText('test3')).toBeTruthy(); // check if state unknown dispenser state is handled - expect(root.getAllByText('N/A').length).toEqual(1); + expect(root.getAllByText('n/a').length).toEqual(1); }); }); diff --git a/packages/rmf-dashboard-framework/src/micro-apps/map-app.ts b/packages/rmf-dashboard-framework/src/micro-apps/map-app.ts index e99faa306..9739abd52 100644 --- a/packages/rmf-dashboard-framework/src/micro-apps/map-app.ts +++ b/packages/rmf-dashboard-framework/src/micro-apps/map-app.ts @@ -1,11 +1,11 @@ import { createMicroApp, MicroAppManifest } from '../components'; -import type { MapProps } from '../components/map'; +import type { MapProps } from '../components/map/map'; export default function createMapApp(config: MapProps): MicroAppManifest { return createMicroApp( 'map', 'Map', - () => import('../components/map'), + () => import('../components/map/map'), () => config, ); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 72c41e0f0..45dea3964 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12484,4 +12484,4 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 immer: 9.0.21 - react: 18.3.1 + react: 18.3.1 \ No newline at end of file