/** * 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(` `); 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(` `); 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(` `); 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(`
`); 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(` `); 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(` `); 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(`
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(`
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(`
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(` `); 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(` `); // 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(`
`); 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(` `); // 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(); }); }); });