Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions apps/docs/.storybook/test-runner-setup.js
Original file line number Diff line number Diff line change
@@ -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
});
15 changes: 15 additions & 0 deletions apps/docs/.storybook/test-runner.js
Original file line number Diff line number Diff line change
@@ -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')],
};
49 changes: 41 additions & 8 deletions apps/docs/src/remix-hook-form/select.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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);
}
Expand All @@ -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);
}
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);
}
Expand All @@ -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);
}
Expand All @@ -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);
}
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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
Expand All @@ -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();
});
},
Expand Down
59 changes: 59 additions & 0 deletions docs/ci-test-stability-fix.md
Original file line number Diff line number Diff line change
@@ -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.
Loading