Merge pull request #492 from bkimmel/issue459/direct_nodes
Fix: issue 459 / sort direct nodes
This commit is contained in:
@@ -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(
|
||||||
|
<Table
|
||||||
|
headings={[]}
|
||||||
|
rows={[]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(screen.getByRole("table")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Can render a table with headers and no rows.", async () => {
|
||||||
|
render(
|
||||||
|
<Table
|
||||||
|
headings={[
|
||||||
|
{ title: "", type: "blank", sortable: false },
|
||||||
|
{ title: "Short Name", type: "normal", sortable: true },
|
||||||
|
{ title: "Long Name", type: "normal", sortable: true },
|
||||||
|
{ title: "Model", type: "normal", sortable: true },
|
||||||
|
{ title: "MAC Address", type: "normal", sortable: true },
|
||||||
|
{ title: "Last Heard", type: "normal", sortable: true },
|
||||||
|
{ title: "SNR", type: "normal", sortable: true },
|
||||||
|
{ title: "Encryption", type: "normal", sortable: false },
|
||||||
|
{ title: "Connection", type: "normal", sortable: true },
|
||||||
|
]}
|
||||||
|
rows={[]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
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 => [
|
||||||
|
<h1 data-testshortname> { node.user.shortName } </h1>,
|
||||||
|
<><TimeAgo timestamp={node.lastHeard * 1000} /></>,
|
||||||
|
<Mono key="hops" data-testhops>
|
||||||
|
{node.lastHeard !== 0
|
||||||
|
? node.hopsAway === 0
|
||||||
|
? "Direct"
|
||||||
|
: `${node.hopsAway?.toString()} ${
|
||||||
|
node.hopsAway > 1 ? "hops" : "hop"
|
||||||
|
} away`
|
||||||
|
: "-"}
|
||||||
|
</Mono>
|
||||||
|
])
|
||||||
|
|
||||||
|
it("Can sort rows appropriately.", async () => {
|
||||||
|
render(
|
||||||
|
<Table
|
||||||
|
headings={[
|
||||||
|
{ title: "Short Name", type: "normal", sortable: true },
|
||||||
|
{ title: "Last Heard", type: "normal", sortable: true },
|
||||||
|
{ title: "Connection", type: "normal", sortable: true },
|
||||||
|
]}
|
||||||
|
rows={mockRows}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
})
|
||||||
@@ -12,6 +12,20 @@ export interface Heading {
|
|||||||
sortable: boolean;
|
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) => {
|
export const Table = ({ headings, rows }: TableProps) => {
|
||||||
const [sortColumn, setSortColumn] = useState<string | null>("Last Heard");
|
const [sortColumn, setSortColumn] = useState<string | null>("Last Heard");
|
||||||
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc");
|
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc");
|
||||||
@@ -46,6 +60,20 @@ export const Table = ({ headings, rows }: TableProps) => {
|
|||||||
return 0;
|
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
|
// Default comparison for other columns
|
||||||
if (aValue < bValue) {
|
if (aValue < bValue) {
|
||||||
return sortOrder === "asc" ? -1 : 1;
|
return sortOrder === "asc" ? -1 : 1;
|
||||||
|
|||||||
Reference in New Issue
Block a user