github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/web/src/instrumentedComponents.test.tsx (about) 1 import { render, screen } from "@testing-library/react" 2 import userEvent from "@testing-library/user-event" 3 import React from "react" 4 import { AnalyticsAction } from "./analytics" 5 import { 6 cleanupMockAnalyticsCalls, 7 expectIncrs, 8 mockAnalyticsCalls, 9 } from "./analytics_test_helpers" 10 import { 11 InstrumentedButton, 12 InstrumentedCheckbox, 13 InstrumentedTextField, 14 textFieldEditDebounceMilliseconds, 15 } from "./instrumentedComponents" 16 17 describe("instrumented components", () => { 18 beforeEach(() => { 19 mockAnalyticsCalls() 20 jest.useFakeTimers() 21 }) 22 23 afterEach(() => { 24 cleanupMockAnalyticsCalls() 25 jest.useRealTimers() 26 }) 27 28 describe("instrumented button", () => { 29 it("reports analytics with default tags and correct name", () => { 30 render( 31 <InstrumentedButton analyticsName="ui.web.foo.bar"> 32 Hello 33 </InstrumentedButton> 34 ) 35 36 userEvent.click(screen.getByRole("button")) 37 38 expectIncrs({ 39 name: "ui.web.foo.bar", 40 tags: { action: AnalyticsAction.Click }, 41 }) 42 }) 43 44 it("reports analytics with any additional custom tags", () => { 45 const customTags = { hello: "goodbye" } 46 render( 47 <InstrumentedButton 48 analyticsName="ui.web.foo.bar" 49 analyticsTags={customTags} 50 > 51 Hello 52 </InstrumentedButton> 53 ) 54 55 userEvent.click(screen.getByRole("button")) 56 57 expectIncrs({ 58 name: "ui.web.foo.bar", 59 tags: { action: AnalyticsAction.Click, ...customTags }, 60 }) 61 }) 62 63 it("invokes the click callback when provided", () => { 64 const onClickSpy = jest.fn() 65 render( 66 <InstrumentedButton 67 analyticsName="ui.web.foo.bar" 68 analyticsTags={{ hello: "goodbye" }} 69 onClick={onClickSpy} 70 > 71 Hello 72 </InstrumentedButton> 73 ) 74 75 expect(onClickSpy).not.toBeCalled() 76 77 userEvent.click(screen.getByRole("button")) 78 79 expect(onClickSpy).toBeCalledTimes(1) 80 }) 81 }) 82 83 describe("instrumented TextField", () => { 84 it("reports analytics, debounced, when edited", () => { 85 render( 86 <InstrumentedTextField 87 analyticsName={"ui.web.TestTextField"} 88 analyticsTags={{ foo: "bar" }} 89 InputProps={{ "aria-label": "Help search" }} 90 /> 91 ) 92 93 const inputField = screen.getByLabelText("Help search") 94 // two changes in rapid succession should result in only one analytics event 95 userEvent.type(inputField, "foo") 96 userEvent.type(inputField, "bar") 97 98 expectIncrs(...[]) 99 jest.advanceTimersByTime(10000) 100 expectIncrs({ 101 name: "ui.web.TestTextField", 102 tags: { action: AnalyticsAction.Edit, foo: "bar" }, 103 }) 104 }) 105 106 // This test is to make sure that the debounce interval is not 107 // shared between instances of the same debounced component. 108 // When a user edits one text field and then edits another, 109 // the debounce internal should start and operate independently 110 // for each text field. 111 it("debounces analytics for text fields on an instance-by-instance basis", () => { 112 const halfDebounce = textFieldEditDebounceMilliseconds / 2 113 render( 114 <> 115 <InstrumentedTextField 116 id="resourceNameFilter" 117 analyticsName={"ui.web.resourceNameFilter"} 118 analyticsTags={{ testing: "true" }} 119 InputProps={{ "aria-label": "Resource name filter" }} 120 /> 121 <InstrumentedTextField 122 id="uibuttonInput" 123 analyticsName={"ui.web.uibutton.inputValue"} 124 analyticsTags={{ testing: "true" }} 125 InputProps={{ "aria-label": "Button value" }} 126 /> 127 </> 128 ) 129 130 // Trigger an event in the first field 131 userEvent.type(screen.getByLabelText("Resource name filter"), "first!") 132 133 // Expect that no analytics calls have been made, since the debounce 134 // time for the first field has not been met 135 jest.advanceTimersByTime(halfDebounce) 136 expectIncrs(...[]) 137 138 // Trigger an event in the second field 139 userEvent.type(screen.getByLabelText("Button value"), "second!") 140 141 // Expect that _only_ the first field's analytics event has occurred, 142 // since that debounce interval has been met for the first field. 143 // If the debounce was shared between multiple instances of the text 144 // field, this analytics call wouldn't occur. 145 jest.advanceTimersByTime(halfDebounce) 146 expectIncrs({ 147 name: "ui.web.resourceNameFilter", 148 tags: { action: AnalyticsAction.Edit, testing: "true" }, 149 }) 150 151 // Expect that the second field's analytics event has occurred, now 152 // that the debounce interval has been met for the first field 153 jest.advanceTimersByTime(halfDebounce) 154 expectIncrs( 155 { 156 name: "ui.web.resourceNameFilter", 157 tags: { action: AnalyticsAction.Edit, testing: "true" }, 158 }, 159 { 160 name: "ui.web.uibutton.inputValue", 161 tags: { action: AnalyticsAction.Edit, testing: "true" }, 162 } 163 ) 164 }) 165 }) 166 167 describe("instrumented Checkbox", () => { 168 it("reports analytics when clicked", () => { 169 render( 170 <InstrumentedCheckbox 171 analyticsName={"ui.web.TestCheckbox"} 172 analyticsTags={{ foo: "bar" }} 173 inputProps={{ "aria-label": "Check me" }} 174 /> 175 ) 176 177 userEvent.click(screen.getByLabelText("Check me")) 178 expectIncrs({ 179 name: "ui.web.TestCheckbox", 180 tags: { action: AnalyticsAction.Edit, foo: "bar" }, 181 }) 182 }) 183 }) 184 })