/**
* Unit tests for wait-helpers.ts - Semantic Wait Helpers
*
* These tests verify the behavior of deterministic wait utilities
* that replace arbitrary `page.waitForTimeout()` calls.
*/
import { test, expect, Page } from '@playwright/test';
import {
waitForDialog,
waitForFormFields,
waitForDebounce,
waitForConfigReload,
waitForNavigation,
} from './wait-helpers';
test.describe('wait-helpers - Semantic Wait Functions', () => {
test.describe('waitForDialog', () => {
test('should wait for dialog to be visible and interactive', async ({ page }) => {
// Create a test page with dialog
await page.setContent(`
Open Dialog
Test Dialog
Close
`);
await test.step('Open dialog and wait for it to be interactive', async () => {
await page.click('#open-dialog');
const dialog = await waitForDialog(page);
// Verify dialog is visible and interactive
await expect(dialog).toBeVisible();
await expect(dialog.getByRole('heading')).toHaveText('Test Dialog');
});
});
test('should handle dialog with aria-busy attribute', async ({ page }) => {
// Create a dialog that starts busy then becomes interactive
await page.setContent(`
Open Dialog
Loading Dialog
`);
await page.click('#open-dialog');
const dialog = await waitForDialog(page);
// Verify dialog is no longer busy
await expect(dialog).not.toHaveAttribute('aria-busy', 'true');
});
test('should handle alertdialog role', async ({ page }) => {
await page.setContent(`
Open Alert
Alert Dialog
`);
await page.click('#open-alert');
const dialog = await waitForDialog(page, { role: 'alertdialog' });
await expect(dialog).toBeVisible();
await expect(dialog).toHaveAttribute('role', 'alertdialog');
});
test('should timeout if dialog never appears', async ({ page }) => {
await page.setContent(`No dialog here
`);
await expect(
waitForDialog(page, { timeout: 1000 })
).rejects.toThrow();
});
});
test.describe('waitForFormFields', () => {
test('should wait for dynamically loaded form fields', async ({ page }) => {
await page.setContent(`
Basic
Advanced
`);
await test.step('Select form type and wait for fields', async () => {
await page.selectOption('#form-type', 'advanced');
await waitForFormFields(page, '#advanced-field');
const field = page.locator('#advanced-field');
await expect(field).toBeVisible();
await expect(field).toBeEnabled();
});
});
test('should wait for field to be enabled', async ({ page }) => {
await page.setContent(`
Enable Field
`);
await page.click('#enable-field');
await waitForFormFields(page, '#test-field', { shouldBeEnabled: true, timeout: 2000 });
const field = page.locator('#test-field');
await expect(field).toBeEnabled({ timeout: 2000 });
});
test('should handle disabled fields when shouldBeEnabled is false', async ({ page }) => {
await page.setContent(`
`);
// Should not throw even though field is disabled
await waitForFormFields(page, '#disabled-field', { shouldBeEnabled: false });
const field = page.locator('#disabled-field');
await expect(field).toBeVisible();
});
});
test.describe('waitForDebounce', () => {
test('should wait for network idle after input', async ({ page }) => {
// Create a page with a search that triggers API call
await page.route('**/api/search*', async (route) => {
await new Promise(resolve => setTimeout(resolve, 200));
await route.fulfill({ json: { results: [] } });
});
await page.setContent(`
`);
await test.step('Type and wait for debounce to settle', async () => {
await page.fill('#search-input', 'test query');
await waitForDebounce(page);
// Network should be idle and API called
// Verify by checking if input is still interactive
const input = page.locator('#search-input');
await expect(input).toHaveValue('test query');
});
});
test('should wait for loading indicator', async ({ page }) => {
await page.setContent(`
Searching...
`);
await page.fill('#search-input', 'test');
await waitForDebounce(page, { indicatorSelector: '.search-loading' });
const loader = page.locator('.search-loading');
await expect(loader).not.toBeVisible();
});
});
test.describe('waitForConfigReload', () => {
test('should wait for config reload overlay to disappear', async ({ page }) => {
await page.setContent(`
Save
Reloading configuration...
`);
await test.step('Save settings and wait for reload', async () => {
await page.click('#save-settings');
await waitForConfigReload(page);
const overlay = page.locator('[data-testid="config-reload-overlay"]');
await expect(overlay).not.toBeVisible();
});
});
test('should handle instant reload (no overlay)', async ({ page }) => {
await page.setContent(`
Save
Settings saved
`);
// Should not throw even if overlay never appears
await page.click('#save-settings');
await waitForConfigReload(page);
});
test('should wait for DOM to be interactive after reload', async ({ page }) => {
await page.setContent(`
Save
Reloading...
`);
await page.click('#save-settings');
await waitForConfigReload(page);
// Page should be interactive
const button = page.locator('#save-settings');
await expect(button).toBeEnabled();
});
});
test.describe('waitForNavigation', () => {
test('should wait for URL change with string match', async ({ page }) => {
await page.route('**/test-page', async (route) => {
await route.fulfill({
status: 200,
contentType: 'text/html',
body: 'New Page ',
});
});
await page.goto('about:blank');
await page.setContent(`
Navigate
`);
const link = page.locator('#nav-link');
await link.click();
// Wait for navigation to complete
await waitForNavigation(page, /\/test-page$/);
// Verify new page loaded
await expect(page.locator('h1')).toHaveText('New Page');
});
test('should wait for URL change with RegExp match', async ({ page }) => {
await page.goto('about:blank');
// Navigate to a data URL
await page.goto('data:text/html,Test Page
');
await waitForNavigation(page, /data:text\/html/);
const content = page.locator('#content');
await expect(content).toHaveText('Test Page');
});
test('should wait for specified load state', async ({ page }) => {
await page.goto('about:blank');
// Navigate with domcontentloaded state
const navigationPromise = page.goto('data:text/html,Page ');
await waitForNavigation(page, /data:text\/html/, { waitUntil: 'domcontentloaded' });
await navigationPromise;
await expect(page.locator('h1')).toHaveText('Page');
});
test('should timeout if navigation never completes', async ({ page }) => {
await page.goto('about:blank');
await expect(
waitForNavigation(page, /never-matching-url/, { timeout: 1000 })
).rejects.toThrow();
});
});
test.describe('Integration tests - Multiple wait helpers', () => {
test('should handle dialog with form fields and debounced search', async ({ page }) => {
await page.setContent(`
Open Dialog
`);
await test.step('Open dialog', async () => {
await page.click('#open-dialog');
const dialog = await waitForDialog(page);
await expect(dialog).toBeVisible();
});
await test.step('Wait for search field', async () => {
await waitForFormFields(page, '#search');
const searchField = page.locator('#search');
await expect(searchField).toBeEnabled();
});
await test.step('Search with debounce', async () => {
await page.fill('#search', 'test query');
await waitForDebounce(page, { indicatorSelector: '.search-loading' });
const results = page.locator('#results');
await expect(results).toHaveText('Results for: test query');
});
});
test('should handle form submission with config reload', async ({ page }) => {
await page.setContent(`
Reloading configuration...
`);
await test.step('Wait for form field and fill', async () => {
await waitForFormFields(page, '#setting-name');
await page.fill('#setting-name', 'test value');
});
await test.step('Submit and wait for config reload', async () => {
await page.click('button[type="submit"]');
await waitForConfigReload(page);
const overlay = page.locator('[data-testid="config-reload-overlay"]');
await expect(overlay).not.toBeVisible();
});
});
});
test.describe('Error handling and edge cases', () => {
test('waitForDialog should handle multiple dialogs', async ({ page }) => {
await page.setContent(`
Dialog 1
Dialog 2
`);
// Should find the first visible dialog
const dialog = await waitForDialog(page);
await expect(dialog).toHaveClass(/dialog-1/);
});
test('waitForFormFields should handle detached elements', async ({ page }) => {
await page.setContent(`
Add Field
`);
await page.click('#add-field');
await waitForFormFields(page, '#new-field');
const field = page.locator('#new-field');
await expect(field).toBeAttached();
});
test('waitForDebounce should handle rapid consecutive inputs', async ({ page }) => {
await page.setContent(`
Loading...
`);
// Rapid typing simulation
await page.fill('#rapid-input', 'a');
await page.fill('#rapid-input', 'ab');
await page.fill('#rapid-input', 'abc');
await waitForDebounce(page, { indicatorSelector: '.loading' });
const loader = page.locator('.loading');
await expect(loader).not.toBeVisible();
});
});
});