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