diff --git a/.claude/skills/gen-rtl-test/SKILL.md b/.claude/skills/gen-rtl-test/SKILL.md index 8776f3732fa..9cee2186eee 100644 --- a/.claude/skills/gen-rtl-test/SKILL.md +++ b/.claude/skills/gen-rtl-test/SKILL.md @@ -59,7 +59,7 @@ RTL emphasizes testing components as users interact with them. Users find button 4. **DRY Helpers** - Use reusable function in frontend/packages/console-shared/src/test-utils directoty and sub-directory if exists else extract repetitive setup into reusable functions -5. **Async-Aware** - Handle asynchronous updates with `findBy*` and `waitFor` +5. **Async-Aware** - Prefer `findByText` / `findByRole` / `findAllByRole` when waiting for content to **appear**; use `waitFor` when the assertion is **not** expressible as a query (e.g. button **disabled** after validation, **absence**, class names, mocks). See **Rule 11**. 6. **TypeScript Safety** - Use proper types for props, state, and mock data @@ -360,7 +360,7 @@ it('should find the heading', () => { **Query Variants:** - **`getBy*`** - Element expected to be present synchronously (throws if not found) - **`queryBy*`** - Only for asserting element is NOT present -- **`findBy*`** - Element will appear asynchronously (returns Promise) +- **`findBy*`** - Element will appear asynchronously (returns Promise). Prefer **`findBy*`** over **`waitFor(() => … getBy* …)`** when the goal is to wait for that node to **appear** (see **Rule 11**). **Anti-pattern:** Avoid `container.querySelector` - it tests implementation details. @@ -456,7 +456,8 @@ it('should render the Name field', async () => { // Don't do this - use verifyInputField instead! expect(screen.getByLabelText('Name')).toBeInTheDocument(); expect(screen.getByLabelText('Name')).toHaveValue(''); - fireEvent.change(screen.getByLabelText('Name'), { target: { value: 'test' } }); + const user = userEvent.setup(); + await user.type(screen.getByLabelText('Name'), 'test'); expect(screen.getByLabelText('Name')).toHaveValue('test'); expect(screen.getByText('Unique name for the resource')).toBeInTheDocument(); }); @@ -482,14 +483,15 @@ When testing form components: ### Rule 10: Test Conditional Rendering by Asserting Both States ```typescript -it('should show content when expanded', () => { +it('should show content when expanded', async () => { + const user = userEvent.setup(); render(); // 1. Assert initial hidden state expect(screen.queryByText('Hidden content')).not.toBeInTheDocument(); // 2. Simulate user action - fireEvent.click(screen.getByRole('button', { name: 'Expand' })); + await user.click(screen.getByRole('button', { name: 'Expand' })); // 3. Assert final visible state expect(screen.getByText('Hidden content')).toBeVisible(); @@ -498,18 +500,54 @@ it('should show content when expanded', () => { ### Rule 11: Handle Asynchronous Behavior +#### Prefer `findBy*` over `waitFor` + `getBy*` when waiting for content to appear + +`findByText`, `findByRole`, `findAllByRole`, etc. are implemented as **`waitFor` + `getBy*`** (they return a Promise and retry until the element is found or the timeout is reached). When the intent is **“wait until this text/role appears in the DOM,”** use **`await screen.findBy…`** instead of wrapping **`getBy…`** in **`waitFor`**. + ```typescript -// Use findBy* to wait for an element to appear -const element = await screen.findByText('Loaded content'); -expect(element).toBeVisible(); +// ✅ GOOD: findBy* expresses “eventually this appears” +expect(await screen.findByText('Loaded content')).toBeVisible(); +const row = await screen.findByRole('row', { name: /my-resource/i }); +expect(await screen.findAllByRole('button', { name: 'Remove' })).toHaveLength(2); -// Use waitFor for complex assertions +// ⚠️ EQUIVALENT but more verbose (prefer findBy* above) await waitFor(() => { - expect(screen.getByText('Updated')).toBeInTheDocument(); + expect(screen.getByText('Loaded content')).toBeInTheDocument(); }); ``` -**Avoid Explicit act():** Rarely needed. `render`, `fireEvent`, `findBy*`, and `waitFor` already wrap operations in `act()`. +#### Keep `waitFor` when `findBy*` cannot express the assertion + +**`findBy*` queries wait for presence** of a matching node. They do **not** replace every `waitFor`. Still use **`waitFor`** when you need to wait for something that is **not** “find this text/role in the tree,” for example: + +| Situation | Why `findBy*` is not enough | Pattern | +|-----------|------------------------------|---------| +| **Control state after async validation** (e.g. Save **disabled** after Formik/yup runs) | Disabled is a **property**, not a new queryable label; there is no `findBy…` for “button became disabled.” | `await waitFor(() => expect(save).toBeDisabled());` — you can still use **`findByText`** / **`findByRole`** in the **same** test for **error messages** that appear as text. | +| **Eventually absent** (list/link/control **not** in the document) | **`findBy*` waits for presence**, not absence. | `await waitFor(() => expect(screen.queryByRole('list')).not.toBeInTheDocument());` or `waitForElementToBeRemoved` when something was visible first. | +| **CSS class** or **non-query** DOM state | Not a text/role query. | `await waitFor(() => expect(input).toHaveClass('invalid-tag'));` | +| **Mock** or **callback** assertions | Not the DOM. | `await waitFor(() => expect(mockFn).toHaveBeenCalledWith(…));` | + +```typescript +// Async validation: message appears → findByText; button disabled → waitFor +await user.type(cpuRequest, '300'); +await waitFor(() => expect(save).toBeDisabled()); +expect( + await screen.findByText('CPU request must be less than or equal to limit.'), +).toBeVisible(); +``` + +#### Quick reference + +```typescript +// Prefer findBy* when waiting for content to appear +const element = await screen.findByText('Loaded content'); +expect(element).toBeVisible(); + +// Use waitFor for disabled state, absence, classes, mocks (see table above) +await waitFor(() => expect(save).toBeDisabled()); +``` + +**Avoid Explicit act():** Rarely needed. `render`, `userEvent` (await its async methods), `findBy*`, and `waitFor` already wrap operations in `act()`. ### Rule 12: Use Lifecycle Hooks for Setup and Cleanup @@ -553,24 +591,31 @@ expect(userName).toBeVisible(); expect(editButton).toBeVisible(); ``` -### Rule 14: Simulate User Events with fireEvent +### Rule 14: Simulate User Events with userEvent + +Use `@testing-library/user-event` for clicks, typing, and other interactions. It simulates full event sequences closer to real browser behavior than low-level `fireEvent`. ```typescript -import { render, screen, fireEvent } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +const user = userEvent.setup(); render(); const input = screen.getByLabelText(/name/i); const button = screen.getByRole('button', { name: /submit/i }); -// Simulate typing -fireEvent.change(input, { target: { value: 'John Doe' } }); +// Simulate typing (prefer over fireEvent.change) +await user.type(input, 'John Doe'); // Simulate clicking -fireEvent.click(button); +await user.click(button); ``` -**Note:** `userEvent` from `@testing-library/user-event` is not supported due to incompatible Jest version (will be updated after Jest upgrade). +**Conventions:** +- Call `const user = userEvent.setup()` per test and **await** `user.click`, `user.type`, `user.keyboard`, etc. +- For text inputs in forms, prefer **`verifyInputField`** (Rule 9) when it applies; use `userEvent` for other controls (selects, checkboxes, buttons, complex widgets). +- Reserve **`fireEvent`** only for rare cases (e.g., triggering a specific low-level DOM event that `userEvent` does not model). ### Rule 15: Test "Unhappy Paths" and Error States @@ -648,12 +693,14 @@ Remove any imports that are not used in the test file: ```typescript // ❌ BAD - Unused imports -import { render, screen, fireEvent, waitFor, within } from '@testing-library/react'; +import { render, screen, waitFor, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { k8sCreate, k8sPatch, k8sUpdate } from '@console/internal/module/k8s'; -// ... but only using render, screen, fireEvent +// ... but only using render, screen, userEvent // ✅ GOOD - Only what's needed -import { render, screen, fireEvent } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { k8sCreate } from '@console/internal/module/k8s'; ``` @@ -717,23 +764,25 @@ Clean up any variables that are declared but never used: ```typescript // ❌ BAD - Unused variables -it('should submit form', () => { +it('should submit form', async () => { const mockData = { foo: 'bar' }; const unusedSpy = jest.spyOn(console, 'log'); const onSubmit = jest.fn(); + const user = userEvent.setup(); render(
); - fireEvent.click(screen.getByRole('button')); + await user.click(screen.getByRole('button')); expect(onSubmit).toHaveBeenCalled(); }); // ✅ GOOD - Only necessary variables -it('should submit form', () => { +it('should submit form', async () => { + const user = userEvent.setup(); const onSubmit = jest.fn(); render(); - fireEvent.click(screen.getByRole('button')); + await user.click(screen.getByRole('button')); expect(onSubmit).toHaveBeenCalled(); }); @@ -950,43 +999,45 @@ act(() => { **Strategy 1: Wrap async interactions in waitFor** ```typescript +import { screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +const user = userEvent.setup(); + // ❌ BAD: Causes act() warning -fireEvent.click(button); +await user.click(button); expect(screen.getByText('Updated')).toBeInTheDocument(); // ✅ GOOD: Use waitFor for async updates -fireEvent.click(button); +await user.click(button); await waitFor(() => { expect(screen.getByText('Updated')).toBeInTheDocument(); }); ``` **Strategy 2: Use findBy* queries (preferred for new elements)** -```typescript -// ❌ BAD: Causes act() warning -fireEvent.click(button); -expect(screen.getByText('Loaded')).toBeInTheDocument(); -// ✅ GOOD: Use findBy* which waits automatically -fireEvent.click(button); -expect(await screen.findByText('Loaded')).toBeInTheDocument(); -``` +When waiting for content to appear after user interactions, prefer `findBy*` queries which automatically wait for elements. See **Rule 11** for comprehensive guidance on `findBy*` vs `waitFor`, including when to use each and a detailed comparison table. -**Strategy 3: Wrap test in act() when needed** +**Strategy 3: Prefer userEvent + findBy* for dropdown/select-style interactions** ```typescript -// Import act from @testing-library/react -import { render, screen, fireEvent, waitFor, act } from '@testing-library/react'; +import { screen, act } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +const user = userEvent.setup(); + +// ❌ BAD: Asserts before async menu content appears +await user.click(screen.getByText('Select Option')); +expect(screen.getByText('Option 1')).toBeInTheDocument(); -// ❌ BAD: Causes act() warning for dropdown/select interactions -const dropdown = screen.getByText('Select Option'); -fireEvent.click(dropdown); +// ✅ GOOD: Wait for option, then click +await user.click(screen.getByText('Select Option')); +await user.click(await screen.findByText('Option 1')); -// ✅ GOOD: Wrap in act() for complex interactions +// If a third-party widget still warns, wrap only that interaction in act(): await act(async () => { - fireEvent.click(dropdown); + await user.click(screen.getByRole('button', { name: /toggle menu/i })); }); -const option = await screen.findByText('Option 1'); -fireEvent.click(option); ``` **Strategy 4: Mock timers or async operations** @@ -1004,7 +1055,7 @@ await waitFor(() => { #### Common Causes of act() Warnings 1. **Dropdown/Select interactions** - PatternFly Select/Dropdown components - - Solution: Wrap in `act()` or use `waitFor` after interaction + - Solution: Use `await user.click` / `user.type` with `findBy*` or `waitFor` after opening; wrap in `act()` only if a widget still emits warnings 2. **Async state updates** - useEffect, setTimeout, promises - Solution: Use `findBy*` or `waitFor` @@ -1455,11 +1506,13 @@ When generating tests for React components: 4. ✅ Mock factories return simple values (null, strings, children) - NO React.createElement 5. ✅ Cast mocked imports to `jest.Mock` when calling `.mockResolvedValue()` etc. 6. ✅ **Import `verifyInputField` for form components** - Rule 9 strictly enforced +7. ✅ **Use `userEvent` from `@testing-library/user-event` for clicks, typing, and similar interactions** - Rule 14 (`userEvent.setup()`, then `await user.click` / `await user.type`, etc.) **Code Generation Pattern:** ```typescript // ✅ ALWAYS - ES6 imports at file top -import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { k8sCreate } from '@console/internal/module/k8s'; import { history } from '@console/internal/components/utils'; import { verifyInputField } from '@console/shared/src/test-utils/unit-test-utils'; // For form components @@ -1584,7 +1637,7 @@ If **ANY** `require()` found that is NOT `jest.requireActual` → **IMMEDIATELY - Fix **EVERY** act() warning using Rule 23 strategies: - Wrap async interactions in `waitFor` - Use `findBy*` queries for async elements - - Add `await waitFor()` after dropdown/select interactions + - After `await user.click` on dropdowns/selects, wait for menu content with `findBy*` or `waitFor` - Ensure async state updates are properly awaited - Re-run tests after fixing warnings - **DO NOT** complete until output has ZERO act() warnings diff --git a/frontend/packages/console-app/src/__tests__/hooks/useCSPViolationDetector.spec.tsx b/frontend/packages/console-app/src/__tests__/hooks/useCSPViolationDetector.spec.tsx index 28b2b92e8b8..5bb7e0d54bc 100644 --- a/frontend/packages/console-app/src/__tests__/hooks/useCSPViolationDetector.spec.tsx +++ b/frontend/packages/console-app/src/__tests__/hooks/useCSPViolationDetector.spec.tsx @@ -1,4 +1,4 @@ -import { act, fireEvent } from '@testing-library/react'; +import { act } from '@testing-library/react'; import { renderWithProviders } from '@console/shared/src/test-utils/unit-test-utils'; import { newPluginCSPViolationEvent, @@ -78,7 +78,7 @@ describe('useCSPViolationDetector', () => { mockCacheEvent.mockReturnValue(true); renderWithProviders(); act(() => { - fireEvent(document, testEvent); + document.dispatchEvent(testEvent); }); expect(mockCacheEvent).toHaveBeenCalledWith(testPluginEvent); expect(mockFireTelemetry).toHaveBeenCalledWith('CSPViolation', testPluginEvent); @@ -89,7 +89,7 @@ describe('useCSPViolationDetector', () => { renderWithProviders(); act(() => { - fireEvent(document, testEvent); + document.dispatchEvent(testEvent); }); expect(mockCacheEvent).toHaveBeenCalledWith(testPluginEvent); @@ -104,7 +104,7 @@ describe('useCSPViolationDetector', () => { const expected = newPluginCSPViolationEvent('foo', testEventWithPlugin); renderWithProviders(); act(() => { - fireEvent(document, testEventWithPlugin); + document.dispatchEvent(testEventWithPlugin); }); expect(mockCacheEvent).toHaveBeenCalledWith(expected); expect(mockFireTelemetry).toHaveBeenCalledWith('CSPViolation', expected); @@ -119,7 +119,7 @@ describe('useCSPViolationDetector', () => { const expected = newPluginCSPViolationEvent('foo', testEventWithPlugin); renderWithProviders(); act(() => { - fireEvent(document, testEventWithPlugin); + document.dispatchEvent(testEventWithPlugin); }); expect(mockCacheEvent).toHaveBeenCalledWith(expected); expect(mockFireTelemetry).toHaveBeenCalledWith('CSPViolation', expected); diff --git a/frontend/packages/console-app/src/components/modals/resource-limits/__tests__/ResourceLimitsModal.spec.tsx b/frontend/packages/console-app/src/components/modals/resource-limits/__tests__/ResourceLimitsModal.spec.tsx index b5737c0208d..58bc7ba8907 100644 --- a/frontend/packages/console-app/src/components/modals/resource-limits/__tests__/ResourceLimitsModal.spec.tsx +++ b/frontend/packages/console-app/src/components/modals/resource-limits/__tests__/ResourceLimitsModal.spec.tsx @@ -1,5 +1,6 @@ import type { ComponentProps } from 'react'; -import { screen, fireEvent } from '@testing-library/react'; +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import type { FormikProps, FormikValues } from 'formik'; import { formikFormProps } from '@console/shared/src/test-utils/formik-props-utils'; import { renderWithProviders } from '@console/shared/src/test-utils/unit-test-utils'; @@ -70,16 +71,18 @@ describe('ResourceLimitsModal Form', () => { }); it('calls the cancel function when the Cancel button is clicked', async () => { + const user = userEvent.setup(); renderWithProviders(); - await fireEvent.click(screen.getByRole('button', { name: 'Cancel' })); + await user.click(screen.getByRole('button', { name: 'Cancel' })); expect(formProps.cancel).toHaveBeenCalledTimes(1); }); it('calls the handleSubmit function when the form is submitted', async () => { + const user = userEvent.setup(); renderWithProviders(); - await fireEvent.submit(screen.getByRole('form')); + await user.click(screen.getByRole('button', { name: 'Save' })); expect(formProps.handleSubmit).toHaveBeenCalledTimes(1); }); }); diff --git a/frontend/packages/console-shared/src/components/alerts/__tests__/SwitchToYAMLAlert.spec.tsx b/frontend/packages/console-shared/src/components/alerts/__tests__/SwitchToYAMLAlert.spec.tsx index 68a89e28231..f37fee90cf7 100644 --- a/frontend/packages/console-shared/src/components/alerts/__tests__/SwitchToYAMLAlert.spec.tsx +++ b/frontend/packages/console-shared/src/components/alerts/__tests__/SwitchToYAMLAlert.spec.tsx @@ -1,4 +1,5 @@ -import { screen, fireEvent } from '@testing-library/react'; +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { renderWithProviders } from '../../../test-utils/unit-test-utils'; import SwitchToYAMLAlert from '../SwitchToYAMLAlert'; @@ -42,12 +43,13 @@ describe('SwitchToYAMLAlert', () => { expect(closeButton).toBeInTheDocument(); }); - it('should call onClose when close button is clicked', () => { + it('should call onClose when close button is clicked', async () => { + const user = userEvent.setup(); const mockOnClose = jest.fn(); renderWithProviders(); const closeButton = screen.getByRole('button', { name: /close/i }); - fireEvent.click(closeButton); + await user.click(closeButton); expect(mockOnClose).toHaveBeenCalledTimes(1); }); diff --git a/frontend/packages/console-shared/src/components/editor/__tests__/CodeEditorToolbar.spec.tsx b/frontend/packages/console-shared/src/components/editor/__tests__/CodeEditorToolbar.spec.tsx index 585e8b16366..5002909bb55 100644 --- a/frontend/packages/console-shared/src/components/editor/__tests__/CodeEditorToolbar.spec.tsx +++ b/frontend/packages/console-shared/src/components/editor/__tests__/CodeEditorToolbar.spec.tsx @@ -1,4 +1,5 @@ -import { render, screen, fireEvent } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { useTranslation } from 'react-i18next'; import { ActionType } from '@console/internal/reducers/ols'; import { useConsoleDispatch } from '@console/shared/src/hooks/useConsoleDispatch'; @@ -48,11 +49,12 @@ describe('CodeEditorToolbar', () => { expect(screen.queryByRole('button')).not.toBeInTheDocument(); }); - it('should dispatch OpenOLS action when "Ask OpenShift Lightspeed" button is clicked', () => { + it('should dispatch OpenOLS action when "Ask OpenShift Lightspeed" button is clicked', async () => { + const user = userEvent.setup(); (useOLSConfig as jest.Mock).mockReturnValue(true); render(); const button = screen.getByRole('button'); - fireEvent.click(button); + await user.click(button); expect(mockDispatch).toHaveBeenCalledWith({ type: ActionType.OpenOLS }); }); }); diff --git a/frontend/packages/console-shared/src/components/form-utils/__tests__/FlexForm.spec.tsx b/frontend/packages/console-shared/src/components/form-utils/__tests__/FlexForm.spec.tsx index ea82fa84aef..179fab1ec3a 100644 --- a/frontend/packages/console-shared/src/components/form-utils/__tests__/FlexForm.spec.tsx +++ b/frontend/packages/console-shared/src/components/form-utils/__tests__/FlexForm.spec.tsx @@ -1,4 +1,5 @@ -import { screen, fireEvent } from '@testing-library/react'; +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { renderWithProviders } from '../../../test-utils/unit-test-utils'; import FlexForm from '../FlexForm'; @@ -22,14 +23,19 @@ describe('FlexForm', () => { }); }); - it('should preserve form props including onSubmit', () => { + it('should preserve form props including onSubmit', async () => { + const user = userEvent.setup(); const handleSubmit = jest.fn((e) => e.preventDefault()); - renderWithProviders(); + renderWithProviders( + + + , + ); const formElement = screen.getByRole('form', { name: 'flex form' }); expect(formElement).toHaveAttribute('style'); - fireEvent.submit(formElement); + await user.click(screen.getByRole('button', { name: 'Submit' })); expect(handleSubmit).toHaveBeenCalledTimes(1); }); @@ -45,7 +51,8 @@ describe('FlexForm', () => { expect(screen.getByRole('button', { name: 'Submit' })).toBeVisible(); }); - it('should handle form submission', () => { + it('should handle form submission', async () => { + const user = userEvent.setup(); const handleSubmit = jest.fn((e) => e.preventDefault()); renderWithProviders( @@ -55,7 +62,7 @@ describe('FlexForm', () => { ); const submitButton = screen.getByRole('button', { name: 'Submit' }); - fireEvent.click(submitButton); + await user.click(submitButton); expect(handleSubmit).toHaveBeenCalledTimes(1); }); diff --git a/frontend/packages/console-shared/src/components/form-utils/__tests__/FormFooter.spec.tsx b/frontend/packages/console-shared/src/components/form-utils/__tests__/FormFooter.spec.tsx index 3e5d5f00056..27230e7c3fc 100644 --- a/frontend/packages/console-shared/src/components/form-utils/__tests__/FormFooter.spec.tsx +++ b/frontend/packages/console-shared/src/components/form-utils/__tests__/FormFooter.spec.tsx @@ -1,4 +1,5 @@ -import { screen, fireEvent } from '@testing-library/react'; +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { renderWithProviders } from '../../../test-utils/unit-test-utils'; import type { FormFooterProps } from '../form-utils-types'; import FormFooter from '../FormFooter'; @@ -81,13 +82,14 @@ describe('FormFooter', () => { expect(screen.getByRole('button', { name: 'Create' })).toHaveAttribute('type', 'button'); }); - it('should call the handler when a button is clicked', () => { + it('should call the handler when a button is clicked', async () => { + const user = userEvent.setup(); const handleSubmit = jest.fn(); renderWithProviders(); - fireEvent.click(screen.getByRole('button', { name: 'Create' })); - fireEvent.click(screen.getByRole('button', { name: 'Reset' })); - fireEvent.click(screen.getByRole('button', { name: 'Cancel' })); + await user.click(screen.getByRole('button', { name: 'Create' })); + await user.click(screen.getByRole('button', { name: 'Reset' })); + await user.click(screen.getByRole('button', { name: 'Cancel' })); expect(handleSubmit).toHaveBeenCalledTimes(1); expect(props.handleReset).toHaveBeenCalledTimes(1); diff --git a/frontend/packages/console-shared/src/components/formik-fields/__tests__/NumberSpinnerField.spec.tsx b/frontend/packages/console-shared/src/components/formik-fields/__tests__/NumberSpinnerField.spec.tsx index e66c97001cf..84e277ebae7 100644 --- a/frontend/packages/console-shared/src/components/formik-fields/__tests__/NumberSpinnerField.spec.tsx +++ b/frontend/packages/console-shared/src/components/formik-fields/__tests__/NumberSpinnerField.spec.tsx @@ -1,4 +1,5 @@ -import { screen, configure, fireEvent, act, waitFor } from '@testing-library/react'; +import { screen, configure, act, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { mockFormikRenderer } from '../../../test-utils/unit-test-utils'; import NumberSpinnerField from '../NumberSpinnerField'; @@ -25,23 +26,23 @@ describe('Number Spinner Field', () => { expect(getInput().value).toEqual('0'); await act(async () => { - await fireEvent.change(getInput(), { - currentTarget: { value: '12' }, - target: { value: '12' }, - }); + const user = userEvent.setup(); + await user.clear(getInput()); + await user.type(getInput(), '12'); }); await waitFor(() => expect(getInput().value).toEqual('12')); }); - it('should increment or decrement value based on clicked button', () => { + it('should increment or decrement value based on clicked button', async () => { + const user = userEvent.setup(); mockFormikRenderer(, { spinnerField: '' }); expect(getInput().value).toEqual('0'); - fireEvent.click(screen.getByTestId('Increment')); - fireEvent.click(screen.getByTestId('Increment')); + await user.click(screen.getByTestId('Increment')); + await user.click(screen.getByTestId('Increment')); expect(getInput().value).toEqual('2'); - fireEvent.click(screen.getByTestId('Decrement')); + await user.click(screen.getByTestId('Decrement')); expect(getInput().value).toEqual('1'); }); }); diff --git a/frontend/packages/console-shared/src/components/formik-fields/key-value-file-input-field/__tests__/KeyValueFileInputField.spec.tsx b/frontend/packages/console-shared/src/components/formik-fields/key-value-file-input-field/__tests__/KeyValueFileInputField.spec.tsx index d5d61180493..d98367db0fb 100644 --- a/frontend/packages/console-shared/src/components/formik-fields/key-value-file-input-field/__tests__/KeyValueFileInputField.spec.tsx +++ b/frontend/packages/console-shared/src/components/formik-fields/key-value-file-input-field/__tests__/KeyValueFileInputField.spec.tsx @@ -1,5 +1,6 @@ import type { FC } from 'react'; -import { screen, render, fireEvent, act, waitFor } from '@testing-library/react'; +import { screen, render, act, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import type { FormikConfig, FormikErrors } from 'formik'; import { Formik } from 'formik'; import KeyValueFileInputField from '../KeyValueFileInputField'; @@ -41,9 +42,10 @@ test('should have validation error given input field is touched and error exists ); const KeyField = screen.getByTestId('key-0'); - act(() => { - fireEvent.click(KeyField); - fireEvent.blur(KeyField); + const user = userEvent.setup(); + await act(async () => { + await user.click(KeyField); + await user.tab(); }); const validationErrors = await screen.findByText(`Required`); @@ -110,7 +112,8 @@ test('should add new entry on clicking Add key/value button', async () => { />, ); const addKeyValueButton = screen.getByTestId('add-key-value-button'); - fireEvent.click(addKeyValueButton); + const user = userEvent.setup(); + await user.click(addKeyValueButton); await waitFor(() => { const keyValuePair = screen.queryAllByTestId('key-value-pair'); diff --git a/frontend/packages/console-shared/src/components/formik-fields/multi-column-field/__tests__/MultiColumnFieldFooter.spec.tsx b/frontend/packages/console-shared/src/components/formik-fields/multi-column-field/__tests__/MultiColumnFieldFooter.spec.tsx index 60bec2c552a..7f24e39d2b2 100644 --- a/frontend/packages/console-shared/src/components/formik-fields/multi-column-field/__tests__/MultiColumnFieldFooter.spec.tsx +++ b/frontend/packages/console-shared/src/components/formik-fields/multi-column-field/__tests__/MultiColumnFieldFooter.spec.tsx @@ -1,4 +1,5 @@ -import { screen, fireEvent, waitFor } from '@testing-library/react'; +import { screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { renderWithProviders } from '../../../../test-utils/unit-test-utils'; import MultiColumnFieldFooter from '../MultiColumnFieldFooter'; @@ -13,7 +14,8 @@ describe('MultiColumnFieldFooter', () => { expect(screen.queryByRole('tooltip')).not.toBeInTheDocument(); }); - it('should render a disabled button when disableAddRow is true', () => { + it('should render a disabled button when disableAddRow is true', async () => { + const user = userEvent.setup(); const onAdd = jest.fn(); renderWithProviders(); @@ -22,11 +24,12 @@ describe('MultiColumnFieldFooter', () => { expect(button).toHaveAttribute('aria-disabled', 'true'); // Verify button doesn't trigger callback when disabled - fireEvent.click(button); + await user.click(button); expect(onAdd).not.toHaveBeenCalled(); }); it('should render a disabled button with tooltipAddRow prop', async () => { + const user = userEvent.setup(); const onAdd = jest.fn(); renderWithProviders( , @@ -37,11 +40,11 @@ describe('MultiColumnFieldFooter', () => { expect(button).toHaveAttribute('aria-disabled', 'true'); // Verify button doesn't trigger callback when disabled - fireEvent.click(button); + await user.click(button); expect(onAdd).not.toHaveBeenCalled(); // Hover over button to show tooltip - fireEvent.mouseEnter(button); + await user.hover(button); // Verify tooltip text appears await waitFor(() => { diff --git a/frontend/packages/console-shared/src/components/getting-started/__tests__/GettingStartedCard.spec.tsx b/frontend/packages/console-shared/src/components/getting-started/__tests__/GettingStartedCard.spec.tsx index cde12f6c5af..983d9990913 100644 --- a/frontend/packages/console-shared/src/components/getting-started/__tests__/GettingStartedCard.spec.tsx +++ b/frontend/packages/console-shared/src/components/getting-started/__tests__/GettingStartedCard.spec.tsx @@ -1,4 +1,5 @@ -import { screen, fireEvent } from '@testing-library/react'; +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { renderWithProviders } from '@console/shared/src/test-utils/unit-test-utils'; import type { GettingStartedCardProps } from '../GettingStartedCard'; import { GettingStartedCard } from '../GettingStartedCard'; @@ -41,7 +42,8 @@ describe('GettingStartedCard', () => { expect(screen.getByTestId('item more-link')).toBeInTheDocument(); }); - it('calls onClick for internal link', () => { + it('calls onClick for internal link', async () => { + const user = userEvent.setup(); const onClick = jest.fn(); const props = { ...defaultProps, @@ -55,11 +57,12 @@ describe('GettingStartedCard', () => { ], }; renderWithProviders(); - fireEvent.click(screen.getByTestId('item link-1')); + await user.click(screen.getByTestId('item link-1')); expect(onClick).toHaveBeenCalled(); }); - it('calls onClick for moreLink', () => { + it('calls onClick for moreLink', async () => { + const user = userEvent.setup(); const onClick = jest.fn(); const props = { ...defaultProps, @@ -71,7 +74,7 @@ describe('GettingStartedCard', () => { }, }; renderWithProviders(); - fireEvent.click(screen.getByTestId('item more-link')); + await user.click(screen.getByTestId('item more-link')); expect(onClick).toHaveBeenCalled(); }); diff --git a/frontend/packages/console-shared/src/components/getting-started/__tests__/RestoreGettingStartedButton.spec.tsx b/frontend/packages/console-shared/src/components/getting-started/__tests__/RestoreGettingStartedButton.spec.tsx index b734447db97..6b1e8d2c451 100644 --- a/frontend/packages/console-shared/src/components/getting-started/__tests__/RestoreGettingStartedButton.spec.tsx +++ b/frontend/packages/console-shared/src/components/getting-started/__tests__/RestoreGettingStartedButton.spec.tsx @@ -1,4 +1,5 @@ -import { screen, fireEvent } from '@testing-library/react'; +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { renderWithProviders } from '../../../test-utils/unit-test-utils'; import { RestoreGettingStartedButton } from '../RestoreGettingStartedButton'; import { useGettingStartedShowState, GettingStartedShowState } from '../useGettingStartedShowState'; @@ -29,7 +30,8 @@ describe('RestoreGettingStartedButton', () => { expect(screen.getByText('Show getting started resources')).toBeVisible(); }); - it('should change user settings to show if button is pressed', () => { + it('should change user settings to show if button is pressed', async () => { + const user = userEvent.setup(); const setGettingStartedShowState = jest.fn(); useGettingStartedShowStateMock.mockReturnValue([ GettingStartedShowState.HIDE, @@ -39,12 +41,13 @@ describe('RestoreGettingStartedButton', () => { renderWithProviders(); - fireEvent.click(screen.getByRole('button', { name: 'Show getting started resources' })); + await user.click(screen.getByRole('button', { name: 'Show getting started resources' })); expect(setGettingStartedShowState).toHaveBeenCalledTimes(1); expect(setGettingStartedShowState).toHaveBeenLastCalledWith(GettingStartedShowState.SHOW); }); - it('should change user settings to disappear if close (x) on the button is pressed', () => { + it('should change user settings to disappear if close (x) on the button is pressed', async () => { + const user = userEvent.setup(); const setGettingStartedShowState = jest.fn(); useGettingStartedShowStateMock.mockReturnValue([ GettingStartedShowState.HIDE, @@ -54,7 +57,7 @@ describe('RestoreGettingStartedButton', () => { renderWithProviders(); - fireEvent.click(screen.getByRole('button', { name: 'Close Show getting started resources' })); + await user.click(screen.getByRole('button', { name: 'Close Show getting started resources' })); expect(setGettingStartedShowState).toHaveBeenCalledTimes(1); expect(setGettingStartedShowState).toHaveBeenLastCalledWith(GettingStartedShowState.DISAPPEAR); }); diff --git a/frontend/packages/console-shared/src/components/modals/__tests__/FetchProgressModal.spec.tsx b/frontend/packages/console-shared/src/components/modals/__tests__/FetchProgressModal.spec.tsx index 0d510011282..b41d3f5c1a8 100644 --- a/frontend/packages/console-shared/src/components/modals/__tests__/FetchProgressModal.spec.tsx +++ b/frontend/packages/console-shared/src/components/modals/__tests__/FetchProgressModal.spec.tsx @@ -1,4 +1,5 @@ -import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { coFetch } from '@console/internal/co-fetch'; import type { FetchProgressModalProps } from '../FetchProgressModal'; import { FetchProgressModal } from '../FetchProgressModal'; @@ -163,6 +164,7 @@ describe('FetchProgressModal', () => { }); it('should show toast when download is cancelled', async () => { + const user = userEvent.setup(); const setIsDownloading = jest.fn(); coFetchMock.mockImplementation( (_url: string, options: { signal: AbortSignal }) => @@ -177,7 +179,7 @@ describe('FetchProgressModal', () => { , ); - fireEvent.click(screen.getByTestId('fetch-progress-modal-cancel')); + await user.click(screen.getByTestId('fetch-progress-modal-cancel')); await waitFor(() => { expect(setIsDownloading).toHaveBeenCalledWith(false); @@ -194,7 +196,8 @@ describe('FetchProgressModal', () => { expect(addToastMock).toHaveBeenCalled(); }); - it('should abort the fetch when Cancel button is clicked', () => { + it('should abort the fetch when Cancel button is clicked', async () => { + const user = userEvent.setup(); const abortSpy = jest.spyOn(AbortController.prototype, 'abort'); const setIsDownloading = jest.fn(); coFetchMock.mockReturnValue(new Promise(() => {})); @@ -203,7 +206,7 @@ describe('FetchProgressModal', () => { , ); - fireEvent.click(screen.getByRole('button', { name: 'Cancel' })); + await user.click(screen.getByRole('button', { name: 'Cancel' })); expect(abortSpy).toHaveBeenCalled(); expect(setIsDownloading).toHaveBeenCalledWith(false); @@ -312,6 +315,7 @@ describe('FetchProgressModal', () => { }); it('should close the modal when PF modal close button is clicked', async () => { + const user = userEvent.setup(); const setIsDownloading = jest.fn(); const mockReader = createMockReader(); coFetchMock.mockResolvedValue({ @@ -334,7 +338,7 @@ describe('FetchProgressModal', () => { />, ); - fireEvent.click(screen.getByLabelText('Close')); + await user.click(screen.getByLabelText('Close')); await waitFor(() => { expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); diff --git a/frontend/packages/console-shared/src/components/modals/__tests__/TextInputModal.spec.tsx b/frontend/packages/console-shared/src/components/modals/__tests__/TextInputModal.spec.tsx index 4adfea963c8..e3b7d8032f1 100644 --- a/frontend/packages/console-shared/src/components/modals/__tests__/TextInputModal.spec.tsx +++ b/frontend/packages/console-shared/src/components/modals/__tests__/TextInputModal.spec.tsx @@ -1,4 +1,5 @@ -import { screen, fireEvent, waitFor } from '@testing-library/react'; +import { screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { renderWithProviders } from '../../../test-utils/unit-test-utils'; import { TextInputModal } from '../TextInputModal'; @@ -33,43 +34,48 @@ describe('TextInputModal', () => { expect(input.value).toBe('initial-value'); }); - it('should call onSubmit with value when save button is clicked', () => { + it('should call onSubmit with value when save button is clicked', async () => { + const user = userEvent.setup(); renderWithProviders(); const saveButton = screen.getByRole('button', { name: 'Save' }); - fireEvent.click(saveButton); + await user.click(saveButton); expect(mockOnSubmit).toHaveBeenCalledWith('initial-value'); expect(mockCloseOverlay).toHaveBeenCalled(); }); - it('should update value when input changes', () => { + it('should update value when input changes', async () => { + const user = userEvent.setup(); renderWithProviders(); const input = screen.getByTestId('input-value') as HTMLInputElement; - fireEvent.change(input, { target: { value: 'new-value' } }); + await user.clear(input); + await user.type(input, 'new-value'); expect(input.value).toBe('new-value'); }); - it('should call closeOverlay when cancel button is clicked', () => { + it('should call closeOverlay when cancel button is clicked', async () => { + const user = userEvent.setup(); renderWithProviders(); const cancelButton = screen.getByRole('button', { name: 'Cancel' }); - fireEvent.click(cancelButton); + await user.click(cancelButton); expect(mockCloseOverlay).toHaveBeenCalled(); expect(mockOnSubmit).not.toHaveBeenCalled(); }); it('should show error when submitting empty value and isRequired is true', async () => { + const user = userEvent.setup(); renderWithProviders(); const input = screen.getByTestId('input-value') as HTMLInputElement; - fireEvent.change(input, { target: { value: '' } }); + await user.clear(input); const saveButton = screen.getByRole('button', { name: 'Save' }); - fireEvent.click(saveButton); + await user.click(saveButton); await waitFor(() => { expect(screen.getByText('This field is required')).toBeInTheDocument(); @@ -80,12 +86,13 @@ describe('TextInputModal', () => { }); it('should call validator and show error when validation fails', async () => { + const user = userEvent.setup(); const mockValidator = jest.fn().mockReturnValue('Invalid value'); renderWithProviders(); const saveButton = screen.getByRole('button', { name: 'Save' }); - fireEvent.click(saveButton); + await user.click(saveButton); await waitFor(() => { expect(screen.getByText('Invalid value')).toBeInTheDocument(); @@ -97,13 +104,14 @@ describe('TextInputModal', () => { }); it('should allow submission after fixing validation error', async () => { + const user = userEvent.setup(); const mockValidator = jest.fn().mockReturnValueOnce('Invalid value').mockReturnValueOnce(null); renderWithProviders(); // Trigger validation error const saveButton = screen.getByRole('button', { name: 'Save' }); - fireEvent.click(saveButton); + await user.click(saveButton); await waitFor(() => { expect(screen.getByText('Invalid value')).toBeInTheDocument(); @@ -111,8 +119,9 @@ describe('TextInputModal', () => { // Change input and retry const input = screen.getByTestId('input-value') as HTMLInputElement; - fireEvent.change(input, { target: { value: 'valid-value' } }); - fireEvent.click(saveButton); + await user.clear(input); + await user.type(input, 'valid-value'); + await user.click(saveButton); expect(mockValidator).toHaveBeenCalledTimes(2); expect(mockOnSubmit).toHaveBeenCalledWith('valid-value'); @@ -145,11 +154,12 @@ describe('TextInputModal', () => { expect(input).toHaveAttribute('type', 'email'); }); - it('should allow empty value submission when isRequired is false', () => { + it('should allow empty value submission when isRequired is false', async () => { + const user = userEvent.setup(); renderWithProviders(); const saveButton = screen.getByRole('button', { name: 'Save' }); - fireEvent.click(saveButton); + await user.click(saveButton); expect(mockOnSubmit).toHaveBeenCalledWith(''); expect(mockCloseOverlay).toHaveBeenCalled(); @@ -163,21 +173,24 @@ describe('TextInputModal', () => { expect(label.closest('.pf-v6-c-form__label')).toBeInTheDocument(); }); - it('should submit form when Enter is pressed in input field', () => { + it('should submit form when Enter is pressed in input field', async () => { + const user = userEvent.setup(); renderWithProviders(); const input = screen.getByTestId('input-value') as HTMLInputElement; - fireEvent.submit(input.closest('form')); + await user.click(input); + await user.keyboard('{Enter}'); expect(mockOnSubmit).toHaveBeenCalledWith('initial-value'); expect(mockCloseOverlay).toHaveBeenCalled(); }); it('should prevent submission when value is empty and isRequired is true', async () => { + const user = userEvent.setup(); renderWithProviders(); const saveButton = screen.getByRole('button', { name: 'Save' }); - fireEvent.click(saveButton); + await user.click(saveButton); // Should show validation error instead of calling onSubmit await waitFor(() => { diff --git a/frontend/packages/console-shared/src/components/progressive-list/__tests__/ProgressiveList.spec.tsx b/frontend/packages/console-shared/src/components/progressive-list/__tests__/ProgressiveList.spec.tsx index 7d02efb2ff6..05e79d4fff9 100644 --- a/frontend/packages/console-shared/src/components/progressive-list/__tests__/ProgressiveList.spec.tsx +++ b/frontend/packages/console-shared/src/components/progressive-list/__tests__/ProgressiveList.spec.tsx @@ -1,5 +1,6 @@ import type { FC, ReactNode } from 'react'; -import { screen, fireEvent } from '@testing-library/react'; +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { renderWithProviders } from '../../../test-utils/unit-test-utils'; import ProgressiveList from '../ProgressiveList'; import ProgressiveListItem from '../ProgressiveListItem'; @@ -49,7 +50,8 @@ describe('ProgressiveList', () => { expect(screen.getByRole('button', { name: 'Dummy' })).toBeVisible(); }); - it('clicking on a button should add that component related to it to visibleItems list', () => { + it('clicking on a button should add that component related to it to visibleItems list', async () => { + const user = userEvent.setup(); const visibleItems: string[] = []; const callback = jest.fn((item: string) => { visibleItems.push(item); @@ -67,7 +69,7 @@ describe('ProgressiveList', () => { expect(screen.queryByText('Dummy Component')).not.toBeInTheDocument(); expect(visibleItems).toHaveLength(0); - fireEvent.click(screen.getByRole('button', { name: 'Dummy' })); + await user.click(screen.getByRole('button', { name: 'Dummy' })); expect(callback).toHaveBeenCalledWith('Dummy'); expect(visibleItems).toHaveLength(1); diff --git a/frontend/packages/console-shared/src/components/toast/__tests__/ToastProvider.spec.tsx b/frontend/packages/console-shared/src/components/toast/__tests__/ToastProvider.spec.tsx index fc3cac43372..c3ae7960827 100644 --- a/frontend/packages/console-shared/src/components/toast/__tests__/ToastProvider.spec.tsx +++ b/frontend/packages/console-shared/src/components/toast/__tests__/ToastProvider.spec.tsx @@ -1,5 +1,6 @@ import { useContext } from 'react'; -import { act, screen, fireEvent, waitFor } from '@testing-library/react'; +import { act, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { renderWithProviders } from '../../../test-utils/unit-test-utils'; import type { ToastContextType } from '../ToastContext'; import ToastContext, { ToastVariant } from '../ToastContext'; @@ -70,6 +71,7 @@ describe('ToastProvider', () => { }); it('should dismiss toast on action', async () => { + const user = userEvent.setup(); const actionFn = jest.fn(); renderWithProviders( @@ -97,7 +99,7 @@ describe('ToastProvider', () => { }); const actionButton = screen.getByRole('button', { name: /action 1/i }); - fireEvent.click(actionButton); + await user.click(actionButton); expect(actionFn).toHaveBeenCalledTimes(1); @@ -140,6 +142,7 @@ describe('ToastProvider', () => { }); it('should dismiss toast on action on anchor click', async () => { + const user = userEvent.setup(); const actionFn = jest.fn(); renderWithProviders( @@ -169,9 +172,8 @@ describe('ToastProvider', () => { const actionLink = await screen.findByText('action 1'); const anchorElement = actionLink.closest('a'); - if (anchorElement) { - fireEvent.click(anchorElement); - } + expect(anchorElement).toBeTruthy(); + await user.click(anchorElement as HTMLElement); expect(actionFn).toHaveBeenCalledTimes(1); @@ -181,6 +183,7 @@ describe('ToastProvider', () => { }); it('should call onToastClose if provided on toast close', async () => { + const user = userEvent.setup(); const toastClose = jest.fn(); renderWithProviders( @@ -203,7 +206,7 @@ describe('ToastProvider', () => { }); const closeButton = screen.getByRole('button', { name: /close/i }); - fireEvent.click(closeButton); + await user.click(closeButton); expect(toastClose).toHaveBeenCalled(); }); diff --git a/frontend/packages/console-shared/src/test-utils/unit-test-utils.tsx b/frontend/packages/console-shared/src/test-utils/unit-test-utils.tsx index 1b33fbebe51..093b21ce2f0 100644 --- a/frontend/packages/console-shared/src/test-utils/unit-test-utils.tsx +++ b/frontend/packages/console-shared/src/test-utils/unit-test-utils.tsx @@ -3,7 +3,8 @@ import type { PluginStore } from '@openshift/dynamic-plugin-sdk'; import { PluginStoreProvider } from '@openshift/dynamic-plugin-sdk'; import { Form } from '@patternfly/react-core'; import type { RenderOptions, BoundFunctions, Queries } from '@testing-library/react'; -import { render, renderHook, screen, fireEvent, waitFor, within } from '@testing-library/react'; +import { render, renderHook, screen, waitFor, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import type { FormikValues } from 'formik'; import { Formik } from 'formik'; import { Provider } from 'react-redux'; @@ -182,11 +183,10 @@ export const verifyInputField = async ({ } // Simulate an input change if a new value is provided - // TODO: Use the 'userEvent' instead of 'fireEvent' after Jest and React Testing Libraries upgrade if (testValue !== undefined) { - await (async () => { - fireEvent.change(input, { target: { value: testValue } }); - await waitFor(() => expect(input).toHaveValue(testValue)); - }); + const user = userEvent.setup(); + await user.clear(input); + await user.type(input, testValue); + await waitFor(() => expect(input).toHaveValue(testValue)); } }; diff --git a/frontend/packages/container-security/src/components/__tests__/ImageVulnerabilityToggleGroup.spec.tsx b/frontend/packages/container-security/src/components/__tests__/ImageVulnerabilityToggleGroup.spec.tsx index ae386ace3b9..716628bc09b 100644 --- a/frontend/packages/container-security/src/components/__tests__/ImageVulnerabilityToggleGroup.spec.tsx +++ b/frontend/packages/container-security/src/components/__tests__/ImageVulnerabilityToggleGroup.spec.tsx @@ -1,4 +1,5 @@ -import { screen, fireEvent } from '@testing-library/react'; +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { renderWithProviders } from '@console/shared/src/test-utils/unit-test-utils'; import { fakeVulnFor } from '../../../integration-tests/bad-pods'; import { Priority } from '../../const'; @@ -28,11 +29,12 @@ describe('ImageVulnerabilityToggleGroup', () => { expect(screen.getByText(/Quay Security Scanner has detected.*vulnerabilities/i)).toBeVisible(); }); - it('should render empty state when switching to App dependency with no vulnerabilities', () => { + it('should render empty state when switching to App dependency with no vulnerabilities', async () => { + const user = userEvent.setup(); renderWithProviders(); const appDependencyButton = screen.getByText('App dependency'); - fireEvent.click(appDependencyButton); + await user.click(appDependencyButton); expect(screen.getByText(/No.*app dependency.*vulnerabilities/i)).toBeVisible(); }); diff --git a/frontend/packages/dev-console/src/components/deployments/__tests__/AdvancedSection.spec.tsx b/frontend/packages/dev-console/src/components/deployments/__tests__/AdvancedSection.spec.tsx index 7449459fa68..f8df34fbec3 100644 --- a/frontend/packages/dev-console/src/components/deployments/__tests__/AdvancedSection.spec.tsx +++ b/frontend/packages/dev-console/src/components/deployments/__tests__/AdvancedSection.spec.tsx @@ -1,4 +1,5 @@ -import { render, screen, fireEvent, cleanup, waitFor } from '@testing-library/react'; +import { render, screen, cleanup, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import i18n from 'i18next'; import { setI18n } from 'react-i18next'; import { Resources } from '../../import/import-types'; @@ -34,6 +35,7 @@ afterEach(() => cleanup()); describe('AdvancedSection', () => { it('should show Pause rollouts section on click', async () => { + const user = userEvent.setup(); expect(screen.getByTestId('deployment-form-testid').textContent).toEqual( 'Click on the names to access advanced options for Pause rollouts and Scaling.', ); @@ -45,7 +47,7 @@ describe('AdvancedSection', () => { expect(screen.queryByTestId('pause-rollouts')).toBeNull(); expect(screen.queryByRole('checkbox')).toBeNull(); - fireEvent.click(pauseRolloutsButton); + await user.click(pauseRolloutsButton); await waitFor(() => { expect(screen.queryByTestId('pause-rollouts')).not.toBeNull(); @@ -58,6 +60,7 @@ describe('AdvancedSection', () => { }); it('should show Scaling section on click', async () => { + const user = userEvent.setup(); expect(screen.getByTestId('deployment-form-testid').textContent).toEqual( 'Click on the names to access advanced options for Pause rollouts and Scaling.', ); @@ -69,7 +72,7 @@ describe('AdvancedSection', () => { expect(screen.queryByTestId('scaling')).toBeNull(); expect(screen.queryByRole('spinbutton', { name: /input/i })).toBeNull(); - fireEvent.click(scalingButton); + await user.click(scalingButton); await waitFor(() => { expect(screen.queryByTestId('scaling')).not.toBeNull(); @@ -82,6 +85,7 @@ describe('AdvancedSection', () => { }); it('should not show message when both advanced options are clicked', async () => { + const user = userEvent.setup(); expect(screen.getByTestId('deployment-form-testid').textContent).toEqual( 'Click on the names to access advanced options for Pause rollouts and Scaling.', ); @@ -92,7 +96,7 @@ describe('AdvancedSection', () => { expect(screen.queryByTestId('pause-rollouts')).toBeNull(); expect(screen.queryByRole('checkbox')).toBeNull(); - fireEvent.click(pauseRolloutsButton); + await user.click(pauseRolloutsButton); await waitFor(() => { expect(screen.queryByTestId('pause-rollouts')).not.toBeNull(); @@ -110,7 +114,7 @@ describe('AdvancedSection', () => { expect(screen.queryByTestId('scaling')).toBeNull(); expect(screen.queryByRole('spinbutton', { name: /input/i })).toBeNull(); - fireEvent.click(scalingButton); + await user.click(scalingButton); await waitFor(() => { expect(screen.queryByTestId('scaling')).not.toBeNull(); diff --git a/frontend/packages/dev-console/src/components/deployments/__tests__/DeploymentForm.spec.tsx b/frontend/packages/dev-console/src/components/deployments/__tests__/DeploymentForm.spec.tsx index 84e118a56ca..bae9e0dcc22 100644 --- a/frontend/packages/dev-console/src/components/deployments/__tests__/DeploymentForm.spec.tsx +++ b/frontend/packages/dev-console/src/components/deployments/__tests__/DeploymentForm.spec.tsx @@ -1,5 +1,6 @@ import type { FC } from 'react'; -import { fireEvent, screen, cleanup, waitFor } from '@testing-library/react'; +import { screen, cleanup, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import i18n from 'i18next'; import * as _ from 'lodash'; import { setI18n } from 'react-i18next'; @@ -102,19 +103,20 @@ describe('EditDeploymentForm', () => { }); it('should show all the form sections wrt form/YAML view', async () => { + const user = userEvent.setup(); const formButton = screen.getByRole('radio', { name: /form view/i }); const yamlButton = screen.getByRole('radio', { name: /yaml view/i, }); - fireEvent.click(yamlButton); + await user.click(yamlButton); await waitFor(() => { expect(screen.queryByTestId('yaml-editor')).not.toBeNull(); expect(screen.queryByTestId('form-footer')).not.toBeNull(); }); - fireEvent.click(formButton); + await user.click(formButton); await waitFor(() => { expect(screen.queryByTestId('info-alert')).not.toBeNull(); @@ -127,6 +129,7 @@ describe('EditDeploymentForm', () => { }); it('should disable save button and show loader on save button click', async () => { + const user = userEvent.setup(); const saveButton = screen.getByRole('button', { name: /save/i, }); @@ -138,23 +141,25 @@ describe('EditDeploymentForm', () => { mockValues.formData.isSearchingForImage = true; mockValues.formData.deploymentStrategy.rollingParams.timeoutSeconds = 500; - fireEvent.change(timeoutField, { target: { value: 500 } }); + await user.clear(timeoutField); + await user.type(timeoutField, '500'); expect(saveButton.hasAttribute('disabled')).toBeFalsy(); await waitFor(() => { expect(timeoutField.value).toEqual('500'); }); - fireEvent.click(saveButton); + await user.click(saveButton); - expect(saveButton.hasAttribute('disabled')).toBeTruthy(); - expect(saveButton.querySelector('.pf-v6-c-button__progress')).not.toBeNull(); await waitFor(() => { + expect(saveButton.hasAttribute('disabled')).toBeTruthy(); + expect(saveButton.querySelector('.pf-v6-c-button__progress')).not.toBeNull(); expect(handleSubmit).toHaveBeenCalledTimes(1); }); }); it('should load the form with current resource values on reload button click', async () => { + const user = userEvent.setup(); const reloadButton = screen.getByRole('button', { name: /reload/i, }); @@ -162,13 +167,14 @@ describe('EditDeploymentForm', () => { name: /timeout/i, }) as HTMLInputElement; - fireEvent.change(timeoutField, { target: { value: 500 } }); + await user.clear(timeoutField); + await user.type(timeoutField, '500'); await waitFor(() => { expect(timeoutField.value).toEqual('500'); }); - fireEvent.click(reloadButton); + await user.click(reloadButton); await waitFor(() => { expect(timeoutField.value).toEqual('600'); @@ -176,11 +182,12 @@ describe('EditDeploymentForm', () => { }); it('should call handleCancel on Cancel button click ', async () => { + const user = userEvent.setup(); const cancelButton = screen.getByRole('button', { name: /cancel/i, }); - fireEvent.click(cancelButton); + await user.click(cancelButton); await waitFor(() => expect(handleCancel).toHaveBeenCalledTimes(1)); }); diff --git a/frontend/packages/dev-console/src/components/deployments/__tests__/DeploymentStrategySection.spec.tsx b/frontend/packages/dev-console/src/components/deployments/__tests__/DeploymentStrategySection.spec.tsx index 50161ea274c..3b95b51d5fd 100644 --- a/frontend/packages/dev-console/src/components/deployments/__tests__/DeploymentStrategySection.spec.tsx +++ b/frontend/packages/dev-console/src/components/deployments/__tests__/DeploymentStrategySection.spec.tsx @@ -1,4 +1,5 @@ -import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { cleanup, render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { Provider } from 'react-redux'; import store from '@console/internal/redux'; import { Resources } from '../../import/import-types'; @@ -18,6 +19,7 @@ afterEach(() => cleanup()); describe('DeploymentStrategySection(DeploymentConfig)', () => { it('should show strategy fields based on strategy type selected', async () => { + const user = userEvent.setup(); render( {() => ( @@ -39,21 +41,21 @@ describe('DeploymentStrategySection(DeploymentConfig)', () => { name: /strategy type/i, }); - fireEvent.click(strategyDropdown); + await user.click(strategyDropdown); const recreateButton = screen.getByRole('option', { name: /recreate/i }); - fireEvent.click(recreateButton); + await user.click(recreateButton); await waitFor(() => { expect(screen.queryByTestId('recreateParams')).not.toBeNull(); }); - fireEvent.click(strategyDropdown); + await user.click(strategyDropdown); const customButton = screen.getByRole('option', { name: /custom/i }); - fireEvent.click(customButton); + await user.click(customButton); await waitFor(() => { expect(screen.queryByTestId('customParams')).not.toBeNull(); @@ -61,6 +63,7 @@ describe('DeploymentStrategySection(DeploymentConfig)', () => { }); it('should render additional fields for Recreate strategy type', async () => { + const user = userEvent.setup(); render( { name: /strategy type/i, }); - fireEvent.click(strategyDropdown); + await user.click(strategyDropdown); const recreateButton = screen.getByRole('option', { name: /recreate/i }); - fireEvent.click(recreateButton); + await user.click(recreateButton); await waitFor(() => { expect(screen.queryByTestId('recreateParams')).not.toBeNull(); @@ -97,7 +100,7 @@ describe('DeploymentStrategySection(DeploymentConfig)', () => { const advancedSection = screen.getByText('Show additional parameters and lifecycle hooks'); - fireEvent.click(advancedSection); + await user.click(advancedSection); await waitFor(() => { expect(screen.getByText('Pre Lifecycle Hook')).not.toBeNull(); @@ -107,7 +110,7 @@ describe('DeploymentStrategySection(DeploymentConfig)', () => { const addMidLifecycleHook = screen.getByText('Add mid lifecycle hook'); - fireEvent.click(addMidLifecycleHook); + await user.click(addMidLifecycleHook); await waitFor(() => { expect( @@ -120,7 +123,7 @@ describe('DeploymentStrategySection(DeploymentConfig)', () => { let action = screen.getByText( 'Runs a command in a new pod using the container from the deployment template. You can add additional environment variables and volumes', ); - fireEvent.click(action); + await user.click(action); await waitFor(() => { expect(screen.getByText('Container name')).not.toBeNull(); @@ -130,7 +133,7 @@ describe('DeploymentStrategySection(DeploymentConfig)', () => { action = screen.getByText( 'Tags the current image as an image stream tag if the deployment succeeds', ); - fireEvent.click(action); + await user.click(action); await waitFor(() => { expect(screen.getByText('Container name')).not.toBeNull(); @@ -141,6 +144,7 @@ describe('DeploymentStrategySection(DeploymentConfig)', () => { describe('DeploymentStrategySection(Deployment)', () => { it('should show strategy fields based on strategy type selected', async () => { + const user = userEvent.setup(); render( { expect(screen.queryByTestId('rollingUpdate')).not.toBeNull(); }); - fireEvent.click(strategyDropdown); + await user.click(strategyDropdown); const recreateButton = screen.getByRole('option', { name: /recreate/i }); - fireEvent.click(recreateButton); + await user.click(recreateButton); await waitFor(() => { expect(screen.queryByTestId('recreateParams')).toBeNull(); }); - fireEvent.click(strategyDropdown); + await user.click(strategyDropdown); await waitFor(() => { expect(screen.queryByRole('option', { name: /custom/i })).toBeNull(); diff --git a/frontend/packages/dev-console/src/components/deployments/__tests__/EnvironmentVariablesSection.spec.tsx b/frontend/packages/dev-console/src/components/deployments/__tests__/EnvironmentVariablesSection.spec.tsx index 97eb86ea1d6..0ed1708eb0f 100644 --- a/frontend/packages/dev-console/src/components/deployments/__tests__/EnvironmentVariablesSection.spec.tsx +++ b/frontend/packages/dev-console/src/components/deployments/__tests__/EnvironmentVariablesSection.spec.tsx @@ -1,5 +1,6 @@ import type { FC } from 'react'; -import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { cleanup, render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import * as _ from 'lodash'; import { Provider } from 'react-redux'; import store from '@console/internal/redux'; @@ -53,9 +54,10 @@ describe('EnvironmentVariablesSection', () => { }); it('should add a new row when (+) Add button is clicked', async () => { + const user = userEvent.setup(); const addButton = screen.getByRole('button', { name: /add value/i }); - fireEvent.click(addButton); + await user.click(addButton); const names = screen.getAllByPlaceholderText(/name/i).map((ele: HTMLInputElement) => ele.value); const values = screen @@ -69,30 +71,32 @@ describe('EnvironmentVariablesSection', () => { }); it('should add new row with resourse and key dropdowns when (+) Add ConfigMap or Secret button is clicked', async () => { + const user = userEvent.setup(); const addCMSButton = screen.getByRole('button', { name: /add from configmap or secret/i, }); - fireEvent.click(addCMSButton); + await user.click(addCMSButton); const resourceButton = screen.getByRole('button', { name: /select a resource/i }); const keyButton = screen.getByRole('button', { name: /select a key/i }); - fireEvent.click(resourceButton); + await user.click(resourceButton); const resourceDropdown = screen.queryByPlaceholderText(/configmap or secret/i); await waitFor(() => expect(resourceDropdown).not.toBeNull()); - fireEvent.click(keyButton); + await user.click(keyButton); const keyDropdown = screen.queryByPlaceholderText(/key/i); await waitFor(() => expect(keyDropdown).not.toBeNull()); }); it('should remove row when (-) button is clicked', async () => { + const user = userEvent.setup(); const deleteButtons = screen.getAllByRole('button', { name: /delete/i }); - fireEvent.click(deleteButtons[0]); + await user.click(deleteButtons[0]); const names = screen.getAllByPlaceholderText(/name/i).map((ele: HTMLInputElement) => ele.value); const values = screen diff --git a/frontend/packages/dev-console/src/components/deployments/__tests__/ImagesSection.spec.tsx b/frontend/packages/dev-console/src/components/deployments/__tests__/ImagesSection.spec.tsx index f7c8e211745..64f0add889d 100644 --- a/frontend/packages/dev-console/src/components/deployments/__tests__/ImagesSection.spec.tsx +++ b/frontend/packages/dev-console/src/components/deployments/__tests__/ImagesSection.spec.tsx @@ -1,6 +1,7 @@ import type { FC } from 'react'; import type { RenderResult } from '@testing-library/react'; -import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { cleanup, render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { Provider } from 'react-redux'; import store from '@console/internal/redux'; import { Resources } from '../../import/import-types'; @@ -41,6 +42,7 @@ afterEach(() => cleanup()); describe('ImagesSection', () => { it('should have image-stream-tag dropdowns or image-name text field based on fromImageStreamTagCheckbox value', async () => { + const user = userEvent.setup(); const fromImageStreamTagCheckbox = screen.getByRole('checkbox', { name: /deploy image from an image stream tag/i, }); @@ -48,7 +50,7 @@ describe('ImagesSection', () => { expect(screen.queryByTestId('image-stream-tag')).not.toBeNull(); expect(screen.queryByTestId('image-name')).toBeNull(); - fireEvent.click(fromImageStreamTagCheckbox); + await user.click(fromImageStreamTagCheckbox); await waitFor(() => { expect(screen.queryByTestId('image-name')).not.toBeNull(); @@ -57,6 +59,7 @@ describe('ImagesSection', () => { }); it('should have the required trigger checkbox fields based on fromImageStreamTagCheckbox value', async () => { + const user = userEvent.setup(); const fromImageStreamTagCheckbox = screen.getByRole('checkbox', { name: /deploy image from an image stream tag/i, }); @@ -72,7 +75,7 @@ describe('ImagesSection', () => { }), ).not.toBeNull(); - fireEvent.click(fromImageStreamTagCheckbox); + await user.click(fromImageStreamTagCheckbox); await waitFor(() => { expect( @@ -89,6 +92,7 @@ describe('ImagesSection', () => { }); it('should have the required trigger checkbox fields based on resourceType', async () => { + const user = userEvent.setup(); renderResults.rerender( {() => ( @@ -114,7 +118,7 @@ describe('ImagesSection', () => { }), ).toBeNull(); - fireEvent.click(fromImageStreamTagCheckbox); + await user.click(fromImageStreamTagCheckbox); await waitFor(() => { expect( @@ -131,6 +135,7 @@ describe('ImagesSection', () => { }); it('should have the advanced options expand/collapse button', async () => { + const user = userEvent.setup(); const showAdvancedOptions = screen.getByRole('button', { name: /show advanced image options/i, }); @@ -146,7 +151,7 @@ describe('ImagesSection', () => { }), ).toBeNull(); - fireEvent.click(showAdvancedOptions); + await user.click(showAdvancedOptions); await waitFor(() => { expect( diff --git a/frontend/packages/dev-console/src/components/deployments/__tests__/PauseRolloutsSection.spec.tsx b/frontend/packages/dev-console/src/components/deployments/__tests__/PauseRolloutsSection.spec.tsx index 790065d9fbc..7b397ccf24d 100644 --- a/frontend/packages/dev-console/src/components/deployments/__tests__/PauseRolloutsSection.spec.tsx +++ b/frontend/packages/dev-console/src/components/deployments/__tests__/PauseRolloutsSection.spec.tsx @@ -1,10 +1,12 @@ -import { render, fireEvent, screen, waitFor } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { Resources } from '../../import/import-types'; import MockForm from '../__mocks__/MockForm'; import PauseRolloutsSection from '../PauseRolloutsSection'; describe('PauseRolloutsSection', () => { it('checkbox should work correctly', async () => { + const user = userEvent.setup(); const handleSubmit = jest.fn(); render( @@ -13,7 +15,7 @@ describe('PauseRolloutsSection', () => { ); const checkbox = screen.getByRole('checkbox') as HTMLInputElement; expect(checkbox.value).toEqual('false'); - fireEvent.click(checkbox); + await user.click(checkbox); await waitFor(() => expect(checkbox.value).toEqual('true')); }); }); diff --git a/frontend/packages/dev-console/src/components/user-preferences/__tests__/SecureRouteFields.spec.tsx b/frontend/packages/dev-console/src/components/user-preferences/__tests__/SecureRouteFields.spec.tsx index ea0bf205410..a8c85f237cc 100644 --- a/frontend/packages/dev-console/src/components/user-preferences/__tests__/SecureRouteFields.spec.tsx +++ b/frontend/packages/dev-console/src/components/user-preferences/__tests__/SecureRouteFields.spec.tsx @@ -1,4 +1,5 @@ -import { configure, fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { configure, render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import SecureRouteFields from '../SecureRouteFields'; import { usePreferredRoutingOptions } from '../usePreferredRoutingOptions'; @@ -35,6 +36,7 @@ describe('SecureRouteFields', () => { }); it('should not show Allow option in Insecure traffic dropdown if TLS termination is Passthrough', async () => { + const user = userEvent.setup(); mockUsePreferredRoutingOptions.mockReturnValue([ { secure: true, @@ -45,8 +47,8 @@ describe('SecureRouteFields', () => { true, ]); render(); - const inSecureTraffic = screen.queryByTestId('insecure-traffic'); - fireEvent.click(inSecureTraffic); + const inSecureTraffic = screen.getByTestId('insecure-traffic'); + await user.click(inSecureTraffic); await waitFor(() => { expect(screen.queryByRole('option', { name: /Allow/i })).toBeNull(); expect(screen.queryByRole('option', { name: /None/i })).not.toBeNull(); @@ -55,6 +57,7 @@ describe('SecureRouteFields', () => { }); it('should show Allow, None and Redirect options in Insecure traffic dropdown if TLS termination is not Passthrough', async () => { + const user = userEvent.setup(); mockUsePreferredRoutingOptions.mockReturnValue([ { secure: true, @@ -65,8 +68,8 @@ describe('SecureRouteFields', () => { true, ]); render(); - const inSecureTraffic = screen.queryByTestId('insecure-traffic'); - fireEvent.click(inSecureTraffic); + const inSecureTraffic = screen.getByTestId('insecure-traffic'); + await user.click(inSecureTraffic); await waitFor(() => { expect(screen.queryByRole('option', { name: /Allow/i })).not.toBeNull(); expect(screen.queryByRole('option', { name: /None/i })).not.toBeNull(); diff --git a/frontend/packages/knative-plugin/src/components/overview/__tests__/RevisionsOverviewList.spec.tsx b/frontend/packages/knative-plugin/src/components/overview/__tests__/RevisionsOverviewList.spec.tsx index cc4b2ba2537..93ecf952490 100644 --- a/frontend/packages/knative-plugin/src/components/overview/__tests__/RevisionsOverviewList.spec.tsx +++ b/frontend/packages/knative-plugin/src/components/overview/__tests__/RevisionsOverviewList.spec.tsx @@ -1,4 +1,5 @@ -import { render, screen, fireEvent } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import * as _ from 'lodash'; import { useAccessReview } from '@console/internal/components/utils'; import { referenceForModel } from '@console/internal/module/k8s'; @@ -129,7 +130,8 @@ describe('RevisionsOverviewList', () => { expect(button?.outerHTML).not.toContain('isdisabled'); }); - it('should call setTrafficDistributionModal on click', () => { + it('should call setTrafficDistributionModal on click', async () => { + const user = userEvent.setup(); const trafficSplitModalLauncherMock = jest.fn(); useTrafficSplittingModalLauncherMock.mockImplementation(() => trafficSplitModalLauncherMock); const { container } = render( @@ -140,7 +142,7 @@ describe('RevisionsOverviewList', () => { ); const button = container.querySelector('Button'); expect(button).toBeInTheDocument(); - fireEvent.click(button); + await user.click(button as HTMLElement); expect(trafficSplitModalLauncherMock).toHaveBeenCalled(); }); diff --git a/frontend/packages/knative-plugin/src/components/sink-source/__tests__/SinkSourceModal.spec.tsx b/frontend/packages/knative-plugin/src/components/sink-source/__tests__/SinkSourceModal.spec.tsx index b055f64e6c6..8723ba1fe3d 100644 --- a/frontend/packages/knative-plugin/src/components/sink-source/__tests__/SinkSourceModal.spec.tsx +++ b/frontend/packages/knative-plugin/src/components/sink-source/__tests__/SinkSourceModal.spec.tsx @@ -1,5 +1,6 @@ import type { ComponentProps } from 'react'; -import { render, fireEvent } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { formikFormProps } from '@console/shared/src/test-utils/formik-props-utils'; import { ServiceModel } from '../../../models'; import SinkSourceModal from '../SinkSourceModal'; @@ -8,7 +9,13 @@ jest.mock('@patternfly/react-core', () => ({ ...jest.requireActual('@patternfly/react-core'), ModalHeader: jest.fn(() => null), ModalBody: jest.fn(({ children }) =>
{children}
), - Button: jest.fn(() => null), + Button: jest.fn(({ children, type, ...props }) => + type === 'submit' ? ( + + ) : null, + ), Form: jest.fn(({ children, ...props }) => {children}), })); @@ -33,6 +40,28 @@ jest.mock('react-i18next', () => ({ Trans: jest.fn(() => null), })); +// jsdom does not implement requestSubmit; user-event uses it for type="submit" clicks +let originalRequestSubmit: typeof HTMLFormElement.prototype.requestSubmit; + +beforeAll(() => { + originalRequestSubmit = HTMLFormElement.prototype.requestSubmit; + if (!HTMLFormElement.prototype.requestSubmit) { + HTMLFormElement.prototype.requestSubmit = function requestSubmitPolyfill( + this: HTMLFormElement, + ) { + this.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })); + }; + } +}); + +afterAll(() => { + if (originalRequestSubmit !== undefined) { + HTMLFormElement.prototype.requestSubmit = originalRequestSubmit; + } else { + delete (HTMLFormElement.prototype as any).requestSubmit; + } +}); + type SinkSourceModalProps = ComponentProps; describe('SinkSourceModal Form', () => { @@ -65,12 +94,10 @@ describe('SinkSourceModal Form', () => { expect(form).toHaveAttribute('id', 'sink-source-form'); }); - it('should call handleSubmit on form submit', () => { - const { container } = render(); - const form = container.querySelector('form'); - if (form) { - fireEvent.submit(form); - } + it('should call handleSubmit on form submit', async () => { + const user = userEvent.setup(); + render(); + await user.click(screen.getByRole('button', { name: 'knative-plugin~Save' })); expect(formProps.handleSubmit).toHaveBeenCalled(); }); diff --git a/frontend/packages/knative-plugin/src/components/traffic-splitting/__tests__/TrafficSplittingModal.spec.tsx b/frontend/packages/knative-plugin/src/components/traffic-splitting/__tests__/TrafficSplittingModal.spec.tsx index d2f8b55267e..a5c662a1af9 100644 --- a/frontend/packages/knative-plugin/src/components/traffic-splitting/__tests__/TrafficSplittingModal.spec.tsx +++ b/frontend/packages/knative-plugin/src/components/traffic-splitting/__tests__/TrafficSplittingModal.spec.tsx @@ -1,5 +1,6 @@ import type { ComponentProps } from 'react'; -import { render, fireEvent } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { formikFormProps } from '@console/shared/src/test-utils/formik-props-utils'; import { mockTrafficData, @@ -11,7 +12,13 @@ jest.mock('@patternfly/react-core', () => ({ ...jest.requireActual('@patternfly/react-core'), ModalHeader: jest.fn(() => null), ModalBody: jest.fn(({ children }) =>
{children}
), - Button: jest.fn(() => null), + Button: jest.fn(({ children, type, ...props }) => + type === 'submit' ? ( + + ) : null, + ), Form: jest.fn(({ children, ...props }) =>
{children}
), })); @@ -30,6 +37,28 @@ jest.mock('react-i18next', () => ({ }), })); +// jsdom does not implement requestSubmit; user-event uses it for type="submit" clicks +let originalRequestSubmit: typeof HTMLFormElement.prototype.requestSubmit; + +beforeAll(() => { + originalRequestSubmit = HTMLFormElement.prototype.requestSubmit; + if (!HTMLFormElement.prototype.requestSubmit) { + HTMLFormElement.prototype.requestSubmit = function requestSubmitPolyfill( + this: HTMLFormElement, + ) { + this.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })); + }; + } +}); + +afterAll(() => { + if (originalRequestSubmit !== undefined) { + HTMLFormElement.prototype.requestSubmit = originalRequestSubmit; + } else { + delete (HTMLFormElement.prototype as any).requestSubmit; + } +}); + type TrafficSplittingModalProps = ComponentProps; describe('TrafficSplittingModal', () => { @@ -51,12 +80,10 @@ describe('TrafficSplittingModal', () => { expect(container.querySelector('form')).toHaveAttribute('id', 'traffic-splitting-form'); }); - it('should call handleSubmit on form submit', () => { - const { container } = render(); - const form = container.querySelector('form'); - if (form) { - fireEvent.submit(form); - } + it('should call handleSubmit on form submit', async () => { + const user = userEvent.setup(); + render(); + await user.click(screen.getByRole('button', { name: 'knative-plugin~Save' })); expect(formProps.handleSubmit).toHaveBeenCalled(); }); }); diff --git a/frontend/packages/operator-lifecycle-manager-v1/src/components/cluster-extension/__tests__/ClusterExtensionForm.spec.tsx b/frontend/packages/operator-lifecycle-manager-v1/src/components/cluster-extension/__tests__/ClusterExtensionForm.spec.tsx index 57ec7dd7d4a..9c0733c16a2 100644 --- a/frontend/packages/operator-lifecycle-manager-v1/src/components/cluster-extension/__tests__/ClusterExtensionForm.spec.tsx +++ b/frontend/packages/operator-lifecycle-manager-v1/src/components/cluster-extension/__tests__/ClusterExtensionForm.spec.tsx @@ -1,4 +1,5 @@ -import { screen, fireEvent } from '@testing-library/react'; +import { screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { renderWithProviders } from '@console/shared/src/test-utils/unit-test-utils'; import ClusterExtensionForm from '../ClusterExtensionForm'; @@ -76,17 +77,59 @@ describe('ClusterExtensionForm', () => { expect(screen.getByLabelText('Version or Version range')).toHaveValue('1.0.0'); }); - it('should auto-generate namespace and service account names based on ClusterExtension name', () => { - renderWithProviders(); + it('should auto-generate namespace and service account names based on ClusterExtension name', async () => { + const user = userEvent.setup(); + let currentFormData = {}; + + // Render with onChange that tracks formData updates + const { rerender } = renderWithProviders( + , + ); + + const nameInput = screen.getByRole('textbox', { name: /^Name$/i }); + await user.click(nameInput); + await user.paste('my-operator'); + + // Wait for onChange to be called with the name + await waitFor(() => { + expect(mockOnChange).toHaveBeenCalledWith( + expect.objectContaining({ + metadata: expect.objectContaining({ name: 'my-operator' }), + }), + ); + }); - const nameInput = screen.getByLabelText('Name'); - fireEvent.change(nameInput, { target: { value: 'my-operator' } }); + // Simulate parent component updating formData + [currentFormData] = mockOnChange.mock.calls[mockOnChange.mock.calls.length - 1]; + rerender(); + + // Wait for debounce (500ms) and auto-generation to trigger + await waitFor( + () => { + expect(mockOnChange).toHaveBeenCalledWith( + expect.objectContaining({ + spec: expect.objectContaining({ + namespace: 'my-operator', + }), + }), + ); + }, + { timeout: 2000 }, + ); - // Should call onChange with auto-generated names - expect(mockOnChange).toHaveBeenCalledWith( - expect.objectContaining({ - metadata: expect.objectContaining({ name: 'my-operator' }), - }), + await waitFor( + () => { + expect(mockOnChange).toHaveBeenCalledWith( + expect.objectContaining({ + spec: expect.objectContaining({ + serviceAccount: expect.objectContaining({ + name: 'my-operator-service-account', + }), + }), + }), + ); + }, + { timeout: 2000 }, ); }); @@ -108,96 +151,117 @@ describe('ClusterExtensionForm', () => { expect(createNewRadio).toBeChecked(); }); - it('should switch to "Select from cluster" when radio button is clicked', () => { + it('should switch to "Select from cluster" when radio button is clicked', async () => { + const user = userEvent.setup(); renderWithProviders(); const selectFromClusterRadios = screen.getAllByLabelText('Select from cluster'); const namespaceRadio = selectFromClusterRadios[0]; - fireEvent.click(namespaceRadio); + await user.click(namespaceRadio); expect(namespaceRadio).toBeChecked(); }); - it('should update formData when package name changes', () => { + it('should update formData when package name changes', async () => { + const user = userEvent.setup(); renderWithProviders(); - const packageNameInput = screen.getByLabelText('Package name'); - fireEvent.change(packageNameInput, { target: { value: 'test-package' } }); - - expect(mockOnChange).toHaveBeenCalledWith( - expect.objectContaining({ - spec: expect.objectContaining({ - source: expect.objectContaining({ - catalog: expect.objectContaining({ - packageName: 'test-package', + const packageNameInput = screen.getByRole('textbox', { name: /Package name/i }); + await user.click(packageNameInput); + await user.paste('test-package'); + + // Wait for onChange to be called with updated package name + await waitFor(() => { + expect(mockOnChange).toHaveBeenCalledWith( + expect.objectContaining({ + spec: expect.objectContaining({ + source: expect.objectContaining({ + catalog: expect.objectContaining({ + packageName: 'test-package', + }), }), }), }), - }), - ); + ); + }); }); - it('should update formData when version changes', () => { + it('should update formData when version changes', async () => { + const user = userEvent.setup(); renderWithProviders(); - const versionInput = screen.getByLabelText('Version or Version range'); - fireEvent.change(versionInput, { target: { value: '1.2.3' } }); - - expect(mockOnChange).toHaveBeenCalledWith( - expect.objectContaining({ - spec: expect.objectContaining({ - source: expect.objectContaining({ - catalog: expect.objectContaining({ - version: '1.2.3', + const versionInput = screen.getByRole('textbox', { name: /Version or Version range/i }); + await user.click(versionInput); + await user.paste('1.2.3'); + + // Wait for onChange to be called with updated version + await waitFor(() => { + expect(mockOnChange).toHaveBeenCalledWith( + expect.objectContaining({ + spec: expect.objectContaining({ + source: expect.objectContaining({ + catalog: expect.objectContaining({ + version: '1.2.3', + }), }), }), }), - }), - ); + ); + }); }); - it('should add channel when Enter is pressed', () => { + it('should add channel when Enter is pressed', async () => { + const user = userEvent.setup(); renderWithProviders(); const channelInput = screen.getByLabelText('Channels'); - fireEvent.change(channelInput, { target: { value: 'stable' } }); - fireEvent.keyDown(channelInput, { key: 'Enter', code: 'Enter' }); - - expect(mockOnChange).toHaveBeenCalledWith( - expect.objectContaining({ - spec: expect.objectContaining({ - source: expect.objectContaining({ - catalog: expect.objectContaining({ - channels: ['stable'], + await user.click(channelInput); + await user.clear(channelInput); + await user.type(channelInput, 'stable'); + await user.keyboard('{Enter}'); + + await waitFor(() => { + expect(mockOnChange).toHaveBeenLastCalledWith( + expect.objectContaining({ + spec: expect.objectContaining({ + source: expect.objectContaining({ + catalog: expect.objectContaining({ + channels: ['stable'], + }), }), }), }), - }), - ); + ); + }); }); - it('should add catalog when Enter is pressed', () => { + it('should add catalog when Enter is pressed', async () => { + const user = userEvent.setup(); renderWithProviders(); const catalogInput = screen.getByLabelText('Catalogs'); - fireEvent.change(catalogInput, { target: { value: 'redhat-operators' } }); - fireEvent.keyDown(catalogInput, { key: 'Enter', code: 'Enter' }); - - expect(mockOnChange).toHaveBeenCalledWith( - expect.objectContaining({ - spec: expect.objectContaining({ - source: expect.objectContaining({ - catalog: expect.objectContaining({ - selector: expect.objectContaining({ - matchLabels: expect.objectContaining({ - 'olm.operatorframework.io/metadata.name': 'redhat-operators', + await user.click(catalogInput); + await user.clear(catalogInput); + await user.type(catalogInput, 'redhat-operators'); + await user.keyboard('{Enter}'); + + await waitFor(() => { + expect(mockOnChange).toHaveBeenLastCalledWith( + expect.objectContaining({ + spec: expect.objectContaining({ + source: expect.objectContaining({ + catalog: expect.objectContaining({ + selector: expect.objectContaining({ + matchLabels: expect.objectContaining({ + 'olm.operatorframework.io/metadata.name': 'redhat-operators', + }), }), }), }), }), }), - }), - ); + ); + }); }); it('should render channel labels when channels are added', () => { diff --git a/frontend/packages/operator-lifecycle-manager/src/components/__tests__/install-plan.spec.tsx b/frontend/packages/operator-lifecycle-manager/src/components/__tests__/install-plan.spec.tsx index 5ae72f69510..85457d253b2 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/__tests__/install-plan.spec.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/__tests__/install-plan.spec.tsx @@ -1,4 +1,5 @@ -import { screen, fireEvent, waitFor } from '@testing-library/react'; +import { screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import * as _ from 'lodash'; import * as Router from 'react-router'; import * as k8sResourceModule from '@console/dynamic-plugin-sdk/src/utils/k8s/k8s-resource'; @@ -307,8 +308,9 @@ describe('InstallPlanPreview', () => { renderWithProviders(); + const user = userEvent.setup(); const approveButton = screen.getByRole('button', { name: 'Approve' }); - fireEvent.click(approveButton); + await user.click(approveButton); await waitFor(() => { expect(k8sPatchMock).toHaveBeenCalledWith( diff --git a/frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/__tests__/resource-requirements.spec.tsx b/frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/__tests__/resource-requirements.spec.tsx index a5ca45bcb98..1589acb6ae5 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/__tests__/resource-requirements.spec.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/__tests__/resource-requirements.spec.tsx @@ -1,4 +1,5 @@ -import { screen, waitFor, fireEvent } from '@testing-library/react'; +import { screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import * as k8sResourceModule from '@console/dynamic-plugin-sdk/src/utils/k8s/k8s-resource'; import type { K8sKind, K8sResourceKind } from '@console/internal/module/k8s'; import { renderWithProviders } from '@console/shared/src/test-utils/unit-test-utils'; @@ -61,22 +62,23 @@ describe('ResourceRequirementsModal', () => { }); it('should call k8sUpdate when form is submitted', async () => { + const user = userEvent.setup(); k8sUpdateMock.mockResolvedValue({} as K8sResourceKind); renderModal(); - fireEvent.change(screen.getByRole('textbox', { name: 'CPU cores' }), { - target: { value: '200m' }, - }); - fireEvent.change(screen.getByRole('textbox', { name: 'Memory' }), { - target: { value: '20Mi' }, - }); - fireEvent.change(screen.getByRole('textbox', { name: 'Storage' }), { - target: { value: '50Mi' }, - }); + const cpuInput = screen.getByRole('textbox', { name: 'CPU cores' }); + await user.clear(cpuInput); + await user.type(cpuInput, '200m'); + const memoryInput = screen.getByRole('textbox', { name: 'Memory' }); + await user.clear(memoryInput); + await user.type(memoryInput, '20Mi'); + const storageInput = screen.getByRole('textbox', { name: 'Storage' }); + await user.clear(storageInput); + await user.type(storageInput, '50Mi'); const saveButton = screen.getByRole('button', { name: 'Save' }); - fireEvent.click(saveButton); + await user.click(saveButton); await waitFor(() => { expect(k8sUpdateMock).toHaveBeenCalled(); @@ -157,9 +159,10 @@ describe('ResourceRequirementsModalLink', () => { }); it('should open resource requirements modal when button is clicked', async () => { + const user = userEvent.setup(); renderWithProviders(); - fireEvent.click(screen.getByRole('button')); + await user.click(screen.getByRole('button')); await waitFor(() => { expect(launchModalMock).toHaveBeenCalled(); diff --git a/frontend/packages/operator-lifecycle-manager/src/components/modals/__tests__/installplan-approval-modal.spec.tsx b/frontend/packages/operator-lifecycle-manager/src/components/modals/__tests__/installplan-approval-modal.spec.tsx index 13dcfb509dc..80bd9a8cafe 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/modals/__tests__/installplan-approval-modal.spec.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/modals/__tests__/installplan-approval-modal.spec.tsx @@ -1,4 +1,5 @@ -import { screen, fireEvent, waitFor } from '@testing-library/react'; +import { screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import * as _ from 'lodash'; import { modelFor } from '@console/internal/module/k8s'; import { renderWithProviders } from '@console/shared/src/test-utils/unit-test-utils'; @@ -78,17 +79,15 @@ describe('InstallPlanApprovalModal', () => { }); it('calls k8sUpdate with updated subscription when form is submitted', async () => { + const user = userEvent.setup(); mockModelFor.mockReturnValue(SubscriptionModel); renderWithProviders(); const manualRadio = screen.getByRole('radio', { name: 'Manual' }); - fireEvent.click(manualRadio); + await user.click(manualRadio); - const form = screen.getByRole('radio', { name: 'Manual' }).closest('form'); - if (form) { - fireEvent.submit(form); - } + await user.click(screen.getByRole('button', { name: 'Save' })); await waitFor(() => { expect(installPlanApprovalModalProps.k8sUpdate).toHaveBeenCalledTimes(1); @@ -105,6 +104,7 @@ describe('InstallPlanApprovalModal', () => { }); it('calls k8sUpdate with updated install plan when form is submitted', async () => { + const user = userEvent.setup(); const installPlan = _.cloneDeep(testInstallPlan); mockModelFor.mockReturnValue(InstallPlanModel); @@ -113,12 +113,9 @@ describe('InstallPlanApprovalModal', () => { ); const manualRadio = screen.getByRole('radio', { name: 'Manual' }); - fireEvent.click(manualRadio); + await user.click(manualRadio); - const form = screen.getByRole('radio', { name: 'Manual' }).closest('form'); - if (form) { - fireEvent.submit(form); - } + await user.click(screen.getByRole('button', { name: 'Save' })); await waitFor(() => { expect(installPlanApprovalModalProps.k8sUpdate).toHaveBeenCalledTimes(1); @@ -135,12 +132,14 @@ describe('InstallPlanApprovalModal', () => { }); it('calls close callback after successful submit', async () => { + const user = userEvent.setup(); + mockModelFor.mockReturnValue(SubscriptionModel); + renderWithProviders(); - const form = screen.getByRole('radio', { name: 'Automatic (default)' }).closest('form'); - if (form) { - fireEvent.submit(form); - } + // Save stays disabled when the selected strategy matches the resource; change first so submit runs. + await user.click(screen.getByRole('radio', { name: 'Manual' })); + await user.click(screen.getByRole('button', { name: 'Save' })); await waitFor(() => { expect(installPlanApprovalModalProps.close).toHaveBeenCalledTimes(1); diff --git a/frontend/packages/operator-lifecycle-manager/src/components/modals/__tests__/subscription-channel-modal.spec.tsx b/frontend/packages/operator-lifecycle-manager/src/components/modals/__tests__/subscription-channel-modal.spec.tsx index 49d360c51d6..57a7e6d89cc 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/modals/__tests__/subscription-channel-modal.spec.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/modals/__tests__/subscription-channel-modal.spec.tsx @@ -1,4 +1,5 @@ -import { screen, fireEvent, waitFor } from '@testing-library/react'; +import { screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import * as _ from 'lodash'; import { renderWithProviders } from '@console/shared/src/test-utils/unit-test-utils'; import { testSubscription, testPackageManifest } from '../../../../mocks'; @@ -83,13 +84,14 @@ describe('SubscriptionChannelModal', () => { }); it('updates subscription when different channel is selected and form is submitted', async () => { + const user = userEvent.setup(); renderWithProviders(); const nightlyRadio = screen.getByRole('radio', { name: /nightly/i }); - fireEvent.click(nightlyRadio); + await user.click(nightlyRadio); const saveButton = screen.getByRole('button', { name: 'Save' }); - fireEvent.click(saveButton); + await user.click(saveButton); await waitFor(() => { expect(k8sUpdate).toHaveBeenCalledTimes(1); @@ -106,13 +108,14 @@ describe('SubscriptionChannelModal', () => { }); it('calls close callback after successful form submission', async () => { + const user = userEvent.setup(); renderWithProviders(); const nightlyRadio = screen.getByRole('radio', { name: /nightly/i }); - fireEvent.click(nightlyRadio); + await user.click(nightlyRadio); const saveButton = screen.getByRole('button', { name: 'Save' }); - fireEvent.click(saveButton); + await user.click(saveButton); await waitFor(() => { expect(close).toHaveBeenCalledTimes(1); @@ -126,14 +129,15 @@ describe('SubscriptionChannelModal', () => { expect(saveButton).toBeDisabled(); }); - it('enables submit button when channel selection changes', () => { + it('enables submit button when channel selection changes', async () => { + const user = userEvent.setup(); renderWithProviders(); const saveButton = screen.getByRole('button', { name: 'Save' }); expect(saveButton).toBeDisabled(); const nightlyRadio = screen.getByRole('radio', { name: /nightly/i }); - fireEvent.click(nightlyRadio); + await user.click(nightlyRadio); expect(saveButton).not.toBeDisabled(); }); diff --git a/frontend/packages/operator-lifecycle-manager/src/components/modals/__tests__/uninstall-operator-modal.spec.tsx b/frontend/packages/operator-lifecycle-manager/src/components/modals/__tests__/uninstall-operator-modal.spec.tsx index 3f79b3bf5c8..6afc851ec98 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/modals/__tests__/uninstall-operator-modal.spec.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/modals/__tests__/uninstall-operator-modal.spec.tsx @@ -1,4 +1,5 @@ -import { screen, fireEvent, waitFor } from '@testing-library/react'; +import { screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import * as _ from 'lodash'; import { useK8sWatchResource } from '@console/internal/components/utils/k8s-watch-hook'; import { useAccessReview } from '@console/internal/components/utils/rbac'; @@ -76,10 +77,11 @@ describe(UninstallOperatorModal.name, () => { }); it('deletes subscription when form is submitted', async () => { + const user = userEvent.setup(); renderWithProviders(); const uninstallButton = screen.getByRole('button', { name: 'Uninstall' }); - fireEvent.click(uninstallButton); + await user.click(uninstallButton); await waitFor(() => { expect(mockK8sKill).toHaveBeenCalledTimes(2); @@ -99,10 +101,11 @@ describe(UninstallOperatorModal.name, () => { }); it('deletes ClusterServiceVersion when form is submitted', async () => { + const user = userEvent.setup(); renderWithProviders(); const uninstallButton = screen.getByRole('button', { name: 'Uninstall' }); - fireEvent.click(uninstallButton); + await user.click(uninstallButton); await waitFor(() => { expect(mockK8sKill).toHaveBeenCalledTimes(2); @@ -127,12 +130,13 @@ describe(UninstallOperatorModal.name, () => { }); it('does not delete ClusterServiceVersion when installedCSV is missing from subscription', async () => { + const user = userEvent.setup(); renderWithProviders( , ); const uninstallButton = screen.getByRole('button', { name: 'Uninstall' }); - fireEvent.click(uninstallButton); + await user.click(uninstallButton); await waitFor(() => { expect(mockK8sKill).toHaveBeenCalledTimes(1); @@ -140,10 +144,11 @@ describe(UninstallOperatorModal.name, () => { }); it('calls close callback after successful form submission', async () => { + const user = userEvent.setup(); renderWithProviders(); const uninstallButton = screen.getByRole('button', { name: 'Uninstall' }); - fireEvent.click(uninstallButton); + await user.click(uninstallButton); await waitFor(() => { expect(uninstallOperatorModalProps.close).toHaveBeenCalledTimes(1); diff --git a/frontend/packages/topology/src/__tests__/TopologySideBar.spec.tsx b/frontend/packages/topology/src/__tests__/TopologySideBar.spec.tsx index 02972352595..3122ed9bf43 100644 --- a/frontend/packages/topology/src/__tests__/TopologySideBar.spec.tsx +++ b/frontend/packages/topology/src/__tests__/TopologySideBar.spec.tsx @@ -1,4 +1,5 @@ -import { render, screen, fireEvent } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import TopologySideBar from '../components/side-bar/TopologySideBar'; jest.mock('@console/shared/src/hooks/useUserPreference', () => ({ @@ -21,7 +22,8 @@ describe('TopologySideBar', () => { expect(screen.getByText('Test Content')).toBeInTheDocument(); }); - it('calls onClose when close button is clicked', () => { + it('calls onClose when close button is clicked', async () => { + const user = userEvent.setup(); const handleClose = jest.fn(); render( @@ -29,7 +31,7 @@ describe('TopologySideBar', () => { , ); - fireEvent.click(screen.getByTestId('sidebar-close-button')); + await user.click(screen.getByTestId('sidebar-close-button')); expect(handleClose).toHaveBeenCalledTimes(1); }); }); diff --git a/frontend/packages/topology/src/components/export-app/__tests__/ExportApplicationModal.spec.tsx b/frontend/packages/topology/src/components/export-app/__tests__/ExportApplicationModal.spec.tsx index b524c0cd295..412039ae083 100644 --- a/frontend/packages/topology/src/components/export-app/__tests__/ExportApplicationModal.spec.tsx +++ b/frontend/packages/topology/src/components/export-app/__tests__/ExportApplicationModal.spec.tsx @@ -1,4 +1,5 @@ -import { act, screen, fireEvent } from '@testing-library/react'; +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import * as _ from 'lodash'; import * as k8sResourceModule from '@console/dynamic-plugin-sdk/src/utils/k8s/k8s-resource'; import * as useToastModule from '@console/shared/src/components/toast/useToast'; @@ -92,18 +93,18 @@ describe('ExportApplicationModal', () => { }); it('should call k8sCreate with correct data on click of Ok button when the export resource is not created', async () => { + const user = userEvent.setup(); renderWithProviders( , ); - await act(async () => { - fireEvent.click(screen.getByTestId('close-btn')); - }); + await user.click(screen.getByTestId('close-btn')); expect(spyk8sCreate).toHaveBeenCalledTimes(1); expect(spyk8sCreate).toHaveBeenCalledWith(ExportModel, getExportAppData('my-export', 'my-app')); }); it('should call k8sKill and k8sCreate with correct data on click of Ok button when the export resource already exists', async () => { + const user = userEvent.setup(); renderWithProviders( { cancel={jest.fn()} />, ); - await act(async () => { - fireEvent.click(screen.getByTestId('close-btn')); - }); + await user.click(screen.getByTestId('close-btn')); expect(spyk8sKill).toHaveBeenCalledTimes(1); expect(spyk8sKill).toHaveBeenCalledWith(ExportModel, mockExportData); @@ -123,15 +122,14 @@ describe('ExportApplicationModal', () => { }); it('should call k8sKill and k8sCreate with correct data on click of restart button when export app is in progress', async () => { + const user = userEvent.setup(); const exportData = _.cloneDeep(mockExportData); exportData.status.completed = false; renderWithProviders( , ); - await act(async () => { - fireEvent.click(screen.getByTestId('export-restart-btn')); - }); + await user.click(screen.getByTestId('export-restart-btn')); expect(spyk8sKill).toHaveBeenCalledTimes(1); expect(spyk8sKill).toHaveBeenCalledWith(ExportModel, exportData); @@ -140,15 +138,14 @@ describe('ExportApplicationModal', () => { }); it('should call k8sKill with correct data on click of cancel button when export app is in progress', async () => { + const user = userEvent.setup(); const exportData = _.cloneDeep(mockExportData); exportData.status.completed = false; renderWithProviders( , ); - await act(async () => { - fireEvent.click(screen.getByTestId('export-restart-btn')); - }); + await user.click(screen.getByTestId('export-cancel-btn')); expect(spyk8sKill).toHaveBeenCalledTimes(1); expect(spyk8sKill).toHaveBeenCalledWith(ExportModel, exportData); diff --git a/frontend/packages/webterminal-plugin/src/components/cloud-shell/__tests__/MinimizeRestoreButton.spec.tsx b/frontend/packages/webterminal-plugin/src/components/cloud-shell/__tests__/MinimizeRestoreButton.spec.tsx index ef413c7018a..b0483bbbf32 100644 --- a/frontend/packages/webterminal-plugin/src/components/cloud-shell/__tests__/MinimizeRestoreButton.spec.tsx +++ b/frontend/packages/webterminal-plugin/src/components/cloud-shell/__tests__/MinimizeRestoreButton.spec.tsx @@ -1,4 +1,5 @@ -import { render, screen, fireEvent } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { MinimizeRestoreButton } from '../MinimizeRestoreButton'; describe('MinimizeRestoreButton', () => { @@ -34,6 +35,7 @@ describe('MinimizeRestoreButton', () => { }); it('should invoke onclose callback with argument true when minimized button clicked and false when restore button clicked', async () => { + const user = userEvent.setup(); const onClose = jest.fn(); const { rerender } = render( @@ -49,7 +51,7 @@ describe('MinimizeRestoreButton', () => { expect(screen.queryByRole('button', { name: 'Restore' })).not.toBeInTheDocument(); // click on minimize button - await fireEvent.click(screen.getByRole('button', { name: 'Minimize' })); + await user.click(screen.getByRole('button', { name: 'Minimize' })); expect(onClose).toHaveBeenLastCalledWith(true); // Re-render with minimize=false to test restore button @@ -66,7 +68,7 @@ describe('MinimizeRestoreButton', () => { expect(screen.getByRole('button', { name: 'Restore' })).toBeInTheDocument(); // click on restore button - await fireEvent.click(screen.getByRole('button', { name: 'Restore' })); + await user.click(screen.getByRole('button', { name: 'Restore' })); expect(onClose).toHaveBeenLastCalledWith(false); }); }); diff --git a/frontend/packages/webterminal-plugin/src/components/cloud-shell/__tests__/MultiTabbedTerminal.spec.tsx b/frontend/packages/webterminal-plugin/src/components/cloud-shell/__tests__/MultiTabbedTerminal.spec.tsx index 35b34c93305..c6e623526ad 100644 --- a/frontend/packages/webterminal-plugin/src/components/cloud-shell/__tests__/MultiTabbedTerminal.spec.tsx +++ b/frontend/packages/webterminal-plugin/src/components/cloud-shell/__tests__/MultiTabbedTerminal.spec.tsx @@ -1,4 +1,5 @@ -import { act, fireEvent } from '@testing-library/react'; +import { act } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { renderWithProviders } from '@console/shared/src/test-utils/unit-test-utils'; import { sendActivityTick } from '../cloud-shell-utils'; import { MultiTabbedTerminal } from '../MultiTabbedTerminal'; @@ -17,19 +18,28 @@ const originalWindowRequestAnimationFrame = window.requestAnimationFrame; const originalWindowCancelAnimationFrame = window.cancelAnimationFrame; // Helper to click an element multiple times sequentially with act() -const clickMultipleTimes = async (element: HTMLElement, times: number) => { +const clickMultipleTimes = async ( + user: ReturnType, + element: HTMLElement, + times: number, +) => { for (let i = 0; i < times; i++) { // eslint-disable-next-line no-await-in-loop await act(async () => { - fireEvent.click(element); + await user.click(element); }); } }; describe('MultiTabTerminal', () => { jest.useFakeTimers(); + let user: ReturnType; (sendActivityTick as jest.Mock).mockImplementation((a, b) => [a, b]); + beforeEach(() => { + user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); + }); + beforeAll(() => { window.requestAnimationFrame = (cb) => setTimeout(cb, 0); window.cancelAnimationFrame = (id) => clearTimeout(id); @@ -57,14 +67,14 @@ describe('MultiTabTerminal', () => { const addTerminalButton = multiTabTerminalWrapper.getByLabelText('Add new tab'); await act(async () => { - fireEvent.click(addTerminalButton); + await user.click(addTerminalButton); }); expect(multiTabTerminalWrapper.getAllByText('Terminal content').length).toBe(2); await act(async () => { - fireEvent.click(addTerminalButton); + await user.click(addTerminalButton); }); await act(async () => { - fireEvent.click(addTerminalButton); + await user.click(addTerminalButton); }); expect(multiTabTerminalWrapper.getAllByText('Terminal content').length).toBe(4); }); @@ -76,7 +86,7 @@ describe('MultiTabTerminal', () => { }); const addTerminalButton = multiTabTerminalWrapper.getByLabelText('Add new tab'); - await clickMultipleTimes(addTerminalButton, 8); + await clickMultipleTimes(user, addTerminalButton, 8); expect(multiTabTerminalWrapper.getAllByText('Terminal content')).toHaveLength(8); expect(multiTabTerminalWrapper.queryByLabelText('Add new tab')).toBeNull(); }); @@ -88,17 +98,24 @@ describe('MultiTabTerminal', () => { }); const addTerminalButton = multiTabTerminalWrapper.getByLabelText('Add new tab'); - await clickMultipleTimes(addTerminalButton, 8); + await clickMultipleTimes(user, addTerminalButton, 8); + const closeTerminalTabs = () => multiTabTerminalWrapper.getAllByLabelText('Close terminal tab'); await act(async () => { - fireEvent.click(multiTabTerminalWrapper.getAllByLabelText('Close terminal tab').at(7)); + const tabs = closeTerminalTabs(); + expect(tabs[7]).toBeTruthy(); + await user.click(tabs[7]); }); expect(multiTabTerminalWrapper.getAllByText('Terminal content').length).toBe(7); await act(async () => { - fireEvent.click(multiTabTerminalWrapper.getAllByLabelText('Close terminal tab').at(6)); + const tabs = closeTerminalTabs(); + expect(tabs[6]).toBeTruthy(); + await user.click(tabs[6]); }); await act(async () => { - fireEvent.click(multiTabTerminalWrapper.getAllByLabelText('Close terminal tab').at(5)); + const tabs = closeTerminalTabs(); + expect(tabs[5]).toBeTruthy(); + await user.click(tabs[5]); }); expect(multiTabTerminalWrapper.getAllByText('Terminal content').length).toBe(5); }); diff --git a/frontend/public/components/__tests__/storage-class-form.spec.tsx b/frontend/public/components/__tests__/storage-class-form.spec.tsx index 43dca1e9551..74383fd832c 100644 --- a/frontend/public/components/__tests__/storage-class-form.spec.tsx +++ b/frontend/public/components/__tests__/storage-class-form.spec.tsx @@ -37,20 +37,19 @@ describe('StorageClassForm', () => { }); it('verifies a text input for storage class name', async () => { - await waitFor(() => { - verifyInputField({ - inputLabel: 'Name', - inputType: 'text', - }); + await verifyInputField({ + inputLabel: 'Name', + inputType: 'text', }); }); it('verifies a text input for storage class description', async () => { await waitFor(() => { - verifyInputField({ - inputLabel: 'Description', - testValue: 'Test storage class description', - }); + expect(screen.getByLabelText('Description')).toBeInTheDocument(); + }); + await verifyInputField({ + inputLabel: 'Description', + testValue: 'Test storage class description', }); }); diff --git a/frontend/public/components/cluster-settings/__tests__/basicauth-idp-form.spec.tsx b/frontend/public/components/cluster-settings/__tests__/basicauth-idp-form.spec.tsx index 72c9fff42f7..1f4afdb3cc6 100644 --- a/frontend/public/components/cluster-settings/__tests__/basicauth-idp-form.spec.tsx +++ b/frontend/public/components/cluster-settings/__tests__/basicauth-idp-form.spec.tsx @@ -39,8 +39,8 @@ describe('Add Identity Provider: Basic Authentication', () => { }); }); - it('should render the Name label, input element, and help text', () => { - verifyInputField({ + it('should render the Name label, input element, and help text', async () => { + await verifyInputField({ inputLabel: 'Name', initialValue: 'basic-auth', testValue: mockData.updatedFormValues.name, @@ -49,8 +49,8 @@ describe('Add Identity Provider: Basic Authentication', () => { }); }); - it('should render the URL label, input element, and help text', () => { - verifyInputField({ + it('should render the URL label, input element, and help text', async () => { + await verifyInputField({ inputLabel: 'URL', inputType: 'url', testValue: mockData.updatedFormValues.url, diff --git a/frontend/public/components/cluster-settings/__tests__/github-idp-form.spec.tsx b/frontend/public/components/cluster-settings/__tests__/github-idp-form.spec.tsx index 36351193865..d174d56c12b 100644 --- a/frontend/public/components/cluster-settings/__tests__/github-idp-form.spec.tsx +++ b/frontend/public/components/cluster-settings/__tests__/github-idp-form.spec.tsx @@ -40,8 +40,8 @@ describe('Add Identity Provider: GitHub', () => { }); }); - it('should render the Name label, input element, and help text', () => { - verifyInputField({ + it('should render the Name label, input element, and help text', async () => { + await verifyInputField({ inputLabel: 'Name', initialValue: 'github', testValue: mockData.updatedFormValues.name, @@ -50,16 +50,16 @@ describe('Add Identity Provider: GitHub', () => { }); }); - it('should render the Client ID label, input element, and help text', () => { - verifyInputField({ + it('should render the Client ID label, input element, and help text', async () => { + await verifyInputField({ inputLabel: 'Client ID', testValue: mockData.updatedFormValues.id, isRequired: true, }); }); - it('should render the Client Secret label and input password element', () => { - verifyInputField({ + it('should render the Client Secret label and input password element', async () => { + await verifyInputField({ inputLabel: 'Client secret', inputType: 'password', testValue: mockData.updatedFormValues.secret, @@ -67,8 +67,8 @@ describe('Add Identity Provider: GitHub', () => { }); }); - it('should render the Hostname label, input element, and help text', () => { - verifyInputField({ + it('should render the Hostname label, input element, and help text', async () => { + await verifyInputField({ inputLabel: 'Hostname', testValue: mockData.updatedFormValues.name, helpText: 'Optional domain for use with a hosted instance of GitHub Enterprise.', @@ -83,7 +83,7 @@ describe('Add Identity Provider: GitHub', () => { }); }); - it('should render the Organizations sub heading and input element', () => { + it('should render the Organizations sub heading and input element', async () => { expect(screen.getByRole('heading', { name: 'Organizations' })).toBeVisible(); // Verify the text content @@ -98,7 +98,7 @@ describe('Add Identity Provider: GitHub', () => { expect(strongElement).toBeVisible(); expect(strongElement.tagName).toBe('STRONG'); - verifyIDPListInputFields({ + await verifyIDPListInputFields({ inputLabel: 'Organization', testValue: 'Example organization', testId: 'organization-list-input', @@ -106,7 +106,7 @@ describe('Add Identity Provider: GitHub', () => { }); }); - it('should render the Teams sub heading', () => { + it('should render the Teams sub heading', async () => { expect(screen.getByRole('heading', { name: 'Teams' })).toBeVisible(); // Verify the text content @@ -121,7 +121,7 @@ describe('Add Identity Provider: GitHub', () => { expect(strongElement).toBeVisible(); expect(strongElement.tagName).toBe('STRONG'); - verifyIDPListInputFields({ + await verifyIDPListInputFields({ inputLabel: 'Team', testValue: 'Example team', testId: 'team-list-input', diff --git a/frontend/public/components/cluster-settings/__tests__/gitlab-idp-form.spec.tsx b/frontend/public/components/cluster-settings/__tests__/gitlab-idp-form.spec.tsx index a0859fa242d..5626daa4ebd 100644 --- a/frontend/public/components/cluster-settings/__tests__/gitlab-idp-form.spec.tsx +++ b/frontend/public/components/cluster-settings/__tests__/gitlab-idp-form.spec.tsx @@ -38,8 +38,8 @@ describe('Add Identity Provider: GitLab', () => { }); }); - it('should render the Name label, input element, and help text', () => { - verifyInputField({ + it('should render the Name label, input element, and help text', async () => { + await verifyInputField({ inputLabel: 'Name', initialValue: 'gitlab', testValue: mockData.updatedFormValues.name, @@ -48,8 +48,8 @@ describe('Add Identity Provider: GitLab', () => { }); }); - it('should render the URL label, input element, and help text', () => { - verifyInputField({ + it('should render the URL label, input element, and help text', async () => { + await verifyInputField({ inputLabel: 'URL', inputType: 'url', testValue: mockData.updatedFormValues.url, @@ -58,16 +58,16 @@ describe('Add Identity Provider: GitLab', () => { }); }); - it('should render the Client ID label, input element, and help text', () => { - verifyInputField({ + it('should render the Client ID label, input element, and help text', async () => { + await verifyInputField({ inputLabel: 'Client ID', testValue: mockData.updatedFormValues.id, isRequired: true, }); }); - it('should render the Client Secret label and input password element', () => { - verifyInputField({ + it('should render the Client Secret label and input password element', async () => { + await verifyInputField({ inputLabel: 'Client secret', inputType: 'password', testValue: mockData.updatedFormValues.secret, diff --git a/frontend/public/components/cluster-settings/__tests__/google-idp-form.spec.tsx b/frontend/public/components/cluster-settings/__tests__/google-idp-form.spec.tsx index c1eceb4f569..7cf8fc96eec 100644 --- a/frontend/public/components/cluster-settings/__tests__/google-idp-form.spec.tsx +++ b/frontend/public/components/cluster-settings/__tests__/google-idp-form.spec.tsx @@ -22,8 +22,8 @@ describe('Add Identity Provider: Google', () => { }); }); - it('should render the Name label, input element, and help text', () => { - verifyInputField({ + it('should render the Name label, input element, and help text', async () => { + await verifyInputField({ inputLabel: 'Name', initialValue: 'google', testValue: mockData.updatedFormValues.name, @@ -32,16 +32,16 @@ describe('Add Identity Provider: Google', () => { }); }); - it('should render the Client ID label, input element, and help text', () => { - verifyInputField({ + it('should render the Client ID label, input element, and help text', async () => { + await verifyInputField({ inputLabel: 'Client ID', testValue: mockData.updatedFormValues.id, isRequired: true, }); }); - it('should render the Client Secret label and input password element', () => { - verifyInputField({ + it('should render the Client Secret label and input password element', async () => { + await verifyInputField({ inputLabel: 'Client secret', inputType: 'password', testValue: mockData.updatedFormValues.secret, @@ -49,8 +49,8 @@ describe('Add Identity Provider: Google', () => { }); }); - it('should render the Hosted Domain label, input element, and help text', () => { - verifyInputField({ + it('should render the Hosted Domain label, input element, and help text', async () => { + await verifyInputField({ inputLabel: 'Hosted domain', testValue: mockData.updatedFormValues.domain, helpText: 'Restrict users to a Google App domain.', diff --git a/frontend/public/components/cluster-settings/__tests__/htpasswd-idp-form.spec.tsx b/frontend/public/components/cluster-settings/__tests__/htpasswd-idp-form.spec.tsx index fa225be8375..ea47f738190 100644 --- a/frontend/public/components/cluster-settings/__tests__/htpasswd-idp-form.spec.tsx +++ b/frontend/public/components/cluster-settings/__tests__/htpasswd-idp-form.spec.tsx @@ -39,8 +39,8 @@ describe('Add Identity Provider: HTPasswd', () => { }); }); - it('should render the Name label, input element, and help text', () => { - verifyInputField({ + it('should render the Name label, input element, and help text', async () => { + await verifyInputField({ inputLabel: 'Name', initialValue: 'htpasswd', testValue: mockData.updatedFormValues.name, diff --git a/frontend/public/components/cluster-settings/__tests__/keystone-idp-form.spec.tsx b/frontend/public/components/cluster-settings/__tests__/keystone-idp-form.spec.tsx index 6b85bedd846..e97436c94cf 100644 --- a/frontend/public/components/cluster-settings/__tests__/keystone-idp-form.spec.tsx +++ b/frontend/public/components/cluster-settings/__tests__/keystone-idp-form.spec.tsx @@ -39,8 +39,8 @@ describe('Add Identity Provider: Keystone Authentication', () => { }); }); - it('should render the Name label, input element, and help text', () => { - verifyInputField({ + it('should render the Name label, input element, and help text', async () => { + await verifyInputField({ inputLabel: 'Name', initialValue: 'keystone', testValue: mockData.updatedFormValues.name, @@ -49,16 +49,16 @@ describe('Add Identity Provider: Keystone Authentication', () => { }); }); - it('should render the Domain name label and input element', () => { - verifyInputField({ + it('should render the Domain name label and input element', async () => { + await verifyInputField({ inputLabel: 'Domain name', testValue: mockData.updatedFormValues.url, isRequired: true, }); }); - it('should render the URL label, input element, and help text', () => { - verifyInputField({ + it('should render the URL label, input element, and help text', async () => { + await verifyInputField({ inputLabel: 'URL', inputType: 'url', testValue: mockData.updatedFormValues.url, diff --git a/frontend/public/components/cluster-settings/__tests__/ldap-idp-form.spec.tsx b/frontend/public/components/cluster-settings/__tests__/ldap-idp-form.spec.tsx index 82fb201bd29..d0a5c6e411e 100644 --- a/frontend/public/components/cluster-settings/__tests__/ldap-idp-form.spec.tsx +++ b/frontend/public/components/cluster-settings/__tests__/ldap-idp-form.spec.tsx @@ -82,8 +82,8 @@ describe('Add Identity Provider: LDAP', () => { expect(screen.getByText('Attributes map LDAP attributes to identities.')).toBeVisible(); }); - it('should render the Attributes > ID label, input element, and help text', () => { - verifyIDPListInputFields({ + it('should render the Attributes > ID label, input element, and help text', async () => { + await verifyIDPListInputFields({ inputLabel: 'ID', initialValue: 'dn', testValue: mockData.updatedFormValues.id, @@ -92,8 +92,8 @@ describe('Add Identity Provider: LDAP', () => { }); }); - it('should render the Attributes > Preferred username label, input element, and help text', () => { - verifyIDPListInputFields({ + it('should render the Attributes > Preferred username label, input element, and help text', async () => { + await verifyIDPListInputFields({ inputLabel: 'Preferred username', initialValue: 'uid', testValue: mockData.updatedFormValues.id, @@ -102,8 +102,8 @@ describe('Add Identity Provider: LDAP', () => { }); }); - it('should render the Attributes Name label, input element, and help text', () => { - verifyIDPListInputFields({ + it('should render the Attributes Name label, input element, and help text', async () => { + await verifyIDPListInputFields({ inputLabel: 'Name', initialValue: 'cn', testValue: mockData.updatedFormValues.name, @@ -112,8 +112,8 @@ describe('Add Identity Provider: LDAP', () => { }); }); - it('should render the Attributes Email label, input element, and help text', () => { - verifyIDPListInputFields({ + it('should render the Attributes Email label, input element, and help text', async () => { + await verifyIDPListInputFields({ inputLabel: 'Email', testValue: mockData.updatedFormValues.email, testId: 'ldap-attribute-email', diff --git a/frontend/public/components/cluster-settings/__tests__/openid-idp-form.spec.tsx b/frontend/public/components/cluster-settings/__tests__/openid-idp-form.spec.tsx index 3a7e96d1290..40fb8b9d9cc 100644 --- a/frontend/public/components/cluster-settings/__tests__/openid-idp-form.spec.tsx +++ b/frontend/public/components/cluster-settings/__tests__/openid-idp-form.spec.tsx @@ -88,8 +88,8 @@ describe('Add Identity Provider: OpenID Connect', () => { ).toBeVisible(); }); - it('should render the Preferred username label, input element, and help text', () => { - verifyIDPListInputFields({ + it('should render the Preferred username label, input element, and help text', async () => { + await verifyIDPListInputFields({ inputLabel: 'Preferred username', initialValue: 'preferred_username', testValue: mockData.updatedFormValues.username, @@ -98,8 +98,8 @@ describe('Add Identity Provider: OpenID Connect', () => { }); }); - it('should render the Name label, input element, and help text', () => { - verifyIDPListInputFields({ + it('should render the Name label, input element, and help text', async () => { + await verifyIDPListInputFields({ inputLabel: 'Name', initialValue: 'name', testValue: mockData.updatedFormValues.name, @@ -108,8 +108,8 @@ describe('Add Identity Provider: OpenID Connect', () => { }); }); - it('should render the Email label, input element, and help text', () => { - verifyIDPListInputFields({ + it('should render the Email label, input element, and help text', async () => { + await verifyIDPListInputFields({ inputLabel: 'Email', initialValue: 'email', testValue: mockData.updatedFormValues.email, @@ -126,8 +126,8 @@ describe('Add Identity Provider: OpenID Connect', () => { }); }); - it('should render the Extra scopes label, input element, and help text', () => { - verifyIDPListInputFields({ + it('should render the Extra scopes label, input element, and help text', async () => { + await verifyIDPListInputFields({ inputLabel: 'Extra scopes', testValue: mockData.updatedFormValues.updatedValue, testId: 'openid-more-options-extra-scopes', diff --git a/frontend/public/components/cluster-settings/__tests__/request-header-idp-form.spec.tsx b/frontend/public/components/cluster-settings/__tests__/request-header-idp-form.spec.tsx index b4e8d375340..6daa4403d59 100644 --- a/frontend/public/components/cluster-settings/__tests__/request-header-idp-form.spec.tsx +++ b/frontend/public/components/cluster-settings/__tests__/request-header-idp-form.spec.tsx @@ -40,8 +40,8 @@ describe('Add Identity Provider: Request Header', () => { }); }); - it('should render the Name label, input element, and help text', () => { - verifyInputField({ + it('should render the Name label, input element, and help text', async () => { + await verifyInputField({ inputLabel: 'Name', initialValue: 'request-header', testValue: mockData.updatedFormValues.name, @@ -55,8 +55,8 @@ describe('Add Identity Provider: Request Header', () => { expect(screen.getByText('At least one URL must be provided.')).toBeVisible(); }); - it('should render the Challenge URL label, input element, and help text', () => { - verifyInputField({ + it('should render the Challenge URL label, input element, and help text', async () => { + await verifyInputField({ inputLabel: 'Challenge URL', inputType: 'url', testValue: mockData.updatedFormValues.url, @@ -65,8 +65,8 @@ describe('Add Identity Provider: Request Header', () => { }); }); - it('should render the Login URL label, input element, and help text', () => { - verifyInputField({ + it('should render the Login URL label, input element, and help text', async () => { + await verifyInputField({ inputLabel: 'Login URL', inputType: 'url', testValue: mockData.updatedFormValues.url, @@ -86,8 +86,8 @@ describe('Add Identity Provider: Request Header', () => { }); }); - it('should render the Client common names label, input element, and help text', () => { - verifyIDPListInputFields({ + it('should render the Client common names label, input element, and help text', async () => { + await verifyIDPListInputFields({ inputLabel: 'Client common names', testValue: mockData.updatedFormValues.name, testId: 'request-header-client-common-names', @@ -95,8 +95,8 @@ describe('Add Identity Provider: Request Header', () => { }); }); - it('should render the Headers label, input element, and help text', () => { - verifyIDPListInputFields({ + it('should render the Headers label, input element, and help text', async () => { + await verifyIDPListInputFields({ inputLabel: 'Headers', testValue: mockData.updatedFormValues.headers, testId: 'request-header-headers', @@ -105,8 +105,8 @@ describe('Add Identity Provider: Request Header', () => { }); }); - it('should render the Preferred username headers label, input element, and help text', () => { - verifyIDPListInputFields({ + it('should render the Preferred username headers label, input element, and help text', async () => { + await verifyIDPListInputFields({ inputLabel: 'Preferred username headers', testValue: mockData.updatedFormValues.name, testId: 'request-header-preferred-username-headers', @@ -114,8 +114,8 @@ describe('Add Identity Provider: Request Header', () => { }); }); - it('should render the Name headers label, input element, and help text', () => { - verifyIDPListInputFields({ + it('should render the Name headers label, input element, and help text', async () => { + await verifyIDPListInputFields({ inputLabel: 'Name headers', testValue: mockData.updatedFormValues.headers, testId: 'request-header-name-headers', @@ -123,8 +123,8 @@ describe('Add Identity Provider: Request Header', () => { }); }); - it('should render the Email headers label, input element, and help text', () => { - verifyIDPListInputFields({ + it('should render the Email headers label, input element, and help text', async () => { + await verifyIDPListInputFields({ inputLabel: 'Email headers', testValue: mockData.updatedFormValues.email, testId: 'request-header-email-headers', diff --git a/frontend/public/components/cluster-settings/__tests__/test-utils.ts b/frontend/public/components/cluster-settings/__tests__/test-utils.ts index 7ff866b0d52..d2d8c34c911 100644 --- a/frontend/public/components/cluster-settings/__tests__/test-utils.ts +++ b/frontend/public/components/cluster-settings/__tests__/test-utils.ts @@ -1,5 +1,5 @@ // Reusable test utilities for Identity Provider (IDP) form components -import { screen, fireEvent, within } from '@testing-library/react'; +import { screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { verifyFormElementBasics } from '@console/shared/src/test-utils/unit-test-utils'; @@ -176,7 +176,7 @@ export const verifyIDPFileFields = async ({ * @param helpText - The expected help text for the input group (optional) * @param isRequired - Whether the first input element should be required (optional) */ -export const verifyIDPListInputFields = ({ +export const verifyIDPListInputFields = async ({ inputLabel, testId, initialValue = '', @@ -191,6 +191,7 @@ export const verifyIDPListInputFields = ({ testValue?: string; isRequired?: boolean; }) => { + const user = userEvent.setup(); const listInputContainer = screen.getByTestId(testId); expect(listInputContainer).toBeInTheDocument(); @@ -206,14 +207,15 @@ export const verifyIDPListInputFields = ({ } // Test input element functionality by entering a value - fireEvent.change(initialInputFields[0], { target: { value: testValue } }); + await user.clear(initialInputFields[0]); + await user.type(initialInputFields[0], testValue); expect(initialInputFields[0]).toHaveValue(testValue); const addMoreButton = within(listInputContainer).getByRole('button', { name: 'Add more' }); verifyFormElementBasics(addMoreButton, 'button'); // Click "Add more" and verify length increases - fireEvent.click(addMoreButton); + await user.click(addMoreButton); const updatedInputFields = within(listInputContainer).getAllByLabelText(inputLabel); expect(updatedInputFields.length).toBe(initialInputFields.length + 1); @@ -225,7 +227,8 @@ export const verifyIDPListInputFields = ({ // Test the new element by entering a value const newTestValue = `${testValue}-2`; - fireEvent.change(newField, { target: { value: newTestValue } }); + await user.clear(newField); + await user.type(newField, newTestValue); expect(newField).toHaveValue(newTestValue); // Verify remove buttons exist for all elements @@ -234,7 +237,7 @@ export const verifyIDPListInputFields = ({ verifyFormElementBasics(removeButtons[0], 'button'); // Click remove button and verify length decreases - fireEvent.click(removeButtons[0]); + await user.click(removeButtons[0]); const finalInputFields = within(listInputContainer).getAllByLabelText(inputLabel); expect(finalInputFields.length).toBe(updatedInputFields.length - 1); }; diff --git a/frontend/public/components/factory/__tests__/list-page.spec.tsx b/frontend/public/components/factory/__tests__/list-page.spec.tsx index 5475009380e..f508692814e 100644 --- a/frontend/public/components/factory/__tests__/list-page.spec.tsx +++ b/frontend/public/components/factory/__tests__/list-page.spec.tsx @@ -1,4 +1,5 @@ -import { screen, fireEvent } from '@testing-library/react'; +import { screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { TextFilter } from '@console/internal/components/factory/text-filter'; import { ListPageWrapper, @@ -57,26 +58,29 @@ describe('TextFilter component', () => { expect(input).toHaveValue(defaultValue); }); - it('calls onChange with event and new value when input changes', () => { + it('calls onChange with event and new value when input changes', async () => { + const user = userEvent.setup(); const onChange = jest.fn(); renderWithProviders(); const input = screen.getByRole('textbox'); - fireEvent.change(input, { target: { value: 'test-value' } }); + await user.type(input, 'test-value'); expect(onChange).toHaveBeenCalled(); - expect(onChange).toHaveBeenCalledTimes(1); - expect(onChange).toHaveBeenCalledWith(expect.any(Object), 'test-value'); + await waitFor(() => { + expect(onChange).toHaveBeenLastCalledWith(expect.any(Object), 'test-value'); + }); }); - it('calls onChange with empty string when input is cleared', () => { + it('calls onChange with empty string when input is cleared', async () => { + const user = userEvent.setup(); const onChange = jest.fn(); renderWithProviders(); const input = screen.getByRole('textbox'); - fireEvent.change(input, { target: { value: '' } }); + await user.clear(input); expect(onChange).toHaveBeenCalledWith(expect.any(Object), ''); }); diff --git a/frontend/public/components/modals/__tests__/configure-update-strategy-modal.spec.tsx b/frontend/public/components/modals/__tests__/configure-update-strategy-modal.spec.tsx index 69f8e6fc8c1..0a7b476fb80 100644 --- a/frontend/public/components/modals/__tests__/configure-update-strategy-modal.spec.tsx +++ b/frontend/public/components/modals/__tests__/configure-update-strategy-modal.spec.tsx @@ -1,4 +1,6 @@ -import { screen, fireEvent } from '@testing-library/react'; +import { useState } from 'react'; +import { screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { ConfigureUpdateStrategy } from '@console/internal/components/modals/configure-update-strategy-modal'; import { renderWithProviders } from '@console/shared/src/test-utils/unit-test-utils'; @@ -24,11 +26,8 @@ describe('ConfigureUpdateStrategy component', () => { }); describe('RollingUpdate strategy', () => { - beforeEach(() => { - renderWithProviders(); - }); - it('has proper labels', () => { + renderWithProviders(); expect(screen.getByText('RollingUpdate (default)')).toBeVisible(); expect(screen.getByText('Recreate')).toBeVisible(); expect(screen.getByText('Max unavailable')).toBeVisible(); @@ -36,6 +35,7 @@ describe('ConfigureUpdateStrategy component', () => { }); it('shows rolling update strategy as selected', () => { + renderWithProviders(); const rollingUpdateRadio = screen.getByTestId('rolling-update-strategy-radio'); const recreateRadio = screen.getByTestId('recreate-update-strategy-radio'); @@ -44,6 +44,7 @@ describe('ConfigureUpdateStrategy component', () => { }); it('renders max unavailable and max surge inputs as enabled', () => { + renderWithProviders(); const maxUnavailableInput = screen.getByTestId('max-unavailable-input'); const maxSurgeInput = screen.getByTestId('max-surge-input'); @@ -54,6 +55,7 @@ describe('ConfigureUpdateStrategy component', () => { }); it('displays current values in input fields', () => { + renderWithProviders(); const maxUnavailableInput = screen.getByTestId('max-unavailable-input'); const maxSurgeInput = screen.getByTestId('max-surge-input'); @@ -61,20 +63,76 @@ describe('ConfigureUpdateStrategy component', () => { expect(maxSurgeInput).toHaveValue('25%'); }); - it('calls onChangeMaxUnavailable when input changes', () => { + it('calls onChangeMaxUnavailable when input changes', async () => { + const user = userEvent.setup(); + + const RollingUpdateInputsHarness = () => { + const [maxUnavailable, setMaxUnavailable] = useState('25%'); + const [maxSurge, setMaxSurge] = useState('25%'); + return ( + { + setMaxUnavailable(String(v)); + mockProps.onChangeMaxUnavailable(v); + }} + onChangeMaxSurge={(v) => { + setMaxSurge(String(v)); + mockProps.onChangeMaxSurge(v); + }} + /> + ); + }; + + renderWithProviders(); + const maxUnavailableInput = screen.getByTestId('max-unavailable-input'); - fireEvent.change(maxUnavailableInput, { target: { value: '50%' } }); + await user.clear(maxUnavailableInput); + await user.type(maxUnavailableInput, '50%'); - expect(mockProps.onChangeMaxUnavailable).toHaveBeenCalledWith('50%'); + await waitFor(() => { + expect(mockProps.onChangeMaxUnavailable).toHaveBeenLastCalledWith('50%'); + }); }); - it('calls onChangeMaxSurge when input changes', () => { + it('calls onChangeMaxSurge when input changes', async () => { + const user = userEvent.setup(); + + const RollingUpdateInputsHarness = () => { + const [maxUnavailable, setMaxUnavailable] = useState('25%'); + const [maxSurge, setMaxSurge] = useState('25%'); + return ( + { + setMaxUnavailable(String(v)); + mockProps.onChangeMaxUnavailable(v); + }} + onChangeMaxSurge={(v) => { + setMaxSurge(String(v)); + mockProps.onChangeMaxSurge(v); + }} + /> + ); + }; + + renderWithProviders(); + const maxSurgeInput = screen.getByTestId('max-surge-input'); - fireEvent.change(maxSurgeInput, { target: { value: '1' } }); + await user.clear(maxSurgeInput); + await user.type(maxSurgeInput, '1'); - expect(mockProps.onChangeMaxSurge).toHaveBeenCalledWith('1'); + await waitFor(() => { + expect(mockProps.onChangeMaxSurge).toHaveBeenLastCalledWith('1'); + }); }); }); @@ -103,20 +161,22 @@ describe('ConfigureUpdateStrategy component', () => { }); describe('strategy switching', () => { - it('calls onChangeStrategyType when switching to rolling update', () => { + it('calls onChangeStrategyType when switching to rolling update', async () => { + const user = userEvent.setup(); renderWithProviders(); const rollingUpdateRadio = screen.getByTestId('rolling-update-strategy-radio'); - fireEvent.click(rollingUpdateRadio); + await user.click(rollingUpdateRadio); expect(mockProps.onChangeStrategyType).toHaveBeenCalledWith('RollingUpdate'); }); - it('calls onChangeStrategyType when switching to recreate', () => { + it('calls onChangeStrategyType when switching to recreate', async () => { + const user = userEvent.setup(); renderWithProviders(); const recreateRadio = screen.getByTestId('recreate-update-strategy-radio'); - fireEvent.click(recreateRadio); + await user.click(recreateRadio); expect(mockProps.onChangeStrategyType).toHaveBeenCalledWith('Recreate'); }); diff --git a/frontend/public/components/modals/__tests__/impersonate-user-modal-integration.spec.tsx b/frontend/public/components/modals/__tests__/impersonate-user-modal-integration.spec.tsx index 760da16596d..a46460facd4 100644 --- a/frontend/public/components/modals/__tests__/impersonate-user-modal-integration.spec.tsx +++ b/frontend/public/components/modals/__tests__/impersonate-user-modal-integration.spec.tsx @@ -3,7 +3,8 @@ * Tests the modal integrated with Redux actions and state */ -import { render, screen, fireEvent, waitFor, act } from '@testing-library/react'; +import { render, screen, waitFor, act } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { Provider } from 'react-redux'; import { createStore } from 'redux'; import { ImpersonateUserModal } from '../impersonate-user-modal'; @@ -54,6 +55,7 @@ describe('ImpersonateUserModal Integration Tests', () => { describe('Form submission with Redux integration', () => { it('should dispatch startImpersonate action with user only', async () => { + const user = userEvent.setup(); const onClose = jest.fn(); const onImpersonate = jest.fn((username) => { mockStartImpersonate('User', username); @@ -68,10 +70,11 @@ describe('ImpersonateUserModal Integration Tests', () => { }); const usernameInput = screen.getByTestId('username-input'); - fireEvent.change(usernameInput, { target: { value: 'testuser' } }); + await user.clear(usernameInput); + await user.type(usernameInput, 'testuser'); const submitButton = screen.getByTestId('impersonate-button'); - fireEvent.click(submitButton); + await user.click(submitButton); await waitFor(() => { expect(onImpersonate).toHaveBeenCalledWith('testuser', []); @@ -80,6 +83,7 @@ describe('ImpersonateUserModal Integration Tests', () => { }); it('should dispatch startImpersonate action with user and groups', async () => { + const user = userEvent.setup(); const onClose = jest.fn(); const onImpersonate = jest.fn((username, groups) => { if (groups.length > 0) { @@ -98,12 +102,13 @@ describe('ImpersonateUserModal Integration Tests', () => { }); const usernameInput = screen.getByTestId('username-input'); - fireEvent.change(usernameInput, { target: { value: 'multiuser' } }); + await user.clear(usernameInput); + await user.type(usernameInput, 'multiuser'); // Open groups dropdown const groupsInput = screen.getByPlaceholderText('Enter groups'); await act(async () => { - fireEvent.click(groupsInput); + await user.click(groupsInput); }); // Wait for dropdown to open and select first group @@ -114,11 +119,11 @@ describe('ImpersonateUserModal Integration Tests', () => { const developersOption = screen.getByText('developers'); await act(async () => { - fireEvent.click(developersOption); + await user.click(developersOption); }); const submitButton = screen.getByTestId('impersonate-button'); - fireEvent.click(submitButton); + await user.click(submitButton); await waitFor(() => { expect(onImpersonate).toHaveBeenCalledWith('multiuser', ['developers']); @@ -131,6 +136,7 @@ describe('ImpersonateUserModal Integration Tests', () => { describe('Group selection workflow', () => { it('should handle complete group selection flow', async () => { + const user = userEvent.setup(); const onImpersonate = jest.fn(); await act(async () => { @@ -143,12 +149,13 @@ describe('ImpersonateUserModal Integration Tests', () => { // Enter username const usernameInput = screen.getByTestId('username-input'); - fireEvent.change(usernameInput, { target: { value: 'groupuser' } }); + await user.clear(usernameInput); + await user.type(usernameInput, 'groupuser'); // Open dropdown const groupsInput = screen.getByPlaceholderText('Enter groups'); await act(async () => { - fireEvent.click(groupsInput); + await user.click(groupsInput); }); // Select first group @@ -158,7 +165,7 @@ describe('ImpersonateUserModal Integration Tests', () => { const developersOption = screen.getByText('developers'); await act(async () => { - fireEvent.click(developersOption); + await user.click(developersOption); }); // Verify group chip appears @@ -170,7 +177,7 @@ describe('ImpersonateUserModal Integration Tests', () => { // Select second group const adminsOption = screen.getByText('admins'); await act(async () => { - fireEvent.click(adminsOption); + await user.click(adminsOption); }); // Verify two groups are selected @@ -181,7 +188,7 @@ describe('ImpersonateUserModal Integration Tests', () => { // Submit const submitButton = screen.getByTestId('impersonate-button'); - fireEvent.click(submitButton); + await user.click(submitButton); await waitFor(() => { expect(onImpersonate).toHaveBeenCalledWith('groupuser', ['developers', 'admins']); @@ -189,6 +196,7 @@ describe('ImpersonateUserModal Integration Tests', () => { }); it('should allow deselecting a group', async () => { + const user = userEvent.setup(); const onImpersonate = jest.fn(); await act(async () => { @@ -200,12 +208,13 @@ describe('ImpersonateUserModal Integration Tests', () => { }); const usernameInput = screen.getByTestId('username-input'); - fireEvent.change(usernameInput, { target: { value: 'deselectuser' } }); + await user.clear(usernameInput); + await user.type(usernameInput, 'deselectuser'); // Open and select group const groupsInput = screen.getByPlaceholderText('Enter groups'); await act(async () => { - fireEvent.click(groupsInput); + await user.click(groupsInput); }); await waitFor(() => { @@ -214,7 +223,7 @@ describe('ImpersonateUserModal Integration Tests', () => { const developersOption = screen.getByText('developers'); await act(async () => { - fireEvent.click(developersOption); + await user.click(developersOption); }); // Wait for chip to appear @@ -227,7 +236,7 @@ describe('ImpersonateUserModal Integration Tests', () => { const closeButtons = document.querySelectorAll('.pf-v6-c-label__actions button'); if (closeButtons.length > 0) { await act(async () => { - fireEvent.click(closeButtons[0]); + await user.click(closeButtons[0]); }); await waitFor(() => { @@ -238,7 +247,7 @@ describe('ImpersonateUserModal Integration Tests', () => { // Submit with no groups const submitButton = screen.getByTestId('impersonate-button'); - fireEvent.click(submitButton); + await user.click(submitButton); await waitFor(() => { expect(onImpersonate).toHaveBeenCalledWith('deselectuser', []); @@ -248,6 +257,7 @@ describe('ImpersonateUserModal Integration Tests', () => { describe('Search and filter workflow', () => { it('should filter groups based on search input', async () => { + const user = userEvent.setup(); await act(async () => { render( @@ -260,7 +270,7 @@ describe('ImpersonateUserModal Integration Tests', () => { // Open dropdown first await act(async () => { - fireEvent.click(groupsInput); + await user.click(groupsInput); }); // Wait for dropdown to open @@ -270,7 +280,7 @@ describe('ImpersonateUserModal Integration Tests', () => { // Type to filter await act(async () => { - fireEvent.change(groupsInput, { target: { value: 'dev' } }); + await user.type(groupsInput, 'dev'); }); await waitFor(() => { @@ -282,6 +292,7 @@ describe('ImpersonateUserModal Integration Tests', () => { }); it('should show no results when filter matches nothing', async () => { + const user = userEvent.setup(); await act(async () => { render( @@ -294,7 +305,7 @@ describe('ImpersonateUserModal Integration Tests', () => { // Open dropdown first await act(async () => { - fireEvent.click(groupsInput); + await user.click(groupsInput); }); // Wait for dropdown to open @@ -304,7 +315,7 @@ describe('ImpersonateUserModal Integration Tests', () => { // Type to filter with non-matching text await act(async () => { - fireEvent.change(groupsInput, { target: { value: 'nonexistent' } }); + await user.type(groupsInput, 'nonexistent'); }); await waitFor(() => { @@ -331,8 +342,10 @@ describe('ImpersonateUserModal Integration Tests', () => { }); // Should still allow impersonation without groups + const ue = userEvent.setup(); const usernameInput = screen.getByTestId('username-input'); - fireEvent.change(usernameInput, { target: { value: 'erroruser' } }); + await ue.clear(usernameInput); + await ue.type(usernameInput, 'erroruser'); const submitButton = screen.getByTestId('impersonate-button'); expect(submitButton).not.toBeDisabled(); @@ -341,6 +354,7 @@ describe('ImpersonateUserModal Integration Tests', () => { describe('Modal lifecycle workflow', () => { it('should reset form when reopening modal', async () => { + const ue = userEvent.setup(); const { rerender } = render( @@ -349,7 +363,8 @@ describe('ImpersonateUserModal Integration Tests', () => { // Fill form const usernameInput = screen.getByTestId('username-input'); - fireEvent.change(usernameInput, { target: { value: 'tempuser' } }); + await ue.clear(usernameInput); + await ue.type(usernameInput, 'tempuser'); // Close modal await act(async () => { @@ -375,6 +390,7 @@ describe('ImpersonateUserModal Integration Tests', () => { }); it('should call onClose when cancel is clicked', async () => { + const ue = userEvent.setup(); const onClose = jest.fn(); await act(async () => { @@ -386,7 +402,7 @@ describe('ImpersonateUserModal Integration Tests', () => { }); const cancelButton = screen.getByTestId('cancel-button'); - fireEvent.click(cancelButton); + await ue.click(cancelButton); expect(onClose).toHaveBeenCalled(); }); @@ -413,7 +429,6 @@ describe('ImpersonateUserModal Integration Tests', () => { expect(usernameInput).toHaveAttribute('readonly'); // Readonly attribute should be present (prevents browser editing) - // Note: In test environment, fireEvent.change still works, but readonly // attribute is correctly set for browser behavior }); }); diff --git a/frontend/public/components/modals/__tests__/impersonate-user-modal.spec.tsx b/frontend/public/components/modals/__tests__/impersonate-user-modal.spec.tsx index f72dbb48aa9..71eb16a8a77 100644 --- a/frontend/public/components/modals/__tests__/impersonate-user-modal.spec.tsx +++ b/frontend/public/components/modals/__tests__/impersonate-user-modal.spec.tsx @@ -1,4 +1,5 @@ -import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { ImpersonateUserModal } from '../impersonate-user-modal'; import { useK8sWatchResource } from '../../utils/k8s-watch-hook'; import { GroupKind } from '../../../module/k8s'; @@ -108,7 +109,8 @@ describe('ImpersonateUserModal', () => { }); describe('Username Input', () => { - it('should allow typing username', () => { + it('should allow typing username', async () => { + const user = userEvent.setup(); render( { ); const usernameInput = screen.getByTestId('username-input') as HTMLInputElement; - fireEvent.change(usernameInput, { target: { value: 'testuser' } }); + await user.clear(usernameInput); + await user.type(usernameInput, 'testuser'); expect(usernameInput.value).toBe('testuser'); }); @@ -204,6 +207,7 @@ describe('ImpersonateUserModal', () => { describe('Form Submission', () => { it('should call onImpersonate with username only when no groups selected', async () => { + const user = userEvent.setup(); render( { ); const usernameInput = screen.getByTestId('username-input'); - fireEvent.change(usernameInput, { target: { value: 'testuser' } }); + await user.clear(usernameInput); + await user.type(usernameInput, 'testuser'); const submitButton = screen.getByTestId('impersonate-button'); - fireEvent.click(submitButton); + await user.click(submitButton); await waitFor(() => { expect(mockOnImpersonate).toHaveBeenCalledWith('testuser', []); @@ -224,6 +229,7 @@ describe('ImpersonateUserModal', () => { }); it('should trim whitespace from username', async () => { + const user = userEvent.setup(); render( { ); const usernameInput = screen.getByTestId('username-input'); - fireEvent.change(usernameInput, { target: { value: ' testuser ' } }); + await user.clear(usernameInput); + await user.type(usernameInput, ' testuser '); const submitButton = screen.getByTestId('impersonate-button'); - fireEvent.click(submitButton); + await user.click(submitButton); await waitFor(() => { expect(mockOnImpersonate).toHaveBeenCalledWith('testuser', []); @@ -244,6 +251,7 @@ describe('ImpersonateUserModal', () => { }); it('should close modal after successful submission', async () => { + const user = userEvent.setup(); render( { ); const usernameInput = screen.getByTestId('username-input'); - fireEvent.change(usernameInput, { target: { value: 'testuser' } }); + await user.clear(usernameInput); + await user.type(usernameInput, 'testuser'); const submitButton = screen.getByTestId('impersonate-button'); - fireEvent.click(submitButton); + await user.click(submitButton); await waitFor(() => { expect(mockOnClose).toHaveBeenCalled(); @@ -265,7 +274,8 @@ describe('ImpersonateUserModal', () => { }); describe('Modal Close Behavior', () => { - it('should call onClose when cancel button is clicked', () => { + it('should call onClose when cancel button is clicked', async () => { + const user = userEvent.setup(); render( { ); const cancelButton = screen.getByTestId('cancel-button'); - fireEvent.click(cancelButton); + await user.click(cancelButton); expect(mockOnClose).toHaveBeenCalled(); }); - it('should reset form when modal is closed and reopened', () => { + it('should reset form when modal is closed and reopened', async () => { + const user = userEvent.setup(); const { rerender } = render( { // Enter username const usernameInput = screen.getByTestId('username-input'); - fireEvent.change(usernameInput, { target: { value: 'testuser' } }); + await user.clear(usernameInput); + await user.type(usernameInput, 'testuser'); // Close modal rerender( @@ -350,6 +362,7 @@ describe('ImpersonateUserModal', () => { describe('Expandable Groups (More than 5 selected)', () => { it('should show all groups when 5 or fewer are selected', async () => { + const user = userEvent.setup(); (useK8sWatchResource as jest.Mock).mockReturnValue([ [ { metadata: { name: 'group1' } }, @@ -372,13 +385,13 @@ describe('ImpersonateUserModal', () => { // Open dropdown and select all 5 groups const groupInput = screen.getByPlaceholderText('Enter groups'); - fireEvent.click(groupInput); + await user.click(groupInput); await waitFor(() => { expect(screen.getByText('Select all')).toBeInTheDocument(); }); - fireEvent.click(screen.getByText('Select all')); + await user.click(screen.getByText('Select all')); // All 5 groups should be visible as chips await waitFor(() => { @@ -394,6 +407,7 @@ describe('ImpersonateUserModal', () => { }); it('should hide groups beyond 5 and show "+N" button when more than 5 groups selected', async () => { + const user = userEvent.setup(); (useK8sWatchResource as jest.Mock).mockReturnValue([ [ { metadata: { name: 'group1' } }, @@ -419,13 +433,13 @@ describe('ImpersonateUserModal', () => { // Open dropdown and select all 8 groups const groupInput = screen.getByPlaceholderText('Enter groups'); - fireEvent.click(groupInput); + await user.click(groupInput); await waitFor(() => { expect(screen.getByText('Select all')).toBeInTheDocument(); }); - fireEvent.click(screen.getByText('Select all')); + await user.click(screen.getByText('Select all')); // First 5 groups should be visible as chips await waitFor(() => { @@ -452,6 +466,7 @@ describe('ImpersonateUserModal', () => { }); it('should expand and show all groups when "+N" button is clicked', async () => { + const user = userEvent.setup(); (useK8sWatchResource as jest.Mock).mockReturnValue([ [ { metadata: { name: 'group1' } }, @@ -476,13 +491,13 @@ describe('ImpersonateUserModal', () => { // Open dropdown and select all 7 groups const groupInput = screen.getByPlaceholderText('Enter groups'); - fireEvent.click(groupInput); + await user.click(groupInput); await waitFor(() => { expect(screen.getByText('Select all')).toBeInTheDocument(); }); - fireEvent.click(screen.getByText('Select all')); + await user.click(screen.getByText('Select all')); // Wait for "+2" button to appear await waitFor(() => { @@ -490,7 +505,7 @@ describe('ImpersonateUserModal', () => { }); // Click the "+2" button to expand - fireEvent.click(screen.getByText('+2')); + await user.click(screen.getByText('+2')); // Now all 7 groups should be visible await waitFor(() => { @@ -508,6 +523,7 @@ describe('ImpersonateUserModal', () => { }); it('should collapse back when groups are removed to 5 or fewer', async () => { + const user = userEvent.setup(); (useK8sWatchResource as jest.Mock).mockReturnValue([ [ { metadata: { name: 'group1' } }, @@ -531,19 +547,19 @@ describe('ImpersonateUserModal', () => { // Select all 6 groups const groupInput = screen.getByPlaceholderText('Enter groups'); - fireEvent.click(groupInput); + await user.click(groupInput); await waitFor(() => { expect(screen.getByText('Select all')).toBeInTheDocument(); }); - fireEvent.click(screen.getByText('Select all')); + await user.click(screen.getByText('Select all')); // Expand to show all groups await waitFor(() => { expect(screen.getByText('+1')).toBeInTheDocument(); }); - fireEvent.click(screen.getByText('+1')); + await user.click(screen.getByText('+1')); await waitFor(() => { expect(screen.getByText('group6')).toBeInTheDocument(); @@ -554,7 +570,7 @@ describe('ImpersonateUserModal', () => { const closeButton = group6Chip?.querySelector('button'); expect(closeButton).toBeInTheDocument(); - fireEvent.click(closeButton!); + await user.click(closeButton!); // Now only 5 groups remain, so it should collapse automatically await waitFor(() => { diff --git a/frontend/public/components/modals/__tests__/replace-code-modal.spec.tsx b/frontend/public/components/modals/__tests__/replace-code-modal.spec.tsx index 2670cc6e7de..0d3d1b78f93 100644 --- a/frontend/public/components/modals/__tests__/replace-code-modal.spec.tsx +++ b/frontend/public/components/modals/__tests__/replace-code-modal.spec.tsx @@ -1,4 +1,5 @@ -import { render, fireEvent } from '@testing-library/react'; +import { render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { ReplaceCodeModal } from '../replace-code-modal'; jest.mock('react-i18next', () => ({ @@ -32,34 +33,38 @@ describe('ReplaceCodeModal', () => { expect(getByText('Keep both')).toBeTruthy(); }); - it('should call handleCodeReplace when "Yes" button is clicked', () => { + it('should call handleCodeReplace when "Yes" button is clicked', async () => { + const user = userEvent.setup(); const { getByText } = renderComponent(); - fireEvent.click(getByText('Yes')); + await user.click(getByText('Yes')); expect(handleCodeReplaceMock).toHaveBeenCalledTimes(1); }); - it('should call handleCodeReplace when "No" button is clicked', () => { + it('should call handleCodeReplace when "No" button is clicked', async () => { + const user = userEvent.setup(); const { getByText } = renderComponent(); - fireEvent.click(getByText('No')); + await user.click(getByText('No')); expect(handleCodeReplaceMock).toHaveBeenCalledTimes(1); }); - it('should call handleCodeReplace when "Keep both" button is clicked', () => { + it('should call handleCodeReplace when "Keep both" button is clicked', async () => { + const user = userEvent.setup(); const { getByText } = renderComponent(); - fireEvent.click(getByText('Keep both')); + await user.click(getByText('Keep both')); expect(handleCodeReplaceMock).toHaveBeenCalledTimes(1); }); - it('should call handleCodeReplace when close button (X) is clicked', () => { + it('should call handleCodeReplace when close button (X) is clicked', async () => { + const user = userEvent.setup(); const { getByLabelText } = renderComponent(); const closeButton = getByLabelText('Close'); expect(closeButton).toBeTruthy(); - fireEvent.click(closeButton); + await user.click(closeButton); expect(handleCodeReplaceMock).toHaveBeenCalledTimes(1); }); }); diff --git a/frontend/public/components/utils/__tests__/download-button.spec.tsx b/frontend/public/components/utils/__tests__/download-button.spec.tsx index 63782a777fc..9c6e496e49b 100644 --- a/frontend/public/components/utils/__tests__/download-button.spec.tsx +++ b/frontend/public/components/utils/__tests__/download-button.spec.tsx @@ -1,4 +1,5 @@ -import { render, screen, fireEvent, waitFor, act } from '@testing-library/react'; +import { render, screen, waitFor, act } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import * as fileSaver from 'file-saver'; import { DownloadButton } from '../../../components/utils/download-button'; @@ -31,6 +32,7 @@ describe('DownloadButton', () => { }); it('renders button which calls `consoleFetch` to download URL when clicked', async () => { + const user = userEvent.setup(); await act(async () => { render(); }); @@ -39,7 +41,7 @@ describe('DownloadButton', () => { expect(downloadButton).toBeInTheDocument(); await act(async () => { - fireEvent.click(downloadButton); + await user.click(downloadButton); }); // Verify consoleFetch was called with the correct URL @@ -47,6 +49,7 @@ describe('DownloadButton', () => { }); it('renders "Downloading..." if download is in flight', async () => { + const user = userEvent.setup(); let resolvePromise: (value: Blob) => void; const controlledPromise = new Promise((resolve) => { resolvePromise = resolve; @@ -65,7 +68,7 @@ describe('DownloadButton', () => { // Click to start download await act(async () => { - fireEvent.click(downloadButton); + await user.click(downloadButton); }); // Check that button shows "Downloading..." while in flight diff --git a/frontend/public/components/utils/__tests__/kebab.spec.tsx b/frontend/public/components/utils/__tests__/kebab.spec.tsx index d81d705262d..98465a79536 100644 --- a/frontend/public/components/utils/__tests__/kebab.spec.tsx +++ b/frontend/public/components/utils/__tests__/kebab.spec.tsx @@ -1,4 +1,5 @@ -import { render, fireEvent } from '@testing-library/react'; +import { render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { KebabItem, KebabItemAccessReview_ } from '../kebab'; import { useAccessReview } from '../rbac'; @@ -14,25 +15,28 @@ const mockOption = { }; describe('KebabItem', () => { - it('should disable option without callback / href (i.e. option does nothing)', () => { + it('should disable option without callback / href (i.e. option does nothing)', async () => { + const user = userEvent.setup(); const nothingOption = { ...mockOption, href: undefined }; const trackOnClick = jest.fn(); const renderItem = render(); - fireEvent.click(renderItem.getByRole('menuitem')); + await user.click(renderItem.getByRole('menuitem')); expect(trackOnClick).toHaveBeenCalledTimes(0); }); - it('should enable when option has href', () => { + it('should enable when option has href', async () => { + const user = userEvent.setup(); const hrefOption = { ...mockOption }; const trackOnClick = jest.fn(); const renderItem = render(); - fireEvent.click(renderItem.getByRole('menuitem')); + await user.click(renderItem.getByRole('menuitem')); expect(trackOnClick).toHaveBeenCalledTimes(1); }); - it('should enable when option has a callback', () => { + it('should enable when option has a callback', async () => { + const user = userEvent.setup(); const callbackOption = { ...mockOption, href: undefined, callback: () => {} }; const trackOnClick = jest.fn(); const renderItem = render(); - fireEvent.click(renderItem.getByRole('menuitem')); + await user.click(renderItem.getByRole('menuitem')); expect(trackOnClick).toHaveBeenCalledTimes(1); }); }); @@ -44,7 +48,8 @@ describe('KebabItemAccessReview_', () => { name: 'dummy', subprotocols: ['dummy'], }; - it('should disable option when option.accessReview present and not allowed', () => { + it('should disable option when option.accessReview present and not allowed', async () => { + const user = userEvent.setup(); const useAccessReviewMock = useAccessReview as jest.Mock; const trackOnClick = jest.fn(); useAccessReviewMock.mockReturnValue(false); @@ -55,10 +60,11 @@ describe('KebabItemAccessReview_', () => { impersonate={mockImpersonate} />, ); - fireEvent.click(renderItem.getByRole('menuitem')); + await user.click(renderItem.getByRole('menuitem')); expect(trackOnClick).toHaveBeenCalledTimes(0); }); - it('should enable option when option.accessReview present and allowed', () => { + it('should enable option when option.accessReview present and allowed', async () => { + const user = userEvent.setup(); const useAccessReviewMock = useAccessReview as jest.Mock; const trackOnClick = jest.fn(); useAccessReviewMock.mockReturnValue(true); @@ -69,7 +75,7 @@ describe('KebabItemAccessReview_', () => { impersonate={mockImpersonate} />, ); - fireEvent.click(renderItem.getByRole('menuitem')); + await user.click(renderItem.getByRole('menuitem')); expect(trackOnClick).toHaveBeenCalledTimes(1); }); }); diff --git a/frontend/public/components/utils/__tests__/name-value-editor.spec.tsx b/frontend/public/components/utils/__tests__/name-value-editor.spec.tsx index a23f21131a2..053ef956f28 100644 --- a/frontend/public/components/utils/__tests__/name-value-editor.spec.tsx +++ b/frontend/public/components/utils/__tests__/name-value-editor.spec.tsx @@ -1,49 +1,74 @@ -import { render, screen, fireEvent } from '@testing-library/react'; +import { useState } from 'react'; +import type { FC } from 'react'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { NameValueEditor } from '../../../components/utils/name-value-editor'; jest.mock('react-i18next'); +/** Keeps nameValuePairs in state so controlled inputs match typed values after updateParentData. */ +const NameValueEditorHarness: FC<{ + initialPairs: Array<[string, string, number]>; + onUpdate: jest.Mock; +}> = ({ initialPairs, onUpdate }) => { + const [pairs, setPairs] = useState(initialPairs); + return ( + { + setPairs(data.nameValuePairs); + onUpdate(data, nameValueId); + }} + nameValueId={0} + /> + ); +}; + describe('NameValueEditor', () => { beforeEach(() => { jest.clearAllMocks(); }); describe('User can manage name-value pairs', () => { - it('allows user to edit existing key and value', () => { + it('allows user to edit existing key and value', async () => { + const user = userEvent.setup(); const mockUpdate = jest.fn(); render( - , + , ); // User can see and edit the key const keyInput = screen.getByDisplayValue('mykey'); - fireEvent.change(keyInput, { target: { value: 'newkey' } }); - - expect(mockUpdate).toHaveBeenCalledWith( - { - nameValuePairs: [['newkey', 'myvalue', 0]], - }, - 0, - ); + await user.clear(keyInput); + await user.type(keyInput, 'newkey'); + + await waitFor(() => { + expect(mockUpdate).toHaveBeenLastCalledWith( + { + nameValuePairs: [['newkey', 'myvalue', 0]], + }, + 0, + ); + }); // User can see and edit the value const valueInput = screen.getByDisplayValue('myvalue'); - fireEvent.change(valueInput, { target: { value: 'newvalue' } }); - - expect(mockUpdate).toHaveBeenCalledWith( - { - nameValuePairs: [['mykey', 'newvalue', 0]], - }, - 0, - ); + await user.clear(valueInput); + await user.type(valueInput, 'newvalue'); + + await waitFor(() => { + expect(mockUpdate).toHaveBeenLastCalledWith( + { + nameValuePairs: [['newkey', 'newvalue', 0]], + }, + 0, + ); + }); }); - it('allows user to add a new pair', () => { + it('allows user to add a new pair', async () => { + const user = userEvent.setup(); const mockUpdate = jest.fn(); render( @@ -56,7 +81,7 @@ describe('NameValueEditor', () => { // User clicks "Add more" button const addButton = screen.getByRole('button', { name: /add more/i }); - fireEvent.click(addButton); + await user.click(addButton); // Should add a new empty pair expect(mockUpdate).toHaveBeenCalledWith( @@ -70,7 +95,8 @@ describe('NameValueEditor', () => { ); }); - it('allows user to delete a pair', () => { + it('allows user to delete a pair', async () => { + const user = userEvent.setup(); const mockUpdate = jest.fn(); render( @@ -86,7 +112,7 @@ describe('NameValueEditor', () => { // User clicks delete button for first pair const deleteButtons = screen.getAllByTestId('delete-button'); - fireEvent.click(deleteButtons[0]); + await user.click(deleteButtons[0]); // Should remove the first pair and reindex expect(mockUpdate).toHaveBeenCalledWith( @@ -97,7 +123,8 @@ describe('NameValueEditor', () => { ); }); - it('maintains at least one empty pair when deleting the last pair', () => { + it('maintains at least one empty pair when deleting the last pair', async () => { + const user = userEvent.setup(); const mockUpdate = jest.fn(); render( @@ -110,7 +137,7 @@ describe('NameValueEditor', () => { // User deletes the only pair const deleteButton = screen.getByTestId('delete-button'); - fireEvent.click(deleteButton); + await user.click(deleteButton); // Should replace with empty pair instead of empty array expect(mockUpdate).toHaveBeenCalledWith( @@ -205,23 +232,23 @@ describe('NameValueEditor', () => { }); describe('User interactions update data correctly', () => { - it('calls updateParentData with correct parameters when typing', () => { + it('calls updateParentData with correct parameters when typing', async () => { + const user = userEvent.setup(); const mockUpdate = jest.fn(); render( - , + , ); const keyInput = screen.getByDisplayValue('test'); - fireEvent.change(keyInput, { target: { value: 'testX' } }); - expect(mockUpdate).toHaveBeenCalledWith( - { - nameValuePairs: [['testX', 'value', 0]], - }, - 0, - ); + await user.clear(keyInput); + await user.type(keyInput, 'testX'); + await waitFor(() => { + expect(mockUpdate).toHaveBeenLastCalledWith( + { + nameValuePairs: [['testX', 'value', 0]], + }, + 0, + ); + }); }); }); }); diff --git a/frontend/public/components/utils/__tests__/single-typeahead-dropdown.spec.tsx b/frontend/public/components/utils/__tests__/single-typeahead-dropdown.spec.tsx index 5c4e5f03c59..4188021fff6 100644 --- a/frontend/public/components/utils/__tests__/single-typeahead-dropdown.spec.tsx +++ b/frontend/public/components/utils/__tests__/single-typeahead-dropdown.spec.tsx @@ -1,4 +1,5 @@ -import { screen, act, waitFor, fireEvent } from '@testing-library/react'; +import { screen, act, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { renderWithProviders } from '@console/shared/src/test-utils/unit-test-utils'; import { SingleTypeaheadDropdown } from '../single-typeahead-dropdown'; @@ -30,6 +31,7 @@ describe('SingleTypeaheadDropdown', () => { }); it('should display the clear button when input value is present', async () => { + const user = userEvent.setup(); await act(async () => { renderWithProviders( { // Type some text into the input await act(async () => { - fireEvent.change(combobox, { target: { value: 'test' } }); + await user.click(combobox); + await user.type(combobox, 'test'); }); await waitFor(() => { @@ -56,6 +59,7 @@ describe('SingleTypeaheadDropdown', () => { }); it('should not display the clear button when hideClearButton is true', async () => { + const user = userEvent.setup(); await act(async () => { renderWithProviders( { const combobox = screen.getByRole('combobox'); await act(async () => { - fireEvent.change(combobox, { target: { value: 'test' } }); + await user.click(combobox); + await user.type(combobox, 'test'); }); await waitFor(() => { @@ -80,6 +85,7 @@ describe('SingleTypeaheadDropdown', () => { }); it('should focus the first item when ArrowDown key is pressed', async () => { + const user = userEvent.setup(); await act(async () => { renderWithProviders( { // Press ArrowDown to open dropdown and focus first item await act(async () => { - fireEvent.click(combobox); - fireEvent.keyDown(combobox, { key: 'ArrowDown' }); + await user.click(combobox); + await user.keyboard('{ArrowDown}'); }); await waitFor(() => { @@ -114,6 +120,7 @@ describe('SingleTypeaheadDropdown', () => { }); it('should focus the last item when ArrowUp key is pressed on the first item', async () => { + const user = userEvent.setup(); await act(async () => { renderWithProviders( { // Press ArrowUp to open dropdown and focus last item await act(async () => { - fireEvent.click(combobox); - fireEvent.keyDown(combobox, { key: 'ArrowUp' }); + await user.click(combobox); + await user.keyboard('{ArrowUp}'); }); await waitFor(() => { @@ -151,6 +158,7 @@ describe('SingleTypeaheadDropdown', () => { }); it('should call onChange when an option is selected', async () => { + const user = userEvent.setup(); await act(async () => { renderWithProviders( { const combobox = screen.getByRole('combobox'); await act(async () => { - fireEvent.click(combobox); + await user.click(combobox); }); // Wait for dropdown to open and click on the first option @@ -178,7 +186,7 @@ describe('SingleTypeaheadDropdown', () => { const firstOption = screen.getByRole('option', { name: 'test1' }); await act(async () => { - fireEvent.click(firstOption); + await user.click(firstOption); }); await waitFor(() => { @@ -187,6 +195,7 @@ describe('SingleTypeaheadDropdown', () => { }); it('should clear the input when clear button is clicked', async () => { + const user = userEvent.setup(); await act(async () => { renderWithProviders( { const combobox = screen.getByRole('combobox'); await act(async () => { - fireEvent.change(combobox, { target: { value: 'some text' } }); + await user.click(combobox); + await user.clear(combobox); + await user.type(combobox, 'some text'); }); await waitFor(() => { @@ -211,7 +222,7 @@ describe('SingleTypeaheadDropdown', () => { const clearButton = screen.getByRole('button', { name: /clear input value/i }); await act(async () => { - fireEvent.click(clearButton); + await user.click(clearButton); }); await waitFor(() => {