Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/Frontend/test/preconditions/hasHeartbeatEndpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,3 +182,8 @@ export const HasHealthyAndUnHealthyNamedEndpoints = (numberOfHealthyEndpoints: n
};

export const HasHealthyAndUnHealthyEndpoints = (numberOfHealthyEndpoints: number, numberOfUnhealthyEndpoints: number) => HasHealthyAndUnHealthyNamedEndpoints(numberOfHealthyEndpoints, numberOfUnhealthyEndpoints, "TestEndpoint");

export const heartbeatEndpointNames = (endpointNamePrefix: string, count: number) => Array.from({ length: count }, (_unused, index) => `${endpointNamePrefix}_${index}`);

export const endpointsWithHeartbeatStatus = (sendingNames: string[], stoppedNames: string[] = []) =>
hasHeartbeatsEndpoints([...sendingNames.map((name) => ({ ...healthyEndpointTemplate, id: name, name })), ...stoppedNames.map((name) => ({ ...unHealthyEndpointTemplate, id: name, name }))]);
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { screen, within } from "@testing-library/vue";
import UserEvent from "@testing-library/user-event";
import type { HeartbeatsTab } from "../questions/getHeartbeatsTabCount";

const tabRegionName: Record<HeartbeatsTab, string> = {
active: "Healthy Endpoints",
inactive: "Unhealthy Endpoints",
};

const destinationReached = { timeout: 5000 };

export async function navigateToEndpointInstances(endpointName: string) {
const endpointRow = await screen.findByRole("row", { name: endpointName });
const detailsLink = within(endpointRow).getByRole("link", { name: "details-link" });

await UserEvent.click(detailsLink);

await screen.findByRole("heading", { name: `${endpointName} Instances` }, destinationReached);
}

