diff --git a/web/src/AccountMenu.test.tsx b/web/src/AccountMenu.test.tsx index d725b241da..6f7a429349 100644 --- a/web/src/AccountMenu.test.tsx +++ b/web/src/AccountMenu.test.tsx @@ -1,11 +1,7 @@ -import { mount } from "enzyme" +import { render, screen } from "@testing-library/react" import React from "react" import ReactModal from "react-modal" -import { - AccountMenuContent, - MenuContentButtonSignUp, - MenuContentButtonTiltCloud, -} from "./AccountMenu" +import { AccountMenuContent } from "./AccountMenu" beforeEach(() => { // Note: `body` is used as the app element _only_ in a test env @@ -15,7 +11,7 @@ beforeEach(() => { }) it("renders Sign Up button when user is not signed in", () => { - const root = mount( + render( { /> ) - expect(root.find(MenuContentButtonSignUp)).toHaveLength(1) + expect( + screen.getByRole("button", { name: "Link Tilt to Tilt Cloud" }) + ).toBeInTheDocument() }) it("renders TiltCloud button when user is signed in", () => { - const root = mount( + render( { isSnapshot={false} /> ) - - expect(root.find(MenuContentButtonTiltCloud)).toHaveLength(1) + expect( + screen.getByRole("link", { name: "View Tilt Cloud" }) + ).toBeInTheDocument() }) diff --git a/web/src/BrowerStorage.test.tsx b/web/src/BrowerStorage.test.tsx index 9695c6a7ee..b9137152fd 100644 --- a/web/src/BrowerStorage.test.tsx +++ b/web/src/BrowerStorage.test.tsx @@ -1,4 +1,4 @@ -import { mount } from "enzyme" +import { render, screen } from "@testing-library/react" import React from "react" import { makeKey, @@ -23,7 +23,7 @@ describe("localStorageContext", () => { return null } - mount( + render( @@ -41,18 +41,20 @@ describe("localStorageContext", () => { ) function ValueGetter() { - const [value, setValue] = usePersistentState( + const [value, _setValue] = usePersistentState( "test-key", "initial" ) - return

{value}

+ return

{value}

} - let root = mount( + render( ) - expect(root.find("p").text()).toEqual("test-read-value") + expect(screen.getByLabelText("saved value")).toHaveTextContent( + "test-read-value" + ) }) }) diff --git a/web/src/ClearLogs.test.tsx b/web/src/ClearLogs.test.tsx index f4f4f67d48..877495da01 100644 --- a/web/src/ClearLogs.test.tsx +++ b/web/src/ClearLogs.test.tsx @@ -1,4 +1,5 @@ -import { mount } from "enzyme" +import { render, screen } from "@testing-library/react" +import userEvent from "@testing-library/user-event" import React from "react" import { AnalyticsAction } from "./analytics" import { @@ -48,12 +49,14 @@ describe("ClearLogs", () => { it("clears all resources", () => { const logStore = createPopulatedLogStore() - const root = mount( + render( ) - root.find(ClearLogs).simulate("click") + + userEvent.click(screen.getByRole("button")) + expect(logStore.spans).toEqual({}) expect(logStore.allLog()).toHaveLength(0) @@ -65,17 +68,20 @@ describe("ClearLogs", () => { it("clears a specific resource", () => { const logStore = createPopulatedLogStore() - const root = mount( + render( ) - root.find(ClearLogs).simulate("click") + + userEvent.click(screen.getByRole("button")) + expect(Object.keys(logStore.spans).sort()).toEqual([ "_", "build:m2", "pod:m2-def456", ]) + expect(logLinesToString(logStore.allLog(), false)).toEqual( "global 1\nglobal 2\nm2 build line 1\nm2 runtime line 1" ) diff --git a/web/src/HeaderBar.test.tsx b/web/src/HeaderBar.test.tsx index 90532aedf5..06d72b7ae6 100644 --- a/web/src/HeaderBar.test.tsx +++ b/web/src/HeaderBar.test.tsx @@ -1,43 +1,57 @@ -import { fireEvent } from "@testing-library/dom" -import { mount } from "enzyme" +import { render, screen } from "@testing-library/react" +import userEvent from "@testing-library/user-event" import React from "react" import { act } from "react-dom/test-utils" import { MemoryRouter } from "react-router-dom" -import { TwoResources } from "./HeaderBar.stories" -import HelpDialog from "./HelpDialog" +import { AnalyticsType } from "./analytics" +import HeaderBar from "./HeaderBar" import { SnapshotActionTestProvider } from "./snapshot" +import { nResourceView } from "./testdata" -it("renders shortcuts dialog on ?", () => { - const root = mount( - {TwoResources()} - ) +describe("HeaderBar", () => { + describe("keyboard shortcuts", () => { + const openModal = jest.fn() - expect(root.find(HelpDialog).props().open).toEqual(false) - act(() => void fireEvent.keyDown(document.body, { key: "?" })) - root.update() - expect(root.find(HelpDialog).props().open).toEqual(true) - root.unmount() -}) + beforeEach(() => { + openModal.mockReset() + + const snapshotAction = { + enabled: true, + openModal, + } + + render( + + + + + + ) + }) + + it("opens the help dialog on '?' keypress", () => { + // Expect that the help dialog is NOT visible at start + expect(screen.queryByRole("heading", { name: /Help/i })).toBeNull() + + act(() => { + userEvent.keyboard("?") + }) + + expect(screen.getByRole("heading", { name: /Help/i })).toBeInTheDocument() + }) + + it("calls `openModal` snapshot callback on 's' keypress", () => { + expect(openModal).not.toBeCalled() + + act(() => { + userEvent.keyboard("s") + }) -it("opens snapshot modal on s", () => { - let opened = 0 - let snapshot = { - enabled: true, - openModal: () => { - opened++ - }, - } - const root = mount( - - - {TwoResources()} - - - ) - - expect(opened).toEqual(0) - act(() => void fireEvent.keyDown(document.body, { key: "s" })) - root.update() - expect(opened).toEqual(1) - root.unmount() + expect(openModal).toBeCalledTimes(1) + }) + }) }) diff --git a/web/src/HelpDialog.test.tsx b/web/src/HelpDialog.test.tsx deleted file mode 100644 index bd1341424d..0000000000 --- a/web/src/HelpDialog.test.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { mount } from "enzyme" -import React from "react" -import { MemoryRouter } from "react-router-dom" -import { DialogOverview } from "./HelpDialog.stories" - -it("renders overview dialog", () => { - mount( - - - - ) -}) diff --git a/web/src/HelpSearchBar.test.tsx b/web/src/HelpSearchBar.test.tsx index ec36976748..ed1f0b51d3 100644 --- a/web/src/HelpSearchBar.test.tsx +++ b/web/src/HelpSearchBar.test.tsx @@ -1,41 +1,42 @@ -import { mount } from "enzyme" +import { render, RenderOptions, screen } from "@testing-library/react" +import userEvent from "@testing-library/user-event" import { MemoryRouter } from "react-router" import { tiltfileKeyContext } from "./BrowserStorage" -import { ClearHelpSearchBarButton, HelpSearchBar } from "./HelpSearchBar" +import { HelpSearchBar } from "./HelpSearchBar" -const HelpSearchBarTestWrapper = () => ( - - - - - -) +function customRender(component: JSX.Element, options?: RenderOptions) { + return render( + + + {component} + + , + options + ) +} describe("HelpSearchBar", () => { it("does NOT display 'clear' button when there is NO input", () => { - const root = mount() - const button = root.find(ClearHelpSearchBarButton) - expect(button.length).toBe(0) + customRender() + + expect(screen.queryByLabelText("Clear search term")).toBeNull() }) it("displays 'clear' button when there is input", () => { - const searchTerm = "wow" - const root = mount() - const searchField = root.find("input") - searchField.simulate("change", { target: { value: searchTerm } }) + customRender() + + userEvent.type(screen.getByLabelText("Search Tilt Docs"), "wow") - const button = root.find(ClearHelpSearchBarButton) - expect(button.length).toBe(1) + expect(screen.getByLabelText("Clear search term")).toBeInTheDocument() }) it("should change the search value on input change", () => { const searchTerm = "so search" - const root = mount() - const searchField = root.find("input") - searchField.simulate("change", { target: { value: searchTerm } }) + customRender() + + userEvent.type(screen.getByLabelText("Search Tilt Docs"), searchTerm) - const searchFieldAfterChange = root.find("input") - expect(searchFieldAfterChange.prop("value")).toBe(searchTerm) + expect(screen.getByRole("textbox")).toHaveValue(searchTerm) }) it("should open search in new tab on submision", () => { @@ -46,22 +47,21 @@ describe("HelpSearchBar", () => { searchResultsPage.searchParams.set("q", searchTerm) searchResultsPage.searchParams.set("utm_source", "tiltui") - const root = mount() - const searchField = root.find("input") - searchField.simulate("change", { target: { value: searchTerm } }) - searchField.simulate("keyPress", { key: "Enter" }) + customRender() + + userEvent.type(screen.getByLabelText("Search Tilt Docs"), searchTerm) + userEvent.keyboard("{Enter}") expect(windowOpenSpy).toBeCalledWith(searchResultsPage) }) it("should clear the search value after submission", () => { const searchTerm = "much find" - const root = mount() - const searchField = root.find("input") - searchField.simulate("change", { target: { value: searchTerm } }) - searchField.simulate("keyPress", { key: "Enter" }) + customRender() + + userEvent.type(screen.getByLabelText("Search Tilt Docs"), searchTerm) + userEvent.keyboard("{Enter}") - const searchFieldAfterChange = root.find("input") - expect(searchFieldAfterChange.prop("value")).toBe("") + expect(screen.getByRole("textbox")).toHaveValue("") }) }) diff --git a/web/src/HelpSearchBar.tsx b/web/src/HelpSearchBar.tsx index cefb7c27ee..5c7619e094 100644 --- a/web/src/HelpSearchBar.tsx +++ b/web/src/HelpSearchBar.tsx @@ -67,6 +67,7 @@ export function HelpSearchBar(props: { className?: string }) { ), + "aria-label": "Search Tilt Docs", } function handleKeyPress(e: KeyboardEvent) { @@ -90,8 +91,9 @@ export function HelpSearchBar(props: { className?: string }) { - + ) @@ -99,7 +101,6 @@ export function HelpSearchBar(props: { className?: string }) { return ( { - const cleanup = () => { - localStorage.clear() - document.documentElement.style.removeProperty("--log-font-scale") - cleanupMockAnalyticsCalls() - } - beforeEach(() => { - cleanup() // CSS won't be loaded in test context, so just explicitly set it document.documentElement.style.setProperty("--log-font-scale", "100%") mockAnalyticsCalls() }) - afterEach(cleanup) + afterEach(() => { + localStorage.clear() + document.documentElement.style.removeProperty("--log-font-scale") + cleanupMockAnalyticsCalls() + }) const getCSSValue = () => getComputedStyle(document.documentElement).getPropertyValue( @@ -43,14 +39,17 @@ describe("LogsFontSize", () => { it("restores persisted font scale on load", () => { setLocalStorageValue("360%") - mount() + render() expect(getCSSValue()).toEqual("360%") }) - it("decreases font scale", () => { - const root = mount() - root.find(FontSizeDecreaseButton).simulate("click") - expect(getCSSValue()).toEqual("95%") + it("decreases font scale", async () => { + render() + userEvent.click(screen.getByLabelText("Decrease log font size")) + + await waitFor(() => { + expect(getCSSValue()).toEqual("95%") + }) expect(getLocalStorageValue()).toEqual(`95%`) // JSON serialized expectIncrs({ name: "ui.web.zoomLogs", @@ -58,18 +57,24 @@ describe("LogsFontSize", () => { }) }) - it("has a minimum font scale", () => { + it("has a minimum font scale", async () => { setLocalStorageValue(`${LogFontSizeScaleMinimumPercentage}%`) - const root = mount() - root.find(FontSizeDecreaseButton).simulate("click") - expect(getCSSValue()).toEqual("10%") + render() + userEvent.click(screen.getByLabelText("Decrease log font size")) + + await waitFor(() => { + expect(getCSSValue()).toEqual("10%") + }) expect(getLocalStorageValue()).toEqual(`10%`) }) - it("increases font scale", () => { - const root = mount() - root.find(FontSizeIncreaseButton).simulate("click") - expect(getCSSValue()).toEqual("105%") + it("increases font scale", async () => { + render() + userEvent.click(screen.getByLabelText("Increase log font size")) + + await waitFor(() => { + expect(getCSSValue()).toEqual("105%") + }) expect(getLocalStorageValue()).toEqual(`105%`) expectIncrs({ name: "ui.web.zoomLogs", diff --git a/web/src/LogActions.tsx b/web/src/LogActions.tsx index 99a4a976ae..b49dc30385 100644 --- a/web/src/LogActions.tsx +++ b/web/src/LogActions.tsx @@ -88,7 +88,7 @@ export const LogsFontSize: React.FC = () => { return ( adjustLogFontScale(-zoomStep)} analyticsName="ui.web.zoomLogs" analyticsTags={{ dir: "out" }} @@ -97,7 +97,7 @@ export const LogsFontSize: React.FC = () => { | adjustLogFontScale(zoomStep)} analyticsName="ui.web.zoomLogs" analyticsTags={{ dir: "in" }} diff --git a/web/src/instrumentedComponents.test.tsx b/web/src/instrumentedComponents.test.tsx index 598f4aaff2..b22ef1d163 100644 --- a/web/src/instrumentedComponents.test.tsx +++ b/web/src/instrumentedComponents.test.tsx @@ -1,4 +1,5 @@ -import { mount } from "enzyme" +import { render, screen } from "@testing-library/react" +import userEvent from "@testing-library/user-event" import React from "react" import { AnalyticsAction } from "./analytics" import { @@ -25,73 +26,75 @@ describe("instrumented components", () => { }) describe("instrumented button", () => { - it("performs the underlying onClick and also reports analytics", () => { - let underlyingOnClickCalled = false - const onClick = () => { - underlyingOnClickCalled = true - } - const button = mount( - + it("reports analytics with default tags and correct name", () => { + render( + Hello ) - button.simulate("click") + userEvent.click(screen.getByRole("button")) - expect(underlyingOnClickCalled).toEqual(true) expectIncrs({ name: "ui.web.foo.bar", - tags: { action: AnalyticsAction.Click, hello: "goodbye" }, + tags: { action: AnalyticsAction.Click }, }) }) - it("works without an underlying onClick", () => { - const button = mount( - + it("reports analytics with any additional custom tags", () => { + const customTags = { hello: "goodbye" } + render( + Hello ) - button.simulate("click") + userEvent.click(screen.getByRole("button")) expectIncrs({ name: "ui.web.foo.bar", - tags: { action: AnalyticsAction.Click }, + tags: { action: AnalyticsAction.Click, ...customTags }, }) }) - it("works without tags", () => { - const button = mount( - + it("invokes the click callback when provided", () => { + const onClickSpy = jest.fn() + render( + Hello ) - button.simulate("click") + expect(onClickSpy).not.toBeCalled() - expectIncrs({ - name: "ui.web.foo.bar", - tags: { action: AnalyticsAction.Click }, - }) + userEvent.click(screen.getByRole("button")) + + expect(onClickSpy).toBeCalledTimes(1) }) }) describe("instrumented TextField", () => { it("reports analytics, debounced, when edited", () => { - const root = mount( + render( ) - const tf = root.find(InstrumentedTextField).find('input[type="text"]') + + const inputField = screen.getByLabelText("Help search") // two changes in rapid succession should result in only one analytics event - tf.simulate("change", { target: { value: "foo" } }) - tf.simulate("change", { target: { value: "foobar" } }) + userEvent.type(inputField, "foo") + userEvent.type(inputField, "bar") + expectIncrs(...[]) jest.advanceTimersByTime(10000) expectIncrs({ @@ -107,28 +110,25 @@ describe("instrumented components", () => { // for each text field. it("debounces analytics for text fields on an instance-by-instance basis", () => { const halfDebounce = textFieldEditDebounceMilliseconds / 2 - const root = mount( + render( <> ) - const allInputFields = root - .find(InstrumentedTextField) - .find('input[type="text"]') - const inputField1 = allInputFields.at(0) - const inputField2 = allInputFields.at(1) // Trigger an event in the first field - inputField1.simulate("change", { target: { value: "first!" } }) + userEvent.type(screen.getByLabelText("Resource name filter"), "first!") // Expect that no analytics calls have been made, since the debounce // time for the first field has not been met @@ -136,7 +136,7 @@ describe("instrumented components", () => { expectIncrs(...[]) // Trigger an event in the second field - inputField2.simulate("change", { target: { value: "second!" } }) + userEvent.type(screen.getByLabelText("Button value"), "second!") // Expect that _only_ the first field's analytics event has occurred, // since that debounce interval has been met for the first field. @@ -166,14 +166,15 @@ describe("instrumented components", () => { describe("instrumented Checkbox", () => { it("reports analytics when clicked", () => { - const root = mount( + render( ) - const tf = root.find(InstrumentedCheckbox).find('input[type="checkbox"]') - tf.simulate("change", { target: { checked: true } }) + + userEvent.click(screen.getByLabelText("Check me")) expectIncrs({ name: "ui.web.TestCheckbox", tags: { action: AnalyticsAction.Edit, foo: "bar" },