-
Notifications
You must be signed in to change notification settings - Fork 319
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Overhaul Venues components #3038
Open
taneliang
wants to merge
32
commits into
master
Choose a base branch
from
eliang/overhaul-venues-components
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+909
−854
Open
Changes from all commits
Commits
Show all changes
32 commits
Select commit
Hold shift + click to select a range
7948eb8
Don't memoize `VenueDetails` as it should be cheap to render
taneliang 1b81be4
Inline single-use `pageHead`
taneliang f573658
Memoize expensive VenueList variable computation
taneliang 564e2e1
window.location -> useLocation in VenueList
taneliang 689b03a
Improve AvailabilitySearch memoization
taneliang 0284997
Roughly functionalize VenuesContainer
taneliang 4d4c5a3
Cache sorted venues in LoadableVenuesContainer to prevent memo invali…
taneliang 100433e
Fix updateURL (and inline it into a useEffect)
taneliang 26790bc
Functionalize FeedbackModal
taneliang c732513
Functionalize VenueLocation
taneliang 806c77e
Inline + extract AddLocationModal from VenueLocation renderFeedbackMe…
taneliang ee6266a
Reduce VenueList renders as they are very expensive
taneliang 9c14cef
Extract SecondaryPane into VenueDetailsPane.tsx
taneliang fe07f0b
Rewrite VenuesContainer tests
taneliang aa79d23
Remove unstable_batchedUpdates as React already batches updates withi…
taneliang 2238100
SearchBox: Remove useInstantSearch as it's always true
taneliang 8e2b0be
SearchBox: Rewrite test for comprehensiveness
taneliang bce257b
Functionalize SearchBox
taneliang fc989e7
SearchBox: Replace hasChanges state with mutable ref
taneliang 9cb8c72
SearchBox: Inline search callback into debouncedSearch
taneliang 76b03a3
Merge branch 'master' into eliang/overhaul-venues-components
taneliang 4d234e8
Fix review comment: hasChanges -> isDirty
taneliang d5b270e
Fix review comment: inline search
taneliang 73f8c96
Fix review comment: improve clear button accessibility
taneliang 768fa79
Fix review comment: remove unsafe casting
taneliang 46ab5bd
Remove unnecessary type cast
taneliang 4427239
Fix review comment: remove handleInput instanceof check
taneliang 98b62af
Fix review comment: remove special modal element in favor of view.con…
taneliang db261b5
Add SearchBox debouncedSearch FIXME note
taneliang 9d72b35
Split VenuesContainer modal test into 2
taneliang 701099a
Merge branch 'master' into eliang/overhaul-venues-components
taneliang 381ced3
Add comment explaining why we don't flush in clearInput
taneliang File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,48 +1,154 @@ | ||
import { shallow } from 'enzyme'; | ||
import SearchBox from './SearchBox'; | ||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; | ||
import userEvent from '@testing-library/user-event'; | ||
import SearchBox, { Props } from './SearchBox'; | ||
|
||
const SOME_TEXT = 'existing'; | ||
|
||
const defaultProps: Props = { | ||
throttle: 0, | ||
isLoading: false, | ||
value: SOME_TEXT, | ||
onChange: jest.fn(), | ||
onSearch: jest.fn(), | ||
}; | ||
|
||
describe(SearchBox, () => { | ||
test('should match snapshot', () => { | ||
expect( | ||
shallow( | ||
<SearchBox | ||
throttle={0} | ||
useInstantSearch={false} | ||
isLoading={false} | ||
value="" | ||
placeholder="" | ||
onChange={jest.fn()} | ||
onSearch={jest.fn()} | ||
/>, | ||
), | ||
).toMatchSnapshot(); | ||
|
||
expect( | ||
shallow( | ||
<SearchBox | ||
throttle={0} | ||
useInstantSearch={false} | ||
isLoading | ||
value="" | ||
placeholder="" | ||
onChange={jest.fn()} | ||
onSearch={jest.fn()} | ||
/>, | ||
), | ||
).toMatchSnapshot(); | ||
|
||
expect( | ||
shallow( | ||
<SearchBox | ||
throttle={0} | ||
useInstantSearch={false} | ||
isLoading={false} | ||
value="Hello world" | ||
placeholder="Testing testing 123" | ||
onChange={jest.fn()} | ||
onSearch={jest.fn()} | ||
/>, | ||
), | ||
).toMatchSnapshot(); | ||
test('should display loading indicator if isLoading', () => { | ||
render(<SearchBox {...defaultProps} isLoading />); | ||
expect(screen.getByText(/loading/i)).toBeInTheDocument(); | ||
}); | ||
|
||
test('should not display loading indicator if not isLoading', () => { | ||
render(<SearchBox {...defaultProps} isLoading={false} />); | ||
expect(screen.queryByText(/loading/i)).not.toBeInTheDocument(); | ||
}); | ||
|
||
test('should display value', () => { | ||
render(<SearchBox {...defaultProps} value={SOME_TEXT} />); | ||
expect(screen.getByRole('searchbox')).toHaveValue(SOME_TEXT); | ||
}); | ||
|
||
test('should display placeholder', () => { | ||
render(<SearchBox {...defaultProps} value="" placeholder="abc" />); | ||
expect(screen.getByRole('searchbox')).toHaveAttribute('placeholder', 'abc'); | ||
expect(screen.getByRole('searchbox')).toHaveValue(''); | ||
}); | ||
|
||
test('should call onChange and onSearch when user types into the input', async () => { | ||
const mockOnChange = jest.fn(); | ||
const mockOnSearch = jest.fn(); | ||
render(<SearchBox {...defaultProps} onChange={mockOnChange} onSearch={mockOnSearch} />); | ||
|
||
userEvent.type(screen.getByRole('searchbox'), 'ing'); | ||
|
||
expect(mockOnChange).toHaveBeenCalledTimes(3); | ||
// `SearchBox` is a fully controlled component, so we expect the input to be | ||
// appended to `value`. | ||
expect(mockOnChange).toHaveBeenNthCalledWith(1, `${SOME_TEXT}i`); | ||
expect(mockOnChange).toHaveBeenNthCalledWith(2, `${SOME_TEXT}n`); | ||
expect(mockOnChange).toHaveBeenNthCalledWith(3, `${SOME_TEXT}g`); | ||
|
||
await waitFor(() => expect(mockOnSearch).toHaveBeenCalledTimes(1)); | ||
}); | ||
|
||
test('should handle blur correctly', async () => { | ||
const mockHandlers = [jest.fn(), jest.fn(), jest.fn()]; | ||
const [mockOnBlur, mockOnChange, mockOnSearch] = mockHandlers; | ||
render( | ||
<SearchBox | ||
{...defaultProps} | ||
onBlur={mockOnBlur} | ||
onChange={mockOnChange} | ||
onSearch={mockOnSearch} | ||
/>, | ||
); | ||
|
||
const box = screen.getByRole('searchbox'); | ||
|
||
// Test blur with no changes | ||
userEvent.click(box); | ||
fireEvent.blur(box); | ||
expect(mockOnBlur).toHaveBeenCalledTimes(1); | ||
expect(mockOnChange).not.toHaveBeenCalled(); | ||
expect(mockOnSearch).not.toHaveBeenCalled(); | ||
mockHandlers.forEach((m) => m.mockClear()); | ||
|
||
// Test blur with changes | ||
userEvent.click(box); | ||
userEvent.type(screen.getByRole('searchbox'), '.'); | ||
fireEvent.blur(box); | ||
expect(mockOnBlur).toHaveBeenCalledTimes(1); | ||
expect(mockOnChange).toHaveBeenCalledTimes(2); // 1 call when character typed, 1 call on blur | ||
expect(mockOnSearch).toHaveBeenCalledTimes(1); // No `waitFor` as we expect onSearch to be called immediately on blur | ||
mockHandlers.forEach((m) => m.mockClear()); | ||
|
||
// Test blur with no changes *after* changes | ||
userEvent.click(box); | ||
fireEvent.blur(box); | ||
expect(mockOnBlur).toHaveBeenCalledTimes(1); | ||
expect(mockOnChange).not.toHaveBeenCalled(); | ||
expect(mockOnSearch).not.toHaveBeenCalled(); | ||
mockHandlers.forEach((m) => m.mockClear()); | ||
}); | ||
|
||
test('should handle form submit correctly', () => { | ||
const mockHandlers = [jest.fn(), jest.fn(), jest.fn()]; | ||
const [mockOnBlur, mockOnChange, mockOnSearch] = mockHandlers; | ||
const { container } = render( | ||
<SearchBox | ||
{...defaultProps} | ||
onBlur={mockOnBlur} | ||
onChange={mockOnChange} | ||
onSearch={mockOnSearch} | ||
/>, | ||
); | ||
const box = screen.getByRole('searchbox'); | ||
const form = container.getElementsByTagName('form')[0]; | ||
|
||
// Test submit with no changes | ||
userEvent.click(box); | ||
fireEvent.submit(form); | ||
expect(box).not.toHaveFocus(); | ||
expect(mockOnBlur).toHaveBeenCalledTimes(1); | ||
expect(mockOnChange).not.toHaveBeenCalled(); | ||
expect(mockOnSearch).not.toHaveBeenCalled(); | ||
mockHandlers.forEach((m) => m.mockClear()); | ||
|
||
// Test submit with changes | ||
userEvent.click(box); | ||
userEvent.type(screen.getByRole('searchbox'), '.'); | ||
fireEvent.submit(form); | ||
expect(mockOnBlur).toHaveBeenCalledTimes(1); | ||
expect(mockOnChange).toHaveBeenCalledTimes(2); // 1 call when character typed, 1 call on blur | ||
expect(mockOnSearch).toHaveBeenCalledTimes(1); // No `waitFor` as we expect onSearch to be called immediately on blur | ||
mockHandlers.forEach((m) => m.mockClear()); | ||
}); | ||
|
||
test('should have a clear button', async () => { | ||
const mockOnChange = jest.fn(); | ||
const mockOnSearch = jest.fn(); | ||
|
||
render( | ||
<SearchBox | ||
{...defaultProps} | ||
value={SOME_TEXT} | ||
onChange={mockOnChange} | ||
onSearch={mockOnSearch} | ||
/>, | ||
); | ||
const clearButton = screen.getByRole('button', { name: 'Clear search' }); | ||
expect(clearButton).toBeInTheDocument(); | ||
|
||
userEvent.click(clearButton); | ||
|
||
expect(mockOnChange).toHaveBeenCalledTimes(1); | ||
expect(mockOnChange).toHaveBeenCalledWith(''); | ||
|
||
await waitFor(() => expect(mockOnSearch).toHaveBeenCalledTimes(1)); | ||
}); | ||
|
||
test('should not have a clear button if the box is empty', async () => { | ||
render(<SearchBox {...defaultProps} value="" />); | ||
expect(screen.queryByTestId('react-feather X icon')).not.toBeInTheDocument(); | ||
}); | ||
}); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
onSearch is debounced, so testing by counting calls might not be accurate. Not sure if there is a better way though, maybe add some mock timer waits to force for debounce to clear?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we could do something like
await expect(waitFor(() => expect(mockOnSearch).toHaveBeenCalled())).rejects.toThrowError()
? HahahaThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could mock
debounce
instead to immediately invoke the function.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a bad idea, but this will couple our test to an implementation detail of
SearchBox
:(Tbh I think this debouncing/throttling logic should be moved into SearchkitSearchBox instead since it's now only relevant for module search. I think we should just file an issue for this and defer this discussion to another time?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't buy into the implementation detail test article as much. It is useful to keep in mind, but patterns like the page object pattern (why I asked if we should extract some of the queries into constants or functions) are more useful IMO to serve the goal of improving test robustness and reducing breakage when implementation changes.
Here specifically we want to test implementation details, because testing exactly what the user sees would be kind of ridiculous - the goal of the debounce is to improve rendering performance, and to test that in a concrete manner would probably involve, what, rendering this with some arbitrary CPU throttling to prove it is fast enough?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is true anymore. The venues page previously used this for performance, but now that the venues search uses
useDeferredValue
for perf (at least after #3040), this is only used to throttle our module search so that we don't make too many requests. Venues search no longer usesonSearch
.That's why this throttling/debouncing stuff should be moved into
SearchkitSearchBox
imo, and we can lazily sidestep this discussion entirely sinceSearchkitSearchBox
doesn't have tests 😆