export async function navigateBackToEndpointList(tab: HeartbeatsTab) {
const backLink = await screen.findByRole("link", { name: "Back" });

await UserEvent.click(backLink);

await screen.findByRole("region", { name: tabRegionName[tab] }, destinationReached);
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export async function navigateToHeartbeatsConfiguration() {
}

export async function navigateToHealthyHeartbeats() {
const healthyHeartbeatsTab = await screen.findByRole("tab", { name: /Healthy Endpoints \(\d+\)/i });
const healthyHeartbeatsTab = await screen.findByRole("tab", { name: /^\s*Healthy Endpoints \(\d+\)\s*$/i });
healthyHeartbeatsTab.click();

// Wait for the tab to switch
Expand Down
9 changes: 9 additions & 0 deletions src/Frontend/test/specs/heartbeats/actions/sortEndpointsBy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { screen, within } from "@testing-library/vue";
import UserEvent from "@testing-library/user-event";

export async function sortEndpointsBy(columnName: string) {
const columnHeader = await screen.findByRole("columnheader", { name: columnName });
const sortButton = within(columnHeader).getByRole("button", { name: columnName });

await UserEvent.click(sortButton);
}
381 changes: 313 additions & 68 deletions src/Frontend/test/specs/heartbeats/active-endpoints.spec.ts

Large diffs are not rendered by default.

129 changes: 92 additions & 37 deletions src/Frontend/test/specs/heartbeats/configuring-heartbeats.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,20 @@ import { test, describe } from "../../drivers/vitest/driver";
import * as precondition from "../../preconditions";
import { expect, vi } from "vitest";
import { getNothingToConfigureStatus } from "./questions/getNothingToConfigureStatus";
import { navigateToHeartbeatsConfiguration, navigateToUnHealthyHeartbeats } from "./actions/navigateToHeartbeatsTabs";
import { navigateToHeartbeatsConfiguration, navigateToHealthyHeartbeats, navigateToUnHealthyHeartbeats } from "./actions/navigateToHeartbeatsTabs";
import { getEndpointsForConfiguration } from "./questions/getEndpointsForConfiguration";
import { getEndpointInstance } from "./questions/getEndpointInstance";
import { toggleHeartbeatMonitoring } from "./actions/toggleHeartbeatMonitoring";
import { getAllHeartbeatEndpointRecords, getHeartbeatEndpointRecord } from "./questions/getHeartbeatEndpointRecord";
import { healthyEndpointTemplate } from "../../mocks/heartbeat-endpoint-template";
import { setHeartbeatFilter } from "./actions/setHeartbeatFilter";
import { getHeartbeatFilterValue } from "./questions/getHeartbeatFilterValue";
import { getListedEndpointNames } from "./questions/getListedEndpointNames";
import { getColumnSortDirection } from "./questions/getColumnSortDirection";
import { sortEndpointsBy } from "./actions/sortEndpointsBy";
import { waitFor } from "@testing-library/vue";
import { flushPromises } from "@vue/test-utils";
import { EndpointStatus } from "@/resources/Heartbeat";

vi.mock("@vueuse/core", async (importOriginal) => {
const originalModule = await importOriginal<typeof import("@vueuse/core")>();
Expand All @@ -21,6 +26,20 @@ vi.mock("@vueuse/core", async (importOriginal) => {
};
});

const endpointsStagedByHeartbeatAge = () => [endpointWithHeartbeatAge("Foo1", 2), endpointWithHeartbeatAge("Foo2", 3), endpointWithHeartbeatAge("Foo3", 1)];

function endpointWithHeartbeatAge(name: string, hoursSinceHeartbeat: number) {
return {
...healthyEndpointTemplate,
id: name,
name,
heartbeat_information: {
reported_status: EndpointStatus.Alive,
last_report_at: new Date(Date.now() - hoursSinceHeartbeat * 60 * 60 * 1000).toISOString(),
},
};
}

describe("FEATURE: Heartbeats configuration", () => {
describe("RULE: A list of all endpoints with the heartbeats plug-in installed should be displayed", () => {
test("EXAMPLE: With no endpoints, the text 'Nothing to configure' should be displayed", async ({ driver }) => {
Expand Down Expand Up @@ -133,47 +152,78 @@ describe("FEATURE: Heartbeats configuration", () => {
});

describe("RULE: Sorting by of the name of an endpoint should be possible in all displays", () => {
test.todo("EXAMPLE: List of endpoints should be sorted by name in ascending order");
test("EXAMPLE: List of endpoints should be sorted by name in ascending order", async ({ driver }) => {
await driver.setUp(precondition.serviceControlWithMonitoring);
await driver.setUp(precondition.hasHeartbeatsEndpoints(endpointsStagedByHeartbeatAge()));
await driver.goTo("heartbeats/unhealthy");

/* SCENARIO
Given 3 endpoint instance
Name |
Foo1
Foo2
Foo3
When the sort by is set to Name
Then the instances should be listed in order
*/
await navigateToHeartbeatsConfiguration();

/* NOTES
Name (asc/desc)
Latest heartbeat (asc/dec)
*/
await waitFor(() => expect(getListedEndpointNames()).toEqual(["Foo1", "Foo2", "Foo3"]));
expect(getColumnSortDirection("name")).toBe("ascending");
});

test.todo("EXAMPLE: List of endpoints should be sorted by name in descending order");
/* SCENARIO
Given 3 endpoint instance
Name |
Foo1
Foo2
Foo3
When the sort by is set to Name (descending)
Then the instances should be listed in reverse order
*/
test("EXAMPLE: List of endpoints should be sorted by name in descending order", async ({ driver }) => {
await driver.setUp(precondition.serviceControlWithMonitoring);
await driver.setUp(precondition.hasHeartbeatsEndpoints(endpointsStagedByHeartbeatAge()));
await driver.goTo("heartbeats/unhealthy");

test.todo("EXAMPLE: List of endpoints should be sorted by latest heartbeat in ascending order");
test.todo("EXAMPLE: List of endpoints should be sorted by latest heartbeat in descending order");
/* SCENARIO
Same again for Latest heartbeat
*/
await navigateToHeartbeatsConfiguration();
await waitFor(() => expect(getListedEndpointNames()).toEqual(["Foo1", "Foo2", "Foo3"]));

test.todo("EXAMPLE: Sort by should be persisted on page refresh and across tabs");
/* SCENARIO
Given the Sort By field has been changed
When the page is refreshed
Then the Sort By field retains its value
And the Sort By field has the same value on all other Endpoint Heartbeats tabs
*/
await sortEndpointsBy("name");

await waitFor(() => expect(getListedEndpointNames()).toEqual(["Foo3", "Foo2", "Foo1"]));
expect(getColumnSortDirection("name")).toBe("descending");
});

test("EXAMPLE: List of endpoints should be sorted by latest heartbeat in descending order", async ({ driver }) => {
await driver.setUp(precondition.serviceControlWithMonitoring);
await driver.setUp(precondition.hasHeartbeatsEndpoints(endpointsStagedByHeartbeatAge()));
await driver.goTo("heartbeats/unhealthy");

await navigateToHeartbeatsConfiguration();
await waitFor(() => expect(getListedEndpointNames()).toEqual(["Foo1", "Foo2", "Foo3"]));

await sortEndpointsBy("latestHeartbeat");

await waitFor(() => expect(getListedEndpointNames()).toEqual(["Foo3", "Foo1", "Foo2"]));
expect(getColumnSortDirection("latestHeartbeat")).toBe("descending");
});

test("EXAMPLE: List of endpoints should be sorted by latest heartbeat in ascending order", async ({ driver }) => {
await driver.setUp(precondition.serviceControlWithMonitoring);
await driver.setUp(precondition.hasHeartbeatsEndpoints(endpointsStagedByHeartbeatAge()));
await driver.goTo("heartbeats/unhealthy");

await navigateToHeartbeatsConfiguration();
await waitFor(() => expect(getListedEndpointNames()).toEqual(["Foo1", "Foo2", "Foo3"]));

await sortEndpointsBy("latestHeartbeat"); // newest first
await sortEndpointsBy("latestHeartbeat"); // oldest first

await waitFor(() => expect(getListedEndpointNames()).toEqual(["Foo2", "Foo1", "Foo3"]));
expect(getColumnSortDirection("latestHeartbeat")).toBe("ascending");
});

test("EXAMPLE: Sort by is shared across the Endpoint Heartbeats tabs", async ({ driver }) => {
await driver.setUp(precondition.serviceControlWithMonitoring);
await driver.setUp(precondition.hasHeartbeatsEndpoints(endpointsStagedByHeartbeatAge()));
await driver.goTo("heartbeats/unhealthy");

await navigateToHeartbeatsConfiguration();
await waitFor(() => expect(getListedEndpointNames()).toEqual(["Foo1", "Foo2", "Foo3"]));

await sortEndpointsBy("latestHeartbeat");
await waitFor(() => expect(getListedEndpointNames()).toEqual(["Foo3", "Foo1", "Foo2"]));

await navigateToHealthyHeartbeats();

await waitFor(() => expect(getListedEndpointNames()).toEqual(["Foo3", "Foo1", "Foo2"]));
expect(getColumnSortDirection("latestHeartbeat")).toBe("descending");
});

test.todo("EXAMPLE: Sort by should be persisted on page refresh");
});

describe("RULE: Filtering endpoints by name should be possible", () => {
Expand Down Expand Up @@ -265,6 +315,11 @@ describe("FEATURE: Heartbeats configuration", () => {
/* SCENARIO
When the configuration screen is loaded
Then a warning should be displayed about this being disconnected to performance monitoring

NOTES
The configuration screen shows no such warning. The only advisory on the
heartbeats pages is the MassTransit compatibility notice, which is unrelated
to performance monitoring, so there is nothing to assert against yet.
*/
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { screen, within } from "@testing-library/vue";

export type ColumnSortDirection = "ascending" | "descending" | "unsorted";

export function getColumnSortDirection(columnName: string): ColumnSortDirection {
const columnHeader = screen.queryByRole("columnheader", { name: columnName });

if (!columnHeader) {
return "unsorted";
}

if (within(columnHeader).queryByRole("img", { name: "sort-up" })) {
return "ascending";
}

if (within(columnHeader).queryByRole("img", { name: "sort-down" })) {
return "descending";
}

return "unsorted";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { screen, within } from "@testing-library/vue";

export type EndpointInstanceRow = {
hostName: string;
lastHeartbeat: string;
};

export function getEndpointInstanceRows(): EndpointInstanceRow[] {
const instanceRowgroup = screen.queryByRole("rowgroup", { name: "endpoints" });

if (!instanceRowgroup) {
return [];
}

return within(instanceRowgroup)
.queryAllByRole("row")
.map((row) => ({
hostName: within(row).getByLabelText("instance-name").textContent ?? "",
lastHeartbeat: within(row).getByTitle("Last Heartbeat").textContent ?? "",
}));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { screen } from "@testing-library/vue";

export type HeartbeatsTab = "active" | "inactive";

const tabAccessibleName: Record<HeartbeatsTab, RegExp> = {
active: /^\s*Healthy Endpoints \(\d+\)\s*$/i,
inactive: /^\s*Unhealthy Endpoints \(\d+\)\s*$/i,
};

export function getHeartbeatsTabCount(tab: HeartbeatsTab): number | null {
const tabElement = screen.queryAllByRole("tab").find((element) => tabAccessibleName[tab].test(element.textContent ?? ""));
const [, count] = tabElement?.textContent?.match(/\((\d+)\)/) ?? [];

return count === undefined ? null : Number(count);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { screen, within } from "@testing-library/vue";

export function getListedEndpointNames(): string[] {
const endpointRowgroup = screen.queryByRole("rowgroup", { name: "endpoints" });

if (!endpointRowgroup) {
return [];
}

return within(endpointRowgroup)
.queryAllByRole("row")
.map((row) => within(row).getByRole("link", { name: "details-link" }).textContent ?? "");
}
Loading