From bdd794696e2aef7b5c2cafbb1799272ec8a8db93 Mon Sep 17 00:00:00 2001 From: Michelle Chamberlin Date: Mon, 28 Sep 2026 16:48:00 -0700 Subject: [PATCH] consolidate test files, add testing for to-dos in the files --- .../test/preconditions/recoverability.ts | 49 ++- .../actions/selectFailedMessageGroup.ts | 7 + .../questions/getFailedMessagesView.ts | 14 + .../questions/getMessageView.ts | 25 ++ .../viewing-failed-message.spec.ts | 70 ---- .../viewing-failed-messages.spec.ts | 326 +++++++++++++++++- 6 files changed, 400 insertions(+), 91 deletions(-) create mode 100644 src/Frontend/test/specs/failedmessages/actions/selectFailedMessageGroup.ts create mode 100644 src/Frontend/test/specs/failedmessages/questions/getFailedMessagesView.ts create mode 100644 src/Frontend/test/specs/failedmessages/questions/getMessageView.ts delete mode 100644 src/Frontend/test/specs/failedmessages/viewing-failed-message.spec.ts diff --git a/src/Frontend/test/preconditions/recoverability.ts b/src/Frontend/test/preconditions/recoverability.ts index b7b3fab30c..140746caf4 100644 --- a/src/Frontend/test/preconditions/recoverability.ts +++ b/src/Frontend/test/preconditions/recoverability.ts @@ -2,7 +2,7 @@ import type Configuration from "@/resources/Configuration"; import type { EditAndRetryConfig } from "@/resources/Configuration"; import type { SetupFactoryOptions } from "../driver"; import type RecoverabilityHistoryResponse from "@/resources/RecoverabilityHistoryResponse"; -import type { FailedMessage } from "@/resources/FailedMessage"; +import { type FailedMessage, FailedMessageStatus } from "@/resources/FailedMessage"; import type Message from "@/resources/Message"; export const serviceControlConfigurationDefaultHandler = ({ driver }: SetupFactoryOptions) => { @@ -128,7 +128,23 @@ export const enableEditAndRetry = ({ driver }: SetupFactoryOptions) => { }; export const hasFailedMessage = - ({ withGroupId, withMessageId, withContentType, withBody }: { withGroupId: string; withMessageId: string; withContentType: string; withBody: Record | string | number | boolean | null | undefined }) => + ({ + withGroupId, + withMessageId, + withContentType, + withBody, + withStatus = FailedMessageStatus.Unresolved, + withTimeOfFailure = "2024-06-27T06:14:48.912923Z", + withLastModified = "2024-06-27T06:14:49.2216249Z", + }: { + withGroupId: string; + withMessageId: string; + withContentType: string; + withBody: Record | string | number | boolean | null | undefined; + withStatus?: FailedMessageStatus; + withTimeOfFailure?: string; + withLastModified?: string; + }) => ({ driver }: SetupFactoryOptions) => { const serviceControlUrl = window.defaultConfig.service_control_url; @@ -146,26 +162,26 @@ export const hasFailedMessage = }, message_id: withMessageId, number_of_processing_attempts: 1, - status: "unresolved", + status: withStatus, sending_endpoint: { name: "Sender", host_id: "abb12931-1352-fd70-02c2-b78f6daab553", host: "mobvm2" }, receiving_endpoint: { name: "Endpoint1", host_id: "abb12931-1352-fd70-02c2-b78f6daab553", host: "mobvm2" }, queue_address: "Endpoint1", - time_of_failure: "2024-06-27T06:14:48.912923Z", - last_modified: "2024-06-27T06:14:49.2216249Z", + time_of_failure: withTimeOfFailure, + last_modified: withLastModified, edited: false, edit_of: "", }; driver.mockEndpointDynamic(`${serviceControlUrl}errors`, "get", (url) => { const status = url.searchParams.get("status"); - if (status === "unresolved") { + if (status === withStatus) { return Promise.resolve({ body: [failedMessage], headers: { "Total-Count": "1" }, }); } - //For status=archived or status=retryissued + //For any other status return Promise.resolve({ body: [], headers: { "Total-Count": "0" }, @@ -244,7 +260,7 @@ export const hasFailedMessage = body: failedMessage, }); - driver.mockEndpoint(`${serviceControlUrl}recoverability/groups{/:classifier}`, { + driver.mockEndpoint(`${serviceControlUrl}recoverability/groups{/:classifier}?`, { body: [ { id: withGroupId, @@ -270,3 +286,20 @@ export const hasFailedMessage = body: { id: withGroupId, title: "Endpoint1", type: "Endpoint Name", count: 1, first: "2024-06-27T06:14:48.912923Z", last: "2024-06-27T06:14:48.912923Z" }, }); }; + +export const hasFailedMessageNotFound = + ({ withId }: { withId: string }) => + ({ driver }: SetupFactoryOptions) => { + const serviceControlUrl = window.defaultConfig.service_control_url; + driver.mockEndpoint(`${serviceControlUrl}errors/last/${withId}`, { + body: {}, + status: 404, + }); + }; + +export const hasFailedMessageRetrievalError = + ({ withId, networkError = false }: { withId: string; networkError?: boolean }) => + ({ driver }: SetupFactoryOptions) => { + const serviceControlUrl = window.defaultConfig.service_control_url; + driver.mockEndpoint(`${serviceControlUrl}errors/last/${withId}`, networkError ? { networkError: true } : { body: { error: "Internal server error" }, status: 500 }); + }; diff --git a/src/Frontend/test/specs/failedmessages/actions/selectFailedMessageGroup.ts b/src/Frontend/test/specs/failedmessages/actions/selectFailedMessageGroup.ts new file mode 100644 index 0000000000..50ba71824b --- /dev/null +++ b/src/Frontend/test/specs/failedmessages/actions/selectFailedMessageGroup.ts @@ -0,0 +1,7 @@ +import { screen } from "@testing-library/vue"; +import UserEvent from "@testing-library/user-event"; + +export async function selectFailedMessageGroup(groupName: string) { + const group = await screen.findByText(groupName, { selector: ".failed-message-group p.lead" }); + await UserEvent.click(group); +} diff --git a/src/Frontend/test/specs/failedmessages/questions/getFailedMessagesView.ts b/src/Frontend/test/specs/failedmessages/questions/getFailedMessagesView.ts new file mode 100644 index 0000000000..dad89b61a7 --- /dev/null +++ b/src/Frontend/test/specs/failedmessages/questions/getFailedMessagesView.ts @@ -0,0 +1,14 @@ +import { screen } from "@testing-library/vue"; + +export function getFailedMessagesView() { + return { + async groupHeading(groupName: string) { + return await screen.findByRole("heading", { level: 1, name: groupName }); + }, + + async messageTypes() { + const messages = await screen.findAllByText((_, element) => element?.matches(".failed-message p.lead") ?? false); + return messages.map((message) => message.textContent?.trim()); + }, + }; +} diff --git a/src/Frontend/test/specs/failedmessages/questions/getMessageView.ts b/src/Frontend/test/specs/failedmessages/questions/getMessageView.ts new file mode 100644 index 0000000000..3f2c9e08f6 --- /dev/null +++ b/src/Frontend/test/specs/failedmessages/questions/getMessageView.ts @@ -0,0 +1,25 @@ +import { screen } from "@testing-library/vue"; + +export function getMessageView() { + return { + async notice(text: RegExp) { + return await screen.findByText(text, { selector: "[role=status]" }); + }, + + async heading(messageType: string) { + return await screen.findByRole("heading", { level: 1, name: messageType }); + }, + + async statusLabel(text: string) { + return await screen.findByText(text, { selector: "span.label" }); + }, + + async metadataItem(label: string) { + return await screen.findByText((_, element) => element?.tagName === "SPAN" && element.classList.contains("metadata") && (element.textContent?.trim().startsWith(label) ?? false)); + }, + + async actionButton(name: string) { + return await screen.findByRole("button", { name }); + }, + }; +} diff --git a/src/Frontend/test/specs/failedmessages/viewing-failed-message.spec.ts b/src/Frontend/test/specs/failedmessages/viewing-failed-message.spec.ts deleted file mode 100644 index eeb5149a3b..0000000000 --- a/src/Frontend/test/specs/failedmessages/viewing-failed-message.spec.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { test, describe } from "../../drivers/vitest/driver"; - -describe("FEATURE: Message View", () => { - describe("RULE: Error scenarios should be handled gracefully", () => { - test.todo("EXAMPLE: When no data is returned for a message, text should be shown indicating that the message could not be found"); - - /* SCENARIO - Given no data is returned from ServiceControl for a message id - When the message view is shown - Then a text will be shown indicating that the message could not be found - */ - - test.todo("EXAMPLE: When an error is returned for a message, text should be shown indicating that an error occurred"); - /* SCENARIO - Given an error is returned from ServiceControl for a message id - When the message view is shown - Then a text will be shown indicating that an error occured and to investigate ServiceControl logs for the reason - */ - }); - describe("RULE: Header should show meta information and action buttons for a message that has not been deleted", () => { - test.todo("EXAMPLE: Message name should be displayed as a heading"); - test.todo("EXAMPLE: Time period indicating how long ago the failure happened should be displayed"); - test.todo("EXAMPLE: Name of the Endpoint that the message failed on should be displayed"); - test.todo("EXAMPLE: Name of the Machine that the message failed on should be displayed"); - test.todo("EXAMPLE: Delete message button should be displayed"); - test.todo("EXAMPLE: Retry message button should be displayed"); - test.todo("EXAMPLE: View in ServiceInsight button should be displayed"); - test.todo("EXAMPLE: Export message button should be displayed"); - - /* SCENARIO - Given the message is not deleted - When the message view is shown - Then the view header will display the message name as a heading - and the header will display a time period indicating how long ago the failure happened (retry failure if there is one) - and the header will display the name of the Endpoint that the message failed on - and the header will display the name of the Machine that the message failed on - and the header will display a "Delete message" button - and the header will display buttons for "Retry message", "View in ServiceInsight" and "Export Message" - */ - }); - describe("RULE: Header should show meta information and action buttons for a message that has been deleted", () => { - test.todo("EXAMPLE: Message name should be displayed as a heading"); - test.todo("EXAMPLE: Header should display prominently that the message is deleted"); - test.todo("EXAMPLE: Time period indicating how long ago the failure happened should be displayed"); - test.todo("EXAMPLE: Name of the Endpoint that the message failed on should be displayed"); - test.todo("EXAMPLE: Name of the Machine that the message failed on should be displayed"); - test.todo("EXAMPLE: Time period indicating how long ago the message was deleted should be displayed"); - test.todo("EXAMPLE: Time period indicating when the messages is schedule for hard deletion should be displayed prominently"); - test.todo("EXAMPLE: Restore button should be displayed"); - test.todo("EXAMPLE: View in ServiceInsight button should be displayed"); - - /* SCENARIO - Given the message is deleted - When the message view is shown - Then the view header will display the message name as a heading - and the header will display prominently that the message is deleted - and the header will display a time period indicating how long ago the failure happened (retry failure if there is one) - and the header will display the name of the Endpoint that the message failed on - and the header will display the name of the Machine that the message failed on - and the header will display a time period indicating how long ago the messsage was deleted - and the header will display, in a prominent style, a time period indicating when the messages is schedule for hard deletion - and the header will display a "Restore" button - and the header will display buttons for "View in ServiceInsight" and "Export Message" - */ - - /* QUESTIONS - this view also displays the "Retry message" button, but it's always disabled. Should the button be hidden instead? - */ - }); -}); diff --git a/src/Frontend/test/specs/failedmessages/viewing-failed-messages.spec.ts b/src/Frontend/test/specs/failedmessages/viewing-failed-messages.spec.ts index d9ccdd81c2..37cee308d6 100644 --- a/src/Frontend/test/specs/failedmessages/viewing-failed-messages.spec.ts +++ b/src/Frontend/test/specs/failedmessages/viewing-failed-messages.spec.ts @@ -1,19 +1,319 @@ import { test, describe } from "../../drivers/vitest/driver"; +import * as precondition from "../../preconditions"; +import { expect } from "vitest"; +import { waitFor } from "@testing-library/vue"; +import dayjs from "@/utils/dayjs"; +import { FailedMessageStatus } from "@/resources/FailedMessage"; +import { selectFailedMessageGroup } from "./actions/selectFailedMessageGroup"; +import { getFailedMessagesView } from "./questions/getFailedMessagesView"; +import { getMessageView } from "./questions/getMessageView"; + +const groupId = "81dca64e-76fc-e1c3-11a2-3069f51c58c8"; +const messageId = "40134401-bab9-41aa-9acb-b19c0066f22d"; +const messageType = "ServiceControl.SmokeTest.SimpleCommand"; describe("FEATURE: Viewing the details of a message group", () => { describe("RULE: Viewing the details of a Failed message group should be possible", () => { - test.todo("EXAMPLE: Selecting a group from the failed messages view should show all messages associated with that group"); - test.todo("EXAMPLE: The group heading should be the group name of the group that was selected"); - test.todo("EXAMPLE: The browser tab title should show 'Failed Messages', not 'All Failed Messages'"); - - /* SCENARIO - Selecting a group from the failed messages view - - Given there are 1 or more groups are shown in the "Failed Message Groups" tab - When a group is selected - Then all messages associated with that group should be shown in the Failed Messages view - and the group heading should be the group name of the group that was selected - and the browser tab title should show "Failed Messages", not "All Failed Messages" - */ + const failedMessage = precondition.hasFailedMessage({ + withGroupId: groupId, + withMessageId: messageId, + withContentType: "application/json", + withBody: { Index: 0, Data: "" }, + }); + + test("EXAMPLE: Selecting a group from the failed messages view should show all messages associated with that group", async ({ driver }) => { + // Given there are 1 or more groups shown in the "Failed Message Groups" tab + await driver.setUp(precondition.serviceControlWithMonitoring); + await driver.setUp(failedMessage); + await driver.goTo("/failed-messages/failed-message-groups"); + + // When a group is selected + await selectFailedMessageGroup("Endpoint1"); + + // Then all messages associated with that group should be shown in the Failed Messages view + const failedMessagesView = getFailedMessagesView(); + expect(await failedMessagesView.messageTypes()).toEqual([messageType]); + }); + + test("EXAMPLE: The group heading should be the group name of the group that was selected", async ({ driver }) => { + // Given there are 1 or more groups shown in the "Failed Message Groups" tab + await driver.setUp(precondition.serviceControlWithMonitoring); + await driver.setUp(failedMessage); + await driver.goTo("/failed-messages/failed-message-groups"); + + // When a group is selected + await selectFailedMessageGroup("Endpoint1"); + + // Then the group heading should be the group name of the group that was selected + const failedMessagesView = getFailedMessagesView(); + expect(await failedMessagesView.groupHeading("Endpoint1")).toBeInTheDocument(); + }); + + test("EXAMPLE: The browser tab title should show 'Failed Messages', not 'All Failed Messages'", async ({ driver }) => { + // Given there are 1 or more groups shown in the "Failed Message Groups" tab + await driver.setUp(precondition.serviceControlWithMonitoring); + await driver.setUp(failedMessage); + await driver.goTo("/failed-messages/failed-message-groups"); + + // When a group is selected + await selectFailedMessageGroup("Endpoint1"); + + // Then the browser tab title should show "Failed Messages", not "All Failed Messages" + await waitFor(() => expect(document.title).toBe("Failed Messages • ServicePulse")); + }); + }); +}); + +describe("FEATURE: Message View", () => { + describe("RULE: Error scenarios should be handled gracefully", () => { + test("EXAMPLE: When no data is returned for a message, text should be shown indicating that the message could not be found", async ({ driver }) => { + // Given no data is returned from ServiceControl for a message id + await driver.setUp(precondition.serviceControlWithMonitoring); + await driver.setUp(precondition.hasFailedMessageNotFound({ withId: groupId })); + + // When the message view is shown + await driver.goTo(`/messages/${groupId}`); + + // Then a text will be shown indicating that the message could not be found + const messageView = getMessageView(); + expect(await messageView.notice(/could not find message/i)).toBeInTheDocument(); + }); + + [ + { errorType: "an error response", networkError: false }, + { errorType: "a network error", networkError: true }, + ].forEach(({ errorType, networkError }) => { + test(`EXAMPLE: When an error is returned for a message, text should be shown indicating that an error occurred (${errorType})`, async ({ driver }) => { + // Given an error is returned from ServiceControl for a message id + await driver.setUp(precondition.serviceControlWithMonitoring); + await driver.setUp(precondition.hasFailedMessageRetrievalError({ withId: groupId, networkError })); + + // When the message view is shown + await driver.goTo(`/messages/${groupId}`); + + // Then a text will be shown indicating that an error occured and to investigate ServiceControl logs for the reason + const messageView = getMessageView(); + expect(await messageView.notice(/an error occurred while trying to load the message\. please check the servicecontrol logs/i)).toBeInTheDocument(); + }); + }); + }); + + describe("RULE: Header should show meta information and action buttons for a message that has not been deleted", () => { + const failedMessage = precondition.hasFailedMessage({ + withGroupId: groupId, + withMessageId: messageId, + withContentType: "application/json", + withBody: { Index: 0, Data: "" }, + withTimeOfFailure: dayjs.utc().subtract(2, "hours").toISOString(), + }); + + test("EXAMPLE: Message name should be displayed as a heading", async ({ driver }) => { + // Given the message is not deleted + await driver.setUp(precondition.serviceControlWithMonitoring); + await driver.setUp(failedMessage); + + // When the message view is shown + await driver.goTo(`/messages/${groupId}`); + + // Then the view header will display the message name as a heading + const messageView = getMessageView(); + expect(await messageView.heading(messageType)).toBeInTheDocument(); + }); + + test("EXAMPLE: Time period indicating how long ago the failure happened should be displayed", async ({ driver }) => { + // Given the message is not deleted + await driver.setUp(precondition.serviceControlWithMonitoring); + await driver.setUp(failedMessage); + + // When the message view is shown + await driver.goTo(`/messages/${groupId}`); + + // Then the header will display a time period indicating how long ago the failure happened + const messageView = getMessageView(); + expect(await messageView.metadataItem("Failed:")).toHaveTextContent("Failed: 2 hours ago"); + }); + + test("EXAMPLE: Name of the Endpoint that the message failed on should be displayed", async ({ driver }) => { + // Given the message is not deleted + await driver.setUp(precondition.serviceControlWithMonitoring); + await driver.setUp(failedMessage); + + // When the message view is shown + await driver.goTo(`/messages/${groupId}`); + + // Then the header will display the name of the Endpoint that the message failed on + const messageView = getMessageView(); + expect(await messageView.metadataItem("Endpoint:")).toHaveTextContent("Endpoint: Endpoint1"); + }); + + test("EXAMPLE: Name of the Machine that the message failed on should be displayed", async ({ driver }) => { + // Given the message is not deleted + await driver.setUp(precondition.serviceControlWithMonitoring); + await driver.setUp(failedMessage); + + // When the message view is shown + await driver.goTo(`/messages/${groupId}`); + + // Then the header will display the name of the Machine that the message failed on + const messageView = getMessageView(); + expect(await messageView.metadataItem("Machine:")).toHaveTextContent("Machine: mobvm2"); + }); + + test("EXAMPLE: Delete message button should be displayed", async ({ driver }) => { + // Given the message is not deleted + await driver.setUp(precondition.serviceControlWithMonitoring); + await driver.setUp(failedMessage); + + // When the message view is shown + await driver.goTo(`/messages/${groupId}`); + + // Then the header will display a "Delete message" button + const messageView = getMessageView(); + expect(await messageView.actionButton("Delete message")).toBeInTheDocument(); + }); + + test("EXAMPLE: Retry message button should be displayed", async ({ driver }) => { + // Given the message is not deleted + await driver.setUp(precondition.serviceControlWithMonitoring); + await driver.setUp(failedMessage); + + // When the message view is shown + await driver.goTo(`/messages/${groupId}`); + + // Then the header will display a "Retry message" button + const messageView = getMessageView(); + expect(await messageView.actionButton("Retry message")).toBeInTheDocument(); + }); + + test("EXAMPLE: Export message button should be displayed", async ({ driver }) => { + // Given the message is not deleted + await driver.setUp(precondition.serviceControlWithMonitoring); + await driver.setUp(failedMessage); + + // When the message view is shown + await driver.goTo(`/messages/${groupId}`); + + // Then the header will display an "Export message" button + const messageView = getMessageView(); + expect(await messageView.actionButton("Export message")).toBeInTheDocument(); + }); + }); + + describe("RULE: Header should show meta information and action buttons for a message that has been deleted", () => { + const deletedMessage = precondition.hasFailedMessage({ + withGroupId: groupId, + withMessageId: messageId, + withContentType: "application/json", + withBody: { Index: 0, Data: "" }, + withStatus: FailedMessageStatus.Archived, + withTimeOfFailure: dayjs.utc().subtract(2, "hours").toISOString(), + withLastModified: dayjs.utc().subtract(1, "hour").toISOString(), + }); + + test("EXAMPLE: Message name should be displayed as a heading", async ({ driver }) => { + // Given the message is deleted + await driver.setUp(precondition.serviceControlWithMonitoring); + await driver.setUp(deletedMessage); + + // When the message view is shown + await driver.goTo(`/messages/${groupId}`); + + // Then the view header will display the message name as a heading + const messageView = getMessageView(); + expect(await messageView.heading(messageType)).toBeInTheDocument(); + }); + + test("EXAMPLE: Header should display prominently that the message is deleted", async ({ driver }) => { + // Given the message is deleted + await driver.setUp(precondition.serviceControlWithMonitoring); + await driver.setUp(deletedMessage); + + // When the message view is shown + await driver.goTo(`/messages/${groupId}`); + + // Then the header will display prominently that the message is deleted + const messageView = getMessageView(); + expect(await messageView.statusLabel("Deleted")).toHaveClass("label-warning"); + }); + + test("EXAMPLE: Time period indicating how long ago the failure happened should be displayed", async ({ driver }) => { + // Given the message is deleted + await driver.setUp(precondition.serviceControlWithMonitoring); + await driver.setUp(deletedMessage); + + // When the message view is shown + await driver.goTo(`/messages/${groupId}`); + + // Then the header will display a time period indicating how long ago the failure happened + const messageView = getMessageView(); + expect(await messageView.metadataItem("Failed:")).toHaveTextContent("Failed: 2 hours ago"); + }); + + test("EXAMPLE: Name of the Endpoint that the message failed on should be displayed", async ({ driver }) => { + // Given the message is deleted + await driver.setUp(precondition.serviceControlWithMonitoring); + await driver.setUp(deletedMessage); + + // When the message view is shown + await driver.goTo(`/messages/${groupId}`); + + // Then the header will display the name of the Endpoint that the message failed on + const messageView = getMessageView(); + expect(await messageView.metadataItem("Endpoint:")).toHaveTextContent("Endpoint: Endpoint1"); + }); + + test("EXAMPLE: Name of the Machine that the message failed on should be displayed", async ({ driver }) => { + // Given the message is deleted + await driver.setUp(precondition.serviceControlWithMonitoring); + await driver.setUp(deletedMessage); + + // When the message view is shown + await driver.goTo(`/messages/${groupId}`); + + // Then the header will display the name of the Machine that the message failed on + const messageView = getMessageView(); + expect(await messageView.metadataItem("Machine:")).toHaveTextContent("Machine: mobvm2"); + }); + + test("EXAMPLE: Time period indicating how long ago the message was deleted should be displayed", async ({ driver }) => { + // Given the message is deleted + await driver.setUp(precondition.serviceControlWithMonitoring); + await driver.setUp(deletedMessage); + + // When the message view is shown + await driver.goTo(`/messages/${groupId}`); + + // Then the header will display a time period indicating how long ago the messsage was deleted + const messageView = getMessageView(); + expect(await messageView.metadataItem("Deleted:")).toHaveTextContent("Deleted: an hour ago"); + }); + + test("EXAMPLE: Time period indicating when the messages is schedule for hard deletion should be displayed prominently", async ({ driver }) => { + // Given the message is deleted + await driver.setUp(precondition.serviceControlWithMonitoring); + await driver.setUp(deletedMessage); + + // When the message view is shown + await driver.goTo(`/messages/${groupId}`); + + // Then the header will display, in a prominent style, a time period indicating when the messages is schedule for hard deletion + const messageView = getMessageView(); + const scheduledForDeletion = await messageView.metadataItem("Scheduled for permanent deletion:"); + await waitFor(() => expect(scheduledForDeletion).toHaveTextContent("Scheduled for permanent deletion: in a month")); + expect(scheduledForDeletion).toHaveClass("danger"); + }); + + test("EXAMPLE: Restore button should be displayed", async ({ driver }) => { + // Given the message is deleted + await driver.setUp(precondition.serviceControlWithMonitoring); + await driver.setUp(deletedMessage); + + // When the message view is shown + await driver.goTo(`/messages/${groupId}`); + + // Then the header will display a "Restore" button + const messageView = getMessageView(); + expect(await messageView.actionButton("Restore")).toBeInTheDocument(); + }); }); });