From 7d4e1e1b6e3c997593e970404ea9dc8fa61c9c4a Mon Sep 17 00:00:00 2001 From: rebelchris Date: Fri, 24 Jul 2026 14:04:39 +0000 Subject: [PATCH] fix: prevent stale skill query after clearing --- .../profile/components/ProfileSkills.spec.tsx | 83 +++++++++++++++++++ .../profile/components/ProfileSkills.tsx | 33 +++++--- 2 files changed, 105 insertions(+), 11 deletions(-) create mode 100644 packages/shared/src/features/profile/components/ProfileSkills.spec.tsx diff --git a/packages/shared/src/features/profile/components/ProfileSkills.spec.tsx b/packages/shared/src/features/profile/components/ProfileSkills.spec.tsx new file mode 100644 index 00000000000..a1fbc8d0fec --- /dev/null +++ b/packages/shared/src/features/profile/components/ProfileSkills.spec.tsx @@ -0,0 +1,83 @@ +import React, { type ReactNode } from 'react'; +import { act, fireEvent, render, screen } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { FormProvider, useForm } from 'react-hook-form'; +import ProfileSkills from './ProfileSkills'; + +type FormWrapperProps = { + children: ReactNode; +}; + +const FormWrapper = ({ children }: FormWrapperProps) => { + const methods = useForm({ + defaultValues: { + skills: [], + }, + }); + + return ( + + {children} + + ); +}; + +const renderComponent = () => + render( + + + , + ); + +const advanceDebounce = () => { + act(() => { + jest.advanceTimersByTime(300); + }); +}; + +describe('ProfileSkills', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.clearAllTimers(); + jest.useRealTimers(); + }); + + it('keeps the input empty after clearing a pending debounced query', () => { + renderComponent(); + + const input = screen.getByPlaceholderText('Search skills'); + + fireEvent.change(input, { target: { value: 'R' } }); + fireEvent.change(input, { target: { value: '' } }); + + expect(input).toHaveValue(''); + + advanceDebounce(); + + expect(input).toHaveValue(''); + }); + + it('keeps the input empty after submitting while a debounced query is pending', () => { + renderComponent(); + + const input = screen.getByPlaceholderText('Search skills'); + + fireEvent.change(input, { target: { value: 'R' } }); + advanceDebounce(); + + expect(input).toHaveValue('R'); + + fireEvent.change(input, { target: { value: 'A' } }); + fireEvent.keyDown(input, { code: 'Enter', key: 'Enter' }); + + expect(screen.getByRole('button', { name: 'R' })).toBeInTheDocument(); + expect(input).toHaveValue(''); + + advanceDebounce(); + + expect(input).toHaveValue(''); + }); +}); diff --git a/packages/shared/src/features/profile/components/ProfileSkills.tsx b/packages/shared/src/features/profile/components/ProfileSkills.tsx index 3039501f050..70412150b8f 100644 --- a/packages/shared/src/features/profile/components/ProfileSkills.tsx +++ b/packages/shared/src/features/profile/components/ProfileSkills.tsx @@ -30,8 +30,17 @@ const ProfileSkills = ({ name }: ProfileSkillsProps): ReactElement => { const { data: autocompleteKeywords, isFetching } = useQuery( getKeywordAutocompleteOptions(query), ); + const hasAutocompleteKeywords = (autocompleteKeywords?.length ?? 0) > 0; - const [debouncedQuery] = useDebounceFn((data) => setQuery(data), 300); + const [debouncedQuery, cancelDebouncedQuery] = useDebounceFn( + (data) => setQuery(data ?? ''), + 300, + ); + + const clearQuery = () => { + cancelDebouncedQuery(); + setQuery(''); + }; const handlePopoverClose: PopoverContentProps['onInteractOutside'] = (e) => { if (e.target === inputRef.current) { @@ -66,7 +75,7 @@ const ProfileSkills = ({ name }: ProfileSkillsProps): ReactElement => { Skills - 0}> + { @@ -76,14 +85,16 @@ const ProfileSkills = ({ name }: ProfileSkillsProps): ReactElement => { label="Search skills" leftIcon={} rightIcon={ - isFetching && + isFetching ? ( + + ) : undefined } hint="Add commas (,) to add multiple skills. Press Enter to submit them." hintIcon={} value={query} onChange={({ target }) => { if (target.value === '') { - return setQuery(''); + return clearQuery(); } return debouncedQuery(target.value); @@ -105,13 +116,13 @@ const ProfileSkills = ({ name }: ProfileSkillsProps): ReactElement => { if (newSkills.length === 0) { if (query) { - setQuery(''); + clearQuery(); } return; } field.onChange([...skills, ...newSkills]); - setQuery(''); + clearQuery(); return; } @@ -141,11 +152,11 @@ const ProfileSkills = ({ name }: ProfileSkillsProps): ReactElement => { key={keyword} tag={{ name: keyword }} isSelected={isSelected} - onClick={({ tag }) => { + onClick={() => { if (isSelected) { - removeSkill(tag.name); + removeSkill(keyword); } else { - addSkill(tag.name); + addSkill(keyword); } }} /> @@ -162,8 +173,8 @@ const ProfileSkills = ({ name }: ProfileSkillsProps): ReactElement => { key={skill} tag={{ name: skill }} isSelected - onClick={({ tag }) => { - removeSkill(tag.name); + onClick={() => { + removeSkill(skill); }} /> ))}