Add more tests and cleanup missions more
This commit is contained in:
@@ -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(
|
||||
<MantineProvider>
|
||||
<MissionsTable missions={getMockMissions()} />
|
||||
</MantineProvider>,
|
||||
);
|
||||
|
||||
expect(screen.queryByText('Launches')).toBeNull();
|
||||
});
|
||||
|
||||
it('shows launches when table row is expanded', async () => {
|
||||
render(
|
||||
<MantineProvider>
|
||||
<MissionsTable missions={getMockMissions()} />
|
||||
</MantineProvider>,
|
||||
);
|
||||
|
||||
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(
|
||||
<MantineProvider>
|
||||
<MissionsTable missions={getMockMissions()} />
|
||||
</MantineProvider>,
|
||||
);
|
||||
|
||||
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(
|
||||
<MantineProvider>
|
||||
<MissionsTable missions={getMockMissions(true)} />
|
||||
</MantineProvider>,
|
||||
);
|
||||
|
||||
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(
|
||||
// <QueryClientProvider client={queryClient}>
|
||||
// <MantineProvider>
|
||||
// <MissionsTable missions={missions} />
|
||||
// </MantineProvider>,
|
||||
// </QueryClientProvider>
|
||||
// );
|
||||
|
||||
// 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!
|
||||
});
|
||||
@@ -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 <Loader />;
|
||||
}
|
||||
|
||||
if (missionsQuery.isError) {
|
||||
return (
|
||||
<Alert color="red" title="Could not load missions">
|
||||
Is the API running?
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
type Props = {
|
||||
missions: Mission[];
|
||||
invalidateMissions?: () => void;
|
||||
};
|
||||
|
||||
export function MissionsTable({ missions, invalidateMissions }: Props) {
|
||||
return (
|
||||
<Table highlightOnHover>
|
||||
<Table.Thead>
|
||||
@@ -40,22 +42,35 @@ export function MissionsTable() {
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{missionsQuery.data.map((mission) => (
|
||||
<MissionsTableRow mission={mission} />
|
||||
{missions.map((mission) => (
|
||||
<MissionsTableRow
|
||||
key={mission.id}
|
||||
mission={mission}
|
||||
invalidateMissions={invalidateMissions}
|
||||
/>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
function MissionsTableRow({ mission }: { mission: Mission }) {
|
||||
function MissionsTableRow({
|
||||
mission,
|
||||
invalidateMissions,
|
||||
}: {
|
||||
mission: Mission;
|
||||
invalidateMissions?: () => void;
|
||||
}) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table.Tr key={mission.id}>
|
||||
<Table.Tr>
|
||||
<Table.Td>
|
||||
<ActionIcon onClick={() => setExpanded((expanded) => !expanded)}>
|
||||
<ActionIcon
|
||||
onClick={() => setExpanded((expanded) => !expanded)}
|
||||
data-testid="row-expand"
|
||||
>
|
||||
{expanded ? <IconChevronDown /> : <IconChevronRight />}
|
||||
</ActionIcon>
|
||||
</Table.Td>
|
||||
@@ -73,29 +88,33 @@ function MissionsTableRow({ mission }: { mission: Mission }) {
|
||||
<>
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={12}>
|
||||
<h3>
|
||||
Launches
|
||||
</h3>
|
||||
<Table>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Scheduled For</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{mission.missionLaunches.map((missionLaunch) => (
|
||||
<h3>Launches</h3>
|
||||
{mission.missionLaunches.length === 0 ? (
|
||||
<span>No launches have been created for this mission yet!</span>
|
||||
) : (
|
||||
<Table>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Td>{toReadableDateTime(missionLaunch.scheduledFor)}</Table.Td>
|
||||
<Table.Td>
|
||||
<MissionLaunchStatusBadge
|
||||
status={missionLaunch.status}
|
||||
/>
|
||||
</Table.Td>
|
||||
<Table.Th>Scheduled For</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{mission.missionLaunches.map((missionLaunch) => (
|
||||
<Table.Tr key={missionLaunch.id}>
|
||||
<Table.Td>
|
||||
{toReadableDateTime(missionLaunch.scheduledFor)}
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<MissionLaunchStatusBadge
|
||||
status={missionLaunch.status}
|
||||
/>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
)}
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
<Table.Tr>
|
||||
@@ -105,7 +124,10 @@ function MissionsTableRow({ mission }: { mission: Mission }) {
|
||||
<Button variant="default">Create new Launch</Button>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown>
|
||||
<CreateMissionLaunchForm missionId={mission.id} />
|
||||
<CreateMissionLaunchForm
|
||||
missionId={mission.id}
|
||||
invalidateMissions={invalidateMissions}
|
||||
/>
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
</Table.Td>
|
||||
@@ -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({
|
||||
|
||||
@@ -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 (
|
||||
<Stack maw={900}>
|
||||
<Title order={2}>Missions</Title>
|
||||
<CreateMissionForm />
|
||||
<MissionsTable />
|
||||
{missionsQuery.isPending ? <Loader /> : null}
|
||||
{missionsQuery.isError ? (
|
||||
<Alert color="red" title="Could not load missions">
|
||||
An error has occurred!
|
||||
</Alert>
|
||||
) : null}
|
||||
{missionsQuery.isSuccess && missionsQuery.data ? (
|
||||
<MissionsTable
|
||||
missions={missionsQuery.data}
|
||||
invalidateMissions={() => missionsQuery.refetch()}
|
||||
/>
|
||||
) : null}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -91,4 +109,4 @@ function CreateMissionForm() {
|
||||
</form>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
export function toReadableDateTime(dateTime: string) {
|
||||
const date = new Date(dateTime);
|
||||
const date = new Date(dateTime);
|
||||
|
||||
return date.toLocaleString();
|
||||
}
|
||||
return date.toLocaleString();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user