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();
+}