From 34e6fbd8374a96ec9b894cc4b27a490edde12682 Mon Sep 17 00:00:00 2001 From: Kim Tao Date: Tue, 22 Aug 2023 11:34:27 -0400 Subject: [PATCH 01/14] v3.0.129 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4fa98fe27b..6157f318a7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "spruce", - "version": "3.0.128", + "version": "3.0.129", "private": true, "scripts": { "bootstrap-logkeeper": "./scripts/bootstrap-logkeeper.sh", From f1393fe21360ff0a06d591735f24b981660a7f57 Mon Sep 17 00:00:00 2001 From: Sophie Stadler Date: Tue, 22 Aug 2023 14:33:24 -0400 Subject: [PATCH 02/14] EVG-19953: Distro event logs (#1998) --- .../Settings/EventLog/EventDiffTable.tsx | 83 +++ src/components/Settings/EventLog/EventLog.tsx | 71 +++ .../Settings/EventLog}/EventLogDiffs.test.ts | 0 .../Settings/EventLog}/EventLogDiffs.ts | 14 +- src/components/Settings/EventLog/Header.tsx | 24 + src/components/Settings/EventLog/index.ts | 4 + src/components/Settings/EventLog/types.ts | 19 + src/components/Settings/EventLog/useEvents.ts | 22 + src/gql/GQLWrapper.tsx | 17 + src/gql/generated/types.ts | 35 +- src/gql/queries/distro-events.graphql | 12 + src/gql/queries/index.ts | 2 + src/pages/distroSettings/Tabs.tsx | 6 +- .../tabs/EventLogTab/EventLogTab.test.tsx | 514 ++++++++++++++++++ .../tabs/EventLogTab/EventLogTab.tsx | 44 ++ .../tabs/EventLogTab/LegacyEventEntry.tsx | 10 + .../tabs/EventLogTab/useDistroEvents.ts | 40 ++ src/pages/distroSettings/tabs/index.tsx | 1 + src/pages/distroSettings/tabs/testData.ts | 5 +- .../tabs/EventLogTab/EventLogTab.tsx | 161 +----- ...eEvents.ts => useProjectSettingsEvents.ts} | 20 +- 21 files changed, 928 insertions(+), 176 deletions(-) create mode 100644 src/components/Settings/EventLog/EventDiffTable.tsx create mode 100644 src/components/Settings/EventLog/EventLog.tsx rename src/{pages/projectSettings/tabs/EventLogTab => components/Settings/EventLog}/EventLogDiffs.test.ts (100%) rename src/{pages/projectSettings/tabs/EventLogTab => components/Settings/EventLog}/EventLogDiffs.ts (82%) create mode 100644 src/components/Settings/EventLog/Header.tsx create mode 100644 src/components/Settings/EventLog/index.ts create mode 100644 src/components/Settings/EventLog/types.ts create mode 100644 src/components/Settings/EventLog/useEvents.ts create mode 100644 src/gql/queries/distro-events.graphql create mode 100644 src/pages/distroSettings/tabs/EventLogTab/EventLogTab.test.tsx create mode 100644 src/pages/distroSettings/tabs/EventLogTab/EventLogTab.tsx create mode 100644 src/pages/distroSettings/tabs/EventLogTab/LegacyEventEntry.tsx create mode 100644 src/pages/distroSettings/tabs/EventLogTab/useDistroEvents.ts rename src/pages/projectSettings/tabs/EventLogTab/{useEvents.ts => useProjectSettingsEvents.ts} (80%) diff --git a/src/components/Settings/EventLog/EventDiffTable.tsx b/src/components/Settings/EventLog/EventDiffTable.tsx new file mode 100644 index 0000000000..b9f4b3ee2d --- /dev/null +++ b/src/components/Settings/EventLog/EventDiffTable.tsx @@ -0,0 +1,83 @@ +import styled from "@emotion/styled"; +import Badge, { Variant } from "@leafygreen-ui/badge"; +import { Table, TableHeader, Row, Cell } from "@leafygreen-ui/table"; +import { fontFamilies } from "@leafygreen-ui/tokens"; +import { getEventDiffLines } from "./EventLogDiffs"; +import { Event, EventDiffLine, EventValue } from "./types"; + +type TableProps = { + after: Event["after"]; + before: Event["before"]; +}; + +export const EventDiffTable: React.FC = ({ after, before }) => ( + datum.key} + />, + JSON.stringify(datum.before)} + />, + JSON.stringify(datum.after)} + />, + ]} + > + {({ datum }) => ( + + + {datum.key} + + + {renderEventValue(datum.before)} + + + {renderEventValue(datum.after) === null ? ( + Deleted + ) : ( + {renderEventValue(datum.after)} + )} + + + )} +
+); + +const CellText = styled.span` + font-family: ${fontFamilies.code}; + font-size: 12px; + line-height: 16px; + word-break: break-all; +`; + +const renderEventValue = (value: EventValue): string => { + if (value === null || value === undefined) { + return null; + } + if (typeof value === "boolean") { + return String(value); + } + + if (typeof value === "string") { + return `"${value}"`; + } + + if (typeof value === "number") { + return value; + } + + if (Array.isArray(value)) { + return JSON.stringify(value).replaceAll(",", ",\n"); + } + + return JSON.stringify(value); +}; diff --git a/src/components/Settings/EventLog/EventLog.tsx b/src/components/Settings/EventLog/EventLog.tsx new file mode 100644 index 0000000000..4ab57ae962 --- /dev/null +++ b/src/components/Settings/EventLog/EventLog.tsx @@ -0,0 +1,71 @@ +import styled from "@emotion/styled"; +import Button from "@leafygreen-ui/button"; +import Card from "@leafygreen-ui/card"; +import { Spinner } from "@leafygreen-ui/loading-indicator"; +import { Subtitle } from "@leafygreen-ui/typography"; +import { size } from "constants/tokens"; +import { EventDiffTable } from "./EventDiffTable"; +import { Header } from "./Header"; +import { Event } from "./types"; + +type EventLogProps = { + allEventsFetched: boolean; + eventRenderer?: (event: Event) => React.ReactNode; + events: Event[]; + handleFetchMore: () => void; + loading?: boolean; +}; + +export const EventLog: React.FC = ({ + allEventsFetched, + eventRenderer, + events, + handleFetchMore, + loading, +}) => { + const allEventsFetchedCopy = + events.length > 0 ? "No more events to show." : "No events to show."; + + return ( + + {events.map((event) => { + const { after, before, timestamp, user } = event; + return ( + +
+ {eventRenderer ? ( + eventRenderer(event) + ) : ( + + )} + + ); + })} + {!allEventsFetched && !!events.length && ( + + )} + {allEventsFetched && {allEventsFetchedCopy}} + + ); +}; + +const Container = styled.div` + display: flex; + flex-direction: column; + align-items: center; + width: 150%; +`; + +const EventLogCard = styled(Card)` + width: 100%; + margin-bottom: ${size.l}; + padding: ${size.m}; +`; diff --git a/src/pages/projectSettings/tabs/EventLogTab/EventLogDiffs.test.ts b/src/components/Settings/EventLog/EventLogDiffs.test.ts similarity index 100% rename from src/pages/projectSettings/tabs/EventLogTab/EventLogDiffs.test.ts rename to src/components/Settings/EventLog/EventLogDiffs.test.ts diff --git a/src/pages/projectSettings/tabs/EventLogTab/EventLogDiffs.ts b/src/components/Settings/EventLog/EventLogDiffs.ts similarity index 82% rename from src/pages/projectSettings/tabs/EventLogTab/EventLogDiffs.ts rename to src/components/Settings/EventLog/EventLogDiffs.ts index 8d577aea50..eea711972c 100644 --- a/src/pages/projectSettings/tabs/EventLogTab/EventLogDiffs.ts +++ b/src/components/Settings/EventLog/EventLogDiffs.ts @@ -1,7 +1,6 @@ import { diff } from "deep-object-diff"; -import { ProjectEventSettings } from "gql/generated/types"; -import { Subset } from "types/utils"; import { string } from "utils"; +import { Event, EventDiffLine, EventValue } from "./types"; const { omitTypename } = string; @@ -27,16 +26,9 @@ const formatArrayElements = (eventKey: string): string => const getNestedObject = (nestedObj: object, pathArr: string[]): EventValue => pathArr.reduce((obj, key) => (obj ? obj[key] : undefined), nestedObj); -export type EventValue = boolean | string | Array; -export type EventDiffLine = { - key: string; - before: EventValue; - after: EventValue; -}; - export const getEventDiffLines = ( - before: Subset, - after: Subset + before: Event["before"], + after: Event["after"] ): EventDiffLine[] => { const beforeNoTypename = omitTypename(before); const afterNoTypename = omitTypename(after); diff --git a/src/components/Settings/EventLog/Header.tsx b/src/components/Settings/EventLog/Header.tsx new file mode 100644 index 0000000000..250a986bf3 --- /dev/null +++ b/src/components/Settings/EventLog/Header.tsx @@ -0,0 +1,24 @@ +import styled from "@emotion/styled"; +import { Subtitle } from "@leafygreen-ui/typography"; +import { size } from "constants/tokens"; +import { useDateFormat } from "hooks"; + +interface Props { + timestamp: Date; + user: string; +} + +export const Header: React.FC = ({ timestamp, user }) => { + const getDateCopy = useDateFormat(); + + return ( + + {getDateCopy(timestamp)} +
{user}
+
+ ); +}; + +const StyledHeader = styled.div` + padding-bottom: ${size.s}; +`; diff --git a/src/components/Settings/EventLog/index.ts b/src/components/Settings/EventLog/index.ts new file mode 100644 index 0000000000..c061ae102c --- /dev/null +++ b/src/components/Settings/EventLog/index.ts @@ -0,0 +1,4 @@ +export { EventDiffTable } from "./EventDiffTable"; +export { EventLog } from "./EventLog"; +export { EVENT_LIMIT, useEvents } from "./useEvents"; +export type { Event } from "./types"; diff --git a/src/components/Settings/EventLog/types.ts b/src/components/Settings/EventLog/types.ts new file mode 100644 index 0000000000..820fed79f8 --- /dev/null +++ b/src/components/Settings/EventLog/types.ts @@ -0,0 +1,19 @@ +export type EventValue = + | boolean + | string + | object + | Array; + +export type Event = { + after?: Record; + before?: Record; + data?: Record; + timestamp: Date; + user: string; +}; + +export type EventDiffLine = { + key: string; + before: EventValue; + after: EventValue; +}; diff --git a/src/components/Settings/EventLog/useEvents.ts b/src/components/Settings/EventLog/useEvents.ts new file mode 100644 index 0000000000..19826d05f1 --- /dev/null +++ b/src/components/Settings/EventLog/useEvents.ts @@ -0,0 +1,22 @@ +import { useState } from "react"; + +export const EVENT_LIMIT = 15; + +export const useEvents = (limit: number) => { + const [allEventsFetched, setAllEventsFetched] = useState(false); + const [prevCount, setPrevCount] = useState(0); + + // Hide Load More button when event count < limit is returned, + // or when an additional fetch fails to load more events. + const onCompleted = (count: number) => { + if (count - prevCount < limit) { + setAllEventsFetched(true); + } + }; + + return { + allEventsFetched, + onCompleted, + setPrevCount, + }; +}; diff --git a/src/gql/GQLWrapper.tsx b/src/gql/GQLWrapper.tsx index b3d9ac7474..c89ee7d686 100644 --- a/src/gql/GQLWrapper.tsx +++ b/src/gql/GQLWrapper.tsx @@ -41,6 +41,9 @@ const cache = new InMemoryCache({ typePolicies: { Query: { fields: { + distroEvents: { + keyArgs: ["$distroId"], + }, projectEvents: { keyArgs: ["$identifier"], }, @@ -52,6 +55,20 @@ const cache = new InMemoryCache({ GeneralSubscription: { keyFields: false, }, + DistroEventsPayload: { + fields: { + count: { + merge(existing = 0, incoming = 0) { + return existing + incoming; + }, + }, + eventLogEntries: { + merge(existing = [], incoming = []) { + return [...existing, ...incoming]; + }, + }, + }, + }, ProjectEvents: { fields: { count: { diff --git a/src/gql/generated/types.ts b/src/gql/generated/types.ts index 61c1c68b5b..0aee3e75a0 100644 --- a/src/gql/generated/types.ts +++ b/src/gql/generated/types.ts @@ -337,7 +337,7 @@ export type Distro = { name: Scalars["String"]; note: Scalars["String"]; plannerSettings: PlannerSettings; - provider: Scalars["String"]; + provider: Provider; providerSettingsList: Array; setup: Scalars["String"]; setupAsSudo: Scalars["Boolean"]; @@ -401,7 +401,7 @@ export type DistroInput = { name: Scalars["String"]; note: Scalars["String"]; plannerSettings: PlannerSettingsInput; - provider: Scalars["String"]; + provider: Provider; providerSettingsList: Array; setup: Scalars["String"]; setupAsSudo: Scalars["Boolean"]; @@ -1805,6 +1805,13 @@ export type ProjectVarsInput = { vars?: InputMaybe; }; +export enum Provider { + Docker = "DOCKER", + Ec2Fleet = "EC2_FLEET", + Ec2OnDemand = "EC2_ON_DEMAND", + Static = "STATIC", +} + /** PublicKey models a public key. Users can save/modify/delete their public keys. */ export type PublicKey = { __typename?: "PublicKey"; @@ -5072,6 +5079,28 @@ export type AwsRegionsQuery = { awsRegions?: Array | null; }; +export type DistroEventsQueryVariables = Exact<{ + distroId: Scalars["String"]; + limit?: InputMaybe; + before?: InputMaybe; +}>; + +export type DistroEventsQuery = { + __typename?: "Query"; + distroEvents: { + __typename?: "DistroEventsPayload"; + count: number; + eventLogEntries: Array<{ + __typename?: "DistroEvent"; + after?: any | null; + before?: any | null; + data?: any | null; + timestamp: Date; + user: string; + }>; + }; +}; + export type DistroTaskQueueQueryVariables = Exact<{ distroId: Scalars["String"]; }>; @@ -5111,7 +5140,7 @@ export type DistroQuery = { isVirtualWorkStation: boolean; name: string; note: string; - provider: string; + provider: Provider; providerSettingsList: Array; setup: string; setupAsSudo: boolean; diff --git a/src/gql/queries/distro-events.graphql b/src/gql/queries/distro-events.graphql new file mode 100644 index 0000000000..8335b76832 --- /dev/null +++ b/src/gql/queries/distro-events.graphql @@ -0,0 +1,12 @@ +query DistroEvents($distroId: String!, $limit: Int, $before: Time) { + distroEvents(opts: { distroId: $distroId, limit: $limit, before: $before }) { + count + eventLogEntries { + after + before + data + timestamp + user + } + } +} diff --git a/src/gql/queries/index.ts b/src/gql/queries/index.ts index 2a80d744e7..69b2a98ba8 100644 --- a/src/gql/queries/index.ts +++ b/src/gql/queries/index.ts @@ -1,4 +1,5 @@ import GET_AWS_REGIONS from "./aws-regions.graphql"; +import DISTRO_EVENTS from "./distro-events.graphql"; import DISTRO_TASK_QUEUE from "./distro-task-queue.graphql"; import DISTRO from "./distro.graphql"; import GET_FAILED_TASK_STATUS_ICON_TOOLTIP from "./failed-task-status-icon-tooltip.graphql"; @@ -79,6 +80,7 @@ import USER_SUBSCRIPTIONS from "./user-subscriptions.graphql"; export { DISTRO, + DISTRO_EVENTS, DISTRO_TASK_QUEUE, GET_AGENT_LOGS, GET_ALL_LOGS, diff --git a/src/pages/distroSettings/Tabs.tsx b/src/pages/distroSettings/Tabs.tsx index 758b0cdf23..d26ce5ad75 100644 --- a/src/pages/distroSettings/Tabs.tsx +++ b/src/pages/distroSettings/Tabs.tsx @@ -6,7 +6,7 @@ import { DistroQuery } from "gql/generated/types"; import { useDistroSettingsContext } from "./Context"; import { Header } from "./Header"; import { NavigationModal } from "./NavigationModal"; -import { GeneralTab, ProjectTab, TaskTab } from "./tabs/index"; +import { EventLogTab, GeneralTab, ProjectTab, TaskTab } from "./tabs/index"; import { gqlToFormMap } from "./tabs/transformers"; interface Props { @@ -51,6 +51,10 @@ export const DistroSettingsTabs: React.FC = ({ distro }) => { } /> + } + path={DistroSettingsTabRoutes.EventLog} + /> ); diff --git a/src/pages/distroSettings/tabs/EventLogTab/EventLogTab.test.tsx b/src/pages/distroSettings/tabs/EventLogTab/EventLogTab.test.tsx new file mode 100644 index 0000000000..e372ba55ba --- /dev/null +++ b/src/pages/distroSettings/tabs/EventLogTab/EventLogTab.test.tsx @@ -0,0 +1,514 @@ +import { MockedProvider } from "@apollo/client/testing"; +import { RenderFakeToastContext } from "context/toast/__mocks__"; +import { + DistroEventsQuery, + DistroEventsQueryVariables, +} from "gql/generated/types"; +import { DISTRO_EVENTS } from "gql/queries"; +import { renderWithRouterMatch as render, screen, waitFor } from "test_utils"; +import { ApolloMock } from "types/gql"; +import { EventLogTab } from "./EventLogTab"; + +const Wrapper = ({ children, mocks = [query()] }) => ( + {children} +); + +describe("loading events", () => { + it("does not show a load more button when the event count is less than the limit", async () => { + const { Component } = RenderFakeToastContext( + + + + ); + render(, { + route: "/distro/rhel71-power8-large/settings", + path: "/distro/:distroId/settings", + }); + await waitFor(() => { + expect(screen.queryAllByDataCy("event-log-card")).toHaveLength(2); + }); + expect(screen.queryByDataCy("load-more-button")).not.toBeInTheDocument(); + expect(screen.getByText("No more events to show.")).toBeInTheDocument(); + }); + + it("shows a 'Load more' button when the number of events loaded meets the limit", async () => { + const limit = 2; + const { Component } = RenderFakeToastContext( + + + + ); + render(, { + route: "/distro/rhel71-power8-large/settings", + path: "/distro/:distroId/settings", + }); + await waitFor(() => { + expect(screen.queryAllByDataCy("event-log-card")).toHaveLength(2); + }); + expect(screen.getByDataCy("load-more-button")).toBeInTheDocument(); + expect( + screen.queryByText("No more events to show.") + ).not.toBeInTheDocument(); + }); + + it("shows a legacy event entry for event lacking before and after fields", async () => { + const { Component } = RenderFakeToastContext( + + + + ); + render(, { + route: "/distro/rhel71-power8-large/settings", + path: "/distro/:distroId/settings", + }); + await waitFor(() => { + expect(screen.queryAllByDataCy("event-log-card")).toHaveLength(2); + }); + expect(screen.queryAllByDataCy("event-diff-table")).toHaveLength(1); + expect(screen.queryAllByDataCy("legacy-event")).toHaveLength(1); + }); +}); + +const query = ( + limit: number = 15 +): ApolloMock => ({ + request: { + query: DISTRO_EVENTS, + variables: { + distroId: "rhel71-power8-large", + limit, + }, + }, + result: { + data: distroEvents, + }, +}); + +const distroEvents: DistroEventsQuery = { + distroEvents: { + count: 2, + eventLogEntries: [ + { + after: { + _id: "rhel71-power8-large", + arch: "linux_ppc64le", + bootstrap_settings: { + client_dir: "/home/mci-exec/evergreen_provisioning", + communication: "rpc", + env: [ + { + key: "foo", + value: "bar", + }, + ], + jasper_binary_dir: "/home/mci-exec/evergreen_provisioning", + jasper_credentials_path: + "/home/mci-exec/evergreen_provisioning/jasper_credentials.json", + method: "ssh", + resource_limits: { + locked_memory: -1, + num_files: 66000, + num_processes: -1, + virtual_memory: -1, + }, + root_dir: "C:/cygwin", + shell_path: "/bin/fish", + }, + clone_method: "legacy-ssh", + disable_shallow_clone: true, + disabled: true, + dispatcher_settings: { + version: "revised-with-dependencies", + }, + expansions: [ + { + key: "decompress", + value: "tar xzvf", + }, + { + key: "ps", + value: "ps aux", + }, + ], + finder_settings: { + version: "legacy", + }, + home_volume_settings: { + format_command: "", + }, + host_allocator_settings: { + acceptable_host_idle_time: 30000000000, + feedback_rule: "", + future_host_fraction: 0, + hosts_overallocated_rule: "", + maximum_hosts: 1, + minimum_hosts: 0, + rounding_rule: "", + version: "utilization", + }, + is_cluster: true, + is_virtual_workstation: false, + note: "This is an updated note", + planner_settings: { + commit_queue_factor: 0, + expected_runtime_factor: 0, + generate_task_factor: 0, + group_versions: false, + mainline_time_in_queue_factor: 0, + patch_time_in_queue_factor: 0, + patch_zipper_factor: 0, + stepback_task_factor: 0, + target_time: 0, + version: "tunable", + }, + provider: "ec2-ondemand", + provider_settings_list: [ + { + ami: "who-ami2", + instance_type: "m4.4xlarge", + is_vpc: false, + region: "us-east-1", + security_group_ids: ["1"], + subnet_id: "subnet-123", + }, + { + ami: "who-ami-2", + instance_type: "m4.2xlarge", + is_vpc: false, + region: "us-west-1", + security_group_ids: ["2"], + }, + ], + setup: "ls -alF", + setup_as_sudo: true, + spawn_allowed: false, + ssh_key: "mci", + ssh_options: [ + "StrictHostKeyChecking=no", + "BatchMode=yes", + "ConnectTimeout=10", + ], + user: "mci-exec", + work_dir: "/data/mci/hi/again/haaa/!!!", + }, + before: { + _id: "rhel71-power8-large", + arch: "linux_ppc64le", + bootstrap_settings: { + client_dir: "/home/mci-exec/evergreen_provisioning", + communication: "rpc", + env: [ + { + key: "foo", + value: "bar", + }, + ], + jasper_binary_dir: "/home/mci-exec/evergreen_provisioning", + jasper_credentials_path: + "/home/mci-exec/evergreen_provisioning/jasper_credentials.json", + method: "ssh", + resource_limits: { + locked_memory: -1, + num_files: 66000, + num_processes: -1, + virtual_memory: -1, + }, + root_dir: "C:/cygwin", + shell_path: "/bin/fish", + }, + clone_method: "legacy-ssh", + disable_shallow_clone: true, + disabled: true, + dispatcher_settings: { + version: "revised-with-dependencies", + }, + expansions: [ + { + key: "decompress", + value: "tar xzvf", + }, + { + key: "ps", + value: "ps aux", + }, + ], + finder_settings: { + version: "legacy", + }, + home_volume_settings: { + format_command: "", + }, + host_allocator_settings: { + acceptable_host_idle_time: 30000000000, + feedback_rule: "", + future_host_fraction: 0, + hosts_overallocated_rule: "", + maximum_hosts: 1, + minimum_hosts: 0, + rounding_rule: "", + version: "utilization", + }, + is_cluster: true, + is_virtual_workstation: false, + note: "This is an updated note", + planner_settings: { + commit_queue_factor: 0, + expected_runtime_factor: 0, + generate_task_factor: 0, + group_versions: false, + mainline_time_in_queue_factor: 0, + patch_time_in_queue_factor: 0, + patch_zipper_factor: 0, + stepback_task_factor: 0, + target_time: 0, + version: "tunable", + }, + provider: "ec2-ondemand", + provider_settings_list: [ + { + ami: "who-ami", + instance_type: "m4.4xlarge", + is_vpc: false, + region: "us-east-1", + security_group_ids: ["1"], + subnet_id: "subnet-123", + }, + { + ami: "who-ami-2", + instance_type: "m4.2xlarge", + is_vpc: false, + region: "us-west-1", + security_group_ids: ["2"], + }, + ], + setup: "ls -alF", + setup_as_sudo: true, + spawn_allowed: false, + ssh_key: "mci", + ssh_options: [ + "StrictHostKeyChecking=no", + "BatchMode=yes", + "ConnectTimeout=10", + ], + user: "mci-exec", + work_dir: "/data/mci/hi/again/haaa/!!!", + }, + data: { + _id: "rhel71-power8-large", + arch: "linux_ppc64le", + bootstrap_settings: { + client_dir: "/home/mci-exec/evergreen_provisioning", + communication: "rpc", + env: [ + { + key: "foo", + value: "bar", + }, + ], + jasper_binary_dir: "/home/mci-exec/evergreen_provisioning", + jasper_credentials_path: + "/home/mci-exec/evergreen_provisioning/jasper_credentials.json", + method: "ssh", + resource_limits: { + locked_memory: -1, + num_files: 66000, + num_processes: -1, + virtual_memory: -1, + }, + root_dir: "C:/cygwin", + shell_path: "/bin/fish", + }, + clone_method: "legacy-ssh", + disable_shallow_clone: true, + disabled: true, + dispatcher_settings: { + version: "revised-with-dependencies", + }, + expansions: [ + { + key: "decompress", + value: "tar xzvf", + }, + { + key: "ps", + value: "ps aux", + }, + ], + finder_settings: { + version: "legacy", + }, + home_volume_settings: { + format_command: "", + }, + host_allocator_settings: { + acceptable_host_idle_time: 30000000000, + feedback_rule: "", + future_host_fraction: 0, + hosts_overallocated_rule: "", + maximum_hosts: 1, + minimum_hosts: 0, + rounding_rule: "", + version: "utilization", + }, + is_cluster: true, + is_virtual_workstation: false, + note: "This is an updated note", + planner_settings: { + commit_queue_factor: 0, + expected_runtime_factor: 0, + generate_task_factor: 0, + group_versions: false, + mainline_time_in_queue_factor: 0, + patch_time_in_queue_factor: 0, + patch_zipper_factor: 0, + stepback_task_factor: 0, + target_time: 0, + version: "tunable", + }, + provider: "ec2-ondemand", + provider_settings_list: [ + { + ami: "who-ami2", + instance_type: "m4.4xlarge", + is_vpc: false, + region: "us-east-1", + security_group_ids: ["1"], + subnet_id: "subnet-123", + }, + { + ami: "who-ami-2", + instance_type: "m4.2xlarge", + is_vpc: false, + region: "us-west-1", + security_group_ids: ["2"], + }, + ], + setup: "ls -alF", + setup_as_sudo: true, + spawn_allowed: false, + ssh_key: "mci", + ssh_options: [ + "StrictHostKeyChecking=no", + "BatchMode=yes", + "ConnectTimeout=10", + ], + user: "mci-exec", + work_dir: "/data/mci/hi/again/haaa/!!!", + }, + timestamp: new Date("2023-08-10T12:57:32.566-04:00"), + user: "admin", + __typename: "DistroEvent", + }, + { + after: null, + before: null, + data: { + _id: "rhel71-power8-large", + arch: "linux_ppc64le", + bootstrap_settings: { + client_dir: "/home/mci-exec/evergreen_provisioning", + communication: "rpc", + env: [ + { + key: "foo", + value: "bar", + }, + ], + jasper_binary_dir: "/home/mci-exec/evergreen_provisioning", + jasper_credentials_path: + "/home/mci-exec/evergreen_provisioning/jasper_credentials.json", + method: "ssh", + resource_limits: { + locked_memory: -1, + num_files: 66000, + num_processes: -1, + virtual_memory: -1, + }, + root_dir: "C:/cygwin", + shell_path: "/bin/fish", + }, + clone_method: "legacy-ssh", + disable_shallow_clone: true, + disabled: true, + dispatcher_settings: { + version: "revised-with-dependencies", + }, + expansions: [ + { + key: "decompress", + value: "tar xzvf", + }, + { + key: "ps", + value: "ps aux", + }, + ], + finder_settings: { + version: "legacy", + }, + home_volume_settings: { + format_command: "", + }, + host_allocator_settings: { + acceptable_host_idle_time: 30000000000, + feedback_rule: "", + future_host_fraction: 0, + hosts_overallocated_rule: "", + maximum_hosts: 1, + minimum_hosts: 0, + rounding_rule: "", + version: "utilization", + }, + is_cluster: true, + is_virtual_workstation: false, + note: "This is an updated note", + planner_settings: { + commit_queue_factor: 0, + expected_runtime_factor: 0, + generate_task_factor: 0, + group_versions: false, + mainline_time_in_queue_factor: 0, + patch_time_in_queue_factor: 0, + patch_zipper_factor: 0, + stepback_task_factor: 0, + target_time: 0, + version: "tunable", + }, + provider: "ec2-ondemand", + provider_settings_list: [ + { + ami: "who-ami", + instance_type: "m4.4xlarge", + is_vpc: false, + region: "us-east-1", + security_group_ids: ["1"], + subnet_id: "subnet-123", + }, + { + ami: "who-ami-2", + instance_type: "m4.2xlarge", + is_vpc: false, + region: "us-west-1", + security_group_ids: ["2"], + }, + ], + setup: "ls -alF", + setup_as_sudo: true, + spawn_allowed: false, + ssh_key: "mci", + ssh_options: [ + "StrictHostKeyChecking=no", + "BatchMode=yes", + "ConnectTimeout=10", + ], + user: "mci-exec", + work_dir: "/data/mci/hi/again/haaa/!!!", + }, + timestamp: new Date("2023-08-09T17:00:06.819-04:00"), + user: "admin", + __typename: "DistroEvent", + }, + ], + __typename: "DistroEventsPayload", + }, +}; diff --git a/src/pages/distroSettings/tabs/EventLogTab/EventLogTab.tsx b/src/pages/distroSettings/tabs/EventLogTab/EventLogTab.tsx new file mode 100644 index 0000000000..f070a63d27 --- /dev/null +++ b/src/pages/distroSettings/tabs/EventLogTab/EventLogTab.tsx @@ -0,0 +1,44 @@ +import { useParams } from "react-router-dom"; +import { EventDiffTable, EventLog } from "components/Settings/EventLog"; +import { LegacyEventEntry } from "./LegacyEventEntry"; +import { useDistroEvents } from "./useDistroEvents"; + +type TabProps = { + limit?: number; +}; + +export const EventLogTab: React.FC = ({ limit }) => { + const { distroId } = useParams<{ + distroId: string; + }>(); + + const { allEventsFetched, events, fetchMore, loading } = useDistroEvents( + distroId, + limit + ); + + const lastEventTimestamp = events[events.length - 1]?.timestamp; + + return ( + + after && before ? ( + + ) : ( + + ) + } + handleFetchMore={() => { + fetchMore({ + variables: { + distroId, + before: lastEventTimestamp, + }, + }); + }} + loading={loading} + /> + ); +}; diff --git a/src/pages/distroSettings/tabs/EventLogTab/LegacyEventEntry.tsx b/src/pages/distroSettings/tabs/EventLogTab/LegacyEventEntry.tsx new file mode 100644 index 0000000000..a96137ea08 --- /dev/null +++ b/src/pages/distroSettings/tabs/EventLogTab/LegacyEventEntry.tsx @@ -0,0 +1,10 @@ +import Code from "@leafygreen-ui/code"; +import { Event } from "components/Settings/EventLog"; + +export const LegacyEventEntry: React.FC<{ data: Event["data"] }> = ({ + data, +}) => ( + + {JSON.stringify(data, null, 2)} + +); diff --git a/src/pages/distroSettings/tabs/EventLogTab/useDistroEvents.ts b/src/pages/distroSettings/tabs/EventLogTab/useDistroEvents.ts new file mode 100644 index 0000000000..bafe3d65e7 --- /dev/null +++ b/src/pages/distroSettings/tabs/EventLogTab/useDistroEvents.ts @@ -0,0 +1,40 @@ +import { useEffect } from "react"; +import { useQuery } from "@apollo/client"; +import { EVENT_LIMIT, useEvents } from "components/Settings/EventLog"; +import { useToastContext } from "context/toast"; +import { + DistroEventsQuery, + DistroEventsQueryVariables, +} from "gql/generated/types"; +import { DISTRO_EVENTS } from "gql/queries"; + +export const useDistroEvents = ( + distroId: string, + limit: number = EVENT_LIMIT +) => { + const dispatchToast = useToastContext(); + + const { allEventsFetched, onCompleted, setPrevCount } = useEvents(limit); + const { data, fetchMore, loading, previousData } = useQuery< + DistroEventsQuery, + DistroEventsQueryVariables + >(DISTRO_EVENTS, { + variables: { + distroId, + limit, + }, + notifyOnNetworkStatusChange: true, + onCompleted: ({ distroEvents: { count } }) => onCompleted(count), + onError: (e) => { + dispatchToast.error(e.message); + }, + }); + + const events = data?.distroEvents?.eventLogEntries ?? []; + + useEffect(() => { + setPrevCount(previousData?.distroEvents?.count ?? 0); + }, [previousData, setPrevCount]); + + return { allEventsFetched, events, fetchMore, loading }; +}; diff --git a/src/pages/distroSettings/tabs/index.tsx b/src/pages/distroSettings/tabs/index.tsx index 775ca5e666..d17a5f887c 100644 --- a/src/pages/distroSettings/tabs/index.tsx +++ b/src/pages/distroSettings/tabs/index.tsx @@ -1,3 +1,4 @@ +export { EventLogTab } from "./EventLogTab/EventLogTab"; export { GeneralTab } from "./GeneralTab/GeneralTab"; export { TaskTab } from "./TaskTab/TaskTab"; export { ProjectTab } from "./ProjectTab/ProjectTab"; diff --git a/src/pages/distroSettings/tabs/testData.ts b/src/pages/distroSettings/tabs/testData.ts index 492b931eae..4d36d17de8 100644 --- a/src/pages/distroSettings/tabs/testData.ts +++ b/src/pages/distroSettings/tabs/testData.ts @@ -1,9 +1,10 @@ import { CloneMethod, + DispatcherVersion, DistroQuery, FinderVersion, PlannerVersion, - DispatcherVersion, + Provider, } from "gql/generated/types"; const distroData: DistroQuery["distro"] = { @@ -91,7 +92,7 @@ const distroData: DistroQuery["distro"] = { targetTime: 0, version: PlannerVersion.Tunable, }, - provider: "static", + provider: Provider.Static, providerSettingsList: [ { ami: "who-ami", diff --git a/src/pages/projectSettings/tabs/EventLogTab/EventLogTab.tsx b/src/pages/projectSettings/tabs/EventLogTab/EventLogTab.tsx index 4e12cfd7ef..2b499c6b46 100644 --- a/src/pages/projectSettings/tabs/EventLogTab/EventLogTab.tsx +++ b/src/pages/projectSettings/tabs/EventLogTab/EventLogTab.tsx @@ -1,35 +1,20 @@ -import styled from "@emotion/styled"; -import Badge, { Variant } from "@leafygreen-ui/badge"; -import Button from "@leafygreen-ui/button"; -import Card from "@leafygreen-ui/card"; -import { Table, TableHeader, Row, Cell } from "@leafygreen-ui/table"; -import { fontFamilies } from "@leafygreen-ui/tokens"; -import { Subtitle } from "@leafygreen-ui/typography"; import { useParams } from "react-router-dom"; -import { size } from "constants/tokens"; -import { useDateFormat } from "hooks"; - +import { EventLog } from "components/Settings/EventLog"; import { ProjectType } from "../utils"; -import { EventDiffLine, EventValue, getEventDiffLines } from "./EventLogDiffs"; -import { useEvents } from "./useEvents"; - -const EVENT_LIMIT = 15; +import { useProjectSettingsEvents } from "./useProjectSettingsEvents"; type TabProps = { limit?: number; projectType: ProjectType; }; -export const EventLogTab: React.FC = ({ - limit = EVENT_LIMIT, - projectType, -}) => { +export const EventLogTab: React.FC = ({ limit, projectType }) => { const { projectIdentifier: identifier } = useParams<{ projectIdentifier: string; }>(); const isRepo = projectType === ProjectType.Repo; - const { allEventsFetched, events, fetchMore } = useEvents( + const { allEventsFetched, events, fetchMore } = useProjectSettingsEvents( identifier, isRepo, limit @@ -37,132 +22,18 @@ export const EventLogTab: React.FC = ({ const lastEventTimestamp = events[events.length - 1]?.timestamp; - const allEventsFetchedCopy = - events.length > 0 ? "No more events to show." : "No events to show."; return ( - - {events.map(({ after, before, timestamp, user }) => ( - - - datum.key} - />, - JSON.stringify(datum.before)} - />, - JSON.stringify(datum.after)} - />, - ]} - > - {({ datum }) => ( - - - {datum.key} - - - {getEventValue(datum.before)} - - - {getEventValue(datum.after) === null ? ( - Deleted - ) : ( - {getEventValue(datum.after)} - )} - - - )} -
-
- ))} - {!allEventsFetched && !!events.length && ( - - )} - {allEventsFetched && {allEventsFetchedCopy}} -
+ { + fetchMore({ + variables: { + identifier, + before: lastEventTimestamp, + }, + }); + }} + /> ); }; - -interface Props { - timestamp: Date; - user: string; -} - -const EventLogHeader: React.FC = ({ timestamp, user }) => { - const getDateCopy = useDateFormat(); - return ( - - {getDateCopy(timestamp)} -
{user}
-
- ); -}; - -const Container = styled.div` - display: flex; - flex-direction: column; - align-items: center; - width: 150%; -`; - -const EventLogCard = styled(Card)` - width: 100%; - margin-bottom: ${size.l}; - padding: ${size.m}; -`; - -const CellText = styled.span` - font-family: ${fontFamilies.code}; - font-size: 12px; - line-height: 16px; - word-break: break-all; -`; - -const StyledHeader = styled.div` - padding-bottom: ${size.s}; -`; - -const getEventValue = (value: EventValue): string => { - if (value === null || value === undefined) { - return null; - } - if (typeof value === "boolean") { - return String(value); - } - - if (typeof value === "string") { - return `"${value}"`; - } - - if (typeof value === "number") { - return value; - } - - if (Array.isArray(value)) { - return JSON.stringify(value).replaceAll(",", ",\n"); - } - - return JSON.stringify(value); -}; diff --git a/src/pages/projectSettings/tabs/EventLogTab/useEvents.ts b/src/pages/projectSettings/tabs/EventLogTab/useProjectSettingsEvents.ts similarity index 80% rename from src/pages/projectSettings/tabs/EventLogTab/useEvents.ts rename to src/pages/projectSettings/tabs/EventLogTab/useProjectSettingsEvents.ts index 95e27f9f15..5b66d32019 100644 --- a/src/pages/projectSettings/tabs/EventLogTab/useEvents.ts +++ b/src/pages/projectSettings/tabs/EventLogTab/useProjectSettingsEvents.ts @@ -1,5 +1,6 @@ -import { useEffect, useState } from "react"; +import { useEffect } from "react"; import { useQuery } from "@apollo/client"; +import { EVENT_LIMIT, useEvents } from "components/Settings/EventLog"; import { useToastContext } from "context/toast"; import { ProjectEventLogsQuery, @@ -9,23 +10,14 @@ import { } from "gql/generated/types"; import { GET_PROJECT_EVENT_LOGS, GET_REPO_EVENT_LOGS } from "gql/queries"; -export const useEvents = ( +export const useProjectSettingsEvents = ( identifier: string, isRepo: boolean, - limit: number + limit: number = EVENT_LIMIT ) => { const dispatchToast = useToastContext(); - const [allEventsFetched, setAllEventsFetched] = useState(false); - const [prevCount, setPrevCount] = useState(0); - - // Hide Load More button when event count < limit is returned, - // or when an additional fetch fails to load more events. - const onCompleted = (count: number) => { - if (count - prevCount < limit) { - setAllEventsFetched(true); - } - }; + const { allEventsFetched, onCompleted, setPrevCount } = useEvents(limit); const { data: projectEventData, @@ -75,7 +67,7 @@ export const useEvents = ( useEffect(() => { setPrevCount(previousData?.count ?? 0); - }, [previousData]); + }, [previousData, setPrevCount]); return { allEventsFetched, events, fetchMore }; }; From 9b96ddfce88565312025e62f97df9d6ccb56e62a Mon Sep 17 00:00:00 2001 From: Sophie Stadler Date: Tue, 22 Aug 2023 15:59:23 -0400 Subject: [PATCH 03/14] EVG-19922: Increase build variant card height (#2002) --- src/components/MetadataCard.tsx | 4 +- .../ConfigurePatchCore.stories.storyshot | 18 ++-- .../__snapshots__/Metadata.stories.storyshot | 90 ++++++++++--------- .../BuildVariantCard.stories.storyshot | 22 ++--- src/pages/version/BuildVariantCard/index.tsx | 9 +- 5 files changed, 81 insertions(+), 62 deletions(-) diff --git a/src/components/MetadataCard.tsx b/src/components/MetadataCard.tsx index 30868ff5eb..f6b98a96cf 100644 --- a/src/components/MetadataCard.tsx +++ b/src/components/MetadataCard.tsx @@ -32,10 +32,10 @@ export const MetadataCard: React.FC = ({ export const MetadataTitle: React.FC<{ children: React.ReactNode }> = ({ children, }) => ( - <> +
{children} - +
); interface ItemProps { diff --git a/src/pages/configurePatch/configurePatchCore/__snapshots__/ConfigurePatchCore.stories.storyshot b/src/pages/configurePatch/configurePatchCore/__snapshots__/ConfigurePatchCore.stories.storyshot index 4442004d3a..406e57dbb7 100644 --- a/src/pages/configurePatch/configurePatchCore/__snapshots__/ConfigurePatchCore.stories.storyshot +++ b/src/pages/configurePatch/configurePatchCore/__snapshots__/ConfigurePatchCore.stories.storyshot @@ -65,14 +65,16 @@ exports[`storybook Storyshots pages/configurePatch/configurePatchCore Configure
-

- Patch Metadata -

-
+
+

+ Patch Metadata +

+
+

diff --git a/src/pages/task/metadata/__snapshots__/Metadata.stories.storyshot b/src/pages/task/metadata/__snapshots__/Metadata.stories.storyshot index e346c5e93a..58cca47f44 100644 --- a/src/pages/task/metadata/__snapshots__/Metadata.stories.storyshot +++ b/src/pages/task/metadata/__snapshots__/Metadata.stories.storyshot @@ -8,14 +8,16 @@ exports[`storybook Storyshots Pages/Task/Metadata Containerized Task 1`] = `

-

- Task Metadata -

-
+
+

+ Task Metadata +

+
+

-

- Task Metadata -

-
+
+

+ Task Metadata +

+
+

-

- Task Metadata -

-
+
+

+ Task Metadata +

+
+

-

- Task Metadata -

-
+
+

+ Task Metadata +

+
+

-

- Depends On -

-
+
+

+ Depends On +

+
+
diff --git a/src/pages/version/BuildVariantCard/__snapshots__/BuildVariantCard.stories.storyshot b/src/pages/version/BuildVariantCard/__snapshots__/BuildVariantCard.stories.storyshot index bf62970806..2e0d9b877d 100644 --- a/src/pages/version/BuildVariantCard/__snapshots__/BuildVariantCard.stories.storyshot +++ b/src/pages/version/BuildVariantCard/__snapshots__/BuildVariantCard.stories.storyshot @@ -3,18 +3,20 @@ exports[`storybook Storyshots Pages/Version/BuildVariantCard Default 1`] = `
-

- Build Variants -

-
+
+

+ Build Variants +

+
+
{ }; const StickyMetadataCard = styled(MetadataCard)` + display: flex; + flex-direction: column; + /* Subtract navbar height, top, and bottom margin from viewport height */ + max-height: calc(100vh - ${navBarHeight} - ${size.m} - ${size.m}); position: sticky; top: 0; `; const ScrollableBuildVariantStatsContainer = styled.div` - max-height: 55vh; - overflow-y: auto; + overflow-y: scroll; `; export default BuildVariantCard; From f400a7ef86042c7a000d18795683aca47d80d094 Mon Sep 17 00:00:00 2001 From: Kim Tao Date: Wed, 23 Aug 2023 11:01:10 -0400 Subject: [PATCH 04/14] v3.0.130 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6157f318a7..cd266ad217 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "spruce", - "version": "3.0.129", + "version": "3.0.130", "private": true, "scripts": { "bootstrap-logkeeper": "./scripts/bootstrap-logkeeper.sh", From 1b41b99fd7062482de9b2c6481e8ab3d4841720a Mon Sep 17 00:00:00 2001 From: minnakt <47064971+minnakt@users.noreply.github.com> Date: Thu, 24 Aug 2023 10:58:47 -0400 Subject: [PATCH 05/14] EVG-20360: Remove redundant test from commit_queue.ts (#2005) --- cypress/integration/commit_queue.ts | 30 ----------------------------- 1 file changed, 30 deletions(-) diff --git a/cypress/integration/commit_queue.ts b/cypress/integration/commit_queue.ts index 0c73f08cc1..e05f0d5221 100644 --- a/cypress/integration/commit_queue.ts +++ b/cypress/integration/commit_queue.ts @@ -3,13 +3,11 @@ const commitQueue = { id2: "mongodb-mongo-test", id3: "non-existent-item", id4: "evergreen", - id5: "logkeeper", }; const COMMIT_QUEUE_ROUTE_1 = `/commit-queue/${commitQueue.id1}`; const COMMIT_QUEUE_ROUTE_2 = `/commit-queue/${commitQueue.id2}`; const INVALID_COMMIT_QUEUE_ROUTE = `/commit-queue/${commitQueue.id3}`; const COMMIT_QUEUE_ROUTE_4 = `/commit-queue/${commitQueue.id4}`; -const COMMIT_QUEUE_ROUTE_PR = `/commit-queue/${commitQueue.id5}`; describe("commit queue page", { testIsolation: false }, () => { describe(COMMIT_QUEUE_ROUTE_1, () => { @@ -84,32 +82,4 @@ describe("commit queue page", { testIsolation: false }, () => { cy.validateToast("error", "There was an error loading the commit queue"); }); }); - - describe(COMMIT_QUEUE_ROUTE_PR, () => { - before(() => { - cy.visit(COMMIT_QUEUE_ROUTE_PR); - }); - it("Clicking on remove a patch for the PR commit queue should work", () => { - cy.dataCy("commit-queue-card").should("have.length", 1); - cy.dataCy("commit-queue-card-title").should( - "have.text", - "patch description here" - ); - cy.dataCy("commit-queue-card-title").within(() => { - cy.get("a").should( - "have.attr", - "href", - "https://github.com/logkeeper/logkeeper/pull/1234" - ); - }); - cy.dataCy("commit-queue-patch-button").should("exist"); - cy.dataCy("commit-queue-patch-button").click(); - cy.dataCy("commit-queue-confirmation-modal").should("be.visible"); - cy.dataCy("commit-queue-confirmation-modal").within(() => { - cy.contains("Remove").click(); - }); - cy.dataCy("commit-queue-confirmation-modal").should("not.exist"); - cy.dataCy("commit-queue-card").should("not.exist"); - }); - }); }); From 9fcad875c0474e1af8e32f9df35606e3d8ee1762 Mon Sep 17 00:00:00 2001 From: minnakt <47064971+minnakt@users.noreply.github.com> Date: Fri, 25 Aug 2023 10:15:08 -0400 Subject: [PATCH 06/14] EVG-20543: Hide merge method if GitHub merge queue is enabled (#2003) --- .../projectSettings/project_settings.ts | 12 +- src/constants/externalResources.ts | 2 + .../GithubCommitQueueTab/getFormSchema.tsx | 141 +++++++++++------- .../GithubCommitQueueTab/transformers.test.ts | 22 ++- .../tabs/GithubCommitQueueTab/transformers.ts | 9 +- .../tabs/GithubCommitQueueTab/types.ts | 6 +- 6 files changed, 123 insertions(+), 69 deletions(-) diff --git a/cypress/integration/projectSettings/project_settings.ts b/cypress/integration/projectSettings/project_settings.ts index abd154aa03..a6a4e52cc6 100644 --- a/cypress/integration/projectSettings/project_settings.ts +++ b/cypress/integration/projectSettings/project_settings.ts @@ -197,7 +197,7 @@ describe("Repo Settings", { testIsolation: false }, () => { countCQFields(2); cy.dataCy("cq-enabled-radio-box").children().first().click(); - countCQFields(7); + countCQFields(4); cy.dataCy("error-banner") .contains( @@ -206,8 +206,16 @@ describe("Repo Settings", { testIsolation: false }, () => { .should("exist"); }); - it("Presents three options for merge method", () => { + it("Shows merge method only if merge queue is Evergreen", () => { const selectId = "merge-method-select"; + + // Hides merge method for GitHub. + cy.getInputByLabel("GitHub").check({ force: true }); + cy.dataCy(selectId).should("not.exist"); + + // Shows merge method for Evergreen. + cy.getInputByLabel("Evergreen").check({ force: true }); + cy.dataCy(selectId).should("exist"); cy.get(`button[name=${selectId}]`).click(); cy.get(`#${selectId}-menu`).children().should("have.length", 3); cy.get(`#${selectId}-menu`).children().first().click(); diff --git a/src/constants/externalResources.ts b/src/constants/externalResources.ts index 412f8f91e5..52fa94100f 100644 --- a/src/constants/externalResources.ts +++ b/src/constants/externalResources.ts @@ -24,6 +24,8 @@ export const gitTagAliasesDocumentationUrl = `${projectDistroSettingsDocumentati export const githubChecksAliasesDocumentationUrl = `${projectDistroSettingsDocumentationUrl}#github-checks-aliases`; +export const githubMergeQueueDocumentationUrl = `${wikiBaseUrl}/Project-Configuration/Merge-Queue`; + export const cliDocumentationUrl = `${wikiBaseUrl}/CLI`; export const windowsPasswordRulesURL = diff --git a/src/pages/projectSettings/tabs/GithubCommitQueueTab/getFormSchema.tsx b/src/pages/projectSettings/tabs/GithubCommitQueueTab/getFormSchema.tsx index bde2c7ff63..febeccf41b 100644 --- a/src/pages/projectSettings/tabs/GithubCommitQueueTab/getFormSchema.tsx +++ b/src/pages/projectSettings/tabs/GithubCommitQueueTab/getFormSchema.tsx @@ -8,6 +8,7 @@ import { pullRequestAliasesDocumentationUrl, gitTagAliasesDocumentationUrl, githubChecksAliasesDocumentationUrl, + githubMergeQueueDocumentationUrl, } from "constants/externalResources"; import { getProjectSettingsRoute, @@ -212,60 +213,82 @@ export const getFormSchema = ( enabled: { enum: [true], }, - mergeQueueTitle: { - title: "Merge Queue", - type: "null", - }, - mergeQueue: { - type: "string" as "string", - oneOf: [ - { - type: "string" as "string", - title: "Evergreen", - enum: [MergeQueue.Evergreen], - description: - "Use the standard commit queue owned and maintained by Evergreen.", - }, - { - type: "string" as "string", - title: "GitHub", - enum: [MergeQueue.Github], - description: "Use the GitHub merge queue.", - }, - ], - }, message: { type: "string" as "string", title: "Commit Queue Message", }, - mergeMethod: { - type: "string" as "string", - title: "Merge Method", - oneOf: [ - { - type: "string" as "string", - title: "Squash", - enum: ["squash"], - }, - { + mergeSettings: { + title: "Merge Queue", + type: "object" as "object", + properties: { + mergeQueue: { type: "string" as "string", - title: "Merge", - enum: ["merge"], + oneOf: [ + { + type: "string" as "string", + title: "Evergreen", + enum: [MergeQueue.Evergreen], + description: "Evergreen's commit queue.", + }, + { + type: "string" as "string", + title: "GitHub", + enum: [MergeQueue.Github], + description: "GitHub's merge queue.", + }, + ], }, - { - type: "string" as "string", - title: "Rebase", - enum: ["rebase"], + }, + dependencies: { + mergeQueue: { + oneOf: [ + { + properties: { + mergeQueue: { + enum: [MergeQueue.Evergreen], + }, + mergeMethod: { + type: "string" as "string", + title: "Merge Method", + oneOf: [ + { + type: "string" as "string", + title: "Squash", + enum: ["squash"], + }, + { + type: "string" as "string", + title: "Merge", + enum: ["merge"], + }, + { + type: "string" as "string", + title: "Rebase", + enum: ["rebase"], + }, + ...insertIf( + projectType === + ProjectType.AttachedProject, + { + type: "string" as "string", + title: `Default to Repo (${repoData?.commitQueue?.mergeSettings?.mergeMethod})`, + enum: [""], + } + ), + ], + }, + }, + }, + { + properties: { + mergeQueue: { + enum: [MergeQueue.Github], + }, + }, + }, + ], }, - ...insertIf( - projectType === ProjectType.AttachedProject, - { - type: "string" as "string", - title: `Default to Repo (${repoData?.commitQueue?.mergeMethod})`, - enum: [""], - } - ), - ], + }, }, patchDefinitions: { type: "object" as "object", @@ -475,17 +498,20 @@ export const getFormSchema = ( "the Commit Queue" ), }, - mergeQueue: { - "ui:widget": "radio", - }, message: { "ui:description": "Shown in commit queue CLI commands & web UI", "ui:data-cy": "cq-message-input", ...placeholderIf(repoData?.commitQueue?.message), }, - mergeMethod: { - "ui:allowDeselect": false, - "ui:data-cy": "merge-method-select", + mergeSettings: { + "ui:description": GitHubMergeQueueDescription, + mergeQueue: { + "ui:widget": "radio", + }, + mergeMethod: { + "ui:allowDeselect": false, + "ui:data-cy": "merge-method-select", + }, }, patchDefinitions: { ...errorStyling( @@ -626,3 +652,12 @@ const GitHubChecksAliasesDescription = ( and no aliases are defined on the project or repo page. ); + +const GitHubMergeQueueDescription = ( + <> + Choose to merge with Evergreen or GitHub. Note that in order to use the + GitHub merge queue, you will need to complete the additional steps outlined + in the docs + . + +); diff --git a/src/pages/projectSettings/tabs/GithubCommitQueueTab/transformers.test.ts b/src/pages/projectSettings/tabs/GithubCommitQueueTab/transformers.test.ts index 54d0d460a0..1736dc83a8 100644 --- a/src/pages/projectSettings/tabs/GithubCommitQueueTab/transformers.test.ts +++ b/src/pages/projectSettings/tabs/GithubCommitQueueTab/transformers.test.ts @@ -108,9 +108,11 @@ const projectForm: GCQFormState = { }, commitQueue: { enabled: null, - mergeMethod: "", - mergeQueue: MergeQueue.Evergreen, message: "", + mergeSettings: { + mergeMethod: "", + mergeQueue: MergeQueue.Evergreen, + }, patchDefinitions: { commitQueueAliasesOverride: true, commitQueueAliases: [ @@ -148,9 +150,9 @@ const projectResult: Pick = { gitTagAuthorizedTeams: [], commitQueue: { enabled: null, + message: "", mergeMethod: "", mergeQueue: MergeQueue.Evergreen, - message: "", }, }, aliases: [ @@ -261,9 +263,11 @@ const repoForm: GCQFormState = { }, commitQueue: { enabled: true, - mergeMethod: "squash", - mergeQueue: MergeQueue.Github, message: "Commit Queue Message", + mergeSettings: { + mergeMethod: "squash", + mergeQueue: MergeQueue.Github, + }, patchDefinitions: { commitQueueAliasesOverride: true, commitQueueAliases: [], @@ -282,9 +286,9 @@ const repoResult: Pick = { gitTagAuthorizedTeams: [], commitQueue: { enabled: true, + message: "Commit Queue Message", mergeMethod: "squash", mergeQueue: MergeQueue.Github, - message: "Commit Queue Message", }, }, aliases: [ @@ -411,9 +415,11 @@ const mergedForm: GCQFormState = { }, commitQueue: { enabled: null, - mergeMethod: "", - mergeQueue: MergeQueue.Evergreen, message: "", + mergeSettings: { + mergeMethod: "", + mergeQueue: MergeQueue.Evergreen, + }, patchDefinitions: { commitQueueAliasesOverride: true, commitQueueAliases: [ diff --git a/src/pages/projectSettings/tabs/GithubCommitQueueTab/transformers.ts b/src/pages/projectSettings/tabs/GithubCommitQueueTab/transformers.ts index b85752060e..fb537d3046 100644 --- a/src/pages/projectSettings/tabs/GithubCommitQueueTab/transformers.ts +++ b/src/pages/projectSettings/tabs/GithubCommitQueueTab/transformers.ts @@ -94,9 +94,11 @@ export const gqlToForm = ((data, options) => { }, commitQueue: { enabled: commitQueue.enabled, - mergeMethod: commitQueue.mergeMethod, - mergeQueue: commitQueue.mergeQueue, message: commitQueue.message, + mergeSettings: { + mergeQueue: commitQueue.mergeQueue, + mergeMethod: commitQueue.mergeMethod, + }, patchDefinitions: { commitQueueAliasesOverride: override(commitQueueAliases), commitQueueAliases, @@ -109,8 +111,7 @@ export const formToGql = (( { commitQueue: { enabled, - mergeMethod, - mergeQueue, + mergeSettings: { mergeMethod, mergeQueue }, message, patchDefinitions, }, diff --git a/src/pages/projectSettings/tabs/GithubCommitQueueTab/types.ts b/src/pages/projectSettings/tabs/GithubCommitQueueTab/types.ts index 3e6ba8c967..e643114693 100644 --- a/src/pages/projectSettings/tabs/GithubCommitQueueTab/types.ts +++ b/src/pages/projectSettings/tabs/GithubCommitQueueTab/types.ts @@ -54,9 +54,11 @@ export interface GCQFormState { }; commitQueue: { enabled: boolean | null; - mergeMethod: string; - mergeQueue: MergeQueue; message: string; + mergeSettings: { + mergeQueue: MergeQueue; + mergeMethod: string; + }; patchDefinitions: { commitQueueAliasesOverride: boolean; commitQueueAliases: Array; From 2848bd6cf2752561a24d92b63621d4373a0cb9b4 Mon Sep 17 00:00:00 2001 From: Kim Tao Date: Fri, 25 Aug 2023 10:29:36 -0400 Subject: [PATCH 07/14] v3.0.131 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cd266ad217..1eeacec0d6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "spruce", - "version": "3.0.130", + "version": "3.0.131", "private": true, "scripts": { "bootstrap-logkeeper": "./scripts/bootstrap-logkeeper.sh", From 4eaa4290254dc82c016117de3999d136c17041ce Mon Sep 17 00:00:00 2001 From: Sophie Stadler Date: Wed, 30 Aug 2023 09:26:56 -0400 Subject: [PATCH 08/14] EVG-19945: Add static provider settings page (#2004) --- .../distroSettings/provider_section.ts | 38 ++++++++ src/components/SettingsCard.tsx | 4 +- .../SpruceForm.stories.storyshot | 6 +- src/pages/distroSettings/Tabs.tsx | 16 +++- .../tabs/ProviderTab/ProviderTab.tsx | 14 +++ .../tabs/ProviderTab/getFormSchema.ts | 87 +++++++++++++++++++ .../tabs/ProviderTab/schemaFields.ts | 26 ++++++ .../tabs/ProviderTab/transformerUtils.ts | 56 ++++++++++++ .../tabs/ProviderTab/transformers.test.ts | 39 +++++++++ .../tabs/ProviderTab/transformers.ts | 42 +++++++++ .../distroSettings/tabs/ProviderTab/types.ts | 19 ++++ src/pages/distroSettings/tabs/index.tsx | 1 + src/pages/distroSettings/tabs/testData.ts | 7 -- src/pages/distroSettings/tabs/transformers.ts | 5 +- src/pages/distroSettings/tabs/types.ts | 3 +- 15 files changed, 346 insertions(+), 17 deletions(-) create mode 100644 cypress/integration/distroSettings/provider_section.ts create mode 100644 src/pages/distroSettings/tabs/ProviderTab/ProviderTab.tsx create mode 100644 src/pages/distroSettings/tabs/ProviderTab/getFormSchema.ts create mode 100644 src/pages/distroSettings/tabs/ProviderTab/schemaFields.ts create mode 100644 src/pages/distroSettings/tabs/ProviderTab/transformerUtils.ts create mode 100644 src/pages/distroSettings/tabs/ProviderTab/transformers.test.ts create mode 100644 src/pages/distroSettings/tabs/ProviderTab/transformers.ts create mode 100644 src/pages/distroSettings/tabs/ProviderTab/types.ts diff --git a/cypress/integration/distroSettings/provider_section.ts b/cypress/integration/distroSettings/provider_section.ts new file mode 100644 index 0000000000..740ad09c89 --- /dev/null +++ b/cypress/integration/distroSettings/provider_section.ts @@ -0,0 +1,38 @@ +import { save } from "./utils"; + +describe("provider section", () => { + beforeEach(() => { + cy.visit("/distro/localhost/settings/provider"); + }); + + it("successfully updates static provider fields", () => { + cy.dataCy("provider-select").contains("Static IP/VM"); + + // Correct fields are displayed + cy.dataCy("provider-settings").within(() => { + cy.get("button").should("have.length", 1); + cy.get("textarea").should("have.length", 1); + cy.get("input[type=checkbox]").should("have.length", 1); + cy.get("input[type=text]").should("have.length", 0); + }); + + cy.getInputByLabel("User Data").type("my user data"); + cy.getInputByLabel("Merge with existing user data").check({ + force: true, + }); + cy.contains("button", "Add security group").click(); + cy.getInputByLabel("Security Group ID").type("group-1234"); + + save(); + cy.validateToast("success"); + + cy.getInputByLabel("User Data").clear(); + cy.getInputByLabel("Merge with existing user data").uncheck({ + force: true, + }); + cy.dataCy("delete-item-button").click(); + + save(); + cy.validateToast("success"); + }); +}); diff --git a/src/components/SettingsCard.tsx b/src/components/SettingsCard.tsx index 6bf0f0bfc3..a6596f7630 100644 --- a/src/components/SettingsCard.tsx +++ b/src/components/SettingsCard.tsx @@ -12,7 +12,5 @@ export const formComponentSpacingCSS = "margin-bottom: 48px;"; export const SettingsCard = styled(Card)` padding: ${size.m}; - :not(:last-of-type) { - ${formComponentSpacingCSS} - } + ${formComponentSpacingCSS} `; diff --git a/src/components/SpruceForm/__snapshots__/SpruceForm.stories.storyshot b/src/components/SpruceForm/__snapshots__/SpruceForm.stories.storyshot index b61e2dd5f8..1f00fbec7f 100644 --- a/src/components/SpruceForm/__snapshots__/SpruceForm.stories.storyshot +++ b/src/components/SpruceForm/__snapshots__/SpruceForm.stories.storyshot @@ -9,7 +9,7 @@ exports[`storybook Storyshots components/SpruceForm Example 1 1`] = ` Distro Projects
= ({ distro }) => { } /> + + } + /> = ({ distroData }) => { + const initialFormState = distroData; + + const formSchema = useMemo(() => getFormSchema(), []); + + return ( + + ); +}; diff --git a/src/pages/distroSettings/tabs/ProviderTab/getFormSchema.ts b/src/pages/distroSettings/tabs/ProviderTab/getFormSchema.ts new file mode 100644 index 0000000000..3cd2ed863d --- /dev/null +++ b/src/pages/distroSettings/tabs/ProviderTab/getFormSchema.ts @@ -0,0 +1,87 @@ +import { GetFormSchema } from "components/SpruceForm"; +import { CardFieldTemplate } from "components/SpruceForm/FieldTemplates"; +import { Provider } from "gql/generated/types"; +import { staticProviderSettings } from "./schemaFields"; + +export const getFormSchema = (): ReturnType => ({ + fields: {}, + schema: { + type: "object" as "object", + properties: { + provider: { + type: "object" as "object", + title: "", + properties: { + providerName: { + type: "string" as "string", + title: "Provider", + oneOf: [ + { + type: "string" as "string", + title: "Static IP/VM", + enum: [Provider.Static], + }, + { + type: "string" as "string", + title: "Docker", + enum: [Provider.Docker], + }, + { + type: "string" as "string", + title: "EC2 Fleet", + enum: [Provider.Ec2Fleet], + }, + { + type: "string" as "string", + title: "EC2 On Demand", + enum: [Provider.Ec2OnDemand], + }, + ], + }, + }, + }, + }, + dependencies: { + provider: { + oneOf: [ + { + properties: { + provider: { + properties: { + providerName: { + enum: [Provider.Static], + }, + }, + }, + providerSettings: { + type: "object" as "object", + title: "", + properties: staticProviderSettings, + }, + }, + }, + ], + }, + }, + }, + uiSchema: { + provider: { + "ui:ObjectFieldTemplate": CardFieldTemplate, + providerName: { + "ui:allowDeselect": false, + "ui:data-cy": "provider-select", + }, + }, + providerSettings: { + "ui:data-cy": "provider-settings", + "ui:ObjectFieldTemplate": CardFieldTemplate, + userData: { + "ui:widget": "textarea", + }, + securityGroups: { + "ui:addButtonText": "Add security group", + "ui:orderable": false, + }, + }, + }, +}); diff --git a/src/pages/distroSettings/tabs/ProviderTab/schemaFields.ts b/src/pages/distroSettings/tabs/ProviderTab/schemaFields.ts new file mode 100644 index 0000000000..eca2061071 --- /dev/null +++ b/src/pages/distroSettings/tabs/ProviderTab/schemaFields.ts @@ -0,0 +1,26 @@ +const mergeUserData = { + type: "boolean" as "boolean", + title: "Merge with existing user data", +}; + +const securityGroups = { + type: "array" as "array", + title: "Security Groups", + items: { + type: "string" as "string", + title: "Security Group ID", + default: "", + minLength: 1, + }, +}; + +const userData = { + type: "string" as "string", + title: "User Data", +}; + +export const staticProviderSettings = { + userData, + mergeUserData, + securityGroups, +}; diff --git a/src/pages/distroSettings/tabs/ProviderTab/transformerUtils.ts b/src/pages/distroSettings/tabs/ProviderTab/transformerUtils.ts new file mode 100644 index 0000000000..6e9e96e3ba --- /dev/null +++ b/src/pages/distroSettings/tabs/ProviderTab/transformerUtils.ts @@ -0,0 +1,56 @@ +type FieldGetter = (providerSettings: Record) => { + form: Record; + gql: Record; +}; + +const getUserData = ((providerSettings) => ({ + form: { + userData: providerSettings.user_data ?? "", + }, + gql: { + user_data: providerSettings.userData, + }, +})) satisfies FieldGetter; + +const getMergeUserData = ((providerSettings) => ({ + form: { + mergeUserData: providerSettings.merge_user_data_parts ?? false, + }, + gql: { + merge_user_data_parts: providerSettings.mergeUserData, + }, +})) satisfies FieldGetter; + +const getSecurityGroups = ((providerSettings) => ({ + form: { + securityGroups: providerSettings.security_group_ids ?? [], + }, + gql: { + security_group_ids: providerSettings.securityGroups, + }, +})) satisfies FieldGetter; + +export const staticProviderSettings = ((providerSettings = {}) => { + const userData = getUserData(providerSettings); + const mergeUserData = getMergeUserData(providerSettings); + const securityGroups = getSecurityGroups(providerSettings); + + return { + form: { + providerSettings: { + ...userData.form, + ...mergeUserData.form, + ...securityGroups.form, + }, + }, + gql: { + providerSettingsList: [ + { + ...userData.gql, + ...mergeUserData.gql, + ...securityGroups.gql, + }, + ], + }, + }; +}) satisfies FieldGetter; diff --git a/src/pages/distroSettings/tabs/ProviderTab/transformers.test.ts b/src/pages/distroSettings/tabs/ProviderTab/transformers.test.ts new file mode 100644 index 0000000000..759789c3ba --- /dev/null +++ b/src/pages/distroSettings/tabs/ProviderTab/transformers.test.ts @@ -0,0 +1,39 @@ +import { DistroInput, Provider } from "gql/generated/types"; +import { distroData } from "../testData"; +import { formToGql, gqlToForm } from "./transformers"; +import { ProviderFormState } from "./types"; + +describe("provider tab", () => { + describe("static provider", () => { + it("correctly converts from GQL to a form", () => { + expect(gqlToForm(distroData)).toStrictEqual(form); + }); + + it("correctly converts from a form to GQL", () => { + expect(formToGql(form, distroData)).toStrictEqual(gql); + }); + }); +}); + +const form: ProviderFormState = { + provider: { + providerName: Provider.Static, + }, + providerSettings: { + userData: "", + mergeUserData: false, + securityGroups: ["1"], + }, +}; + +const gql: DistroInput = { + ...distroData, + provider: Provider.Static, + providerSettingsList: [ + { + merge_user_data_parts: false, + security_group_ids: ["1"], + user_data: "", + }, + ], +}; diff --git a/src/pages/distroSettings/tabs/ProviderTab/transformers.ts b/src/pages/distroSettings/tabs/ProviderTab/transformers.ts new file mode 100644 index 0000000000..e382923d11 --- /dev/null +++ b/src/pages/distroSettings/tabs/ProviderTab/transformers.ts @@ -0,0 +1,42 @@ +import { DistroSettingsTabRoutes } from "constants/routes"; +import { Provider } from "gql/generated/types"; +import { FormToGqlFunction, GqlToFormFunction } from "../types"; +import { staticProviderSettings } from "./transformerUtils"; + +type Tab = DistroSettingsTabRoutes.Provider; + +export const gqlToForm = ((data) => { + if (!data) return null; + + const { provider, providerSettingsList } = data; + + switch (provider) { + case Provider.Static: + return { + provider: { + providerName: Provider.Static, + }, + ...staticProviderSettings(providerSettingsList[0]).form, + }; + default: + throw new Error(`Unknown provider '${provider}'`); + } +}) satisfies GqlToFormFunction; + +export const formToGql = ((data, distro) => { + const { + provider: { providerName }, + } = data; + + switch (providerName) { + case Provider.Static: { + return { + ...distro, + provider: providerName, + ...staticProviderSettings(data.providerSettings).gql, + }; + } + default: + return distro; + } +}) satisfies FormToGqlFunction; diff --git a/src/pages/distroSettings/tabs/ProviderTab/types.ts b/src/pages/distroSettings/tabs/ProviderTab/types.ts new file mode 100644 index 0000000000..c100aead99 --- /dev/null +++ b/src/pages/distroSettings/tabs/ProviderTab/types.ts @@ -0,0 +1,19 @@ +import { Provider } from "gql/generated/types"; + +interface StaticProviderFormState { + provider: { + providerName: Provider.Static; + }; + providerSettings: { + userData: string; + mergeUserData: boolean; + securityGroups: string[]; + }; +} + +// TODO: Append type with additional provider options, e.g. type ProviderFormState = StaticProviderFormState | DockerProviderFormState +export type ProviderFormState = StaticProviderFormState; + +export type TabProps = { + distroData: ProviderFormState; +}; diff --git a/src/pages/distroSettings/tabs/index.tsx b/src/pages/distroSettings/tabs/index.tsx index d17a5f887c..14b74aa025 100644 --- a/src/pages/distroSettings/tabs/index.tsx +++ b/src/pages/distroSettings/tabs/index.tsx @@ -2,3 +2,4 @@ export { EventLogTab } from "./EventLogTab/EventLogTab"; export { GeneralTab } from "./GeneralTab/GeneralTab"; export { TaskTab } from "./TaskTab/TaskTab"; export { ProjectTab } from "./ProjectTab/ProjectTab"; +export { ProviderTab } from "./ProviderTab/ProviderTab"; diff --git a/src/pages/distroSettings/tabs/testData.ts b/src/pages/distroSettings/tabs/testData.ts index 4d36d17de8..d588302373 100644 --- a/src/pages/distroSettings/tabs/testData.ts +++ b/src/pages/distroSettings/tabs/testData.ts @@ -102,13 +102,6 @@ const distroData: DistroQuery["distro"] = { security_group_ids: ["1"], subnet_id: "subnet-123", }, - { - ami: "who-ami-2", - instance_type: "m4.2xlarge", - is_vpc: false, - region: "us-west-1", - security_group_ids: ["2"], - }, ], setup: "ls -alF", setupAsSudo: true, diff --git a/src/pages/distroSettings/tabs/transformers.ts b/src/pages/distroSettings/tabs/transformers.ts index 29ae7ddd32..cd230719ea 100644 --- a/src/pages/distroSettings/tabs/transformers.ts +++ b/src/pages/distroSettings/tabs/transformers.ts @@ -2,6 +2,7 @@ import { DistroSettingsTabRoutes } from "constants/routes"; import { DistroInput } from "gql/generated/types"; import * as general from "./GeneralTab/transformers"; import * as project from "./ProjectTab/transformers"; +import * as provider from "./ProviderTab/transformers"; import * as task from "./TaskTab/transformers"; import { FormToGqlFunction, @@ -18,7 +19,7 @@ export const formToGqlMap: { [DistroSettingsTabRoutes.General]: general.formToGql, [DistroSettingsTabRoutes.Host]: () => fakeReturn, [DistroSettingsTabRoutes.Project]: project.formToGql, - [DistroSettingsTabRoutes.Provider]: () => fakeReturn, + [DistroSettingsTabRoutes.Provider]: provider.formToGql, [DistroSettingsTabRoutes.Task]: task.formToGql, }; @@ -28,6 +29,6 @@ export const gqlToFormMap: { [DistroSettingsTabRoutes.General]: general.gqlToForm, [DistroSettingsTabRoutes.Host]: () => fakeReturn, [DistroSettingsTabRoutes.Project]: project.gqlToForm, - [DistroSettingsTabRoutes.Provider]: () => fakeReturn, + [DistroSettingsTabRoutes.Provider]: provider.gqlToForm, [DistroSettingsTabRoutes.Task]: task.gqlToForm, }; diff --git a/src/pages/distroSettings/tabs/types.ts b/src/pages/distroSettings/tabs/types.ts index 51218c2ce4..345e891bbd 100644 --- a/src/pages/distroSettings/tabs/types.ts +++ b/src/pages/distroSettings/tabs/types.ts @@ -2,6 +2,7 @@ import { DistroSettingsTabRoutes } from "constants/routes"; import { DistroQuery, DistroInput } from "gql/generated/types"; import { GeneralFormState } from "./GeneralTab/types"; import { ProjectFormState } from "./ProjectTab/types"; +import { ProviderFormState } from "./ProviderTab/types"; import { TaskFormState } from "./TaskTab/types"; const { EventLog, ...WritableDistroSettingsTabs } = DistroSettingsTabRoutes; @@ -14,7 +15,7 @@ export type WritableDistroSettingsType = export type FormStateMap = { [T in WritableDistroSettingsType]: { [DistroSettingsTabRoutes.General]: GeneralFormState; - [DistroSettingsTabRoutes.Provider]: any; + [DistroSettingsTabRoutes.Provider]: ProviderFormState; [DistroSettingsTabRoutes.Task]: TaskFormState; [DistroSettingsTabRoutes.Host]: any; [DistroSettingsTabRoutes.Project]: ProjectFormState; From 6472d6b69b15f72b05cb6a73322815222f0650cc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Aug 2023 10:10:44 -0400 Subject: [PATCH 09/14] CHORE(NPM) - bump @emotion/css from 11.11.0 to 11.11.2 (#2009) --- package.json | 2 +- yarn.lock | 13 +------------ 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 1eeacec0d6..0550631033 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "@apollo/client": "3.6.9", "@bugsnag/js": "7.20.2", "@bugsnag/plugin-react": "7.18.0", - "@emotion/css": "11.11.0", + "@emotion/css": "11.11.2", "@emotion/react": "11.11.0", "@emotion/styled": "11.11.0", "@leafygreen-ui/badge": "8.0.2", diff --git a/yarn.lock b/yarn.lock index 36fb0c366c..7c450b1602 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1694,18 +1694,7 @@ "@emotion/memoize" "^0.8.1" stylis "4.2.0" -"@emotion/css@11.11.0": - version "11.11.0" - resolved "https://registry.yarnpkg.com/@emotion/css/-/css-11.11.0.tgz#dad6a27a77d5e5cbb0287674c3ace76d762563ca" - integrity sha512-m4g6nKzZyiKyJ3WOfdwrBdcujVcpaScIWHAnyNKPm/A/xJKwfXPfQAbEVi1kgexWTDakmg+r2aDj0KvnMTo4oQ== - dependencies: - "@emotion/babel-plugin" "^11.11.0" - "@emotion/cache" "^11.11.0" - "@emotion/serialize" "^1.1.2" - "@emotion/sheet" "^1.2.2" - "@emotion/utils" "^1.2.1" - -"@emotion/css@^11.1.3": +"@emotion/css@11.11.2", "@emotion/css@^11.1.3": version "11.11.2" resolved "https://registry.yarnpkg.com/@emotion/css/-/css-11.11.2.tgz#e5fa081d0c6e335352e1bc2b05953b61832dca5a" integrity sha512-VJxe1ucoMYMS7DkiMdC2T7PWNbrEI0a39YRiyDvK2qq4lXwjRbVP/z4lpG+odCsRzadlR+1ywwrTzhdm5HNdew== From 7774c96e6c62c99afd57a3cabf6cc5d8ccbeee5d Mon Sep 17 00:00:00 2001 From: minnakt <47064971+minnakt@users.noreply.github.com> Date: Wed, 30 Aug 2023 10:48:40 -0400 Subject: [PATCH 10/14] EVG-19125: Upgrade React testing library dependencies (#2007) --- {src => config/jest}/setupTests.ts | 15 ++- jest.config.js | 3 +- package.json | 7 +- .../Breadcrumbs/Breadcrumbs.test.tsx | 17 ++- .../CommitChartLabel.test.tsx | 10 +- src/components/Dropdown/Dropdown.test.tsx | 30 +++-- .../EditableTagField.test.tsx | 36 ++++-- .../FilterBadges/FilterBadges.test.tsx | 54 ++++---- .../useFilterBadgeQueryParams.test.tsx | 20 +-- .../GroupedTaskStatusBadge.test.tsx | 17 ++- .../Header/NavDropdown/NavDropdown.test.tsx | 25 ++-- .../HistoryTable/Cell/Cell.test.tsx | 3 +- .../HistoryTable/HistoryTableContext.test.tsx | 2 +- .../HistoryTableIcon.test.tsx | 13 +- .../FoldedCommit/FoldedCommit.test.tsx | 7 +- .../HistoryTableTestSearch.test.tsx | 36 +++--- .../hooks/useTestResults.test.tsx | 89 +++++++------ .../PageSizeSelector.test.tsx | 11 +- src/components/Pagination/Pagination.test.tsx | 17 +-- src/components/Popconfirm/Popconfirm.test.tsx | 26 ++-- .../ProjectSelect/ProjectSelect.test.tsx | 29 +++-- .../SearchableDropdown.test.tsx | 117 +++++++++--------- .../SetPriority/SetPriority.test.tsx | 91 +++++++------- src/components/Settings/Context.test.tsx | 19 +-- src/components/Settings/Form.test.tsx | 16 ++- .../Settings/NavigationWarningModal.test.tsx | 37 +++--- .../ObjectFieldTemplates.test.tsx | 47 ++++--- src/components/SpruceForm/SpruceForm.test.tsx | 100 +++++++-------- src/components/TreeSelect/TreeSelect.test.tsx | 20 +-- .../TupleSelect/TupleSelect.test.tsx | 14 ++- .../TupleSelectWithRegexConditional.test.tsx | 33 ++--- .../WelcomeModal/WelcomeModal.test.tsx | 15 +-- src/context/toast/toast.test.tsx | 20 ++- src/hooks/tests/useBreadcrumbRoot.test.tsx | 18 +-- ...useDisableSpawnExpirationCheckbox.test.tsx | 20 ++- ...useGetUserPatchesPageTitleAndLink.test.tsx | 27 ++-- src/hooks/tests/useLegacyUIURL.test.tsx | 2 +- src/hooks/tests/useNetworkStatus.test.tsx | 2 +- src/hooks/tests/useOnClickOutside.test.tsx | 25 ++-- src/hooks/tests/usePageVisibility.test.tsx | 2 +- src/hooks/tests/usePolling.test.tsx | 2 +- .../useTableFilters/useTableFilters.test.tsx | 25 ++-- ...seUpdateUrlSortParamOnTableChange.test.tsx | 11 +- src/hooks/tests/useUpsertQueryParams.test.tsx | 54 ++++---- .../tests/useVersionStatusSelect.test.ts | 2 +- src/hooks/useDimensions/useDimensions.test.ts | 2 +- .../useIntersectionObserver.test.ts | 2 +- .../useKeyboardShortcut.test.tsx | 54 ++++---- .../useKonamiCode/useKonamiCode.test.tsx | 53 +++----- .../useQueryParam/useQueryParam.test.tsx | 2 +- src/hooks/useResize/useResize.test.ts | 2 +- .../useTabShortcut/useTabShortcut.test.ts | 28 ++--- .../BuildVariantCard.test.tsx | 21 +--- .../WaterfallTaskStatusIcon.test.tsx | 12 +- .../CommitBarChart/CommitBarChart.test.tsx | 6 +- .../InactiveCommits/InactiveCommits.test.tsx | 44 ++++--- .../ConfigureTasks/ConfigureTasks.test.tsx | 30 +++-- .../ConfigurePatchCore.stories.storyshot | 26 ++-- .../useConfigurePatch.test.tsx | 2 +- .../DeleteDistro/DeleteDistro.test.tsx | 14 +-- .../DistroSelect/DistroSelect.test.tsx | 10 +- .../NewDistro/CopyModal.test.tsx | 13 +- .../NewDistro/CreateModal.test.tsx | 13 +- .../NewDistro/NewDistroButton.test.tsx | 37 +++--- src/pages/projectSettings/Context.test.tsx | 40 +++--- .../projectSettings/CopyProjectModal.test.tsx | 30 +++-- .../CreateDuplicateProjectButton.test.tsx | 46 ++++--- .../CreateProjectModal.test.tsx | 81 ++++++------ .../DeactivateStepbackTaskField.test.tsx | 43 +++---- .../Fields/DeleteProjectField.test.tsx | 25 ++-- .../Fields/RepoConfigField.test.tsx | 41 +++--- .../spawnHost/SpawnHostTableActions.test.tsx | 53 ++++---- .../spawnVolume/SpawnVolumeModal.test.tsx | 24 ++-- .../previousCommits/PreviousCommits.test.tsx | 117 +++++++++--------- .../previousCommits/PreviousCommits.tsx | 31 +++-- .../AddIssueModal/AddIssueModal.test.tsx | 23 ++-- .../BuildBaron.test.tsx | 25 ++-- .../ColumnHeaders/ColumnHeaders.test.tsx | 3 +- .../TaskHistoryRow/TaskHistoryRow.test.tsx | 7 +- .../variantHistory/ColumnHeaders.test.tsx | 3 +- .../variantHistory/VariantHistoryRow.test.tsx | 7 +- .../Banners/ErrorBanner/ErrorBanner.test.tsx | 3 +- .../WarningBanner/WarningBanner.test.tsx | 12 +- .../taskDuration/TaskDurationTable.test.tsx | 8 +- src/pages/version/useQueryVariables.test.ts | 2 +- src/storybook.test.ts | 4 +- src/test_utils/index.tsx | 34 +++-- src/test_utils/utils.ts | 5 +- src/utils/errorReporting.test.ts | 2 + tsconfig.json | 3 +- yarn.lock | 110 ++++------------ 91 files changed, 1146 insertions(+), 1193 deletions(-) rename {src => config/jest}/setupTests.ts (53%) diff --git a/src/setupTests.ts b/config/jest/setupTests.ts similarity index 53% rename from src/setupTests.ts rename to config/jest/setupTests.ts index 4b9d1f902a..712d9679d3 100644 --- a/src/setupTests.ts +++ b/config/jest/setupTests.ts @@ -2,7 +2,7 @@ // allows you to do things like: // expect(element).toHaveTextContent(/react/i) // learn more: https://github.com/testing-library/jest-dom -import "@testing-library/jest-dom/extend-expect"; +import "@testing-library/jest-dom"; import MutationObserver from "mutation-observer"; // @ts-ignore @@ -16,3 +16,16 @@ window.crypto.randomUUID = (() => { return value.toString(); }; })(); + +// Mock focus-trap-react to prevent errors in tests that use modals. focus-trap-react is a package used +// by LeafyGreen and is not a direct dependency of Spruce. +jest.mock( + "focus-trap-react", + () => { + const focusTrap = jest.requireActual( + "focus-trap-react" + ); + focusTrap.prototype.setupFocusTrap = () => null; + return focusTrap; + } +); diff --git a/jest.config.js b/jest.config.js index f0f0cc4332..39a0d77559 100644 --- a/jest.config.js +++ b/jest.config.js @@ -14,7 +14,7 @@ module.exports = { modulePaths: ["/src"], resetMocks: true, setupFiles: ["react-app-polyfill/jsdom", "jest-canvas-mock"], - setupFilesAfterEnv: ["/src/setupTests.ts"], + setupFilesAfterEnv: ["/config/jest/setupTests.ts"], snapshotSerializers: ["@emotion/jest/serializer"], testEnvironment: "jsdom", testMatch: ["/{src,scripts}/**/*.{spec,test}.{js,jsx,ts,tsx}"], @@ -43,4 +43,5 @@ module.exports = { globals: { APP_VERSION: JSON.stringify(process.env.npm_package_version), }, + testTimeout: 30000, }; diff --git a/package.json b/package.json index 0550631033..26c9291491 100644 --- a/package.json +++ b/package.json @@ -151,10 +151,9 @@ "@storybook/react-vite": "7.0.21", "@storybook/testing-library": "0.2.0", "@styled/typescript-styled-plugin": "1.0.0", - "@testing-library/jest-dom": "5.16.4", - "@testing-library/react": "12.1.5", - "@testing-library/react-hooks": "8.0.0", - "@testing-library/user-event": "12.5.0", + "@testing-library/jest-dom": "6.1.2", + "@testing-library/react": "14.0.0", + "@testing-library/user-event": "14.4.3", "@types/jest": "29.4.0", "@types/js-cookie": "^3.0.2", "@types/lodash.debounce": "4.0.7", diff --git a/src/components/Breadcrumbs/Breadcrumbs.test.tsx b/src/components/Breadcrumbs/Breadcrumbs.test.tsx index 23e8f2a672..51ed1f2112 100644 --- a/src/components/Breadcrumbs/Breadcrumbs.test.tsx +++ b/src/components/Breadcrumbs/Breadcrumbs.test.tsx @@ -21,6 +21,7 @@ describe("breadcrumbs", () => { expect(screen.queryAllByDataCy("breadcrumb-chevron")).toHaveLength(1); }); it("breadcrumbs with long text should be collapsed and viewable with a tooltip", async () => { + const user = userEvent.setup(); const longMessage = "some really long string that could be a patch title"; const breadcrumbs = [{ text: longMessage }]; render(); @@ -29,31 +30,29 @@ describe("breadcrumbs", () => { expect( screen.getByText(trimStringFromMiddle(longMessage, 30)) ).toBeInTheDocument(); - userEvent.hover(screen.getByText(trimStringFromMiddle(longMessage, 30))); + await user.hover(screen.getByText(trimStringFromMiddle(longMessage, 30))); await waitFor(() => { expect(screen.getByDataCy("breadcrumb-tooltip")).toBeInTheDocument(); }); expect(screen.getByText(longMessage)).toBeInTheDocument(); }); it("should not display a tooltip if the text is short", async () => { + const user = userEvent.setup(); const shortMessage = "short"; const breadcrumbs = [{ text: shortMessage }]; render(); expect(screen.getByText(shortMessage)).toBeInTheDocument(); - userEvent.hover(screen.getByText(shortMessage)); - await waitFor(() => { - expect( - screen.queryByDataCy("breadcrumb-tooltip") - ).not.toBeInTheDocument(); - }); + await user.hover(screen.getByText(shortMessage)); + expect(screen.queryByDataCy("breadcrumb-tooltip")).not.toBeInTheDocument(); }); - it("clicking on a tooltip with a link and event handler should call the event", () => { + it("clicking on a tooltip with a link and event handler should call the event", async () => { + const user = userEvent.setup(); const onClick = jest.fn(); const breadcrumbs = [{ text: "test", onClick, to: "/" }]; render(); expect(screen.getByText("test")).toBeInTheDocument(); expect(screen.getByRole("link")).toHaveAttribute("href", "/"); - userEvent.click(screen.getByText("test")); + await user.click(screen.getByText("test")); expect(onClick).toHaveBeenCalledTimes(1); }); }); diff --git a/src/components/CommitChartLabel/CommitChartLabel.test.tsx b/src/components/CommitChartLabel/CommitChartLabel.test.tsx index d2e94dcec5..b5db97b803 100644 --- a/src/components/CommitChartLabel/CommitChartLabel.test.tsx +++ b/src/components/CommitChartLabel/CommitChartLabel.test.tsx @@ -24,7 +24,8 @@ describe("commitChartLabel", () => { it("displays author, githash and createTime", () => { renderWithRouterMatch(); expect(screen.queryByDataCy("commit-label")).toHaveTextContent( - "4137c33 Jun 16, 2021, 11:38 PM Mohamed Khelif" + "4137c33 Jun 16, 2021, 11:38 PM Mohamed Khelif -SERVER-57332 Create skeleton Internal" + + "Git Tags: v1.2.3, v1.2.3-rc0" ); }); @@ -50,7 +51,8 @@ describe("commitChartLabel", () => { renderWithRouterMatch(); expect(screen.getByText("more")).toBeInTheDocument(); expect(screen.queryByDataCy("commit-label")).toHaveTextContent( - "4137c33 Jun 16, 2021, 11:38 PM Mohamed Khelif -SERVER-57332 Create skeleton Internal...more" + "4137c33 Jun 16, 2021, 11:38 PM Mohamed Khelif -SERVER-57332 Create skeleton Internal...more" + + "Git Tags: v1.2.3, v1.2.3-rc0" ); }); @@ -62,11 +64,11 @@ describe("commitChartLabel", () => { }); it("clicking on the 'more' button should open a tooltip containing commit message", async () => { + const user = userEvent.setup(); renderWithRouterMatch(); expect(screen.queryByDataCy("long-commit-message-tooltip")).toBeNull(); - userEvent.click(screen.queryByText("more")); - + await user.click(screen.queryByText("more")); await waitFor(() => { expect( screen.getByDataCy("long-commit-message-tooltip") diff --git a/src/components/Dropdown/Dropdown.test.tsx b/src/components/Dropdown/Dropdown.test.tsx index ad7a91e56b..641451d315 100644 --- a/src/components/Dropdown/Dropdown.test.tsx +++ b/src/components/Dropdown/Dropdown.test.tsx @@ -9,33 +9,39 @@ describe("dropdown", () => { expect(screen.queryByText("Some Children")).not.toBeInTheDocument(); }); - it("clicking on the button opens and closes the dropdown", () => { + it("clicking on the button opens and closes the dropdown", async () => { + const user = userEvent.setup(); render( {children()} ); - expect(screen.getByText("Some Button")).toBeInTheDocument(); + const button = screen.getByRole("button", { name: "Some Button" }); + expect(button).toBeInTheDocument(); expect(screen.queryByText("Some Children")).not.toBeInTheDocument(); - userEvent.click(screen.queryByText("Some Button")); + await user.click(button); expect(screen.getByText("Some Children")).toBeInTheDocument(); - userEvent.click(screen.queryByText("Some Button")); + await user.click(button); expect(screen.queryByText("Some Children")).not.toBeInTheDocument(); }); - it("clicking on the dropdown contents should not close the dropdown", () => { + it("clicking on the dropdown contents should not close the dropdown", async () => { + const user = userEvent.setup(); render( {children()} ); - expect(screen.getByText("Some Button")).toBeInTheDocument(); + const button = screen.getByRole("button", { name: "Some Button" }); + expect(button).toBeInTheDocument(); expect(screen.queryByText("Some Children")).not.toBeInTheDocument(); - userEvent.click(screen.queryByText("Some Button")); + await user.click(button); expect(screen.getByText("Some Children")).toBeInTheDocument(); - userEvent.click(screen.queryByText("Some Children")); + await user.click(screen.queryByText("Some Children")); expect(screen.getByText("Some Children")).toBeInTheDocument(); }); - it("clicking outside the button and dropdown closes the dropdown", () => { + it("clicking outside the button and dropdown closes the dropdown", async () => { + const user = userEvent.setup(); render( {children()} ); - expect(screen.getByText("Some Button")).toBeInTheDocument(); + const button = screen.getByRole("button", { name: "Some Button" }); + expect(button).toBeInTheDocument(); expect(screen.queryByText("Some Children")).not.toBeInTheDocument(); - userEvent.click(screen.queryByText("Some Button")); + await user.click(button); expect(screen.getByText("Some Children")).toBeInTheDocument(); - userEvent.click(document.body); + await user.click(document.body); expect(screen.queryByText("Some Children")).not.toBeInTheDocument(); }); diff --git a/src/components/EditableTagField/EditableTagField.test.tsx b/src/components/EditableTagField/EditableTagField.test.tsx index 95d834efef..4f1090ed77 100644 --- a/src/components/EditableTagField/EditableTagField.test.tsx +++ b/src/components/EditableTagField/EditableTagField.test.tsx @@ -42,6 +42,7 @@ describe("editableTagField", () => { data = x; }); + const user = userEvent.setup(); render( { expect(data).toStrictEqual(defaultData); expect(screen.queryAllByDataCy("user-tag-trash-icon")[0]).toBeVisible(); - userEvent.clear(screen.queryAllByDataCy("user-tag-value-field")[0]); - userEvent.type( + await user.clear(screen.queryAllByDataCy("user-tag-value-field")[0]); + await user.type( screen.queryAllByDataCy("user-tag-value-field")[0], "new value" ); expect(screen.queryAllByDataCy("user-tag-edit-icon")[0]).toBeVisible(); - userEvent.click(screen.queryAllByDataCy("user-tag-edit-icon")[0]); + await user.click(screen.queryAllByDataCy("user-tag-edit-icon")[0]); expect(updateData).toHaveBeenCalledWith([ { key: "keyA", value: "new value" }, @@ -78,6 +79,7 @@ describe("editableTagField", () => { data = x; }); + const user = userEvent.setup(); render( { expect(data).toStrictEqual(defaultData); expect(screen.queryAllByDataCy("user-tag-trash-icon")[0]).toBeVisible(); - userEvent.click(screen.queryAllByDataCy("user-tag-trash-icon")[0]); + await user.click(screen.queryAllByDataCy("user-tag-trash-icon")[0]); expect(updateData).toHaveBeenCalledWith([...defaultData.slice(1, 3)]); expect(data).toStrictEqual([...defaultData.slice(1, 3)]); @@ -102,6 +104,7 @@ describe("editableTagField", () => { data = x; }); + const user = userEvent.setup(); render( { expect(data).toStrictEqual(defaultData); expect(screen.queryAllByDataCy("user-tag-trash-icon")[0]).toBeVisible(); - userEvent.clear(screen.queryAllByDataCy("user-tag-key-field")[0]); - userEvent.type(screen.queryAllByDataCy("user-tag-key-field")[0], "new key"); + await user.clear(screen.queryAllByDataCy("user-tag-key-field")[0]); + await user.type( + screen.queryAllByDataCy("user-tag-key-field")[0], + "new key" + ); expect(screen.queryAllByDataCy("user-tag-edit-icon")[0]).toBeVisible(); - userEvent.click(screen.queryAllByDataCy("user-tag-edit-icon")[0]); + await user.click(screen.queryAllByDataCy("user-tag-edit-icon")[0]); expect(updateData).toHaveBeenCalledWith([ { ...defaultData[0], key: "new key" }, @@ -135,6 +141,7 @@ describe("editableTagField", () => { data = x; }); + const user = userEvent.setup(); render( { expect(screen.queryAllByDataCy("user-tag-row")).toHaveLength(3); expect(screen.queryByDataCy("add-tag-button")).toBeVisible(); - userEvent.click(screen.queryByDataCy("add-tag-button")); + await user.click(screen.queryByDataCy("add-tag-button")); expect(screen.queryByDataCy("add-tag-button")).toBeNull(); expect(screen.queryAllByDataCy("user-tag-trash-icon")[3]).toBeVisible(); expect(screen.queryAllByDataCy("user-tag-row")).toHaveLength(4); - userEvent.clear(screen.queryAllByDataCy("user-tag-key-field")[3]); - userEvent.type(screen.queryAllByDataCy("user-tag-key-field")[3], "new key"); + await user.clear(screen.queryAllByDataCy("user-tag-key-field")[3]); + await user.type( + screen.queryAllByDataCy("user-tag-key-field")[3], + "new key" + ); - userEvent.clear(screen.queryAllByDataCy("user-tag-value-field")[3]); - userEvent.type( + await user.clear(screen.queryAllByDataCy("user-tag-value-field")[3]); + await user.type( screen.queryAllByDataCy("user-tag-value-field")[3], "new value" ); expect(screen.queryAllByDataCy("user-tag-edit-icon")).toHaveLength(1); - userEvent.click(screen.queryAllByDataCy("user-tag-edit-icon")[0]); + await user.click(screen.queryAllByDataCy("user-tag-edit-icon")[0]); expect(updateData).toHaveBeenCalledTimes(1); expect(data).toStrictEqual([ diff --git a/src/components/FilterBadges/FilterBadges.test.tsx b/src/components/FilterBadges/FilterBadges.test.tsx index 5302ef5b73..e6215326b4 100644 --- a/src/components/FilterBadges/FilterBadges.test.tsx +++ b/src/components/FilterBadges/FilterBadges.test.tsx @@ -12,13 +12,11 @@ describe("filterBadges", () => { }); it("should render badges if there are some passed in", () => { - const onRemove = jest.fn(); - const onClearAll = jest.fn(); render( ); expect(screen.queryAllByDataCy("filter-badge")).toHaveLength(1); @@ -26,16 +24,14 @@ describe("filterBadges", () => { }); it("should render a badge for each key/value pair passed in", () => { - const onRemove = jest.fn(); - const onClearAll = jest.fn(); render( ); expect(screen.queryAllByDataCy("filter-badge")).toHaveLength(2); @@ -44,8 +40,6 @@ describe("filterBadges", () => { }); it("only renders badges up to the limit", () => { - const onRemove = jest.fn(); - const onClearAll = jest.fn(); render( { { key: "test9", value: "value9" }, { key: "test10", value: "value10" }, ]} - onRemove={onRemove} - onClearAll={onClearAll} + onRemove={jest.fn()} + onClearAll={jest.fn()} /> ); expect(screen.queryAllByDataCy("filter-badge")).toHaveLength(8); @@ -70,9 +64,8 @@ describe("filterBadges", () => { expect(screen.getByText("see 2 more")).toBeInTheDocument(); }); - it("clicking see more should display a modal with all of the badges", () => { - const onRemove = jest.fn(); - const onClearAll = jest.fn(); + it("clicking see more should display a modal with all of the badges", async () => { + const user = userEvent.setup(); render( { { key: "test9", value: "value9" }, { key: "test10", value: "value10" }, ]} - onRemove={onRemove} - onClearAll={onClearAll} + onRemove={jest.fn()} + onClearAll={jest.fn()} /> ); - userEvent.click(screen.queryByText("see 2 more")); + await user.click(screen.queryByText("see 2 more")); expect(screen.getByDataCy("see-more-modal")).toBeInTheDocument(); expect( within(screen.queryByDataCy("see-more-modal")).queryAllByDataCy( @@ -107,8 +100,8 @@ describe("filterBadges", () => { } }); - it("clicking clear all should call the clear all callback", () => { - const onRemove = jest.fn(); + it("clicking clear all should call the clear all callback", async () => { + const user = userEvent.setup(); const onClearAll = jest.fn(); render( { { key: "test9", value: "value9" }, { key: "test10", value: "value10" }, ]} - onRemove={onRemove} + onRemove={jest.fn()} onClearAll={onClearAll} /> ); - userEvent.click(screen.queryByText("CLEAR ALL FILTERS")); + await user.click(screen.getByRole("button", { name: "CLEAR ALL FILTERS" })); expect(onClearAll).toHaveBeenCalledTimes(1); }); - it("clicking a badge should call the remove callback", () => { + it("clicking a badge should call the remove callback", async () => { + const user = userEvent.setup(); const onRemove = jest.fn(); - const onClearAll = jest.fn(); render( { { key: "test10", value: "value10" }, ]} onRemove={onRemove} - onClearAll={onClearAll} + onClearAll={jest.fn()} /> ); const closeBadge = screen.queryAllByDataCy("close-badge")[0]; expect(closeBadge).toBeInTheDocument(); - userEvent.click(closeBadge); + await user.click(closeBadge); expect(onRemove).toHaveBeenCalledWith({ key: "test1", value: "value1" }); }); it("should truncate a badge value if it is too long", async () => { - const onRemove = jest.fn(); - const onClearAll = jest.fn(); + const user = userEvent.setup(); const longName = "this is a really long name that should be truncated"; render( ); const truncatedBadge = screen.queryByDataCy("filter-badge"); expect(truncatedBadge).toBeInTheDocument(); expect(truncatedBadge).not.toHaveTextContent(longName); - userEvent.hover(truncatedBadge); + await user.hover(truncatedBadge); await waitFor(() => { expect(screen.queryByText(longName)).toBeVisible(); }); diff --git a/src/components/FilterBadges/useFilterBadgeQueryParams.test.tsx b/src/components/FilterBadges/useFilterBadgeQueryParams.test.tsx index 9f8b13da3d..dc4ab3b1e3 100644 --- a/src/components/FilterBadges/useFilterBadgeQueryParams.test.tsx +++ b/src/components/FilterBadges/useFilterBadgeQueryParams.test.tsx @@ -58,7 +58,8 @@ describe("filterBadges - queryParams", () => { expect(badges[1]).toHaveTextContent("tests: test1"); }); - it("closing out a badge should remove it from the url", () => { + it("closing out a badge should remove it from the url", async () => { + const user = userEvent.setup(); const { router } = render(, { route: "/commits/evergreen?buildVariants=variant1", path: "/commits/:projectId", @@ -68,13 +69,14 @@ describe("filterBadges - queryParams", () => { expect(badge).toHaveTextContent("buildVariants: variant1"); const closeBadge = screen.queryByDataCy("close-badge"); expect(closeBadge).toBeInTheDocument(); - userEvent.click(closeBadge); + await user.click(closeBadge); expect(screen.queryByDataCy("filter-badge")).toBeNull(); expect(router.state.location.search).toBe(""); }); - it("should only remove one badge from the url if it is closed and more remain", () => { + it("should only remove one badge from the url if it is closed and more remain", async () => { + const user = userEvent.setup(); const { router } = render(, { route: "/commits/evergreen?buildVariants=variant1,variant2", path: "/commits/:projectId", @@ -84,7 +86,7 @@ describe("filterBadges - queryParams", () => { expect(badges).toHaveLength(2); expect(screen.getByText("buildVariants: variant1")).toBeInTheDocument(); const closeBadge = screen.queryAllByDataCy("close-badge"); - userEvent.click(closeBadge[0]); + await user.click(closeBadge[0]); badges = screen.queryAllByDataCy("filter-badge"); expect(badges).toHaveLength(1); expect(screen.queryByText("buildVariants: variant1")).toBeNull(); @@ -93,7 +95,8 @@ describe("filterBadges - queryParams", () => { expect(router.state.location.search).toBe("?buildVariants=variant2"); }); - it("should remove all badges when clicking on clear all button", () => { + it("should remove all badges when clicking on clear all button", async () => { + const user = userEvent.setup(); const { router } = render(, { route: "/commits/evergreen?buildVariants=variant1,variant2&tests=test1,test2", @@ -103,14 +106,15 @@ describe("filterBadges - queryParams", () => { let badges = screen.queryAllByDataCy("filter-badge"); expect(badges).toHaveLength(4); - userEvent.click(screen.queryByDataCy("clear-all-filters")); + await user.click(screen.queryByDataCy("clear-all-filters")); badges = screen.queryAllByDataCy("filter-badge"); expect(badges).toHaveLength(0); expect(router.state.location.search).toBe(""); }); - it("should only remove query params for displayable badges when clear all is pressed", () => { + it("should only remove query params for displayable badges when clear all is pressed", async () => { + const user = userEvent.setup(); const { router } = render(, { route: "/commits/evergreen?buildVariants=variant1,variant2&tests=test1,test2¬Related=notRelated", @@ -120,7 +124,7 @@ describe("filterBadges - queryParams", () => { let badges = screen.queryAllByDataCy("filter-badge"); expect(badges).toHaveLength(4); - userEvent.click(screen.queryByDataCy("clear-all-filters")); + await user.click(screen.queryByDataCy("clear-all-filters")); badges = screen.queryAllByDataCy("filter-badge"); expect(badges).toHaveLength(0); diff --git a/src/components/GroupedTaskStatusBadge/GroupedTaskStatusBadge.test.tsx b/src/components/GroupedTaskStatusBadge/GroupedTaskStatusBadge.test.tsx index 87d1d58198..eab9fd0eb5 100644 --- a/src/components/GroupedTaskStatusBadge/GroupedTaskStatusBadge.test.tsx +++ b/src/components/GroupedTaskStatusBadge/GroupedTaskStatusBadge.test.tsx @@ -9,7 +9,10 @@ import { TaskStatus } from "types/task"; import { GroupedTaskStatusBadge } from "."; describe("groupedTaskStatusBadgeIcon", () => { - it("clicking on badge performs an action", () => { + const versionId = "version1"; + + it("clicking on badge performs an action", async () => { + const user = userEvent.setup(); const onClick = jest.fn(); render( { ); const badge = screen.queryByDataCy("grouped-task-status-badge"); expect(badge).toBeInTheDocument(); - userEvent.click(badge); + await user.click(badge); expect(onClick).toHaveBeenCalledWith(); }); @@ -58,6 +61,7 @@ describe("groupedTaskStatusBadgeIcon", () => { }); it("badge should show tooltip when status counts is provided", async () => { + const user = userEvent.setup(); const statusCounts = { started: 30, failed: 15, @@ -76,18 +80,12 @@ describe("groupedTaskStatusBadgeIcon", () => { screen.queryByDataCy("grouped-task-status-badge-tooltip") ).toBeNull(); }); - userEvent.hover(screen.queryByDataCy("grouped-task-status-badge")); - + await user.hover(screen.queryByDataCy("grouped-task-status-badge")); await waitFor(() => { expect( screen.getByDataCy("grouped-task-status-badge-tooltip") ).toBeInTheDocument(); }); - await waitFor(() => { - expect( - screen.queryByDataCy("grouped-task-status-badge-tooltip") - ).toBeVisible(); - }); expect(screen.queryByText("30")).toBeVisible(); expect(screen.queryByText("Running")).toBeVisible(); expect(screen.queryByText("5")).toBeVisible(); @@ -95,5 +93,4 @@ describe("groupedTaskStatusBadgeIcon", () => { expect(screen.queryByText("15")).toBeVisible(); expect(screen.queryByText("Failed")).toBeVisible(); }); - const versionId = "version1"; }); diff --git a/src/components/Header/NavDropdown/NavDropdown.test.tsx b/src/components/Header/NavDropdown/NavDropdown.test.tsx index f74a5b96c4..d1cfbfb907 100644 --- a/src/components/Header/NavDropdown/NavDropdown.test.tsx +++ b/src/components/Header/NavDropdown/NavDropdown.test.tsx @@ -1,4 +1,4 @@ -import { screen, renderWithRouterMatch as render, waitFor } from "test_utils"; +import { screen, renderWithRouterMatch as render, userEvent } from "test_utils"; import { NavDropdown } from "."; const menuItems = [ @@ -20,19 +20,17 @@ describe("navDropdown", () => { expect(screen.getByText("Dropdown")).toBeInTheDocument(); }); it("opening the dropdown renders all of the buttons", async () => { + const user = userEvent.setup(); render(); - screen.getByText("Dropdown").click(); - await waitFor(() => { - expect(screen.getByText("Item 1")).toBeInTheDocument(); - }); + await user.click(screen.getByText("Dropdown")); + expect(screen.getByText("Item 1")).toBeInTheDocument(); expect(screen.getByText("Item 2")).toBeInTheDocument(); }); it("should link to both router and non-router links", async () => { + const user = userEvent.setup(); render(); - screen.getByText("Dropdown").click(); - await waitFor(() => { - expect(screen.getByText("Item 1")).toBeInTheDocument(); - }); + await user.click(screen.getByText("Dropdown")); + expect(screen.getByText("Item 1")).toBeInTheDocument(); expect(screen.getByText("Item 1").closest("a")).toHaveAttribute( "href", "/item1" @@ -43,6 +41,7 @@ describe("navDropdown", () => { ); }); it("clicking on a link triggers a callback", async () => { + const user = userEvent.setup(); const mockCallback = jest.fn(); render( { ]} /> ); - screen.getByText("Dropdown").click(); - await waitFor(() => { - expect(screen.getByText("Item 1")).toBeInTheDocument(); - }); - screen.getByText("Item 1").click(); + await user.click(screen.getByText("Dropdown")); + expect(screen.getByText("Item 1")).toBeInTheDocument(); + await user.click(screen.getByText("Item 1")); expect(mockCallback).toHaveBeenCalledWith(); }); }); diff --git a/src/components/HistoryTable/Cell/Cell.test.tsx b/src/components/HistoryTable/Cell/Cell.test.tsx index c70d7ddec1..0217c95474 100644 --- a/src/components/HistoryTable/Cell/Cell.test.tsx +++ b/src/components/HistoryTable/Cell/Cell.test.tsx @@ -74,6 +74,7 @@ describe("taskCell", () => { }); it("should have a tooltip on hover with failing tests when they are supplied", async () => { + const user = userEvent.setup(); render( { loading={false} /> ); - userEvent.hover(screen.queryByDataCy("history-table-icon")); + await user.hover(screen.queryByDataCy("history-table-icon")); await screen.findByText("some-test"); expect(screen.getByDataCy("test-tooltip")).toBeInTheDocument(); expect(screen.getByText("some-test")).toBeInTheDocument(); diff --git a/src/components/HistoryTable/HistoryTableContext.test.tsx b/src/components/HistoryTable/HistoryTableContext.test.tsx index f39fca3b9b..7456505433 100644 --- a/src/components/HistoryTable/HistoryTableContext.test.tsx +++ b/src/components/HistoryTable/HistoryTableContext.test.tsx @@ -1,4 +1,4 @@ -import { renderHook, act } from "@testing-library/react-hooks"; +import { renderHook, act } from "test_utils"; import { HistoryTableProvider, useHistoryTable } from "./HistoryTableContext"; import { columns, mainlineCommitData } from "./testData"; import { rowType, CommitRowType } from "./types"; diff --git a/src/components/HistoryTable/HistoryTableIcon/HistoryTableIcon.test.tsx b/src/components/HistoryTable/HistoryTableIcon/HistoryTableIcon.test.tsx index 329cc964fd..3328db1067 100644 --- a/src/components/HistoryTable/HistoryTableIcon/HistoryTableIcon.test.tsx +++ b/src/components/HistoryTable/HistoryTableIcon/HistoryTableIcon.test.tsx @@ -3,26 +3,29 @@ import { TaskStatus } from "types/task"; import { HistoryTableIcon } from "."; describe("historyTableIcon", () => { - it("clicking on the icon performs an action", () => { + it("clicking on the icon performs an action", async () => { + const user = userEvent.setup(); const onClick = jest.fn(); render( ); const icon = screen.queryByDataCy("history-table-icon"); expect(icon).toBeInTheDocument(); - userEvent.click(icon); + await user.click(icon); expect(onClick).toHaveBeenCalledWith(); }); - it("hovering over the icon when there no failing tests shouldn't open a tooltip", () => { + it("hovering over the icon when there no failing tests shouldn't open a tooltip", async () => { + const user = userEvent.setup(); render(); const icon = screen.queryByDataCy("history-table-icon"); expect(icon).toBeInTheDocument(); - userEvent.hover(icon); + await user.hover(icon); expect(screen.queryByText("test a")).not.toBeInTheDocument(); }); it("hovering over the icon when there are failing tests should open a tooltip", async () => { + const user = userEvent.setup(); render( { ); const icon = screen.queryByDataCy("history-table-icon"); expect(icon).toBeInTheDocument(); - userEvent.hover(icon); + await user.hover(icon); await waitFor(() => { expect(screen.queryByText("test a")).toBeVisible(); }); diff --git a/src/components/HistoryTable/HistoryTableRow/BaseRow/FoldedCommit/FoldedCommit.test.tsx b/src/components/HistoryTable/HistoryTableRow/BaseRow/FoldedCommit/FoldedCommit.test.tsx index c0742354ab..6739e181c7 100644 --- a/src/components/HistoryTable/HistoryTableRow/BaseRow/FoldedCommit/FoldedCommit.test.tsx +++ b/src/components/HistoryTable/HistoryTableRow/BaseRow/FoldedCommit/FoldedCommit.test.tsx @@ -1,6 +1,6 @@ import { MockedProvider } from "@apollo/client/testing"; import { getSpruceConfigMock } from "gql/mocks/getSpruceConfig"; -import { renderWithRouterMatch as render, screen } from "test_utils"; +import { renderWithRouterMatch as render, userEvent, screen } from "test_utils"; import FoldedCommit from "."; import { foldedCommitData } from "./testData"; @@ -28,7 +28,7 @@ describe("foldedCommit", () => { expect(screen.queryByText("Collapse 5 inactive")).toBeNull(); }); - it("can be expanded to show all of the commits", () => { + it("can be expanded to show all of the commits", async () => { const data = { ...foldedCommitData, }; @@ -36,6 +36,7 @@ describe("foldedCommit", () => { data.expanded = expanded; }); + const user = userEvent.setup(); render( { /> ); - screen.queryByText("Expand 5 inactive").click(); + await user.click(screen.queryByText("Expand 5 inactive")); expect(screen.queryByText("Expand 5 inactive")).toBeNull(); expect(screen.getByText("Collapse 5 inactive")).toBeInTheDocument(); expect(onToggleFoldedCommit).toHaveBeenCalledWith({ diff --git a/src/components/HistoryTable/HistoryTableTestSearch/HistoryTableTestSearch.test.tsx b/src/components/HistoryTable/HistoryTableTestSearch/HistoryTableTestSearch.test.tsx index a0aa9e5c19..0c9db75e37 100644 --- a/src/components/HistoryTable/HistoryTableTestSearch/HistoryTableTestSearch.test.tsx +++ b/src/components/HistoryTable/HistoryTableTestSearch/HistoryTableTestSearch.test.tsx @@ -15,7 +15,8 @@ describe("historyTableTestSearch", () => { expect(input).toHaveValue(""); }); - it("should clear input when a value is submitted", () => { + it("should clear input when a value is submitted", async () => { + const user = userEvent.setup(); render(, { route: `/variant-history/evergreen/lint`, path: "/variant-history/:projectId/:variantName", @@ -25,13 +26,14 @@ describe("historyTableTestSearch", () => { ) as HTMLInputElement; expect(input).toHaveValue(""); - userEvent.type(input, "some-test-name"); + await user.type(input, "some-test-name"); expect(input).toHaveValue("some-test-name"); - userEvent.type(input, "{enter}"); + await user.type(input, "{enter}"); expect(input).toHaveValue(""); }); - it("should add input query params to the url", () => { + it("should add input query params to the url", async () => { + const user = userEvent.setup(); const { router } = render(, { route: `/variant-history/evergreen/lint`, path: "/variant-history/:projectId/:variantName", @@ -42,13 +44,14 @@ describe("historyTableTestSearch", () => { // FAILED TEST expect(input).toHaveValue(""); - userEvent.type(input, "some-test-name"); + await user.type(input, "some-test-name"); expect(input).toHaveValue("some-test-name"); - userEvent.type(input, "{enter}"); + await user.type(input, "{enter}"); expect(router.state.location.search).toBe(`?failed=some-test-name`); }); - it("should add multiple input filters to the same key as query params", () => { + it("should add multiple input filters to the same key as query params", async () => { + const user = userEvent.setup(); const { router } = render(, { route: `/variant-history/evergreen/lint`, path: "/variant-history/:projectId/:variantName", @@ -57,18 +60,19 @@ describe("historyTableTestSearch", () => { "Search test name regex" ) as HTMLInputElement; expect(input).toHaveValue(""); - userEvent.type(input, "some-test-name"); + await user.type(input, "some-test-name"); expect(input).toHaveValue("some-test-name"); - userEvent.type(input, "{enter}"); - userEvent.type(input, "some-other-test-name"); + await user.type(input, "{enter}"); + await user.type(input, "some-other-test-name"); expect(input).toHaveValue("some-other-test-name"); - userEvent.type(input, "{enter}"); + await user.type(input, "{enter}"); expect(router.state.location.search).toBe( `?failed=some-test-name,some-other-test-name` ); }); - it("should not allow duplicate input filters for the same key as query params", () => { + it("should not allow duplicate input filters for the same key as query params", async () => { + const user = userEvent.setup(); const { router } = render(, { route: `/variant-history/evergreen/lint`, path: "/variant-history/:projectId/:variantName", @@ -77,12 +81,12 @@ describe("historyTableTestSearch", () => { "Search test name regex" ) as HTMLInputElement; expect(input).toHaveValue(""); - userEvent.type(input, "some-test-name"); + await user.type(input, "some-test-name"); expect(input).toHaveValue("some-test-name"); - userEvent.type(input, "{enter}"); - userEvent.type(input, "some-test-name"); + await user.type(input, "{enter}"); + await user.type(input, "some-test-name"); expect(input).toHaveValue("some-test-name"); - userEvent.type(input, "{enter}"); + await user.type(input, "{enter}"); expect(router.state.location.search).toBe(`?failed=some-test-name`); }); }); diff --git a/src/components/HistoryTable/hooks/useTestResults.test.tsx b/src/components/HistoryTable/hooks/useTestResults.test.tsx index 742ad2187f..bd7da8c9d5 100644 --- a/src/components/HistoryTable/hooks/useTestResults.test.tsx +++ b/src/components/HistoryTable/hooks/useTestResults.test.tsx @@ -1,9 +1,9 @@ -import { act, renderHook } from "@testing-library/react-hooks"; import { TaskTestSampleQuery, TaskTestSampleQueryVariables, } from "gql/generated/types"; import { GET_TASK_TEST_SAMPLE } from "gql/queries"; +import { act, renderHook, waitFor } from "test_utils"; import { ApolloMock } from "types/gql"; import { TestStatus } from "types/history"; import { useHistoryTable } from "../HistoryTableContext"; @@ -65,12 +65,9 @@ describe("useTestResults", () => { }); it("should return all matching test results when there are no filters applied and the row is a commit", async () => { - const { result, waitForNextUpdate } = renderHook( - () => useMergedTestHook(1), - { - wrapper: ({ children }) => ProviderWrapper({ children, mocks }), - } - ); + const { result } = renderHook(() => useMergedTestHook(1), { + wrapper: ({ children }) => ProviderWrapper({ children, mocks }), + }); expect( result.current.hookResponse.getTaskMetadata( "evergreen_ubuntu1604_dist_d4cf298cf0b2536fb3bff875775b93a9ceafb75c_21_09_02_14_20_04" @@ -87,25 +84,24 @@ describe("useTestResults", () => { expect(result.current.historyTable.getItem(2)).toMatchObject({ type: rowType.COMMIT, }); - await waitForNextUpdate(); - const response = result.current.hookResponse.getTaskMetadata( - "evergreen_ubuntu1604_dist_d4cf298cf0b2536fb3bff875775b93a9ceafb75c_21_09_02_14_20_04" - ); - expect(response).toMatchObject({ - inactive: false, - label: "", - failingTests: ["TestJiraIntegration"], - loading: false, + await waitFor(() => { + expect( + result.current.hookResponse.getTaskMetadata( + "evergreen_ubuntu1604_dist_d4cf298cf0b2536fb3bff875775b93a9ceafb75c_21_09_02_14_20_04" + ) + ).toMatchObject({ + inactive: false, + label: "", + failingTests: ["TestJiraIntegration"], + loading: false, + }); }); }); it("should return all matching test results when there are matching filters applied and the row is a commit", async () => { - const { result, waitForNextUpdate } = renderHook( - () => useMergedTestHook(1), - { - wrapper: ({ children }) => ProviderWrapper({ children, mocks }), - } - ); + const { result } = renderHook(() => useMergedTestHook(1), { + wrapper: ({ children }) => ProviderWrapper({ children, mocks }), + }); expect( result.current.hookResponse.getTaskMetadata( "evergreen_ubuntu1604_dist_d4cf298cf0b2536fb3bff875775b93a9ceafb75c_21_09_02_14_20_04" @@ -128,26 +124,24 @@ describe("useTestResults", () => { { testName: "TestJiraIntegration", testStatus: TestStatus.Failed }, ]); }); - await waitForNextUpdate(); - expect( - result.current.hookResponse.getTaskMetadata( - "evergreen_ubuntu1604_dist_d4cf298cf0b2536fb3bff875775b93a9ceafb75c_21_09_02_14_20_04" - ) - ).toMatchObject({ - inactive: false, - label: "1 / 1 Failing Tests", - failingTests: ["TestJiraIntegration"], - loading: false, + await waitFor(() => { + expect( + result.current.hookResponse.getTaskMetadata( + "evergreen_ubuntu1604_dist_d4cf298cf0b2536fb3bff875775b93a9ceafb75c_21_09_02_14_20_04" + ) + ).toMatchObject({ + inactive: false, + label: "1 / 1 Failing Tests", + failingTests: ["TestJiraIntegration"], + loading: false, + }); }); }); it("should not return matching test results when there are non matching filters applied and the row is a commit", async () => { - const { result, waitForNextUpdate } = renderHook( - () => useMergedTestHook(1), - { - wrapper: ({ children }) => ProviderWrapper({ children, mocks }), - } - ); + const { result } = renderHook(() => useMergedTestHook(1), { + wrapper: ({ children }) => ProviderWrapper({ children, mocks }), + }); expect( result.current.hookResponse.getTaskMetadata( "evergreen_ubuntu1604_dist_d4cf298cf0b2536fb3bff875775b93a9ceafb75c_21_09_02_14_20_04" @@ -169,15 +163,16 @@ describe("useTestResults", () => { { testName: "NotARealTest", testStatus: TestStatus.Failed }, ]); }); - await waitForNextUpdate(); - expect( - result.current.hookResponse.getTaskMetadata( - "evergreen_ubuntu1604_dist_d4cf298cf0b2536fb3bff875775b93a9ceafb75c_21_09_02_14_20_04" - ) - ).toMatchObject({ - inactive: true, - label: "0 / 1 Failing Tests", - failingTests: [], + await waitFor(() => { + expect( + result.current.hookResponse.getTaskMetadata( + "evergreen_ubuntu1604_dist_d4cf298cf0b2536fb3bff875775b93a9ceafb75c_21_09_02_14_20_04" + ) + ).toMatchObject({ + inactive: true, + label: "0 / 1 Failing Tests", + failingTests: [], + }); }); }); }); diff --git a/src/components/PageSizeSelector/PageSizeSelector.test.tsx b/src/components/PageSizeSelector/PageSizeSelector.test.tsx index 5e0844c2fb..8b157d2602 100644 --- a/src/components/PageSizeSelector/PageSizeSelector.test.tsx +++ b/src/components/PageSizeSelector/PageSizeSelector.test.tsx @@ -1,8 +1,9 @@ -import { render, screen, userEvent, waitFor } from "test_utils"; +import { render, screen, userEvent } from "test_utils"; import PageSizeSelector from "."; describe("pageSizeSelector", () => { it("selecting page size should call onChange prop", async () => { + const user = userEvent.setup(); const onChange = jest.fn(); render( { onChange={onChange} /> ); - userEvent.click(screen.queryByText("10 / page")); - await waitFor(() => { - expect(screen.queryByText("20 / page")).toBeVisible(); - }); - userEvent.click(screen.queryByText("20 / page")); + await user.click(screen.getByRole("button", { name: "10 / page" })); + expect(screen.queryByText("20 / page")).toBeVisible(); + await user.click(screen.queryByText("20 / page")); expect(onChange).toHaveBeenCalledWith(20); }); }); diff --git a/src/components/Pagination/Pagination.test.tsx b/src/components/Pagination/Pagination.test.tsx index 534c73084f..cc7e22b6d0 100644 --- a/src/components/Pagination/Pagination.test.tsx +++ b/src/components/Pagination/Pagination.test.tsx @@ -1,4 +1,4 @@ -import { renderWithRouterMatch, screen } from "test_utils"; +import { renderWithRouterMatch, screen, userEvent } from "test_utils"; import Pagination from "."; describe("pagination", () => { @@ -34,26 +34,29 @@ describe("pagination", () => { "true" ); }); - it("paginating forward should update the url with the new page number by default", () => { + it("paginating forward should update the url with the new page number by default", async () => { + const user = userEvent.setup(); const { router } = renderWithRouterMatch( ); expect(router.state.location.search).toBe(""); - screen.getByDataCy("next-page-button").click(); + await user.click(screen.getByDataCy("next-page-button")); expect(router.state.location.search).toBe("?page=1"); }); - it("paginating backward should update the url with the new page number by default", () => { + it("paginating backward should update the url with the new page number by default", async () => { + const user = userEvent.setup(); const { router } = renderWithRouterMatch( ); expect(router.state.location.search).toBe(""); - screen.getByDataCy("prev-page-button").click(); + await user.click(screen.getByDataCy("prev-page-button")); expect(router.state.location.search).toBe("?page=0"); }); - it("should call the onChange callback when the page changes", () => { + it("should call the onChange callback when the page changes", async () => { + const user = userEvent.setup(); const onChange = jest.fn(); renderWithRouterMatch( { onChange={onChange} /> ); - screen.getByDataCy("next-page-button").click(); + await user.click(screen.getByDataCy("next-page-button")); expect(onChange).toHaveBeenCalledWith(1); }); it("should disable pagination if there is only one page", () => { diff --git a/src/components/Popconfirm/Popconfirm.test.tsx b/src/components/Popconfirm/Popconfirm.test.tsx index 11a76fb85d..4c6a3a7b51 100644 --- a/src/components/Popconfirm/Popconfirm.test.tsx +++ b/src/components/Popconfirm/Popconfirm.test.tsx @@ -1,9 +1,4 @@ -import { - renderWithRouterMatch as render, - screen, - userEvent, - waitFor, -} from "test_utils"; +import { renderWithRouterMatch as render, screen, userEvent } from "test_utils"; import Popconfirm from "."; describe("controlled popconfirm", () => { @@ -18,7 +13,8 @@ describe("controlled popconfirm", () => { expect(screen.getByRole("button", { name: "Cancel" })).toBeInTheDocument(); }); - it("pressing the Confirm button calls the onConfirm callback and closes the popconfirm", () => { + it("pressing the Confirm button calls the onConfirm callback and closes the popconfirm", async () => { + const user = userEvent.setup(); const onConfirm = jest.fn(); const setOpen = jest.fn(); render( @@ -26,13 +22,14 @@ describe("controlled popconfirm", () => {
hello
); - userEvent.click(screen.getByRole("button", { name: "Yes" })); + await user.click(screen.getByRole("button", { name: "Yes" })); expect(onConfirm).toHaveBeenCalledTimes(1); expect(setOpen).toHaveBeenCalledTimes(1); expect(setOpen).toHaveBeenCalledWith(false); }); - it("pressing the Cancel button calls the onClose callback and closes the popconfirm", () => { + it("pressing the Cancel button calls the onClose callback and closes the popconfirm", async () => { + const user = userEvent.setup(); const onClose = jest.fn(); const setOpen = jest.fn(); render( @@ -40,7 +37,7 @@ describe("controlled popconfirm", () => {
hello
); - userEvent.click(screen.getByRole("button", { name: "Cancel" })); + await user.click(screen.getByRole("button", { name: "Cancel" })); expect(onClose).toHaveBeenCalledTimes(1); expect(setOpen).toHaveBeenCalledTimes(1); expect(setOpen).toHaveBeenCalledWith(false); @@ -61,6 +58,7 @@ describe("controlled popconfirm", () => { describe("uncontrolled popconfirm", () => { it("uses a trigger to open and close the component", async () => { + const user = userEvent.setup(); const onClose = jest.fn(); render( {
hello
); - userEvent.click(screen.getByRole("button", { name: "Open" })); - await waitFor(() => { - expect(screen.getByText("hello")).toBeVisible(); - }); - userEvent.click(screen.getByRole("button", { name: "Open" })); + await user.click(screen.getByRole("button", { name: "Open" })); + expect(screen.getByText("hello")).toBeVisible(); + await user.click(screen.getByRole("button", { name: "Open" })); expect(onClose).toHaveBeenCalledTimes(1); expect(screen.getByText("hello")).not.toBeVisible(); }); diff --git a/src/components/ProjectSelect/ProjectSelect.test.tsx b/src/components/ProjectSelect/ProjectSelect.test.tsx index c8716dca94..bd83b7424e 100644 --- a/src/components/ProjectSelect/ProjectSelect.test.tsx +++ b/src/components/ProjectSelect/ProjectSelect.test.tsx @@ -35,6 +35,7 @@ describe("projectSelect", () => { }); it("should narrow down search results when filtering on projects", async () => { + const user = userEvent.setup(); const { Component } = RenderFakeToastContext( { expect(screen.getByDataCy("project-select")).toBeInTheDocument(); }); expect(screen.queryByDataCy("project-select-options")).toBeNull(); - userEvent.click(screen.queryByDataCy("project-select")); + await user.click(screen.queryByDataCy("project-select")); expect(screen.getByDataCy("project-select-options")).toBeInTheDocument(); let options = await screen.findAllByDataCy("project-display-name"); expect(options).toHaveLength(6); - userEvent.type( + await user.type( screen.queryByDataCy("project-select-search-input"), "logkeeper" ); @@ -63,6 +64,7 @@ describe("projectSelect", () => { }); it("should be possible to search for projects by a repo name, which should NOT be clickable", async () => { + const user = userEvent.setup(); const { Component } = RenderFakeToastContext( { expect(screen.getByDataCy("project-select")).toBeInTheDocument(); }); expect(screen.queryByDataCy("project-select-options")).toBeNull(); - userEvent.click(screen.queryByDataCy("project-select")); + await user.click(screen.queryByDataCy("project-select")); expect(screen.getByDataCy("project-select-options")).toBeInTheDocument(); - userEvent.type( + await user.type( screen.queryByDataCy("project-select-search-input"), "aaa/totally-different-name" ); @@ -113,6 +115,7 @@ describe("projectSelect", () => { }); it("should narrow down search results when filtering on projects", async () => { + const user = userEvent.setup(); const { Component } = RenderFakeToastContext( { expect(screen.getByDataCy("project-select")).toBeInTheDocument(); }); expect(screen.queryByDataCy("project-select-options")).toBeNull(); - userEvent.click(screen.queryByDataCy("project-select")); + await user.click(screen.queryByDataCy("project-select")); expect(screen.getByDataCy("project-select-options")).toBeInTheDocument(); let options = await screen.findAllByDataCy("project-display-name"); expect(options).toHaveLength(5); - userEvent.type( + await user.type( screen.queryByDataCy("project-select-search-input"), "evergreen" ); @@ -142,6 +145,7 @@ describe("projectSelect", () => { }); it("should be possible to search for projects by a repo name, which should be clickable", async () => { + const user = userEvent.setup(); const { Component } = RenderFakeToastContext( { expect(screen.getByDataCy("project-select")).toBeInTheDocument(); }); expect(screen.queryByDataCy("project-select-options")).toBeNull(); - userEvent.click(screen.queryByDataCy("project-select")); + await user.click(screen.queryByDataCy("project-select")); expect(screen.getByDataCy("project-select-options")).toBeInTheDocument(); - userEvent.type( + await user.type( screen.queryByDataCy("project-select-search-input"), "aaa/totally-different-name" ); @@ -175,6 +179,7 @@ describe("projectSelect", () => { }); it("shows favorited projects twice", async () => { + const user = userEvent.setup(); const { Component } = RenderFakeToastContext( { expect(screen.getByDataCy("project-select")).toBeInTheDocument(); }); expect(screen.queryByDataCy("project-select-options")).toBeNull(); - userEvent.click(screen.queryByDataCy("project-select")); + await user.click(screen.queryByDataCy("project-select")); expect(screen.getByDataCy("project-select-options")).toBeInTheDocument(); // Favorited projects should appear twice. expect(screen.getAllByText("logkeeper")).toHaveLength(2); }); it("shows disabled projects at the bottom of the list", async () => { + const user = userEvent.setup(); const { Component } = RenderFakeToastContext( { expect(screen.getByDataCy("project-select")).toBeInTheDocument(); }); expect(screen.queryByDataCy("project-select-options")).toBeNull(); - userEvent.click(screen.queryByDataCy("project-select")); + await user.click(screen.queryByDataCy("project-select")); expect(screen.getByDataCy("project-select-options")).toBeInTheDocument(); const options = await screen.findAllByDataCy("project-display-name"); @@ -223,6 +229,7 @@ describe("projectSelect", () => { }); it("does not show a heading for disabled projects when all projects are enabled", async () => { + const user = userEvent.setup(); const { Component } = RenderFakeToastContext( { expect(screen.getByDataCy("project-select")).toBeInTheDocument(); }); expect(screen.queryByDataCy("project-select-options")).toBeNull(); - userEvent.click(screen.queryByDataCy("project-select")); + await user.click(screen.queryByDataCy("project-select")); expect(screen.getByDataCy("project-select-options")).toBeInTheDocument(); const options = await screen.findAllByDataCy("project-display-name"); expect(options).toHaveLength(1); diff --git a/src/components/SearchableDropdown/SearchableDropdown.test.tsx b/src/components/SearchableDropdown/SearchableDropdown.test.tsx index 8f05cb517e..b0dac9c257 100644 --- a/src/components/SearchableDropdown/SearchableDropdown.test.tsx +++ b/src/components/SearchableDropdown/SearchableDropdown.test.tsx @@ -7,52 +7,51 @@ const RenderSearchableDropdown = ( describe("searchableDropdown", () => { it("sets the label to what ever the current value is", () => { - const onChange = jest.fn(); render( RenderSearchableDropdown({ value: "evergreen", - onChange, + onChange: jest.fn(), options: ["evergreen", "spruce"], }) ); expect(screen.getByText("evergreen")).toBeInTheDocument(); }); - it("should toggle dropdown when clicking on it", () => { - const onChange = jest.fn(); + it("should toggle dropdown when clicking on it", async () => { + const user = userEvent.setup(); render( RenderSearchableDropdown({ value: "evergreen", - onChange, + onChange: jest.fn(), options: ["evergreen", "spruce"], }) ); expect( screen.queryByDataCy("searchable-dropdown-options") ).not.toBeInTheDocument(); - userEvent.click(screen.queryByDataCy("searchable-dropdown")); + await user.click(screen.queryByDataCy("searchable-dropdown")); expect( screen.getByDataCy("searchable-dropdown-options") ).toBeInTheDocument(); - userEvent.click(screen.queryByDataCy("searchable-dropdown")); + await user.click(screen.queryByDataCy("searchable-dropdown")); expect( screen.queryByDataCy("searchable-dropdown-options") ).not.toBeInTheDocument(); }); - it("should narrow down search results when filtering", () => { - const onChange = jest.fn(); + it("should narrow down search results when filtering", async () => { + const user = userEvent.setup(); render( RenderSearchableDropdown({ value: "evergreen", - onChange, + onChange: jest.fn(), options: ["evergreen", "spruce"], }) ); expect( screen.queryByDataCy("searchable-dropdown-options") ).not.toBeInTheDocument(); - userEvent.click(screen.queryByDataCy("searchable-dropdown")); + await user.click(screen.queryByDataCy("searchable-dropdown")); expect( screen.getByDataCy("searchable-dropdown-options") ).toBeInTheDocument(); @@ -62,7 +61,7 @@ describe("searchableDropdown", () => { expect(screen.queryAllByDataCy("searchable-dropdown-option")).toHaveLength( 2 ); - userEvent.type( + await user.type( screen.queryByDataCy("searchable-dropdown-search-input"), "spru" ); @@ -71,31 +70,31 @@ describe("searchableDropdown", () => { ); }); - it("should reset the search input and options after SearchableDropdown closes", () => { - const onChange = jest.fn(); + it("should reset the search input and options after SearchableDropdown closes", async () => { + const user = userEvent.setup(); render( RenderSearchableDropdown({ value: "evergreen", - onChange, + onChange: jest.fn(), options: ["evergreen", "spruce"], }) ); // use text input to filter and click on document body (which closes the dropdown). - userEvent.click(screen.queryByDataCy("searchable-dropdown")); + await user.click(screen.queryByDataCy("searchable-dropdown")); expect(screen.queryAllByDataCy("searchable-dropdown-option")).toHaveLength( 2 ); - userEvent.type( + await user.type( screen.queryByDataCy("searchable-dropdown-search-input"), "spru" ); expect(screen.queryAllByDataCy("searchable-dropdown-option")).toHaveLength( 1 ); - userEvent.click(screen.queryByText("spruce")); + await user.click(screen.queryByText("spruce")); // when reopening the dropdown, the text input should be cleared and all options should be visible. - userEvent.click(screen.queryByDataCy("searchable-dropdown")); + await user.click(screen.queryByDataCy("searchable-dropdown")); expect(screen.queryAllByDataCy("searchable-dropdown-option")).toHaveLength( 2 ); @@ -104,25 +103,25 @@ describe("searchableDropdown", () => { ).toHaveValue(""); }); - it("should use custom search function when passed in", () => { - const onChange = jest.fn(); + it("should use custom search function when passed in", async () => { + const user = userEvent.setup(); const searchFunc = jest.fn((options, match) => options.filter((o) => o === match) ); render( RenderSearchableDropdown({ value: ["evergreen"], - onChange, + onChange: jest.fn(), options: ["evergreen", "spruce"], searchFunc, }) ); - userEvent.click(screen.queryByDataCy("searchable-dropdown")); + await user.click(screen.queryByDataCy("searchable-dropdown")); expect( screen.getByDataCy("searchable-dropdown-search-input") ).toBeInTheDocument(); - userEvent.type( + await user.type( screen.queryByDataCy("searchable-dropdown-search-input"), "spruce" ); @@ -134,7 +133,8 @@ describe("searchableDropdown", () => { }); describe("when multiselect == false", () => { - it("should call onChange when clicking on an option and should close the option list", () => { + it("should call onChange when clicking on an option and should close the option list", async () => { + const user = userEvent.setup(); const onChange = jest.fn(); const { rerender } = render( RenderSearchableDropdown({ @@ -146,11 +146,11 @@ describe("searchableDropdown", () => { expect( screen.queryByDataCy("searchable-dropdown-options") ).not.toBeInTheDocument(); - userEvent.click(screen.queryByDataCy("searchable-dropdown")); + await user.click(screen.queryByDataCy("searchable-dropdown")); expect( screen.getByDataCy("searchable-dropdown-options") ).toBeInTheDocument(); - userEvent.click(screen.queryByText("spruce")); + await user.click(screen.queryByText("spruce")); expect(onChange).toHaveBeenCalledWith("spruce"); expect( screen.queryByDataCy("searchable-dropdown-options") @@ -166,31 +166,31 @@ describe("searchableDropdown", () => { expect(screen.getByText("spruce")).toBeInTheDocument(); }); - it("should reset the search input and options after user selects an option", () => { - const onChange = jest.fn(); + it("should reset the search input and options after user selects an option", async () => { + const user = userEvent.setup(); render( RenderSearchableDropdown({ value: "evergreen", - onChange, + onChange: jest.fn(), options: ["evergreen", "spruce"], }) ); // use text input to filter and select an option. - userEvent.click(screen.queryByDataCy("searchable-dropdown")); + await user.click(screen.queryByDataCy("searchable-dropdown")); expect( screen.queryAllByDataCy("searchable-dropdown-option") ).toHaveLength(2); - userEvent.type( + await user.type( screen.queryByDataCy("searchable-dropdown-search-input"), "spru" ); expect( screen.queryAllByDataCy("searchable-dropdown-option") ).toHaveLength(1); - userEvent.click(screen.queryByText("spruce")); + await user.click(screen.queryByText("spruce")); // when reopening the dropdown, the text input should be cleared and all options should be visible. - userEvent.click(screen.queryByDataCy("searchable-dropdown")); + await user.click(screen.queryByDataCy("searchable-dropdown")); expect( screen.queryAllByDataCy("searchable-dropdown-option") ).toHaveLength(2); @@ -199,7 +199,8 @@ describe("searchableDropdown", () => { ).toHaveValue(""); }); - it("does not show checkmark next to the selected option", () => { + it("does not show checkmark next to the selected option", async () => { + const user = userEvent.setup(); render( RenderSearchableDropdown({ value: "evergreen", @@ -207,7 +208,7 @@ describe("searchableDropdown", () => { options: ["evergreen", "spruce"], }) ); - userEvent.click(screen.queryByDataCy("searchable-dropdown")); + await user.click(screen.queryByDataCy("searchable-dropdown")); expect( screen.queryAllByDataCy("searchable-dropdown-option") ).toHaveLength(2); @@ -216,7 +217,8 @@ describe("searchableDropdown", () => { }); describe("when multiselect == true", () => { - it("should call onChange when clicking on multiple options and shouldn't close the dropdown", () => { + it("should call onChange when clicking on multiple options and shouldn't close the dropdown", async () => { + const user = userEvent.setup(); const onChange = jest.fn(); const { rerender } = render( RenderSearchableDropdown({ @@ -229,11 +231,11 @@ describe("searchableDropdown", () => { expect( screen.queryByDataCy("searchable-dropdown-options") ).not.toBeInTheDocument(); - userEvent.click(screen.queryByDataCy("searchable-dropdown")); + await user.click(screen.queryByDataCy("searchable-dropdown")); expect( screen.getByDataCy("searchable-dropdown-options") ).toBeInTheDocument(); - userEvent.click(screen.queryByText("spruce")); + await user.click(screen.queryByText("spruce")); expect(onChange).toHaveBeenCalledWith(["spruce"]); rerender( @@ -256,33 +258,33 @@ describe("searchableDropdown", () => { allowMultiSelect: true, }) ); - userEvent.click(screen.queryByText("evergreen")); + await user.click(screen.queryByText("evergreen")); expect(onChange).toHaveBeenCalledWith(["spruce", "evergreen"]); }); - it("should NOT reset the search input and options after user selects an option", () => { - const onChange = jest.fn(); + it("should NOT reset the search input and options after user selects an option", async () => { + const user = userEvent.setup(); render( RenderSearchableDropdown({ value: "evergreen", - onChange, + onChange: jest.fn(), options: ["evergreen", "spruce", "sandbox"], allowMultiSelect: true, }) ); // use text input to filter and select an option. - userEvent.click(screen.queryByDataCy("searchable-dropdown")); + await user.click(screen.queryByDataCy("searchable-dropdown")); expect( screen.queryAllByDataCy("searchable-dropdown-option") ).toHaveLength(3); - userEvent.type( + await user.type( screen.queryByDataCy("searchable-dropdown-search-input"), "s" ); expect( screen.queryAllByDataCy("searchable-dropdown-option") ).toHaveLength(2); - userEvent.click(screen.queryByText("spruce")); + await user.click(screen.queryByText("spruce")); // the dropdown should not be closed and the search state should not be reset. expect( @@ -293,7 +295,8 @@ describe("searchableDropdown", () => { ).toHaveLength(2); }); - it("shows checkmark next to the selected option", () => { + it("shows checkmark next to the selected option", async () => { + const user = userEvent.setup(); render( RenderSearchableDropdown({ value: "evergreen", @@ -302,7 +305,7 @@ describe("searchableDropdown", () => { allowMultiSelect: true, }) ); - userEvent.click(screen.queryByDataCy("searchable-dropdown")); + await user.click(screen.queryByDataCy("searchable-dropdown")); expect( screen.queryAllByDataCy("searchable-dropdown-option") ).toHaveLength(2); @@ -311,12 +314,12 @@ describe("searchableDropdown", () => { }); describe("when using custom render options", () => { - it("should render custom options", () => { - const onChange = jest.fn(); + it("should render custom options", async () => { + const user = userEvent.setup(); render( RenderSearchableDropdown({ value: "evergreen", - onChange, + onChange: jest.fn(), options: [ { label: "Evergreen", @@ -338,14 +341,15 @@ describe("searchableDropdown", () => { ), }) ); - userEvent.click(screen.queryByDataCy("searchable-dropdown")); + await user.click(screen.queryByDataCy("searchable-dropdown")); expect(screen.getByText("Evergreen")).toBeInTheDocument(); expect(screen.getByText("Spruce")).toBeInTheDocument(); expect(screen.queryByText("Evergreen")).toBeInstanceOf(HTMLButtonElement); expect(screen.queryByText("Spruce")).toBeInstanceOf(HTMLButtonElement); }); - it("should be able to click on custom elements", () => { + it("should be able to click on custom elements", async () => { + const user = userEvent.setup(); const onChange = jest.fn(); render( RenderSearchableDropdown({ @@ -372,19 +376,18 @@ describe("searchableDropdown", () => { ), }) ); - userEvent.click(screen.queryByDataCy("searchable-dropdown")); + await user.click(screen.queryByDataCy("searchable-dropdown")); expect(screen.getByText("Spruce")).toBeInTheDocument(); - userEvent.click(screen.queryByText("Spruce")); + await user.click(screen.queryByText("Spruce")); expect(onChange).toHaveBeenCalledWith("spruce"); }); it("should render a custom button", () => { - const onChange = jest.fn(); render( RenderSearchableDropdown({ value: "evergreen", - onChange, + onChange: jest.fn(), options: ["evergreen", "spruce"], buttonRenderer: (option: string) => ( {option} diff --git a/src/components/SetPriority/SetPriority.test.tsx b/src/components/SetPriority/SetPriority.test.tsx index 85e5bcf7e6..c82b815a40 100644 --- a/src/components/SetPriority/SetPriority.test.tsx +++ b/src/components/SetPriority/SetPriority.test.tsx @@ -14,6 +14,7 @@ import SetPriority from "."; describe("setPriority", () => { describe("patch priority", () => { it("shows default message", async () => { + const user = userEvent.setup(); const { Component } = RenderFakeToastContext( @@ -21,18 +22,17 @@ describe("setPriority", () => { ); renderWithRouterMatch(); - userEvent.click(screen.queryByDataCy("prioritize-patch")); - await waitFor(() => { - expect( - screen.queryByDataCy("set-patch-priority-popconfirm") - ).toBeVisible(); - }); + await user.click(screen.queryByDataCy("prioritize-patch")); + expect( + screen.queryByDataCy("set-patch-priority-popconfirm") + ).toBeVisible(); expect(screen.queryByDataCy("priority-default-message")).toBeVisible(); - userEvent.type(screen.queryByDataCy("patch-priority-input"), "9"); + await user.type(screen.queryByDataCy("patch-priority-input"), "9"); expect(screen.queryByDataCy("priority-default-message")).toBeVisible(); }); it("shows warning message", async () => { + const user = userEvent.setup(); const { Component } = RenderFakeToastContext( @@ -40,18 +40,17 @@ describe("setPriority", () => { ); renderWithRouterMatch(); - userEvent.click(screen.queryByDataCy("prioritize-patch")); - await waitFor(() => { - expect( - screen.queryByDataCy("set-patch-priority-popconfirm") - ).toBeVisible(); - }); + await user.click(screen.queryByDataCy("prioritize-patch")); + expect( + screen.queryByDataCy("set-patch-priority-popconfirm") + ).toBeVisible(); expect(screen.queryByDataCy("priority-warning-message")).toBeNull(); - userEvent.type(screen.queryByDataCy("patch-priority-input"), "99"); + await user.type(screen.queryByDataCy("patch-priority-input"), "99"); expect(screen.queryByDataCy("priority-warning-message")).toBeVisible(); }); it("shows admin message", async () => { + const user = userEvent.setup(); const { Component } = RenderFakeToastContext( @@ -59,18 +58,17 @@ describe("setPriority", () => { ); renderWithRouterMatch(); - userEvent.click(screen.queryByDataCy("prioritize-patch")); - await waitFor(() => { - expect( - screen.queryByDataCy("set-patch-priority-popconfirm") - ).toBeVisible(); - }); + await user.click(screen.queryByDataCy("prioritize-patch")); + expect( + screen.queryByDataCy("set-patch-priority-popconfirm") + ).toBeVisible(); expect(screen.queryByDataCy("priority-admin-message")).toBeNull(); - userEvent.type(screen.queryByDataCy("patch-priority-input"), "999"); + await user.type(screen.queryByDataCy("patch-priority-input"), "999"); expect(screen.queryByDataCy("priority-admin-message")).toBeVisible(); }); it("successfully sets priority", async () => { + const user = userEvent.setup(); const { Component, dispatchToast } = RenderFakeToastContext( @@ -78,22 +76,19 @@ describe("setPriority", () => { ); renderWithRouterMatch(); - userEvent.click(screen.queryByDataCy("prioritize-patch")); - await waitFor(() => { - expect( - screen.queryByDataCy("set-patch-priority-popconfirm") - ).toBeVisible(); - }); - userEvent.type(screen.queryByDataCy("patch-priority-input"), "99"); - userEvent.click(screen.getByRole("button", { name: "Set" })); - await waitFor(() => - expect(dispatchToast.success).toHaveBeenCalledTimes(1) - ); + await user.click(screen.queryByDataCy("prioritize-patch")); + expect( + screen.queryByDataCy("set-patch-priority-popconfirm") + ).toBeVisible(); + await user.type(screen.queryByDataCy("patch-priority-input"), "99"); + await user.click(screen.getByRole("button", { name: "Set" })); + expect(dispatchToast.success).toHaveBeenCalledTimes(1); }); }); describe("task priority", () => { it("shows correct initial priority", async () => { + const user = userEvent.setup(); const { Component } = RenderFakeToastContext( @@ -101,16 +96,15 @@ describe("setPriority", () => { ); renderWithRouterMatch(); - userEvent.click(screen.queryByDataCy("prioritize-task")); - await waitFor(() => { - expect( - screen.queryByDataCy("set-task-priority-popconfirm") - ).toBeVisible(); - }); + await user.click(screen.queryByDataCy("prioritize-task")); + expect( + screen.queryByDataCy("set-task-priority-popconfirm") + ).toBeVisible(); expect(screen.queryByDataCy("task-priority-input")).toHaveValue(10); }); it("shows default message", async () => { + const user = userEvent.setup(); const { Component } = RenderFakeToastContext( @@ -118,18 +112,19 @@ describe("setPriority", () => { ); renderWithRouterMatch(); - userEvent.click(screen.queryByDataCy("prioritize-task")); + await user.click(screen.queryByDataCy("prioritize-task")); await waitFor(() => { expect( screen.queryByDataCy("set-task-priority-popconfirm") ).toBeVisible(); }); expect(screen.queryByDataCy("priority-default-message")).toBeVisible(); - userEvent.type(screen.queryByDataCy("task-priority-input"), "9"); + await user.type(screen.queryByDataCy("task-priority-input"), "9"); expect(screen.queryByDataCy("priority-default-message")).toBeVisible(); }); it("shows warning message", async () => { + const user = userEvent.setup(); const { Component } = RenderFakeToastContext( @@ -137,18 +132,19 @@ describe("setPriority", () => { ); renderWithRouterMatch(); - userEvent.click(screen.queryByDataCy("prioritize-task")); + await user.click(screen.queryByDataCy("prioritize-task")); await waitFor(() => { expect( screen.queryByDataCy("set-task-priority-popconfirm") ).toBeVisible(); }); expect(screen.queryByDataCy("priority-warning-message")).toBeNull(); - userEvent.type(screen.queryByDataCy("task-priority-input"), "99"); + await user.type(screen.queryByDataCy("task-priority-input"), "99"); expect(screen.queryByDataCy("priority-warning-message")).toBeVisible(); }); it("shows admin message", async () => { + const user = userEvent.setup(); const { Component } = RenderFakeToastContext( @@ -156,18 +152,19 @@ describe("setPriority", () => { ); renderWithRouterMatch(); - userEvent.click(screen.queryByDataCy("prioritize-task")); + await user.click(screen.queryByDataCy("prioritize-task")); await waitFor(() => { expect( screen.queryByDataCy("set-task-priority-popconfirm") ).toBeVisible(); }); expect(screen.queryByDataCy("priority-admin-message")).toBeNull(); - userEvent.type(screen.queryByDataCy("task-priority-input"), "999"); + await user.type(screen.queryByDataCy("task-priority-input"), "999"); expect(screen.queryByDataCy("priority-admin-message")).toBeVisible(); }); it("successfully sets priority", async () => { + const user = userEvent.setup(); const { Component, dispatchToast } = RenderFakeToastContext( @@ -175,14 +172,14 @@ describe("setPriority", () => { ); renderWithRouterMatch(); - userEvent.click(screen.queryByDataCy("prioritize-task")); + await user.click(screen.queryByDataCy("prioritize-task")); await waitFor(() => { expect( screen.queryByDataCy("set-task-priority-popconfirm") ).toBeVisible(); }); - userEvent.type(screen.queryByDataCy("task-priority-input"), "99"); - userEvent.click(screen.getByRole("button", { name: "Set" })); + await user.type(screen.queryByDataCy("task-priority-input"), "99"); + await user.click(screen.getByRole("button", { name: "Set" })); await waitFor(() => expect(dispatchToast.success).toHaveBeenCalledTimes(1) ); diff --git a/src/components/Settings/Context.test.tsx b/src/components/Settings/Context.test.tsx index a6561ed45c..6e256d121c 100644 --- a/src/components/Settings/Context.test.tsx +++ b/src/components/Settings/Context.test.tsx @@ -1,5 +1,5 @@ import { AjvError } from "@rjsf/core"; -import { act, renderHook } from "@testing-library/react-hooks"; +import { act, renderHook, waitFor } from "test_utils"; import { initialData, TestProvider, @@ -52,7 +52,7 @@ describe("useTestContext", () => { }); it("marks the tab as having changes when updateForm is called", async () => { - const { result, waitForNextUpdate } = renderHook(() => useTestContext(), { + const { result } = renderHook(() => useTestContext(), { wrapper: TestProvider, }); @@ -67,15 +67,16 @@ describe("useTestContext", () => { }); }); - await waitForNextUpdate(); - expect(result.current.getTab("foo").hasChanges).toBe(true); + await waitFor(() => { + expect(result.current.getTab("foo").hasChanges).toBe(true); + }); expect(result.current.getTab("foo").hasError).toBe(false); expect(result.current.getTab("bar").hasChanges).toBe(false); expect(result.current.getTab("bar").hasError).toBe(false); }); it("updating the form state with identical data does not unsave the tab", async () => { - const { result, waitForNextUpdate } = renderHook(() => useTestContext(), { + const { result } = renderHook(() => useTestContext(), { wrapper: TestProvider, }); @@ -90,7 +91,6 @@ describe("useTestContext", () => { }); }); - await waitForNextUpdate(); expect(result.current.getTab("foo").hasChanges).toBe(false); }); @@ -120,7 +120,7 @@ describe("useHasUnsavedTab", () => { }); it("returns names of unsaved tabs", async () => { - const { result, waitForNextUpdate } = renderHook( + const { result } = renderHook( () => ({ ...useHasUnsavedTab(), ...useTestContext(), @@ -139,9 +139,10 @@ describe("useHasUnsavedTab", () => { }); }); - await waitForNextUpdate(); + await waitFor(() => { + expect(result.current.hasUnsaved).toBe(true); + }); expect(result.current.unsavedTabs).toStrictEqual(["bar"]); - expect(result.current.hasUnsaved).toBe(true); }); }); diff --git a/src/components/Settings/Form.test.tsx b/src/components/Settings/Form.test.tsx index 86432f3514..7b59e90bf2 100644 --- a/src/components/Settings/Form.test.tsx +++ b/src/components/Settings/Form.test.tsx @@ -40,22 +40,26 @@ describe("context-based form", () => { expect(screen.getByLabelText("Caps Lock Enabled")).toBeChecked(); }); - it("updates the data", () => { + it("updates the data", async () => { + const user = userEvent.setup(); render(, { wrapper: TestProvider, }); - userEvent.click(screen.getByLabelText("Caps Lock Enabled")); - expect(screen.getByLabelText("Caps Lock Enabled")).not.toBeChecked(); + const checkbox = screen.getByRole("checkbox"); + expect(checkbox).toBeChecked(); + await user.click(screen.getByText("Caps Lock Enabled")); + expect(checkbox).not.toBeChecked(); }); - it("applies a validate function that shows an error message", () => { + it("applies a validate function that shows an error message", async () => { + const user = userEvent.setup(); render(, { wrapper: TestProvider, }); - userEvent.clear(screen.getByLabelText("Age")); + await user.clear(screen.getByLabelText("Age")); expect(screen.getByLabelText("Age")).toHaveValue(""); expect(screen.queryByText("Invalid Age!")).not.toBeInTheDocument(); - userEvent.type(screen.getByLabelText("Age"), "30"); + await user.type(screen.getByLabelText("Age"), "30"); expect(screen.getByText("Invalid Age!")).toBeInTheDocument(); }); diff --git a/src/components/Settings/NavigationWarningModal.test.tsx b/src/components/Settings/NavigationWarningModal.test.tsx index 8025de3b64..2eb756f178 100644 --- a/src/components/Settings/NavigationWarningModal.test.tsx +++ b/src/components/Settings/NavigationWarningModal.test.tsx @@ -4,7 +4,7 @@ import { Outlet, RouterProvider, } from "react-router-dom"; -import { act, render, screen, userEvent } from "test_utils"; +import { render, screen, userEvent } from "test_utils"; import { NavigationModalProps, NavigationWarningModal, @@ -44,13 +44,14 @@ const getRouter = ({ shouldBlock, unsavedTabs }: NavigationModalProps) => ); describe("navigation warning", () => { - it("does not warn when navigating and shouldBlock is false", () => { + it("does not warn when navigating and shouldBlock is false", async () => { const router = getRouter({ shouldBlock: false, unsavedTabs: [] }); + const user = userEvent.setup(); render(); expect(router.state.location.pathname).toBe("/"); - userEvent.click(screen.getByRole("link")); + await user.click(screen.getByRole("link")); expect(router.state.location.pathname).toBe("/about"); expect( screen.queryByDataCy("navigation-warning-modal") @@ -58,76 +59,74 @@ describe("navigation warning", () => { expect(screen.queryByRole("heading")).toHaveTextContent("About Page"); }); - it("warns and shows the modal with unsaved pages", () => { + it("warns and shows the modal with unsaved pages", async () => { const router = getRouter({ shouldBlock: true, unsavedTabs: [{ title: "An Unsaved Page", value: "foo" }], }); + const user = userEvent.setup(); render(); expect(router.state.location.pathname).toBe("/"); - userEvent.click(screen.getByRole("link")); + await user.click(screen.getByRole("link")); expect(router.state.location.pathname).toBe("/"); expect(screen.getByDataCy("navigation-warning-modal")).toBeInTheDocument(); expect(screen.getByText("An Unsaved Page")).toBeInTheDocument(); }); - it("warns and shows the modal with unsaved pages when a function is provided to shouldBlock", () => { + it("warns and shows the modal with unsaved pages when a function is provided to shouldBlock", async () => { const router = getRouter({ shouldBlock: () => true, unsavedTabs: [{ title: "An Unsaved Page", value: "foo" }], }); + const user = userEvent.setup(); render(); expect(router.state.location.pathname).toBe("/"); - userEvent.click(screen.getByRole("link")); + await user.click(screen.getByRole("link")); expect(router.state.location.pathname).toBe("/"); expect(screen.getByDataCy("navigation-warning-modal")).toBeInTheDocument(); expect(screen.getByText("An Unsaved Page")).toBeInTheDocument(); }); - it("navigates to the next page when 'Leave' button is clicked", () => { + it("navigates to the next page when 'Leave' button is clicked", async () => { const router = getRouter({ shouldBlock: true, unsavedTabs: [{ title: "An Unsaved Page", value: "foo" }], }); + const user = userEvent.setup(); render(); expect(router.state.location.pathname).toBe("/"); - userEvent.click(screen.getByRole("link")); + await user.click(screen.getByRole("link")); expect(router.state.location.pathname).toBe("/"); expect(screen.getByDataCy("navigation-warning-modal")).toBeInTheDocument(); expect(screen.getByText("An Unsaved Page")).toBeInTheDocument(); - // eslint-disable-next-line testing-library/no-unnecessary-act - act(() => { - userEvent.click(screen.getByRole("button", { name: "Leave" })); - }); + await user.click(screen.getByRole("button", { name: "Leave" })); expect(router.state.location.pathname).toBe("/about"); expect(screen.queryByRole("heading")).toHaveTextContent("About Page"); }); - it("remains on the initial page when 'Cancel' button is clicked", () => { + it("remains on the initial page when 'Cancel' button is clicked", async () => { const router = getRouter({ shouldBlock: true, unsavedTabs: [{ title: "An Unsaved Page", value: "foo" }], }); + const user = userEvent.setup(); render(); expect(router.state.location.pathname).toBe("/"); - userEvent.click(screen.getByRole("link")); + await user.click(screen.getByRole("link")); expect(router.state.location.pathname).toBe("/"); expect(screen.getByDataCy("navigation-warning-modal")).toBeInTheDocument(); expect(screen.getByText("An Unsaved Page")).toBeInTheDocument(); - // eslint-disable-next-line testing-library/no-unnecessary-act - act(() => { - userEvent.click(screen.getByRole("button", { name: "Cancel" })); - }); + await user.click(screen.getByRole("button", { name: "Cancel" })); expect(router.state.location.pathname).toBe("/"); expect(screen.queryByRole("heading")).toHaveTextContent("Home Page"); }); diff --git a/src/components/SpruceForm/FieldTemplates/ObjectFieldTemplates/ObjectFieldTemplates.test.tsx b/src/components/SpruceForm/FieldTemplates/ObjectFieldTemplates/ObjectFieldTemplates.test.tsx index 0e0725435b..6f4b710546 100644 --- a/src/components/SpruceForm/FieldTemplates/ObjectFieldTemplates/ObjectFieldTemplates.test.tsx +++ b/src/components/SpruceForm/FieldTemplates/ObjectFieldTemplates/ObjectFieldTemplates.test.tsx @@ -46,12 +46,11 @@ describe("objectFieldTemplates", () => { expect(screen.getByDataCy("name")).toBeInTheDocument(); }); it("renders all fields", () => { - const onChange = jest.fn(); render( ); @@ -59,11 +58,13 @@ describe("objectFieldTemplates", () => { expect(screen.getByDataCy("age")).toBeInTheDocument(); }); - it("calls onChange when a field is changed", () => { + it("calls onChange when a field is changed", async () => { let data; const onChange = jest.fn(({ formData }) => { data = formData; }); + + const user = userEvent.setup(); render( { uiSchema={uiSchema} /> ); - userEvent.type(screen.getByDataCy("name"), "Bruce Lee"); - userEvent.type(screen.getByDataCy("age"), "32"); - + await user.type(screen.getByDataCy("name"), "Bruce Lee"); + await user.type(screen.getByDataCy("age"), "32"); expect(data).toStrictEqual({ person: { name: "Bruce Lee", age: 32 } }); }); }); @@ -92,35 +92,35 @@ describe("objectFieldTemplates", () => { }, }; it("applies data-cy attributes", () => { - const onChange = jest.fn(); render( ); expect(screen.getByDataCy("name")).toBeInTheDocument(); }); it("renders all fields in a card", () => { - const onChange = jest.fn(); render( ); expect(screen.getByDataCy("name")).toBeInTheDocument(); expect(screen.getByDataCy("age")).toBeInTheDocument(); }); - it("calls onChange when a field is changed", () => { + it("calls onChange when a field is changed", async () => { let data; const onChange = jest.fn(({ formData }) => { data = formData; }); + + const user = userEvent.setup(); render( { uiSchema={uiSchema} /> ); - userEvent.type(screen.getByDataCy("name"), "Bruce Lee"); - userEvent.type(screen.getByDataCy("age"), "32"); - + await user.type(screen.getByDataCy("name"), "Bruce Lee"); + await user.type(screen.getByDataCy("age"), "32"); expect(data).toStrictEqual({ person: { name: "Bruce Lee", age: 32 } }); }); }); @@ -148,35 +147,35 @@ describe("objectFieldTemplates", () => { }, }; it("applies data-cy attributes", () => { - const onChange = jest.fn(); render( ); expect(screen.getByDataCy("name")).toBeInTheDocument(); }); it("renders all fields in an accordion", () => { - const onChange = jest.fn(); render( ); expect(screen.getByDataCy("name")).toBeInTheDocument(); expect(screen.getByDataCy("age")).toBeInTheDocument(); }); - it("calls onChange when a field is changed", () => { + it("calls onChange when a field is changed", async () => { let data; const onChange = jest.fn(({ formData }) => { data = formData; }); + + const user = userEvent.setup(); render( { uiSchema={uiSchema} /> ); - userEvent.type(screen.getByDataCy("name"), "Bruce Lee"); - userEvent.type(screen.getByDataCy("age"), "32"); + await user.type(screen.getByDataCy("name"), "Bruce Lee"); + await user.type(screen.getByDataCy("age"), "32"); expect(data).toStrictEqual({ person: { name: "Bruce Lee", age: 32 } }); }); it("accordion is expanded by default", () => { - const onChange = jest.fn(); render( ); @@ -205,12 +203,11 @@ describe("objectFieldTemplates", () => { ).toHaveAttribute("aria-expanded", "true"); }); it("accordion is collapsed by default if defaultOpen is false", () => { - const onChange = jest.fn(); render( { it("should render as expected", () => { - const onChange = jest.fn(); render( @@ -28,6 +27,8 @@ describe("spruce form", () => { const { formData } = x; data = formData; }); + + const user = userEvent.setup(); render( { /> ); - userEvent.clear(screen.queryByDataCy("valid-projects-input")); - userEvent.type(screen.queryByDataCy("valid-projects-input"), "new value"); - userEvent.click(screen.queryByDataCy("add-button")); - await waitFor(() => - expect(screen.queryAllByDataCy("new-user-input")).toHaveLength(2) - ); - userEvent.type(screen.queryAllByDataCy("new-user-input")[0], "new-user"); + await user.clear(screen.queryByDataCy("valid-projects-input")); + await user.type(screen.queryByDataCy("valid-projects-input"), "new value"); + await user.click(screen.queryByDataCy("add-button")); + expect(screen.queryAllByDataCy("new-user-input")).toHaveLength(2); + await user.type(screen.queryAllByDataCy("new-user-input")[0], "new-user"); expect(onChange).toHaveBeenCalled(); // eslint-disable-line jest/prefer-called-with expect(screen.queryByDataCy("valid-projects-input")).toHaveValue( "new value" @@ -60,7 +59,7 @@ describe("spruce form", () => { describe("form elements", () => { describe("text input", () => { describe("invisible errors", () => { - it("should work with validate function", () => { + it("should work with validate function", async () => { let formErrors = {}; const onChange = jest.fn((x) => { const { errors } = x; @@ -68,6 +67,7 @@ describe("spruce form", () => { }); const validate = jest.fn((_formData, err) => err); + const user = userEvent.setup(); const { formData, schema, uiSchema } = textInput(); render( @@ -80,8 +80,8 @@ describe("spruce form", () => { /> ); - userEvent.type(screen.queryByDataCy("text-input"), "new value"); - userEvent.clear(screen.queryByDataCy("text-input")); + await user.type(screen.queryByDataCy("text-input"), "new value"); + await user.clear(screen.queryByDataCy("text-input")); expect(screen.queryByDataCy("text-input")).toHaveValue(""); // Invisible errors should be in the form error state but not visible on the page. @@ -91,12 +91,14 @@ describe("spruce form", () => { }); describe("emptyValue", () => { - it("defaults to '' when not specified", () => { + it("defaults to '' when not specified", async () => { let data = {}; const onChange = jest.fn((x) => { const { formData } = x; data = formData; }); + + const user = userEvent.setup(); const { formData, schema, uiSchema } = textInput(); render( @@ -108,20 +110,22 @@ describe("spruce form", () => { /> ); - userEvent.type(screen.queryByDataCy("text-input"), "new value"); - userEvent.clear(screen.queryByDataCy("text-input")); + await user.type(screen.queryByDataCy("text-input"), "new value"); + await user.clear(screen.queryByDataCy("text-input")); expect(screen.queryByDataCy("text-input")).toHaveValue(""); expect(data).toStrictEqual({ textInput: "", }); }); - it("uses provided value when specified", () => { + it("uses provided value when specified", async () => { let data = {}; const onChange = jest.fn((x) => { const { formData } = x; data = formData; }); + + const user = userEvent.setup(); const { formData, schema, uiSchema } = textInput("myEmptyValue"); render( @@ -133,8 +137,8 @@ describe("spruce form", () => { /> ); - userEvent.type(screen.queryByDataCy("text-input"), "new value"); - userEvent.clear(screen.queryByDataCy("text-input")); + await user.type(screen.queryByDataCy("text-input"), "new value"); + await user.clear(screen.queryByDataCy("text-input")); expect(screen.queryByDataCy("text-input")).toHaveValue( "myEmptyValue" ); @@ -147,7 +151,7 @@ describe("spruce form", () => { describe("text area", () => { describe("invisible errors", () => { - it("should work with validate function", () => { + it("should work with validate function", async () => { let formErrors = {}; const onChange = jest.fn((x) => { const { errors } = x; @@ -155,6 +159,7 @@ describe("spruce form", () => { }); const validate = jest.fn((_formData, err) => err); + const user = userEvent.setup(); const { formData, schema, uiSchema } = textArea(); render( @@ -167,8 +172,8 @@ describe("spruce form", () => { /> ); - userEvent.type(screen.queryByDataCy("text-area"), "new value"); - userEvent.clear(screen.queryByDataCy("text-area")); + await user.type(screen.queryByDataCy("text-area"), "new value"); + await user.clear(screen.queryByDataCy("text-area")); expect(screen.queryByDataCy("text-area")).toHaveValue(""); // Invisible errors should be in the form error state but not visible on the page. @@ -178,12 +183,14 @@ describe("spruce form", () => { }); describe("emptyValue", () => { - it("defaults to '' when not specified", () => { + it("defaults to '' when not specified", async () => { let data = {}; const onChange = jest.fn((x) => { const { formData } = x; data = formData; }); + + const user = userEvent.setup(); const { formData, schema, uiSchema } = textArea(); render( @@ -195,20 +202,22 @@ describe("spruce form", () => { /> ); - userEvent.type(screen.queryByDataCy("text-area"), "new value"); - userEvent.clear(screen.queryByDataCy("text-area")); + await user.type(screen.queryByDataCy("text-area"), "new value"); + await user.clear(screen.queryByDataCy("text-area")); expect(screen.queryByDataCy("text-area")).toHaveValue(""); expect(data).toStrictEqual({ textArea: "", }); }); - it("uses provided value when specified", () => { + it("uses provided value when specified", async () => { let data = {}; const onChange = jest.fn((x) => { const { formData } = x; data = formData; }); + + const user = userEvent.setup(); const { formData, schema, uiSchema } = textArea("myEmptyValue"); render( @@ -220,8 +229,8 @@ describe("spruce form", () => { /> ); - userEvent.type(screen.queryByDataCy("text-area"), "new value"); - userEvent.clear(screen.queryByDataCy("text-area")); + await user.type(screen.queryByDataCy("text-area"), "new value"); + await user.clear(screen.queryByDataCy("text-area")); expect(screen.queryByDataCy("text-area")).toHaveValue("myEmptyValue"); expect(data).toStrictEqual({ textArea: "myEmptyValue", @@ -232,13 +241,12 @@ describe("spruce form", () => { describe("select", () => { it("renders with the specified default selected", () => { - const onChange = jest.fn(); const { formData, schema, uiSchema } = select; render( ); @@ -247,36 +255,36 @@ describe("spruce form", () => { expect(screen.queryByText("Strawberry")).not.toBeInTheDocument(); }); - it("shows three options on click", () => { - const onChange = jest.fn(); + it("shows three options on click", async () => { + const user = userEvent.setup(); const { formData, schema, uiSchema } = select; render( ); - userEvent.click(screen.queryByRole("button")); + await user.click(screen.queryByRole("button")); expect(screen.queryAllByText("Vanilla")).toHaveLength(2); expect(screen.getByText("Chocolate")).toBeInTheDocument(); expect(screen.getByText("Strawberry")).toBeInTheDocument(); }); it("closes the menu and displays the new selected option on click", async () => { - const onChange = jest.fn(); + const user = userEvent.setup(); const { formData, schema, uiSchema } = select; render( ); - userEvent.click(screen.queryByRole("button")); - userEvent.click(screen.queryByText("Chocolate")); + await user.click(screen.getByRole("button")); + await user.click(screen.getByRole("option", { name: "Chocolate" })); await waitFor(() => { expect(screen.queryByText("Vanilla")).not.toBeInTheDocument(); }); @@ -285,17 +293,17 @@ describe("spruce form", () => { }); it("disables options included in enumDisabled", async () => { - const onChange = jest.fn(); + const user = userEvent.setup(); const { formData, schema, uiSchema } = select; render( ); - userEvent.click(screen.queryByRole("button")); + await user.click(screen.queryByRole("button")); // LeafyGreen doesn't label disabled options as such, so instead of checking for a property // ensure that the disabled element is not clickable. @@ -310,47 +318,41 @@ describe("spruce form", () => { describe("radio group", () => { it("renders 3 inputs with the specified default selected", () => { const { formData, schema, uiSchema } = radioGroup; - const onChange = jest.fn(); render( ); - expect(screen.getAllByRole("radio")).toHaveLength(3); expect(screen.getByLabelText("New York")).toBeChecked(); }); it("disables options in enumDisabled", () => { const { formData, schema, uiSchema } = radioGroup; - const onChange = jest.fn(); render( ); - expect(screen.getByLabelText("Connecticut")).toBeDisabled(); }); it("shows option descriptions", () => { const { formData, schema, uiSchema } = radioGroup; - const onChange = jest.fn(); render( ); - expect(screen.getByText("The Garden State")).toBeVisible(); }); }); diff --git a/src/components/TreeSelect/TreeSelect.test.tsx b/src/components/TreeSelect/TreeSelect.test.tsx index ac40fabe3a..621710648f 100644 --- a/src/components/TreeSelect/TreeSelect.test.tsx +++ b/src/components/TreeSelect/TreeSelect.test.tsx @@ -19,19 +19,21 @@ describe("treeSelect", () => { expect(checkbox).toBeChecked(); }); - it("clicking a value selects its option in the tree select", () => { + it("clicking a value selects its option in the tree select", async () => { + const user = userEvent.setup(); const onChange = jest.fn(); render(); expect(screen.getByText("Pass")).toBeInTheDocument(); - userEvent.click(screen.queryByText("Pass")); + await user.click(screen.queryByText("Pass")); expect(onChange).toHaveBeenCalledWith(["pass"]); }); - it("clicking all selects all of the options in the tree select", () => { + it("clicking all selects all of the options in the tree select", async () => { + const user = userEvent.setup(); const onChange = jest.fn(); render(); expect(screen.getByText("All")).toBeInTheDocument(); - userEvent.click(screen.queryByText("All")); + await user.click(screen.queryByText("All")); expect(onChange).toHaveBeenCalledWith([ "all", "pass", @@ -51,7 +53,8 @@ describe("treeSelect", () => { expect(screen.getByText("Fail")).toBeInTheDocument(); }); - it("unchecking a child element should uncheck its parent", () => { + it("unchecking a child element should uncheck its parent", async () => { + const user = userEvent.setup(); let state = ["failing-umbrella", "system-failure", "fail"]; const onChange = jest.fn((update) => { state = update; @@ -62,17 +65,18 @@ describe("treeSelect", () => { expect(screen.queryByLabelText("Failing Umbrella")).toBeChecked(); expect(screen.queryByLabelText("System Failure")).toBeChecked(); expect(screen.queryByLabelText("Fail")).toBeChecked(); - userEvent.click(screen.queryByText("Fail")); + await user.click(screen.queryByText("Fail")); expect(onChange).toHaveBeenCalledWith(["system-failure"]); }); - it("checking a parent element should toggle its children", () => { + it("checking a parent element should toggle its children", async () => { + const user = userEvent.setup(); const onChange = jest.fn(); render( ); expect(screen.getByText("Failing Umbrella")).toBeInTheDocument(); - userEvent.click(screen.queryByText("Failing Umbrella")); + await user.click(screen.queryByText("Failing Umbrella")); expect(onChange).toHaveBeenCalledWith([ "failing-umbrella", "system-failure", diff --git a/src/components/TupleSelect/TupleSelect.test.tsx b/src/components/TupleSelect/TupleSelect.test.tsx index 8210ed77f0..21d6731855 100644 --- a/src/components/TupleSelect/TupleSelect.test.tsx +++ b/src/components/TupleSelect/TupleSelect.test.tsx @@ -35,7 +35,8 @@ describe("tupleSelect", () => { expect(input).toHaveValue(""); }); - it("should clear input when a value is submitted", () => { + it("should clear input when a value is submitted", async () => { + const user = userEvent.setup(); const onSubmit = jest.fn(); const validator = jest.fn((v) => v !== "bad"); const validatorErrorMessage = "Invalid Input"; @@ -50,12 +51,13 @@ describe("tupleSelect", () => { const input = screen.queryByDataCy("tuple-select-input"); expect(input).toHaveValue(""); - userEvent.type(input, "some-filter"); - userEvent.type(input, "{enter}"); + await user.type(input, "some-filter"); + await user.type(input, "{enter}"); expect(input).toHaveValue(""); }); it("should validate the input and prevent submission if it fails validation", async () => { + const user = userEvent.setup(); const onSubmit = jest.fn(); const validator = jest.fn((v) => v !== "bad"); const validatorErrorMessage = "Invalid Input"; @@ -70,14 +72,14 @@ describe("tupleSelect", () => { const input = screen.queryByDataCy("tuple-select-input"); expect(input).toHaveValue(""); - userEvent.type(input, "bad"); + await user.type(input, "bad"); expect(input).toHaveValue("bad"); - userEvent.type(input, "{enter}"); + await user.type(input, "{enter}"); expect(input).toHaveValue("bad"); expect(onSubmit).not.toHaveBeenCalled(); expect(validator).toHaveBeenLastCalledWith("bad"); expect(screen.getByDataCy("tuple-select-warning")).toBeInTheDocument(); - userEvent.hover(screen.queryByDataCy("tuple-select-warning")); + await user.hover(screen.queryByDataCy("tuple-select-warning")); await screen.findByText(validatorErrorMessage); }); }); diff --git a/src/components/TupleSelectWithRegexConditional/TupleSelectWithRegexConditional.test.tsx b/src/components/TupleSelectWithRegexConditional/TupleSelectWithRegexConditional.test.tsx index f6923936bb..ff8a23f7bb 100644 --- a/src/components/TupleSelectWithRegexConditional/TupleSelectWithRegexConditional.test.tsx +++ b/src/components/TupleSelectWithRegexConditional/TupleSelectWithRegexConditional.test.tsx @@ -34,7 +34,8 @@ describe("tupleSelectWithRegexConditional", () => { expect(dropdown).toHaveTextContent("Build Variant"); expect(input).toHaveValue(""); }); - it("should clear input when a value is submitted", () => { + it("should clear input when a value is submitted", async () => { + const user = userEvent.setup(); const onSubmit = jest.fn(); const validator = jest.fn((v) => v !== "bad"); const validatorErrorMessage = "Invalid Input"; @@ -49,12 +50,13 @@ describe("tupleSelectWithRegexConditional", () => { const input = screen.queryByDataCy("tuple-select-input"); expect(input).toHaveValue(""); - userEvent.type(input, "some-filter"); - userEvent.type(input, "{enter}"); + await user.type(input, "some-filter"); + await user.type(input, "{enter}"); expect(input).toHaveValue(""); }); it("should validate the input and prevent submission if it fails validation and input type is set to `regex`", async () => { + const user = userEvent.setup(); const onSubmit = jest.fn(); const validator = jest.fn((v) => v !== "bad"); const validatorErrorMessage = "Invalid Input"; @@ -69,19 +71,20 @@ describe("tupleSelectWithRegexConditional", () => { const input = screen.queryByDataCy("tuple-select-input"); expect(input).toHaveValue(""); - userEvent.type(input, "bad"); + await user.type(input, "bad"); expect(input).toHaveValue("bad"); - userEvent.type(input, "{enter}"); + await user.type(input, "{enter}"); expect(input).toHaveValue("bad"); expect(onSubmit).not.toHaveBeenCalled(); expect(validator).toHaveBeenLastCalledWith("bad"); expect(screen.getByDataCy("tuple-select-warning")).toBeInTheDocument(); - userEvent.hover(screen.queryByDataCy("tuple-select-warning")); + await user.hover(screen.queryByDataCy("tuple-select-warning")); await waitFor(() => { expect(screen.getByText(validatorErrorMessage)).toBeInTheDocument(); }); }); - it("toggling the input type selector to `exact` should escape any regex characters", () => { + it("toggling the input type selector to `exact` should escape any regex characters", async () => { + const user = userEvent.setup(); const onSubmit = jest.fn(); const validator = jest.fn((v) => v !== "bad"); const validatorErrorMessage = "Invalid Input"; @@ -93,20 +96,20 @@ describe("tupleSelectWithRegexConditional", () => { validatorErrorMessage={validatorErrorMessage} /> ); - userEvent.click(screen.getByRole("tab", { name: "EXACT" })); + await user.click(screen.getByRole("tab", { name: "EXACT" })); const input = screen.queryByDataCy("tuple-select-input"); expect(input).toHaveValue(""); - userEvent.type(input, "some-*"); + await user.type(input, "some-*"); expect(input).toHaveValue("some-*"); - userEvent.type(input, "{enter}"); + await user.type(input, "{enter}"); expect(onSubmit).toHaveBeenCalledWith({ category: "build_variant", value: "some\\-\\*", }); - expect(input).toHaveValue(""); }); - it("should not attempt to validate input if using the `exact` input type", () => { + it("should not attempt to validate input if using the `exact` input type", async () => { + const user = userEvent.setup(); const onSubmit = jest.fn(); const validator = jest.fn((v) => v !== "bad"); const validatorErrorMessage = "Invalid Input"; @@ -118,11 +121,11 @@ describe("tupleSelectWithRegexConditional", () => { validatorErrorMessage={validatorErrorMessage} /> ); - userEvent.click(screen.getByRole("tab", { name: "EXACT" })); + await user.click(screen.getByRole("tab", { name: "EXACT" })); const input = screen.queryByDataCy("tuple-select-input"); expect(input).toHaveValue(""); - userEvent.type(input, "some-["); - expect(input).toHaveValue("some-["); + await user.type(input, "*"); + expect(input).toHaveValue("*"); expect(validator).toHaveBeenCalledWith(""); }); }); diff --git a/src/components/WelcomeModal/WelcomeModal.test.tsx b/src/components/WelcomeModal/WelcomeModal.test.tsx index fa69c8b88c..cc2e901215 100644 --- a/src/components/WelcomeModal/WelcomeModal.test.tsx +++ b/src/components/WelcomeModal/WelcomeModal.test.tsx @@ -1,5 +1,5 @@ import { MockedProvider } from "@apollo/client/testing"; -import { render, screen, waitFor } from "test_utils"; +import { render, screen, userEvent } from "test_utils"; import WelcomeModal from "./WelcomeModal"; describe("welcomeModal", () => { @@ -59,6 +59,7 @@ describe("welcomeModal", () => { }); it("clicking the pagination buttons change the slides", async () => { + const user = userEvent.setup(); render( { ); expect(screen.getByText("Slide 1")).toBeVisible(); expect(screen.queryByDataCy("carousel-dot-1")).toBeVisible(); - screen.queryByDataCy("carousel-dot-1").click(); - await waitFor(() => { - expect(screen.getByText("Slide 2")).toBeVisible(); - }); - screen.queryByDataCy("carousel-dot-0").click(); - await waitFor(() => { - expect(screen.getByText("Slide 1")).toBeVisible(); - }); + await user.click(screen.queryByDataCy("carousel-dot-1")); + expect(screen.getByText("Slide 2")).toBeVisible(); + await user.click(screen.queryByDataCy("carousel-dot-0")); + expect(screen.getByText("Slide 1")).toBeVisible(); }); }); diff --git a/src/context/toast/toast.test.tsx b/src/context/toast/toast.test.tsx index 0df3be7894..2826f18ae7 100644 --- a/src/context/toast/toast.test.tsx +++ b/src/context/toast/toast.test.tsx @@ -1,5 +1,11 @@ -import { renderHook } from "@testing-library/react-hooks"; -import { act, render, screen, userEvent, waitFor } from "test_utils"; +import { + renderHook, + act, + render, + screen, + userEvent, + waitFor, +} from "test_utils"; import { ToastProvider, useToastContext } from "."; import { RenderFakeToastContext } from "./__mocks__"; import { TOAST_TIMEOUT } from "./constants"; @@ -130,6 +136,7 @@ describe("toast", () => { describe("closing the toast", () => { it("should be able to close a toast by clicking the X button by default", async () => { + const user = userEvent.setup(); const { Component, hook } = renderComponentWithHook(); render(, { wrapper, @@ -138,7 +145,7 @@ describe("toast", () => { hook.current.info("test string"); }); expect(screen.getByDataCy("toast")).toBeInTheDocument(); - userEvent.click(screen.getByLabelText(closeIconLabel)); + await user.click(screen.getByLabelText(closeIconLabel)); await waitFor(() => { expect(screen.queryByDataCy("toast")).not.toBeInTheDocument(); }); @@ -157,6 +164,7 @@ describe("toast", () => { }); it("should trigger a callback function onClose", async () => { + const user = userEvent.setup(); const onClose = jest.fn(); const { Component, hook } = renderComponentWithHook(); render(, { @@ -167,7 +175,7 @@ describe("toast", () => { }); expect(screen.getByDataCy("toast")).toBeInTheDocument(); - userEvent.click(screen.getByLabelText(closeIconLabel)); + await user.click(screen.getByLabelText(closeIconLabel)); await waitFor(() => { expect(screen.queryByDataCy("toast")).not.toBeInTheDocument(); }); @@ -202,6 +210,7 @@ describe("toast", () => { describe("mocked toast", () => { it("should be able to mock the toast in a component test", async () => { + const user = userEvent.setup(); const ToastComponent: React.FC = () => { const dispatchToast = useToastContext(); return ( @@ -215,9 +224,8 @@ describe("mocked toast", () => { dispatchToast, useToastContext: useToastContextSpied, } = RenderFakeToastContext(); - render(); - userEvent.click(screen.getByText("Click Me")); + await user.click(screen.getByText("Click Me")); expect(useToastContextSpied).toHaveBeenCalledTimes(1); expect(dispatchToast.success).toHaveBeenCalledWith("test"); }); diff --git a/src/hooks/tests/useBreadcrumbRoot.test.tsx b/src/hooks/tests/useBreadcrumbRoot.test.tsx index 75bb3bf622..60e61f84d2 100644 --- a/src/hooks/tests/useBreadcrumbRoot.test.tsx +++ b/src/hooks/tests/useBreadcrumbRoot.test.tsx @@ -1,10 +1,10 @@ import { InMemoryCache } from "@apollo/client"; import { MockedProvider } from "@apollo/client/testing"; -import { renderHook } from "@testing-library/react-hooks"; import { OtherUserQuery, OtherUserQueryVariables } from "gql/generated/types"; import { getUserMock } from "gql/mocks/getUser"; import { GET_OTHER_USER } from "gql/queries"; import { useBreadcrumbRoot } from "hooks"; +import { renderHook, waitFor } from "test_utils"; import { ApolloMock } from "types/gql"; const cache = new InMemoryCache({ @@ -29,24 +29,24 @@ const OtherUserProvider = ({ children }) => ( describe("useBreadcrumbRoot", () => { it("returns the correct breadcrumb root when the version is a patch belonging to current user", async () => { - const { result, waitForNextUpdate } = renderHook( + const { result } = renderHook( () => useBreadcrumbRoot(true, "admin", "spruce"), { wrapper: SameUserProvider } ); - await waitForNextUpdate(); - - expect(result.current.to).toBe("/user/admin/patches"); + await waitFor(() => { + expect(result.current.to).toBe("/user/admin/patches"); + }); expect(result.current.text).toBe("My Patches"); }); it("returns the correct breadcrumb root when the version is a patch belonging to other user", async () => { - const { result, waitForNextUpdate } = renderHook( + const { result } = renderHook( () => useBreadcrumbRoot(true, "john.doe", "spruce"), { wrapper: OtherUserProvider } ); - await waitForNextUpdate(); - - expect(result.current.to).toBe("/user/john.doe/patches"); + await waitFor(() => { + expect(result.current.to).toBe("/user/john.doe/patches"); + }); expect(result.current.text).toBe("John Doe's Patches"); }); diff --git a/src/hooks/tests/useDisableSpawnExpirationCheckbox.test.tsx b/src/hooks/tests/useDisableSpawnExpirationCheckbox.test.tsx index c679f2a2ad..1cd04fbee0 100644 --- a/src/hooks/tests/useDisableSpawnExpirationCheckbox.test.tsx +++ b/src/hooks/tests/useDisableSpawnExpirationCheckbox.test.tsx @@ -1,5 +1,4 @@ import { MockedProvider } from "@apollo/client/testing"; -import { renderHook } from "@testing-library/react-hooks"; import { MyHostsQuery, MyHostsQueryVariables, @@ -8,6 +7,7 @@ import { } from "gql/generated/types"; import { getSpruceConfigMock } from "gql/mocks/getSpruceConfig"; import { GET_MY_VOLUMES, GET_MY_HOSTS } from "gql/queries"; +import { renderHook } from "test_utils"; import { ApolloMock } from "types/gql"; import { useDisableSpawnExpirationCheckbox } from ".."; @@ -20,18 +20,17 @@ const getProvider = (mocks) => { describe("useDisableSpawnExpirationCheckbox", () => { it("should return true when the user already has the maximum unexpirable volumes and a target item is not supplied.", async () => { - const { result, waitForNextUpdate } = renderHook( + const { result } = renderHook( () => useDisableSpawnExpirationCheckbox(true), { wrapper: getProvider(mocks), } ); - await waitForNextUpdate(); expect(result.current).toBeTruthy(); }); it("should return false when when the user has the maximum number of unexpirable volumes and the target item is unexpirable.", async () => { - const { result, waitForNextUpdate } = renderHook( + const { result } = renderHook( () => useDisableSpawnExpirationCheckbox(true, { ...volume, @@ -39,12 +38,11 @@ describe("useDisableSpawnExpirationCheckbox", () => { }), { wrapper: getProvider(mocks) } ); - await waitForNextUpdate(); expect(result.current).toBeFalsy(); }); it("should return true when the user has the maximum number of unexpirable volumes and the target item is expirable.", async () => { - const { result, waitForNextUpdate } = renderHook( + const { result } = renderHook( () => useDisableSpawnExpirationCheckbox(true, { ...volume, @@ -54,21 +52,19 @@ describe("useDisableSpawnExpirationCheckbox", () => { wrapper: getProvider(mocks), } ); - await waitForNextUpdate(); expect(result.current).toBeTruthy(); }); it("should return true when the user has the maximum number of hosts and a target item is not supplied.", async () => { - const { result, waitForNextUpdate } = renderHook( + const { result } = renderHook( () => useDisableSpawnExpirationCheckbox(false), { wrapper: getProvider(mocks) } ); - await waitForNextUpdate(); expect(result.current).toBeTruthy(); }); it("should return false when when user has the maximum number of unexpirable hosts and the target item is unexpirable.", async () => { - const { result, waitForNextUpdate } = renderHook( + const { result } = renderHook( () => useDisableSpawnExpirationCheckbox(false, { ...host, @@ -78,12 +74,11 @@ describe("useDisableSpawnExpirationCheckbox", () => { wrapper: getProvider(mocks), } ); - await waitForNextUpdate(); expect(result.current).toBeFalsy(); }); it("should return false when when user has the maximum number of unexpirable hosts and the target item is expirable.", async () => { - const { result, waitForNextUpdate } = renderHook( + const { result } = renderHook( () => useDisableSpawnExpirationCheckbox(false, { ...host, @@ -93,7 +88,6 @@ describe("useDisableSpawnExpirationCheckbox", () => { wrapper: getProvider(mocks), } ); - await waitForNextUpdate(); expect(result.current).toBeTruthy(); }); }); diff --git a/src/hooks/tests/useGetUserPatchesPageTitleAndLink.test.tsx b/src/hooks/tests/useGetUserPatchesPageTitleAndLink.test.tsx index c00f7ca728..50a42a2448 100644 --- a/src/hooks/tests/useGetUserPatchesPageTitleAndLink.test.tsx +++ b/src/hooks/tests/useGetUserPatchesPageTitleAndLink.test.tsx @@ -1,8 +1,8 @@ import { MockedProvider } from "@apollo/client/testing"; -import { renderHook } from "@testing-library/react-hooks"; import { OtherUserQuery, OtherUserQueryVariables } from "gql/generated/types"; import { GET_OTHER_USER } from "gql/queries"; import { useGetUserPatchesPageTitleAndLink } from "hooks"; +import { renderHook, waitFor } from "test_utils"; import { ApolloMock } from "types/gql"; const mocks: ApolloMock[] = [ @@ -68,36 +68,35 @@ const Provider = ({ children }) => ( describe("useGetUserPatchesPageTitleAndLink", () => { it("return correct title and link when the userId passed into the hook parameter is that of the logged in user", async () => { - const { result, waitForNextUpdate } = renderHook( + const { result } = renderHook( () => useGetUserPatchesPageTitleAndLink("admin"), { wrapper: Provider } ); - - await waitForNextUpdate(); - - expect(result.current.title).toBe("My Patches"); + await waitFor(() => { + expect(result.current.title).toBe("My Patches"); + }); expect(result.current.link).toBe("/user/admin/patches"); }); it("return correct title and link when the userId passed into the hook parameter is not that of the logged in user", async () => { - const { result, waitForNextUpdate } = renderHook( + const { result } = renderHook( () => useGetUserPatchesPageTitleAndLink("justin.mathew"), { wrapper: Provider } ); - - await waitForNextUpdate(); - - expect(result.current.title).toBe("Justin Mathew's Patches"); + await waitFor(() => { + expect(result.current.title).toBe("Justin Mathew's Patches"); + }); expect(result.current.link).toBe("/user/justin.mathew/patches"); }); it("return correct title and link when the userId passed into the hook parameter is not that of the logged in user and the display name of the other user ends with the letter 's'", async () => { - const { result, waitForNextUpdate } = renderHook( + const { result } = renderHook( () => useGetUserPatchesPageTitleAndLink("justin.mathews"), { wrapper: Provider } ); - await waitForNextUpdate(); - expect(result.current.title).toBe("Justin Mathews' Patches"); + await waitFor(() => { + expect(result.current.title).toBe("Justin Mathews' Patches"); + }); expect(result.current.link).toBe("/user/justin.mathews/patches"); }); }); diff --git a/src/hooks/tests/useLegacyUIURL.test.tsx b/src/hooks/tests/useLegacyUIURL.test.tsx index 4b81ca7213..372eefa139 100644 --- a/src/hooks/tests/useLegacyUIURL.test.tsx +++ b/src/hooks/tests/useLegacyUIURL.test.tsx @@ -1,5 +1,5 @@ -import { act, renderHook } from "@testing-library/react-hooks"; import { MemoryRouter, useNavigate } from "react-router-dom"; +import { act, renderHook } from "test_utils"; import { useLegacyUIURL } from "../useLegacyUIURL"; describe("useLegacyUIURL", () => { diff --git a/src/hooks/tests/useNetworkStatus.test.tsx b/src/hooks/tests/useNetworkStatus.test.tsx index 008d35b09c..4e159680fc 100644 --- a/src/hooks/tests/useNetworkStatus.test.tsx +++ b/src/hooks/tests/useNetworkStatus.test.tsx @@ -1,7 +1,7 @@ import { MockedProvider } from "@apollo/client/testing"; -import { act, renderHook } from "@testing-library/react-hooks/dom"; import { getUserMock } from "gql/mocks/getUser"; import { useNetworkStatus } from "hooks"; +import { act, renderHook } from "test_utils"; const Provider = ({ children }) => ( {children} diff --git a/src/hooks/tests/useOnClickOutside.test.tsx b/src/hooks/tests/useOnClickOutside.test.tsx index 48cdbd73e5..4da1b78220 100644 --- a/src/hooks/tests/useOnClickOutside.test.tsx +++ b/src/hooks/tests/useOnClickOutside.test.tsx @@ -1,32 +1,34 @@ import { createRef } from "react"; -import { renderHook } from "@testing-library/react-hooks"; import { useOnClickOutside } from "hooks"; -import { render, screen, userEvent } from "test_utils"; +import { renderHook, render, screen, userEvent } from "test_utils"; describe("useOnClickOutside", () => { describe("useOnClickOutside with 1 ref", () => { - it("executes callback when clicking outside element", () => { + it("executes callback when clicking outside element", async () => { + const user = userEvent.setup(); const body = document.body as HTMLElement; const callback = jest.fn(); const ref = createRef(); render(
Test ref
); renderHook(() => useOnClickOutside([ref], callback)); - userEvent.click(body); + await user.click(body); expect(callback).toHaveBeenCalledTimes(1); }); - it("does not execute callback when clicking inside element", () => { + it("does not execute callback when clicking inside element", async () => { + const user = userEvent.setup(); const callback = jest.fn(); const ref = createRef(); render(
Test ref
); renderHook(() => useOnClickOutside([ref], callback)); - userEvent.click(screen.getByText("Test ref")); + await user.click(screen.getByText("Test ref")); expect(callback).not.toHaveBeenCalled(); }); }); describe("useOnClickOutside with multiple refs", () => { - it("executes callback when clicking outside elements", () => { + it("executes callback when clicking outside elements", async () => { + const user = userEvent.setup(); const body = document.body as HTMLElement; const callback = jest.fn(); const ref1 = createRef(); @@ -39,10 +41,11 @@ describe("useOnClickOutside", () => { ); renderHook(() => useOnClickOutside([ref1, ref2], callback)); - userEvent.click(body); + await user.click(body); expect(callback).toHaveBeenCalledTimes(1); }); - it("does not execute callback when clicking inside elements", () => { + it("does not execute callback when clicking inside elements", async () => { + const user = userEvent.setup(); const callback = jest.fn(); const ref1 = createRef(); const ref2 = createRef(); @@ -54,9 +57,9 @@ describe("useOnClickOutside", () => { ); renderHook(() => useOnClickOutside([ref1, ref2], callback)); - userEvent.click(screen.getByText("Test ref 1")); + await user.click(screen.getByText("Test ref 1")); expect(callback).not.toHaveBeenCalled(); - userEvent.click(screen.getByText("Test ref 2")); + await user.click(screen.getByText("Test ref 2")); expect(callback).not.toHaveBeenCalled(); }); }); diff --git a/src/hooks/tests/usePageVisibility.test.tsx b/src/hooks/tests/usePageVisibility.test.tsx index a2b8334334..0444060b6c 100644 --- a/src/hooks/tests/usePageVisibility.test.tsx +++ b/src/hooks/tests/usePageVisibility.test.tsx @@ -1,7 +1,7 @@ import { MockedProvider } from "@apollo/client/testing"; -import { act, renderHook } from "@testing-library/react-hooks/dom"; import { getUserMock } from "gql/mocks/getUser"; import { usePageVisibility } from "hooks"; +import { act, renderHook } from "test_utils"; const Provider = ({ children }) => ( {children} diff --git a/src/hooks/tests/usePolling.test.tsx b/src/hooks/tests/usePolling.test.tsx index 007fb73d6d..0ed522aab9 100644 --- a/src/hooks/tests/usePolling.test.tsx +++ b/src/hooks/tests/usePolling.test.tsx @@ -1,9 +1,9 @@ import { MockedProvider } from "@apollo/client/testing"; -import { renderHook, act } from "@testing-library/react-hooks/dom"; import Cookie from "js-cookie"; import { FASTER_POLL_INTERVAL, DEFAULT_POLL_INTERVAL } from "constants/index"; import { getUserMock } from "gql/mocks/getUser"; import { usePolling } from "hooks"; +import { renderHook, act } from "test_utils"; jest.mock("js-cookie"); diff --git a/src/hooks/tests/useTableFilters/useTableFilters.test.tsx b/src/hooks/tests/useTableFilters/useTableFilters.test.tsx index cc4965a2ab..3e94ad1fe8 100644 --- a/src/hooks/tests/useTableFilters/useTableFilters.test.tsx +++ b/src/hooks/tests/useTableFilters/useTableFilters.test.tsx @@ -6,6 +6,7 @@ import { queryString } from "utils"; describe("useTableInputFilter", () => { it("accepts an input value", async () => { + const user = userEvent.setup(); render(, { route: "/hosts?hostId=123", path: "/hosts", @@ -16,10 +17,10 @@ describe("useTableInputFilter", () => { // starts with initial url params as value expect(input.value).toBe("123"); - userEvent.clear(input); - userEvent.type(input, "abc"); + await user.clear(input); + await user.type(input, "abc"); expect(input).toHaveValue("abc"); - userEvent.type(input, "{enter}"); + await user.type(input, "{enter}"); // returns updates value when component changes expect(input.value).toBe("abc"); @@ -27,31 +28,33 @@ describe("useTableInputFilter", () => { // updates url query params when update fn is called screen.getByText("host id from url: abc"); - userEvent.clear(input); + await user.clear(input); expect(input).toHaveValue(""); - userEvent.type(input, "{enter}"); + await user.type(input, "{enter}"); // resets url query params when reset fn is called expect(input.value).toBe(""); expect(screen.getByText("host id from url: N/A")).toBeInTheDocument(); }); - it("useTableInputFilter - trims whitespace from input value", () => { + it("useTableInputFilter - trims whitespace from input value", async () => { + const user = userEvent.setup(); render(, { route: "/hosts?hostId=123", path: "/hosts", }); const input = screen.getByPlaceholderText("Search ID") as HTMLInputElement; - userEvent.clear(input); - userEvent.type(input, " abc "); - userEvent.type(input, "{enter}"); + await user.clear(input); + await user.type(input, " abc "); + await user.type(input, "{enter}"); expect(screen.getByText("host id from url: abc")).toBeInTheDocument(); }); }); describe("useTableCheckboxFilter", () => { it("useTableCheckboxFilter", async () => { + const user = userEvent.setup(); render(, { route: "/hosts?statuses=running,terminated", path: "/hosts", @@ -69,7 +72,7 @@ describe("useTableCheckboxFilter", () => { expect(terminatedCheckbox.checked).toBe(true); // returns updates value when component changes - userEvent.click(runningCheckbox); + await user.click(screen.getByText("Running")); // LeafyGreen checkbox has pointer-events: none so click on the label instead. // updates url query params when update fn is called expect(runningCheckbox.checked).toBe(false); @@ -80,7 +83,7 @@ describe("useTableCheckboxFilter", () => { ).toBeInTheDocument(); // resets url query params when reset fn is called - userEvent.click(terminatedCheckbox); + await user.click(screen.getByText("Terminated")); // LeafyGreen checkbox has pointer-events: none so click on the label instead. expect(runningCheckbox.checked).toBe(false); expect(terminatedCheckbox.checked).toBe(false); diff --git a/src/hooks/tests/useUpdateUrlSortParamOnTableChange/useUpdateUrlSortParamOnTableChange.test.tsx b/src/hooks/tests/useUpdateUrlSortParamOnTableChange/useUpdateUrlSortParamOnTableChange.test.tsx index 44078aa6c3..3debd0c052 100644 --- a/src/hooks/tests/useUpdateUrlSortParamOnTableChange/useUpdateUrlSortParamOnTableChange.test.tsx +++ b/src/hooks/tests/useUpdateUrlSortParamOnTableChange/useUpdateUrlSortParamOnTableChange.test.tsx @@ -16,7 +16,8 @@ describe("useUpdateUrlSortParamOnTableChange", () => { matchMedia.clear(); }); - it("toggles table headers when clicked", () => { + it("toggles table headers when clicked", async () => { + const user = userEvent.setup(); render(, { route: "/hosts", path: "/hosts", @@ -25,22 +26,22 @@ describe("useUpdateUrlSortParamOnTableChange", () => { const idHeader = screen.getByText("ID"); const statusHeader = screen.getByText("Status"); - userEvent.click(idHeader); + await user.click(idHeader); expect(screen.getByText("sortBy: ID")).toBeInTheDocument(); expect(screen.getByText("sortDir: ASC")).toBeInTheDocument(); - userEvent.click(statusHeader); + await user.click(statusHeader); expect(screen.getByText("sortBy: STATUS")).toBeInTheDocument(); expect(screen.getByText("sortDir: ASC")).toBeInTheDocument(); - userEvent.click(statusHeader); + await user.click(statusHeader); expect(screen.getByText("sortBy: STATUS")).toBeInTheDocument(); expect(screen.getByText("sortDir: DESC")).toBeInTheDocument(); - userEvent.click(statusHeader); + await user.click(statusHeader); expect(screen.getByText("sortBy: none")).toBeInTheDocument(); expect(screen.getByText("sortDir: none")).toBeInTheDocument(); diff --git a/src/hooks/tests/useUpsertQueryParams.test.tsx b/src/hooks/tests/useUpsertQueryParams.test.tsx index e56cf97231..850639243c 100644 --- a/src/hooks/tests/useUpsertQueryParams.test.tsx +++ b/src/hooks/tests/useUpsertQueryParams.test.tsx @@ -33,65 +33,67 @@ const Content = () => { describe("useUpsertQueryParams", () => { it("renders normally and doesn't affect the url", () => { const { router } = renderWithRouterMatch(); - expect(router.state.location.search).toBe(""); }); - it("should add input query params to the url if none exist", () => { + it("should add input query params to the url if none exist", async () => { + const user = userEvent.setup(); const { router } = renderWithRouterMatch(); const category = screen.queryByDataCy("category"); const value = screen.queryByDataCy("value"); - userEvent.type(category, "category"); - userEvent.type(value, "value"); - userEvent.click(screen.queryByDataCy("submit")); - + await user.type(category, "category"); + await user.type(value, "value"); + await user.click(screen.queryByDataCy("submit")); expect(router.state.location.search).toBe(`?category=value`); }); - it("should add multiple input filters to the same key as query params", () => { + it("should add multiple input filters to the same key as query params", async () => { + const user = userEvent.setup(); const { router } = renderWithRouterMatch(); const category = screen.queryByDataCy("category"); const value = screen.queryByDataCy("value"); - userEvent.type(category, "category"); - userEvent.type(value, "value1"); - userEvent.click(screen.queryByDataCy("submit")); + await user.type(category, "category"); + await user.type(value, "value1"); + await user.click(screen.queryByDataCy("submit")); expect(router.state.location.search).toBe(`?category=value1`); - userEvent.clear(value); - userEvent.type(value, "value2"); - userEvent.click(screen.queryByDataCy("submit")); + await user.clear(value); + await user.type(value, "value2"); + await user.click(screen.queryByDataCy("submit")); expect(router.state.location.search).toBe(`?category=value1,value2`); }); - it("should not allow duplicate input filters for the same key as query params", () => { + it("should not allow duplicate input filters for the same key as query params", async () => { + const user = userEvent.setup(); const { router } = renderWithRouterMatch(); const category = screen.queryByDataCy("category"); const value = screen.queryByDataCy("value"); - userEvent.type(category, "category"); - userEvent.type(value, "value1"); - userEvent.click(screen.queryByDataCy("submit")); + await user.type(category, "category"); + await user.type(value, "value1"); + await user.click(screen.queryByDataCy("submit")); expect(router.state.location.search).toBe(`?category=value1`); - userEvent.clear(value); - userEvent.type(value, "value1"); - userEvent.click(screen.queryByDataCy("submit")); + await user.clear(value); + await user.type(value, "value1"); + await user.click(screen.queryByDataCy("submit")); expect(router.state.location.search).toBe(`?category=value1`); }); it("should allow multiple input filters for different keys as query params", async () => { + const user = userEvent.setup(); const { router } = renderWithRouterMatch(); const category = screen.queryByDataCy("category"); const value = screen.queryByDataCy("value"); - userEvent.type(category, "category"); - userEvent.type(value, "value1"); - userEvent.click(screen.queryByDataCy("submit")); + await user.type(category, "category"); + await user.type(value, "value1"); + await user.click(screen.queryByDataCy("submit")); expect(router.state.location.search).toBe(`?category=value1`); - userEvent.clear(category); - userEvent.type(category, "category2"); - userEvent.click(screen.queryByDataCy("submit")); + await user.clear(category); + await user.type(category, "category2"); + await user.click(screen.queryByDataCy("submit")); expect(router.state.location.search).toBe( `?category=value1&category2=value1` ); diff --git a/src/hooks/tests/useVersionStatusSelect.test.ts b/src/hooks/tests/useVersionStatusSelect.test.ts index 34493e9ca0..2b7ced9beb 100644 --- a/src/hooks/tests/useVersionStatusSelect.test.ts +++ b/src/hooks/tests/useVersionStatusSelect.test.ts @@ -1,5 +1,5 @@ -import { renderHook, act } from "@testing-library/react-hooks"; import { useVersionTaskStatusSelect } from "hooks"; +import { renderHook, act } from "test_utils"; const allFalse = { evergreen_lint_generate_lint: false, diff --git a/src/hooks/useDimensions/useDimensions.test.ts b/src/hooks/useDimensions/useDimensions.test.ts index 09fb0897a3..ec28421a0d 100644 --- a/src/hooks/useDimensions/useDimensions.test.ts +++ b/src/hooks/useDimensions/useDimensions.test.ts @@ -1,5 +1,5 @@ -import { act, renderHook } from "@testing-library/react-hooks"; import { useDimensions } from "hooks/useDimensions"; +import { act, renderHook } from "test_utils"; describe("useDimensions", () => { let listener; diff --git a/src/hooks/useIntersectionObserver/useIntersectionObserver.test.ts b/src/hooks/useIntersectionObserver/useIntersectionObserver.test.ts index a23e7f623d..58223a2239 100644 --- a/src/hooks/useIntersectionObserver/useIntersectionObserver.test.ts +++ b/src/hooks/useIntersectionObserver/useIntersectionObserver.test.ts @@ -1,4 +1,4 @@ -import { renderHook } from "@testing-library/react-hooks"; +import { renderHook } from "test_utils"; import useIntersectionObserver from "."; describe("useIntersectionObserver", () => { diff --git a/src/hooks/useKeyboardShortcut/useKeyboardShortcut.test.tsx b/src/hooks/useKeyboardShortcut/useKeyboardShortcut.test.tsx index c57dc8d72e..2a55ecea11 100644 --- a/src/hooks/useKeyboardShortcut/useKeyboardShortcut.test.tsx +++ b/src/hooks/useKeyboardShortcut/useKeyboardShortcut.test.tsx @@ -1,14 +1,11 @@ -/* eslint-disable testing-library/no-node-access */ -import { renderHook } from "@testing-library/react-hooks"; import { CharKey, ModifierKey } from "constants/keys"; -import { render, screen, userEvent } from "test_utils"; +import { renderHook, render, screen, userEvent } from "test_utils"; import useKeyboardShortcut from "."; -const { click, type } = userEvent; - describe("useKeyboardShortcut", () => { describe("multiple keys", () => { - it("should call the callback only when the exact shortcut keys are pressed", () => { + it("should call the callback only when the exact shortcut keys are pressed", async () => { + const user = userEvent.setup(); const callback = jest.fn(); renderHook(() => useKeyboardShortcut( @@ -16,17 +13,18 @@ describe("useKeyboardShortcut", () => { callback ) ); - type(document.body, "{ctrl}"); + await user.keyboard("{Control}"); expect(callback).toHaveBeenCalledTimes(0); - type(document.body, "{a}"); + await user.keyboard("{a}"); expect(callback).toHaveBeenCalledTimes(0); - type(document.body, "{ctrl}{shift}{a}{/ctrl}{/shift}"); + await user.keyboard("{Control>}{Shift>}{a}{/Control}{/Shift}"); expect(callback).toHaveBeenCalledTimes(0); - type(document.body, "{ctrl}{a}{/ctrl}"); + await user.keyboard("{Control>}{a}{/Control}"); expect(callback).toHaveBeenCalledTimes(1); }); - it("should not call the callback if an input element has focus", () => { + it("should not call the callback if an input element has focus", async () => { + const user = userEvent.setup(); const callback = jest.fn(); renderHook(() => useKeyboardShortcut( @@ -36,40 +34,43 @@ describe("useKeyboardShortcut", () => { ); render(); - click(screen.getByDataCy("test-input")); + await user.click(screen.getByDataCy("test-input")); expect(screen.getByDataCy("test-input")).toHaveFocus(); - type(document.activeElement, "{ctrl}a{/ctrl}"); + await user.keyboard("{Control>}{a}{/Control}"); expect(callback).toHaveBeenCalledTimes(0); - expect(screen.getByDataCy("test-input")).toHaveValue("a"); + expect(screen.getByDataCy("test-input")).toHaveValue(""); }); }); describe("single key", () => { - it("should call the callback only when the exact shortcut key is pressed", () => { + it("should call the callback only when the exact shortcut key is pressed", async () => { + const user = userEvent.setup(); const callback = jest.fn(); renderHook(() => useKeyboardShortcut({ charKey: CharKey.A }, callback)); - type(document.activeElement, "{ctrl}{A}{/ctrl}"); + await user.keyboard("{Control>}{A}{/Control}"); expect(callback).toHaveBeenCalledTimes(0); - type(document.activeElement, "{ctrl}{shift}{a}{/ctrl}{/shift}"); + await user.keyboard("{Control>}{Shift>}{a}{/Control}{/Shift}"); expect(callback).toHaveBeenCalledTimes(0); - type(document.activeElement, "{a}"); + await user.keyboard("{a}"); expect(callback).toHaveBeenCalledTimes(1); }); - it("should not call the callback if an input element has focus", () => { + it("should not call the callback if an input element has focus", async () => { + const user = userEvent.setup(); const callback = jest.fn(); renderHook(() => useKeyboardShortcut({ charKey: CharKey.A }, callback)); render(); - click(screen.getByDataCy("test-input")); + await user.click(screen.getByDataCy("test-input")); expect(screen.getByDataCy("test-input")).toHaveFocus(); - type(document.activeElement, "a"); + await user.keyboard("{a}"); expect(callback).toHaveBeenCalledTimes(0); expect(screen.getByDataCy("test-input")).toHaveValue("a"); }); }); - it("should call the callback if an input element has focus and ignoreFocus is enabled", () => { + it("should call the callback if an input element has focus and ignoreFocus is enabled", async () => { + const user = userEvent.setup(); const callback = jest.fn(); renderHook(() => useKeyboardShortcut( @@ -81,13 +82,14 @@ describe("useKeyboardShortcut", () => { ) ); render(); - click(screen.getByDataCy("test-input")); + await user.click(screen.getByDataCy("test-input")); expect(screen.getByDataCy("test-input")).toHaveFocus(); - type(document.activeElement, "{ctrl}{a}{/ctrl}"); + await user.keyboard("{Control>}{a}{/Control}"); expect(callback).toHaveBeenCalledTimes(1); }); - it("should not call the callback if the component is disabled", () => { + it("should not call the callback if the component is disabled", async () => { + const user = userEvent.setup(); const callback = jest.fn(); renderHook(() => useKeyboardShortcut( @@ -98,7 +100,7 @@ describe("useKeyboardShortcut", () => { } ) ); - type(document.activeElement, "{a}"); + await user.keyboard("{a}"); expect(callback).toHaveBeenCalledTimes(0); }); diff --git a/src/hooks/useKonamiCode/useKonamiCode.test.tsx b/src/hooks/useKonamiCode/useKonamiCode.test.tsx index 6dab243097..0bd09fbcad 100644 --- a/src/hooks/useKonamiCode/useKonamiCode.test.tsx +++ b/src/hooks/useKonamiCode/useKonamiCode.test.tsx @@ -1,12 +1,11 @@ import { MockedProvider } from "@apollo/client/testing"; import userEvent from "@testing-library/user-event"; -import { act } from "react-dom/test-utils"; import { RenderFakeToastContext } from "context/toast/__mocks__"; import { TaskQuery, TaskQueryVariables } from "gql/generated/types"; import { cache } from "gql/GQLWrapper"; import { taskQuery } from "gql/mocks/taskData"; import { GET_TASK } from "gql/queries"; -import { render, waitFor, screen } from "test_utils"; +import { render, screen } from "test_utils"; import useKonamiCode from "."; const KonamiCodeWrapper = ({ gqlCache }) => ( @@ -42,22 +41,16 @@ describe("useKonamiCode", () => { }, }); + const user = userEvent.setup(); const { Component, dispatchToast } = RenderFakeToastContext( ); - render(); - // eslint-disable-next-line testing-library/no-unnecessary-act - act(() => { - userEvent.type( - document.body, - "ArrowUpArrowUpArrowDownArrowDownArrowLeftArrowRightArrowLeftArrowRightba" - ); - }); - await waitFor(() => { - expect(audioPlayMock).toHaveBeenCalledTimes(1); - }); + await user.keyboard( + "{ArrowUp}{ArrowUp}{ArrowDown}{ArrowDown}{ArrowLeft}{ArrowRight}{ArrowLeft}{ArrowRight}{b}{a}" + ); + expect(audioPlayMock).toHaveBeenCalledTimes(1); expect(dispatchToast.success).toHaveBeenCalledWith( "To reset just refresh the page", true, @@ -90,22 +83,16 @@ describe("useKonamiCode", () => { }, }); + const user = userEvent.setup(); const { Component, dispatchToast } = RenderFakeToastContext( ); - render(); - // eslint-disable-next-line testing-library/no-unnecessary-act - act(() => { - userEvent.type( - document.body, - "ArrowUpArrowUpArrowDownArrowDownArrowLeftArrowRightArrowLeftArrowRightbb" - ); - }); - await waitFor(() => { - expect(audioPlayMock).toHaveBeenCalledTimes(0); - }); + await user.keyboard( + "{ArrowUp}{ArrowUp}{ArrowDown}{ArrowDown}{ArrowLeft}{ArrowRight}{ArrowLeft}{ArrowRight}{b}{b}" + ); + expect(audioPlayMock).toHaveBeenCalledTimes(0); expect(dispatchToast.success).not.toHaveBeenCalled(); expect( cache.extract()[ @@ -134,22 +121,18 @@ describe("useKonamiCode", () => { }, }); + const user = userEvent.setup(); const { Component, dispatchToast } = RenderFakeToastContext( ); - render(); - // eslint-disable-next-line testing-library/no-unnecessary-act - act(() => { - userEvent.type( - screen.getByRole("textbox"), - "ArrowUpArrowUpArrowDownArrowDownArrowLeftArrowRightArrowLeftArrowRightba" - ); - }); - await waitFor(() => { - expect(audioPlayMock).toHaveBeenCalledTimes(0); - }); + await user.type( + screen.getByRole("textbox"), + "{ArrowUp}{ArrowUp}{ArrowDown}{ArrowDown}{ArrowLeft}{ArrowRight}{ArrowLeft}{ArrowRight}{b}{a}" + ); + expect(screen.getByRole("textbox")).toHaveValue("ba"); + expect(audioPlayMock).toHaveBeenCalledTimes(0); expect(dispatchToast.success).not.toHaveBeenCalled(); expect( cache.extract()[ diff --git a/src/hooks/useQueryParam/useQueryParam.test.tsx b/src/hooks/useQueryParam/useQueryParam.test.tsx index f326801ea2..1c2ac31c2a 100644 --- a/src/hooks/useQueryParam/useQueryParam.test.tsx +++ b/src/hooks/useQueryParam/useQueryParam.test.tsx @@ -1,5 +1,5 @@ -import { act, renderHook } from "@testing-library/react-hooks"; import { MemoryRouter } from "react-router-dom"; +import { act, renderHook } from "test_utils"; import { useQueryParam, useQueryParams } from "."; describe("useQueryParams", () => { diff --git a/src/hooks/useResize/useResize.test.ts b/src/hooks/useResize/useResize.test.ts index 266006b188..6fd280801e 100644 --- a/src/hooks/useResize/useResize.test.ts +++ b/src/hooks/useResize/useResize.test.ts @@ -1,4 +1,4 @@ -import { renderHook, act } from "@testing-library/react-hooks"; +import { renderHook, act } from "test_utils"; import { useResize } from "."; describe("useResize", () => { diff --git a/src/hooks/useTabShortcut/useTabShortcut.test.ts b/src/hooks/useTabShortcut/useTabShortcut.test.ts index c0271c1663..868fa17a7d 100644 --- a/src/hooks/useTabShortcut/useTabShortcut.test.ts +++ b/src/hooks/useTabShortcut/useTabShortcut.test.ts @@ -1,51 +1,49 @@ -/* eslint-disable testing-library/no-node-access */ -import { renderHook } from "@testing-library/react-hooks"; -import { userEvent } from "test_utils"; +import { renderHook, userEvent } from "test_utils"; import { useTabShortcut } from "."; -const { type } = userEvent; - describe("useTabShortcut", () => { - it("should call setSelectedTab with the next tab index when the 'j' key is pressed", () => { + it("should call setSelectedTab with the next tab index when the 'j' key is pressed", async () => { + const user = userEvent.setup(); const setSelectedTab = jest.fn(); let currentTab = 1; const { rerender } = renderHook(() => useTabShortcut({ setSelectedTab, currentTab, numTabs: 4 }) ); - type(document.body, "j"); + await user.keyboard("{j}"); expect(setSelectedTab).toHaveBeenCalledWith(2); currentTab = 2; rerender(); - type(document.body, "j"); + await user.keyboard("{j}"); expect(setSelectedTab).toHaveBeenCalledWith(3); currentTab = 3; rerender(); - type(document.body, "j"); + await user.keyboard("{j}"); expect(setSelectedTab).toHaveBeenCalledWith(0); currentTab = 0; rerender(); - type(document.body, "j"); + await user.keyboard("{j}"); expect(setSelectedTab).toHaveBeenCalledWith(1); }); - it("should call setSelectedTab with the previous tab index when the 'k' key is pressed", () => { + it("should call setSelectedTab with the previous tab index when the 'k' key is pressed", async () => { + const user = userEvent.setup(); const setSelectedTab = jest.fn(); let currentTab = 1; const { rerender } = renderHook(() => useTabShortcut({ setSelectedTab, currentTab, numTabs: 4 }) ); - type(document.body, "k"); + await user.keyboard("{k}"); expect(setSelectedTab).toHaveBeenCalledWith(0); currentTab = 0; rerender(); - type(document.body, "k"); + await user.keyboard("{k}"); expect(setSelectedTab).toHaveBeenCalledWith(3); currentTab = 3; rerender(); - type(document.body, "k"); + await user.keyboard("{k}"); expect(setSelectedTab).toHaveBeenCalledWith(2); currentTab = 2; rerender(); - type(document.body, "k"); + await user.keyboard("{k}"); expect(setSelectedTab).toHaveBeenCalledWith(1); }); }); diff --git a/src/pages/commits/ActiveCommits/BuildVariantCard/BuildVariantCard.test.tsx b/src/pages/commits/ActiveCommits/BuildVariantCard/BuildVariantCard.test.tsx index 800ec3034f..0b940d446c 100644 --- a/src/pages/commits/ActiveCommits/BuildVariantCard/BuildVariantCard.test.tsx +++ b/src/pages/commits/ActiveCommits/BuildVariantCard/BuildVariantCard.test.tsx @@ -3,12 +3,7 @@ import { injectGlobalDimStyle, removeGlobalDimStyle, } from "pages/commits/ActiveCommits/utils"; -import { - renderWithRouterMatch as render, - screen, - userEvent, - waitFor, -} from "test_utils"; +import { renderWithRouterMatch as render, screen, userEvent } from "test_utils"; import { BuildVariantCard } from "."; jest.mock("../utils"); @@ -37,6 +32,7 @@ describe("buildVariantCard", () => { (removeGlobalDimStyle as jest.Mock).mockImplementationOnce(() => {}); + const user = userEvent.setup(); render( { ); - userEvent.hover(screen.queryByDataCy("build-variant-icon-container")); - await waitFor(() => { - expect(injectGlobalDimStyle).toHaveBeenCalledTimes(1); - }); - - userEvent.unhover(screen.queryByDataCy("build-variant-icon-container")); - await waitFor(() => { - expect(removeGlobalDimStyle).toHaveBeenCalledTimes(1); - }); + await user.hover(screen.queryByDataCy("build-variant-icon-container")); + expect(injectGlobalDimStyle).toHaveBeenCalledTimes(1); + await user.unhover(screen.queryByDataCy("build-variant-icon-container")); + expect(removeGlobalDimStyle).toHaveBeenCalledTimes(1); }); }); diff --git a/src/pages/commits/ActiveCommits/BuildVariantCard/WaterfallTaskStatusIcon/WaterfallTaskStatusIcon.test.tsx b/src/pages/commits/ActiveCommits/BuildVariantCard/WaterfallTaskStatusIcon/WaterfallTaskStatusIcon.test.tsx index f5ce09d06d..ddeb0d5c2f 100644 --- a/src/pages/commits/ActiveCommits/BuildVariantCard/WaterfallTaskStatusIcon/WaterfallTaskStatusIcon.test.tsx +++ b/src/pages/commits/ActiveCommits/BuildVariantCard/WaterfallTaskStatusIcon/WaterfallTaskStatusIcon.test.tsx @@ -43,8 +43,9 @@ const Content = ({ ); describe("waterfallTaskStatusIcon", () => { it("tooltip should contain task name, duration, list of failing test names and additonal test count", async () => { + const user = userEvent.setup(); render(); - userEvent.hover(screen.queryByDataCy("waterfall-task-status-icon")); + await user.hover(screen.queryByDataCy("waterfall-task-status-icon")); await waitFor(() => { expect( screen.queryByDataCy("waterfall-task-status-icon-tooltip") @@ -80,6 +81,7 @@ describe("waterfallTaskStatusIcon", () => { }); it("should call the appropriate functions on hover and unhover", async () => { + const user = userEvent.setup(); (injectGlobalHighlightStyle as jest.Mock).mockImplementationOnce( (taskIdentifier: string) => { Promise.resolve(taskIdentifier); @@ -88,16 +90,14 @@ describe("waterfallTaskStatusIcon", () => { (removeGlobalHighlightStyle as jest.Mock).mockImplementationOnce(() => {}); render(); - userEvent.hover(screen.queryByDataCy("waterfall-task-status-icon")); + await user.hover(screen.queryByDataCy("waterfall-task-status-icon")); await waitFor(() => { expect(injectGlobalHighlightStyle).toHaveBeenCalledTimes(1); }); expect(injectGlobalHighlightStyle).toHaveBeenCalledWith(props.identifier); - userEvent.unhover(screen.queryByDataCy("waterfall-task-status-icon")); - await waitFor(() => { - expect(removeGlobalHighlightStyle).toHaveBeenCalledTimes(1); - }); + await user.unhover(screen.queryByDataCy("waterfall-task-status-icon")); + expect(removeGlobalHighlightStyle).toHaveBeenCalledTimes(1); }); }); diff --git a/src/pages/commits/ActiveCommits/CommitBarChart/CommitBarChart.test.tsx b/src/pages/commits/ActiveCommits/CommitBarChart/CommitBarChart.test.tsx index 710bb53817..b06e7f568e 100644 --- a/src/pages/commits/ActiveCommits/CommitBarChart/CommitBarChart.test.tsx +++ b/src/pages/commits/ActiveCommits/CommitBarChart/CommitBarChart.test.tsx @@ -26,6 +26,7 @@ describe("commitChart", () => { }); it("hovering over the chart should open a tooltip", async () => { + const user = userEvent.setup(); render( { ); expect(screen.queryByDataCy("commit-chart-tooltip")).toBeNull(); - userEvent.hover(screen.queryByDataCy("commit-chart-container")); + await user.hover(screen.queryByDataCy("commit-chart-container")); await waitFor(() => { expect(screen.getByDataCy("commit-chart-tooltip")).toBeInTheDocument(); }); }); it("should show all umbrella statuses (normal and dimmed) and their counts", async () => { + const user = userEvent.setup(); render( { ); expect(screen.queryByDataCy("commit-chart-tooltip")).toBeNull(); - userEvent.hover(screen.queryByDataCy("commit-chart-container")); + await user.hover(screen.queryByDataCy("commit-chart-container")); await waitFor(() => { expect(screen.getByDataCy("commit-chart-tooltip")).toBeInTheDocument(); }); diff --git a/src/pages/commits/InactiveCommits/InactiveCommits.test.tsx b/src/pages/commits/InactiveCommits/InactiveCommits.test.tsx index c4d078b9bd..716ffbb6ba 100644 --- a/src/pages/commits/InactiveCommits/InactiveCommits.test.tsx +++ b/src/pages/commits/InactiveCommits/InactiveCommits.test.tsx @@ -35,16 +35,18 @@ describe("inactiveCommitButton", () => { }); it("clicking on the button should open a tooltip", async () => { + const user = userEvent.setup(); render(); expect(screen.queryByDataCy("inactive-commits-tooltip")).toBeNull(); - userEvent.click(screen.queryByDataCy("inactive-commits-button")); - await waitFor(() => - expect(screen.queryByDataCy("inactive-commits-tooltip")).toBeVisible() - ); + await user.click(screen.queryByDataCy("inactive-commits-button")); + await waitFor(() => { + expect(screen.queryByDataCy("inactive-commits-tooltip")).toBeVisible(); + }); }); it("should show all inactive commits if there are 3 or less commits", async () => { + const user = userEvent.setup(); render( { ); expect(screen.queryByDataCy("inactive-commits-tooltip")).toBeNull(); - userEvent.click(screen.queryByDataCy("inactive-commits-button")); - await waitFor(() => - expect(screen.queryByDataCy("inactive-commits-tooltip")).toBeVisible() - ); + await user.click(screen.queryByDataCy("inactive-commits-button")); + await waitFor(() => { + expect(screen.queryByDataCy("inactive-commits-tooltip")).toBeVisible(); + }); expect(screen.queryAllByDataCy("commit-text")).toHaveLength( MAX_COMMIT_COUNT - 1 ); @@ -63,13 +65,14 @@ describe("inactiveCommitButton", () => { }); it("should collapse commits if there are more than 3", async () => { + const user = userEvent.setup(); render(); expect(screen.queryByDataCy("inactive-commits-tooltip")).toBeNull(); - userEvent.click(screen.queryByDataCy("inactive-commits-button")); - await waitFor(() => - expect(screen.queryByDataCy("inactive-commits-tooltip")).toBeVisible() - ); + await user.click(screen.queryByDataCy("inactive-commits-button")); + await waitFor(() => { + expect(screen.queryByDataCy("inactive-commits-tooltip")).toBeVisible(); + }); expect(screen.queryAllByDataCy("commit-text")).toHaveLength( MAX_COMMIT_COUNT ); @@ -77,21 +80,22 @@ describe("inactiveCommitButton", () => { }); it("should open a modal when clicking on the hidden commits text", async () => { + const user = userEvent.setup(); render(); expect(screen.queryByDataCy("inactive-commits-tooltip")).toBeNull(); - userEvent.click(screen.queryByDataCy("inactive-commits-button")); - await waitFor(() => - expect(screen.queryByDataCy("inactive-commits-tooltip")).toBeVisible() - ); + await user.click(screen.queryByDataCy("inactive-commits-button")); + await waitFor(() => { + expect(screen.queryByDataCy("inactive-commits-tooltip")).toBeVisible(); + }); expect(screen.queryAllByDataCy("commit-text")).toHaveLength( MAX_COMMIT_COUNT ); expect(screen.queryByDataCy("inactive-commits-modal")).toBeNull(); - userEvent.click(screen.queryByDataCy("hidden-commits")); - await waitFor(() => - expect(screen.queryByDataCy("inactive-commits-modal")).toBeVisible() - ); + await user.click(screen.queryByDataCy("hidden-commits")); + await waitFor(() => { + expect(screen.queryByDataCy("inactive-commits-modal")).toBeVisible(); + }); }); it("should show unmatching label when there are filters applied", () => { diff --git a/src/pages/configurePatch/configurePatchCore/ConfigureTasks/ConfigureTasks.test.tsx b/src/pages/configurePatch/configurePatchCore/ConfigureTasks/ConfigureTasks.test.tsx index a1c01f5977..42a61d3f47 100644 --- a/src/pages/configurePatch/configurePatchCore/ConfigureTasks/ConfigureTasks.test.tsx +++ b/src/pages/configurePatch/configurePatchCore/ConfigureTasks/ConfigureTasks.test.tsx @@ -101,7 +101,8 @@ describe("configureTasks", () => { expect(checkbox).toBeInTheDocument(); expect(checkbox).toBePartiallyChecked(); }); - it("selecting a task should call setSelectedBuildVariantTasks with the correct arguments selecting only that task", () => { + it("selecting a task should call setSelectedBuildVariantTasks with the correct arguments selecting only that task", async () => { + const user = userEvent.setup(); const selectedBuildVariants = ["ubuntu2004"]; const setSelectedBuildVariantTasks = jest.fn(); render( @@ -124,12 +125,13 @@ describe("configureTasks", () => { expect(checkbox).toBeInTheDocument(); expect(checkbox).not.toBeChecked(); expect(setSelectedBuildVariantTasks).not.toHaveBeenCalled(); - checkbox.click(); + await user.click(screen.getByText("compile")); expect(setSelectedBuildVariantTasks).toHaveBeenCalledWith({ ubuntu2004: { compile: true, test: false }, }); }); - it("selecting all tasks should call setSelectedBuildVariantTasks with the correct arguments selecting all of the visible tasks in one variant", () => { + it("selecting all tasks should call setSelectedBuildVariantTasks with the correct arguments selecting all of the visible tasks in one variant", async () => { + const user = userEvent.setup(); const selectedBuildVariants = ["ubuntu2004"]; const setSelectedBuildVariantTasks = jest.fn(); render( @@ -155,13 +157,14 @@ describe("configureTasks", () => { expect(checkbox).toBeInTheDocument(); expect(checkbox).not.toBeChecked(); expect(setSelectedBuildVariantTasks).not.toHaveBeenCalled(); - checkbox.click(); + await user.click(screen.getByText("Select all tasks in this variant")); expect(setSelectedBuildVariantTasks).toHaveBeenCalledWith({ ubuntu2004: { compile: true, test: true }, ubuntu1804: { compile: false, lint: false }, }); }); - it("selecting a deduplicated task should call setSelectedBuildVariantTasks selecting the task in all variants", () => { + it("selecting a deduplicated task should call setSelectedBuildVariantTasks selecting the task in all variants", async () => { + const user = userEvent.setup(); const selectedBuildVariants = ["ubuntu2004", "ubuntu1804"]; const setSelectedBuildVariantTasks = jest.fn(); render( @@ -185,13 +188,14 @@ describe("configureTasks", () => { expect(checkbox).toBeInTheDocument(); expect(checkbox).not.toBeChecked(); expect(setSelectedBuildVariantTasks).not.toHaveBeenCalled(); - checkbox.click(); + await user.click(screen.getByText("compile")); expect(setSelectedBuildVariantTasks).toHaveBeenCalledWith({ ubuntu2004: { compile: true, test: false }, ubuntu1804: { compile: true, lint: false }, }); }); - it("selecting all tasks should call setSelectedBuildVariantTasks with the correct arguments selecting all of the visible tasks in multiple variants", () => { + it("selecting all tasks should call setSelectedBuildVariantTasks with the correct arguments selecting all of the visible tasks in multiple variants", async () => { + const user = userEvent.setup(); const selectedBuildVariants = ["ubuntu2004", "ubuntu1804"]; const setSelectedBuildVariantTasks = jest.fn(); render( @@ -217,13 +221,14 @@ describe("configureTasks", () => { expect(checkbox).toBeInTheDocument(); expect(checkbox).not.toBeChecked(); expect(setSelectedBuildVariantTasks).not.toHaveBeenCalled(); - checkbox.click(); + await user.click(screen.getByText("Select all tasks in these variants")); expect(setSelectedBuildVariantTasks).toHaveBeenCalledWith({ ubuntu2004: { compile: true, test: true }, ubuntu1804: { compile: true, lint: true }, }); }); - it("applying a search should filter the tasks", () => { + it("applying a search should filter the tasks", async () => { + const user = userEvent.setup(); const selectedBuildVariants = ["ubuntu2004", "ubuntu1804"]; render( { /> ); - userEvent.type(screen.getByDataCy("task-filter-input"), "compile"); + await user.type(screen.getByDataCy("task-filter-input"), "compile"); expect(screen.queryAllByDataCy("task-checkbox")).toHaveLength(1); const checkbox = screen.getByLabelText("compile"); expect(checkbox).toBeInTheDocument(); @@ -400,7 +405,8 @@ describe("configureTasks", () => { expect(screen.getByLabelText("test")).toBeInTheDocument(); expect(screen.getByLabelText("parsley")).toBeInTheDocument(); }); - it("selecting the entire alias calls setSelectedAliases with the correct arguments", () => { + it("selecting the entire alias calls setSelectedAliases with the correct arguments", async () => { + const user = userEvent.setup(); const selectedBuildVariants = ["parsley"]; const setSelectedBuildVariantTasks = jest.fn(); const setSelectedAliases = jest.fn(); @@ -436,7 +442,7 @@ describe("configureTasks", () => { /> ); expect(screen.getByLabelText("Add alias to patch")).toBeInTheDocument(); - screen.getByLabelText("Add alias to patch").click(); + await user.click(screen.getByText("Add alias to patch")); expect(setSelectedAliases).toHaveBeenCalledWith({ parsley: true, }); diff --git a/src/pages/configurePatch/configurePatchCore/__snapshots__/ConfigurePatchCore.stories.storyshot b/src/pages/configurePatch/configurePatchCore/__snapshots__/ConfigurePatchCore.stories.storyshot index 406e57dbb7..a7935e823b 100644 --- a/src/pages/configurePatch/configurePatchCore/__snapshots__/ConfigurePatchCore.stories.storyshot +++ b/src/pages/configurePatch/configurePatchCore/__snapshots__/ConfigurePatchCore.stories.storyshot @@ -293,7 +293,7 @@ exports[`storybook Storyshots pages/configurePatch/configurePatchCore Configure autocomplete="on" class="leafygreen-ui-6i61vl" data-cy="task-filter-input" - id="textinput-238" + id="textinput-236" placeholder="Search tasks" required="" type="text" @@ -309,17 +309,17 @@ exports[`storybook Storyshots pages/configurePatch/configurePatchCore Configure >