diff --git a/playwright/alert-dialog.spec.ts b/playwright/alert-dialog.spec.ts index 7e464be74..92b713c3c 100644 --- a/playwright/alert-dialog.spec.ts +++ b/playwright/alert-dialog.spec.ts @@ -29,3 +29,31 @@ test('test', async ({ page }) => { // Assert the dialog is closed after confirming await expect(dialog).toHaveCount(0); }); + +test('alert dialog marks background content inert', async ({ page }) => { + await page.goto('http://127.0.0.1:8080/component/?name=alert_dialog&', { timeout: 20 * 60 * 1000 }); // Increase timeout to 20 minutes + + // Is the trigger (which sits behind the dialog) inside an inert subtree? + const triggerIsInert = () => page.evaluate(() => { + const trigger = Array.from(document.querySelectorAll('button')).find( + (button) => button.textContent?.trim() === 'Show Alert Dialog' + ); + return trigger ? trigger.closest('[inert]') !== null : null; + }); + + const trigger = page.getByRole('button', { name: 'Show Alert Dialog' }); + await expect(trigger).toBeVisible(); + await expect.poll(triggerIsInert).toBe(false); + await trigger.click(); + await expect(page.getByRole('alertdialog')).toBeVisible(); + + await expect.poll(triggerIsInert).toBe(true); + await expect(page.locator('[data-inert-by]').first()).toBeAttached(); + + await page.keyboard.press('Escape'); + await expect(page.getByRole('alertdialog')).toHaveCount(0); + + await expect.poll(triggerIsInert).toBe(false); + await expect(page.locator('[data-inert-by]')).toHaveCount(0); + await expect(trigger).toBeFocused(); +}); diff --git a/playwright/dialog.spec.ts b/playwright/dialog.spec.ts index 77d288f75..808f8b96a 100644 --- a/playwright/dialog.spec.ts +++ b/playwright/dialog.spec.ts @@ -33,3 +33,121 @@ test('test', async ({ page }) => { await page.mouse.click(2, 2); await expect(dialog).toHaveCount(0); }); + +test('modal dialog marks background content inert', async ({ page }) => { + await page.goto('http://127.0.0.1:8080/component/?name=dialog&', { timeout: 20 * 60 * 1000 }); // Increase timeout to 20 minutes + + // Is the trigger (which sits behind the dialog) inside an inert subtree? + const triggerIsInert = () => page.evaluate(() => { + const trigger = Array.from(document.querySelectorAll('button')).find( + (button) => button.textContent?.trim() === 'Show Dialog' + ); + return trigger ? trigger.closest('[inert]') !== null : null; + }); + + const trigger = page.getByRole('button', { name: 'Show Dialog' }); + await expect(trigger).toBeVisible(); + await expect.poll(triggerIsInert).toBe(false); + await trigger.click(); + await expect(page.getByRole('dialog')).toBeVisible(); + + // Background content is inert while the modal is open, and every element that was made + // inert records the dialog that did it. + await expect.poll(triggerIsInert).toBe(true); + await expect(page.locator('[data-inert-by]').first()).toBeAttached(); + // The dialog itself stays interactive. + await expect(page.getByRole('dialog')).not.toHaveAttribute('inert', ''); + + await page.keyboard.press('Escape'); + await expect(page.getByRole('dialog')).toHaveCount(0); + + // Closing unwinds everything it marked and returns focus to the opener. + await expect.poll(triggerIsInert).toBe(false); + await expect(page.locator('[data-inert-by]')).toHaveCount(0); + await expect(trigger).toBeFocused(); +}); + +// The stacking rules live in the focus trap module rather than in any one component, and two +// dialogs open at once has no demo to drive. Exercise the shipped bundle directly: the dialog +// page loads it, and `createFocusTrap` is the same entry point the primitive calls. +test('stacked focus traps compose and unwind independently', async ({ page }) => { + await page.goto('http://127.0.0.1:8080/component/?name=dialog&', { timeout: 20 * 60 * 1000 }); // Increase timeout to 20 minutes + await page.waitForFunction(() => typeof (window as any).createFocusTrap === 'function'); + + const result = await page.evaluate(() => { + // + //