From 84bfb0d045cc4f32ffb593b658e48ed29baabb0c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Sep 2025 03:26:12 +0000 Subject: [PATCH 1/3] Initial plan From 44f407fcdccf1a385e4c53015e607bbf3190c3ba Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Sep 2025 03:39:30 +0000 Subject: [PATCH 2/3] Fix async timing issues in select stories for CI stability Co-authored-by: jaruesink <4207065+jaruesink@users.noreply.github.com> --- apps/docs/.storybook/test-runner-setup.js | 9 ++++ apps/docs/.storybook/test-runner.js | 15 ++++++ .../src/remix-hook-form/select.stories.tsx | 49 ++++++++++++++++--- 3 files changed, 65 insertions(+), 8 deletions(-) create mode 100644 apps/docs/.storybook/test-runner-setup.js create mode 100644 apps/docs/.storybook/test-runner.js diff --git a/apps/docs/.storybook/test-runner-setup.js b/apps/docs/.storybook/test-runner-setup.js new file mode 100644 index 00000000..3425fc99 --- /dev/null +++ b/apps/docs/.storybook/test-runner-setup.js @@ -0,0 +1,9 @@ +// Configure testing-library for CI environments +import { configure } from '@testing-library/react'; + +// Increase timeout for element queries in CI environments +configure({ + testIdAttribute: 'data-testid', + // Increase default timeout for findBy* queries (especially important for CI) + asyncUtilTimeout: 10000, // 10 seconds instead of default 1 second +}); diff --git a/apps/docs/.storybook/test-runner.js b/apps/docs/.storybook/test-runner.js new file mode 100644 index 00000000..f1134835 --- /dev/null +++ b/apps/docs/.storybook/test-runner.js @@ -0,0 +1,15 @@ +const { getJestConfig } = require('@storybook/test-runner'); + +const testRunnerConfig = getJestConfig(); + +/** + * @type {import('@jest/types').Config.InitialOptions} + */ +module.exports = { + ...testRunnerConfig, + // Increase timeout for CI environments + testTimeout: 30000, // 30 seconds instead of default 15 seconds + + // Configure for CI environments + setupFilesAfterEnv: [...(testRunnerConfig.setupFilesAfterEnv || []), require.resolve('./test-runner-setup.js')], +}; diff --git a/apps/docs/src/remix-hook-form/select.stories.tsx b/apps/docs/src/remix-hook-form/select.stories.tsx index c7e03597..80169f4f 100644 --- a/apps/docs/src/remix-hook-form/select.stories.tsx +++ b/apps/docs/src/remix-hook-form/select.stories.tsx @@ -221,6 +221,8 @@ const RegionSelectExample = () => { await userEvent.click(stateSelect); { const listbox = await within(document.body).findByRole('listbox'); + // Small delay to ensure portal content is stable + await new Promise((resolve) => setTimeout(resolve, 100)); const californiaOption = within(listbox).getByRole('option', { name: 'California' }); await userEvent.click(californiaOption); } @@ -230,6 +232,8 @@ const RegionSelectExample = () => { await userEvent.click(provinceSelect); { const listbox = await within(document.body).findByRole('listbox'); + // Small delay to ensure portal content is stable + await new Promise((resolve) => setTimeout(resolve, 100)); const ontarioOption = within(listbox).getByRole('option', { name: 'Ontario' }); await userEvent.click(ontarioOption); } @@ -239,6 +243,8 @@ const RegionSelectExample = () => { await userEvent.click(regionSelect); { const listbox = await within(document.body).findByRole('listbox'); + // Small delay to ensure portal content is stable + await new Promise((resolve) => setTimeout(resolve, 100)); const customOption = within(listbox).getByRole('option', { name: 'California' }); await userEvent.click(customOption); } @@ -275,6 +281,8 @@ export const USStateSelection: Story = { // Dropdown content renders in a portal; query via document.body roles const listbox = await within(document.body).findByRole('listbox'); + // Small delay to ensure portal content is stable + await new Promise((resolve) => setTimeout(resolve, 100)); const californiaOption = within(listbox).getByRole('option', { name: 'California' }); await userEvent.click(californiaOption); @@ -303,6 +311,8 @@ export const CanadaProvinceSelection: Story = { // Query in portal content by role const listbox = await within(document.body).findByRole('listbox'); + // Small delay to ensure portal content is stable + await new Promise((resolve) => setTimeout(resolve, 100)); const ontarioOption = within(listbox).getByRole('option', { name: 'Ontario' }); await userEvent.click(ontarioOption); @@ -330,6 +340,8 @@ export const FormSubmission: Story = { await userEvent.click(stateSelect); { const listbox = await within(document.body).findByRole('listbox'); + // Small delay to ensure portal content is stable + await new Promise((resolve) => setTimeout(resolve, 100)); const californiaOption = within(listbox).getByRole('option', { name: 'California' }); await userEvent.click(californiaOption); } @@ -339,6 +351,8 @@ export const FormSubmission: Story = { await userEvent.click(provinceSelect); { const listbox = await within(document.body).findByRole('listbox'); + // Small delay to ensure portal content is stable + await new Promise((resolve) => setTimeout(resolve, 100)); const ontarioOption = within(listbox).getByRole('option', { name: 'Ontario' }); await userEvent.click(ontarioOption); } @@ -348,6 +362,8 @@ export const FormSubmission: Story = { await userEvent.click(regionSelect); { const listbox = await within(document.body).findByRole('listbox'); + // Small delay to ensure portal content is stable + await new Promise((resolve) => setTimeout(resolve, 100)); const customOption = within(listbox).getByRole('option', { name: 'California' }); await userEvent.click(customOption); } @@ -454,6 +470,8 @@ export const CustomSearchPlaceholder: Story = { await step('Open select and see custom placeholder', async () => { const regionSelect = canvas.getByLabelText('Custom Region'); await userEvent.click(regionSelect); + // Small delay to ensure portal content is stable + await new Promise((resolve) => setTimeout(resolve, 100)); // The search input is rendered alongside the listbox in the portal, not inside the listbox itself. const searchInput = await within(document.body).findByPlaceholderText('Type to filter…'); expect(searchInput).toBeInTheDocument(); @@ -511,19 +529,26 @@ export const CreatableOption: Story = { // Wait for the component to render before interacting const regionSelect = await canvas.findByLabelText('Custom Region'); await userEvent.click(regionSelect); - - // Wait for the dropdown to appear in the portal + + // Wait for the dropdown to appear in the portal and ensure it's stable const listbox = await within(document.body).findByRole('listbox'); - - // The search input is outside the listbox container; query from the portal root + + // Additional wait to ensure portal content is fully rendered + await new Promise((resolve) => setTimeout(resolve, 200)); + + // Wait for the search input to be available in the portal const input = await within(document.body).findByPlaceholderText('Search...'); + + // Ensure the input is ready for interaction with explicit focus await userEvent.click(input); await userEvent.clear(input); await userEvent.type(input, 'Atlantis'); + // Wait for the create option to appear after typing const createItem = await within(listbox).findByRole('option', { name: 'Select "Atlantis"' }); await userEvent.click(createItem); + // Verify the selection updated await expect(canvas.findByRole('combobox', { name: 'Custom Region' })).resolves.toHaveTextContent('Atlantis'); // Submit and verify server received the created option value @@ -536,16 +561,24 @@ export const CreatableOption: Story = { // Wait for the component to render before interacting const regionSelect = await canvas.findByLabelText('Custom Region'); await userEvent.click(regionSelect); - - // Wait for the dropdown to appear in the portal + + // Wait for the dropdown to appear in the portal and ensure it's stable const listbox = await within(document.body).findByRole('listbox'); - - // The search input is outside the listbox container; query from the portal root + + // Additional wait to ensure portal content is fully rendered + await new Promise((resolve) => setTimeout(resolve, 200)); + + // Wait for the search input to be available in the portal const input = await within(document.body).findByPlaceholderText('Search...'); + + // Ensure the input is ready for interaction with explicit focus await userEvent.click(input); await userEvent.clear(input); await userEvent.type(input, 'California'); + // Allow time for filtering to complete and DOM to stabilize + await new Promise((resolve) => setTimeout(resolve, 500)); + expect(within(listbox).queryByRole('option', { name: 'Select "California"' })).not.toBeInTheDocument(); }); }, From f006536c05d8beead4d64d0ec4c9e885f8e88472 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Sep 2025 03:40:16 +0000 Subject: [PATCH 3/3] Add documentation for CI test stability fix Co-authored-by: jaruesink <4207065+jaruesink@users.noreply.github.com> --- docs/ci-test-stability-fix.md | 59 +++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 docs/ci-test-stability-fix.md diff --git a/docs/ci-test-stability-fix.md b/docs/ci-test-stability-fix.md new file mode 100644 index 00000000..9b6c3c2b --- /dev/null +++ b/docs/ci-test-stability-fix.md @@ -0,0 +1,59 @@ +# CI/CD Test Stability Fix for Select Component Stories + +## Problem +The Storybook test-runner was failing in CI environments but passing locally for the `CreatableOption` story in `select.stories.tsx`. The failure occurred when trying to find portaled dropdown elements with timeouts. + +## Root Cause +**Race conditions in CI environments** where: +1. Portaled dropdown content (rendered in `document.body`) takes longer to mount +2. Search input elements inside portals aren't immediately available +3. CI environments are inherently slower than local development + +## Solution +### 1. Added Explicit Timing Controls +```typescript +// Wait for portal content to stabilize before interaction +await new Promise(resolve => setTimeout(resolve, 200)); +``` + +### 2. Extended Test-Runner Configuration +Created `.storybook/test-runner.js`: +- Increased test timeout from 15s to 30s +- Added custom setup file for testing-library configuration + +Created `.storybook/test-runner-setup.js`: +- Extended `asyncUtilTimeout` from 1s to 10s for `findBy*` queries + +### 3. Enhanced Async Handling in Tests +**Before:** +```typescript +const listbox = await within(document.body).findByRole('listbox'); +const input = await within(document.body).findByPlaceholderText('Search...'); +``` + +**After:** +```typescript +const listbox = await within(document.body).findByRole('listbox'); +await new Promise(resolve => setTimeout(resolve, 200)); // Stabilization delay +const input = await within(document.body).findByPlaceholderText('Search...'); +``` + +## Files Modified +1. `apps/docs/src/remix-hook-form/select.stories.tsx` - Added stabilization delays +2. `apps/docs/.storybook/test-runner.js` - Extended timeouts +3. `apps/docs/.storybook/test-runner-setup.js` - Testing-library config + +## Why This Works +- **Predictable timing**: Explicit delays ensure portal content is ready +- **CI-friendly timeouts**: Longer timeouts accommodate slower CI environments +- **Minimal changes**: Only adds small delays where portal interaction occurs +- **Maintains test integrity**: Tests still verify the same functionality + +## Best Practices for Future Portal Testing +1. Always add small delays after finding portal elements +2. Use `findBy*` queries for portal content (they have built-in waiting) +3. Consider CI environment speed differences when setting timeouts +4. Test locally with network throttling to simulate CI conditions + +## Verification +The fix addresses the specific timeout error that was occurring at line 516 and 519 in the failing CI run, where `findByRole('listbox')` and `findByPlaceholderText('Search...')` were timing out. \ No newline at end of file