diff --git a/src/components/generic/Table/index.test.tsx b/src/components/generic/Table/index.test.tsx new file mode 100644 index 00000000..03f9f382 --- /dev/null +++ b/src/components/generic/Table/index.test.tsx @@ -0,0 +1,111 @@ +import { describe, it, expect } from "vitest"; +import { render, screen, fireEvent } from "@testing-library/react"; +import { Table } from "@components/generic/Table/index.tsx"; +import { TimeAgo } from "@components/generic/TimeAgo.tsx"; +import { Mono } from "@components/generic/Mono.tsx"; + + +describe("Generic Table", () => { + it("Can render an empty table.", () => { + render( + + ); + expect(screen.getByRole("table")).toBeInTheDocument(); + }); + + it("Can render a table with headers and no rows.", async () => { + render( +
+ ); + await screen.findByRole('table'); + expect(screen.getAllByRole("columnheader")).toHaveLength(9); + }); + + // A simplified version of the rows in pages/Nodes.tsx for testing purposes + const mockDevicesWithShortNameAndConnection = [ + {user: {shortName: "TST1"}, hopsAway: 1, lastHeard: Date.now() + 1000 }, + {user: {shortName: "TST2"}, hopsAway: 0, lastHeard: Date.now() + 4000 }, + {user: {shortName: "TST3"}, hopsAway: 4, lastHeard: Date.now() }, + {user: {shortName: "TST4"}, hopsAway: 3, lastHeard: Date.now() + 2000 } + ]; + + const mockRows = mockDevicesWithShortNameAndConnection.map(node => [ +

{ node.user.shortName }

, + <>, + + {node.lastHeard !== 0 + ? node.hopsAway === 0 + ? "Direct" + : `${node.hopsAway?.toString()} ${ + node.hopsAway > 1 ? "hops" : "hop" + } away` + : "-"} + + ]) + + it("Can sort rows appropriately.", async () => { + render( +
+ ); + const renderedTable = await screen.findByRole('table'); + const columnHeaders = screen.getAllByRole("columnheader"); + expect(columnHeaders).toHaveLength(3); + + // Will be sorted "Last heard" "asc" by default + expect( [...renderedTable.querySelectorAll('[data-testshortname]')] + .map(el=>el.textContent) + .map(v=>v?.trim()) + .join(',')) + .toMatch('TST2,TST4,TST1,TST3'); + + fireEvent.click(columnHeaders[0]); + + // Re-sort by Short Name asc + expect( [...renderedTable.querySelectorAll('[data-testshortname]')] + .map(el=>el.textContent) + .map(v=>v?.trim()) + .join(',')) + .toMatch('TST1,TST2,TST3,TST4'); + + fireEvent.click(columnHeaders[0]); + + // Re-sort by Short Name desc + expect( [...renderedTable.querySelectorAll('[data-testshortname]')] + .map(el=>el.textContent) + .map(v=>v?.trim()) + .join(',')) + .toMatch('TST4,TST3,TST2,TST1'); + + fireEvent.click(columnHeaders[2]); + + // Re-sort by Hops Away + expect( [...renderedTable.querySelectorAll('[data-testshortname]')] + .map(el=>el.textContent) + .map(v=>v?.trim()) + .join(',')) + .toMatch('TST2,TST1,TST4,TST3'); + }); +}) \ No newline at end of file diff --git a/src/components/generic/Table/index.tsx b/src/components/generic/Table/index.tsx index 78497d04..ebd5bae4 100755 --- a/src/components/generic/Table/index.tsx +++ b/src/components/generic/Table/index.tsx @@ -12,6 +12,20 @@ export interface Heading { sortable: boolean; } +/** + * @param hopsAway String describing the number of hops away the node is from the current node + * @returns number of hopsAway or `0` if hopsAway is 'Direct' + */ +function numericHops(hopsAway: string): number { + if(hopsAway.match(/direct/i)){ + return 0; + } + if ( hopsAway.match(/\d+\s+hop/gi) ) { + return Number( hopsAway.match(/(\d+)\s+hop/i)?.[1] ); + } + return Number.MAX_SAFE_INTEGER; +} + export const Table = ({ headings, rows }: TableProps) => { const [sortColumn, setSortColumn] = useState("Last Heard"); const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc"); @@ -46,6 +60,20 @@ export const Table = ({ headings, rows }: TableProps) => { return 0; } + // Custom comparison for 'Connection' column + if (sortColumn === "Connection") { + const aNumHops = numericHops(aValue instanceof Array ? aValue[0] : aValue); + const bNumHops = numericHops(bValue instanceof Array ? bValue[0] : bValue); + + if (aNumHops < bNumHops) { + return sortOrder === "asc" ? -1 : 1; + } + if (aNumHops > bNumHops) { + return sortOrder === "asc" ? 1 : -1; + } + return 0; + } + // Default comparison for other columns if (aValue < bValue) { return sortOrder === "asc" ? -1 : 1; @@ -100,4 +128,4 @@ export const Table = ({ headings, rows }: TableProps) => {
); -}; +}; \ No newline at end of file