github.com/tilt-dev/tilt@v0.36.0/web/src/ResourceNav.test.tsx (about) 1 import { render, RenderOptions, screen } from "@testing-library/react" 2 import userEvent from "@testing-library/user-event" 3 import React, { ChangeEvent, useState } from "react" 4 import { act } from "react-dom/test-utils" 5 import { MemoryRouter } from "react-router" 6 import { useLocation, useNavigate } from "react-router-dom" 7 import { ResourceNavProvider, useResourceNav } from "./ResourceNav" 8 import { ResourceName } from "./types" 9 10 const INVALID_RESOURCE = "res3" 11 12 function TestResourceNavConsumer() { 13 const { selectedResource, invalidResource, openResource } = useResourceNav() 14 const [resourceToSelect, setResourceToSelect] = useState("") 15 16 return ( 17 <> 18 <p aria-label="selectedResource">{selectedResource}</p> 19 <p aria-label="invalidResource">{invalidResource}</p> 20 <input 21 aria-label="Resource to select" 22 type="text" 23 value={resourceToSelect} 24 onChange={(e: ChangeEvent<HTMLInputElement>) => 25 setResourceToSelect(e.target.value) 26 } 27 /> 28 <button onClick={() => openResource(resourceToSelect)}> 29 openResource 30 </button> 31 </> 32 ) 33 } 34 35 let location: any = window.location 36 let navigate: any = null 37 38 function LocationCapture() { 39 location = useLocation() 40 navigate = useNavigate() 41 return null 42 } 43 44 function customRender( 45 wrapperOptions: { 46 initialEntries?: string[] 47 validateOverride?: (name: string) => boolean 48 } = {}, 49 options?: RenderOptions 50 ) { 51 const validateResource = 52 wrapperOptions.validateOverride ?? 53 function (name: string) { 54 return name !== INVALID_RESOURCE 55 } 56 return render(<TestResourceNavConsumer />, { 57 wrapper: ({ children }) => ( 58 <MemoryRouter initialEntries={wrapperOptions.initialEntries || ["/"]}> 59 <ResourceNavProvider validateResource={validateResource}> 60 {children} 61 </ResourceNavProvider> 62 <LocationCapture /> 63 </MemoryRouter> 64 ), 65 ...options, 66 }) 67 } 68 69 describe("ResourceNavContext", () => { 70 it("navigates to resource on click", () => { 71 customRender() 72 73 expect(screen.getByLabelText("selectedResource")).toHaveTextContent("") 74 75 userEvent.type(screen.getByRole("textbox"), "res1") 76 userEvent.click(screen.getByRole("button", { name: "openResource" })) 77 78 expect(screen.getByLabelText("selectedResource")).toHaveTextContent("res1") 79 expect(location.pathname.endsWith("/r/res1/overview")).toBe(true) 80 }) 81 82 it("filters resources that don't validate", () => { 83 customRender({ initialEntries: [`/r/${INVALID_RESOURCE}/overview`] }) 84 85 expect(screen.getByLabelText("selectedResource")).toHaveTextContent("") 86 expect(screen.getByLabelText("invalidResource")).toHaveTextContent( 87 INVALID_RESOURCE 88 ) 89 }) 90 91 it("always validates the 'all' resource", () => { 92 customRender({ 93 initialEntries: [`/r/${ResourceName.all}/overview`], 94 validateOverride: (_name: string) => false, 95 }) 96 97 expect(screen.getByLabelText("selectedResource")).toHaveTextContent( 98 ResourceName.all 99 ) 100 expect(screen.getByLabelText("invalidResource")).toHaveTextContent("") 101 }) 102 103 it("encodes resource names", () => { 104 customRender() 105 106 userEvent.type(screen.getByRole("textbox"), "foo/bar") 107 userEvent.click(screen.getByRole("button", { name: "openResource" })) 108 109 expect(screen.getByLabelText("selectedResource")).toHaveTextContent( 110 "foo/bar" 111 ) 112 expect(location.pathname.endsWith("/r/foo%2Fbar/overview")).toBe(true) 113 }) 114 115 it("preserves filters by resource", () => { 116 customRender() 117 118 let nav = (res: string) => { 119 userEvent.clear(screen.getByRole("textbox")) 120 userEvent.type(screen.getByRole("textbox"), res) 121 userEvent.click(screen.getByRole("button", { name: "openResource" })) 122 } 123 124 // We can't directly check the MemoryRouter's location, so just check the selectedResource label 125 nav("foo") 126 expect(screen.getByLabelText("selectedResource")).toHaveTextContent("foo") 127 // Simulate navigation with query param 128 act(() => navigate("/r/foo/overview?term=hi")) 129 nav("bar") 130 expect(screen.getByLabelText("selectedResource")).toHaveTextContent("bar") 131 nav("foo") 132 expect(screen.getByLabelText("selectedResource")).toHaveTextContent("foo") 133 }) 134 135 // Make sure that useResourceNav() doesn't break memoization. 136 it("memoizes renders", () => { 137 let renderCount = 0 138 let FakeEl = React.memo(() => { 139 useResourceNav() 140 renderCount++ 141 return <div></div> 142 }) 143 144 let validateResource = () => true 145 let { rerender } = render( 146 <MemoryRouter> 147 <ResourceNavProvider validateResource={validateResource}> 148 <FakeEl /> 149 </ResourceNavProvider> 150 </MemoryRouter> 151 ) 152 153 expect(renderCount).toEqual(1) 154 155 // Make sure we don't re-render on a no-op update. 156 rerender( 157 <MemoryRouter> 158 <ResourceNavProvider validateResource={validateResource}> 159 <FakeEl /> 160 </ResourceNavProvider> 161 <LocationCapture /> 162 </MemoryRouter> 163 ) 164 expect(renderCount).toEqual(1) 165 166 // Make sure we do re-render on a real location update. 167 act(() => navigate("/r/foo")) 168 expect(renderCount).toEqual(2) 169 }) 170 })