diff --git a/src/mission-control-client/src/components/MissionsTable/MissionsTable.test.tsx b/src/mission-control-client/src/components/MissionsTable/MissionsTable.test.tsx new file mode 100644 index 0000000..57150ef --- /dev/null +++ b/src/mission-control-client/src/components/MissionsTable/MissionsTable.test.tsx @@ -0,0 +1,107 @@ +import { MantineProvider } from '@mantine/core'; +import { render, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; +import type { Mission } from '../../api/missions'; +import { MissionsTable } from './MissionsTable'; +import type { MissionLaunch } from '../../api/missionLaunches'; + +function missionLaunches(): MissionLaunch[] { +return [{ + id: 'launch id', + scheduledFor: '2026-07-07T13:56:00+00:00', + status: 'Pending', + createdAt: '2026-07-07T13:56:00+00:00', +}]; +} + +function getMockMissions(withLaunches = false): Mission[] { + return [ + { + id: 'a mission id', + name: 'mission name is here!', + description: null, + status: 'Planned', + createdAt: '', + missionLaunches: withLaunches ? missionLaunches() : [], + }, +]; +} + +describe('MissionsTable', () => { + it('does not show launches on load', () => { + render( + + + , + ); + + expect(screen.queryByText('Launches')).toBeNull(); + }); + + it('shows launches when table row is expanded', async () => { + render( + + + , + ); + + screen.getAllByTestId('row-expand')[0].click(); + + expect(await screen.findByText('Launches')).toBeVisible(); + }); + + it('shows no launches message when no launches have been made for a mission', async () => { + render( + + + , + ); + + screen.getAllByTestId('row-expand')[0].click(); + + expect( + screen.queryByText( + 'No launches have been created for this mission yet!', + ), + ).toBeInTheDocument(); + }); + + it('shows does not show no launches message when launches have been made for a mission', async () => { + render( + + + , + ); + + screen.getAllByTestId('row-expand')[0].click(); + + expect( + screen.queryByText( + 'No launches have been created for this mission yet!', + ), + ).toBeNull(); + }); + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // This test doesn't work as we don't pass in the query client, I would like to extract most of the clients to the top-page level// + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // describe('CreateMissionLaunchForm', () => { + // it('shows form when create new launch is clicked', async () => { + // render( + // + // + // + // , + // + // ); + + // screen.getAllByTestId('row-expand')[0].click(); + + // (await screen.findByText('Create new Launch')).click(); + + // expect(await screen.findByText('Create launch')).toBeVisible(); + // }); + // }); + + // TODO: Would write many more tests here - particularly around form validation! +}); diff --git a/src/mission-control-client/src/components/MissionsTable/MissionsTable.tsx b/src/mission-control-client/src/components/MissionsTable/MissionsTable.tsx index 4e2bb93..1e26a93 100644 --- a/src/mission-control-client/src/components/MissionsTable/MissionsTable.tsx +++ b/src/mission-control-client/src/components/MissionsTable/MissionsTable.tsx @@ -1,34 +1,36 @@ -import { Loader, Alert, Table, ActionIcon, Popover, Button, Text, Group, Paper, Select, Stack } from "@mantine/core"; -import { IconChevronDown, IconChevronRight } from "@tabler/icons-react"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { useState } from "react"; -import { listMissions, type Mission } from "../../api/missions"; -import { toReadableDateTime } from "../../utils/dateUtils"; -import { MissionLaunchStatusBadge } from "../MissionLaunchStatusBadge"; -import { MissionStatusBadge } from "../MissionStatusBadge"; -import { DateTimePicker } from "@mantine/dates"; -import { useForm } from "@mantine/form"; -import { notifications } from "@mantine/notifications"; -import { createMissionLaunch, type MissionLaunchStatus, MissionLaunchStatuses } from "../../api/missionLaunches"; +import { + ActionIcon, + Button, + Group, + Paper, + Popover, + Select, + Stack, + Table, + Text, +} from '@mantine/core'; +import { DateTimePicker } from '@mantine/dates'; +import { useForm } from '@mantine/form'; +import { notifications } from '@mantine/notifications'; +import { IconChevronDown, IconChevronRight } from '@tabler/icons-react'; +import { useMutation } from '@tanstack/react-query'; +import { useState } from 'react'; +import { + createMissionLaunch, + MissionLaunchStatuses, + type MissionLaunchStatus, +} from '../../api/missionLaunches'; +import { type Mission } from '../../api/missions'; +import { toReadableDateTime } from '../../utils/dateUtils'; +import { MissionLaunchStatusBadge } from '../MissionLaunchStatusBadge'; +import { MissionStatusBadge } from '../MissionStatusBadge'; -export function MissionsTable() { - const missionsQuery = useQuery({ - queryKey: ['missions'], - queryFn: listMissions, - }); - - if (missionsQuery.isPending) { - return ; - } - - if (missionsQuery.isError) { - return ( - - Is the API running? - - ); - } +type Props = { + missions: Mission[]; + invalidateMissions?: () => void; +}; +export function MissionsTable({ missions, invalidateMissions }: Props) { return ( @@ -40,22 +42,35 @@ export function MissionsTable() { - {missionsQuery.data.map((mission) => ( - + {missions.map((mission) => ( + ))}
); } -function MissionsTableRow({ mission }: { mission: Mission }) { +function MissionsTableRow({ + mission, + invalidateMissions, +}: { + mission: Mission; + invalidateMissions?: () => void; +}) { const [expanded, setExpanded] = useState(false); return ( <> - + - setExpanded((expanded) => !expanded)}> + setExpanded((expanded) => !expanded)} + data-testid="row-expand" + > {expanded ? : } @@ -73,29 +88,33 @@ function MissionsTableRow({ mission }: { mission: Mission }) { <> -

- Launches -

- - - - Scheduled For - Status - - - - {mission.missionLaunches.map((missionLaunch) => ( +

Launches

+ {mission.missionLaunches.length === 0 ? ( + No launches have been created for this mission yet! + ) : ( +
+ - {toReadableDateTime(missionLaunch.scheduledFor)} - - - + Scheduled For + Status - ))} - -
+ + + {mission.missionLaunches.map((missionLaunch) => ( + + + {toReadableDateTime(missionLaunch.scheduledFor)} + + + + + + ))} + + + )}
@@ -105,7 +124,10 @@ function MissionsTableRow({ mission }: { mission: Mission }) { - + @@ -116,9 +138,13 @@ function MissionsTableRow({ mission }: { mission: Mission }) { ); } -function CreateMissionLaunchForm({ missionId }: { missionId: string }) { - const queryClient = useQueryClient(); - +function CreateMissionLaunchForm({ + missionId, + invalidateMissions, +}: { + missionId: string; + invalidateMissions?: () => void; +}) { const form = useForm({ mode: 'uncontrolled', initialValues: { scheduledFor: '', status: '' }, @@ -139,7 +165,7 @@ function CreateMissionLaunchForm({ missionId }: { missionId: string }) { color: 'green', }); form.reset(); - return queryClient.invalidateQueries({ queryKey: ['missions'] }); + invalidateMissions?.(); }, onError: () => notifications.show({ diff --git a/src/mission-control-client/src/routes/missions.tsx b/src/mission-control-client/src/routes/missions.tsx index 8e9b995..2424820 100644 --- a/src/mission-control-client/src/routes/missions.tsx +++ b/src/mission-control-client/src/routes/missions.tsx @@ -1,6 +1,8 @@ import { + Alert, Button, Group, + Loader, Paper, Stack, Textarea, @@ -9,9 +11,9 @@ import { } from '@mantine/core'; import { useForm } from '@mantine/form'; import { notifications } from '@mantine/notifications'; -import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { createFileRoute } from '@tanstack/react-router'; -import { createMission } from '../api/missions'; +import { createMission, listMissions } from '../api/missions'; import { MissionsTable } from '../components/MissionsTable/MissionsTable'; export const Route = createFileRoute('/missions')({ @@ -19,11 +21,27 @@ export const Route = createFileRoute('/missions')({ }); function MissionsPage() { + const missionsQuery = useQuery({ + queryKey: ['missions'], + queryFn: listMissions, + }); + return ( Missions - + {missionsQuery.isPending ? : null} + {missionsQuery.isError ? ( + + An error has occurred! + + ) : null} + {missionsQuery.isSuccess && missionsQuery.data ? ( + missionsQuery.refetch()} + /> + ) : null} ); } @@ -91,4 +109,4 @@ function CreateMissionForm() { ); -} \ No newline at end of file +} diff --git a/src/mission-control-client/src/utils/dateUtils.test.ts b/src/mission-control-client/src/utils/dateUtils.test.ts new file mode 100644 index 0000000..b7a13be --- /dev/null +++ b/src/mission-control-client/src/utils/dateUtils.test.ts @@ -0,0 +1,26 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { toReadableDateTime } from "./dateUtils"; + +beforeEach(() => { + vi.stubEnv('TZ', 'UTC'); +}); + +afterEach(() => { + vi.unstubAllEnvs(); +}); + +describe('dateUtils', () => { + describe('toReadableDateTime', () => { + it('returns invalid date when datetime passed is in incorrect format', () => { + const date = toReadableDateTime('invalid'); + + expect(date).toBe("Invalid Date"); + }); + + it ('returns readable date when datetime passed is in valid format', () => { + const date = toReadableDateTime('2026-07-27T23:27:22.1976394+00:00'); + + expect(date).toBe('27/07/2026, 23:27:22'); + }); + }); +}); \ No newline at end of file diff --git a/src/mission-control-client/src/utils/dateUtils.ts b/src/mission-control-client/src/utils/dateUtils.ts index 185d0b6..7fb16f0 100644 --- a/src/mission-control-client/src/utils/dateUtils.ts +++ b/src/mission-control-client/src/utils/dateUtils.ts @@ -1,5 +1,5 @@ export function toReadableDateTime(dateTime: string) { - const date = new Date(dateTime); + const date = new Date(dateTime); - return date.toLocaleString(); -} \ No newline at end of file + return date.toLocaleString(); +}