From c4d340abdf624752e14311fef2ed76085c5a64f5 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Tue, 20 Oct 2026 12:22:27 +0200 Subject: [PATCH 1/2] Use the WebKit Tab key in tabset and revealjs focus tests WebKit on macOS follows Safari's default "Press Tab to highlight each item" setting, so a plain Tab skips buttons and links. The tabset (#730) and off-screen slide (#14795) tests tab to plain buttons, so they failed in the macOS Playwright job of the built-version nightly from the first run after each landed. Linux WebKit, Chromium and Firefox Tab to buttons, which is why pull request CI stayed green. Press tabKey(browserName), which sends Alt+Tab on WebKit. The second Tab in the off-screen slide tests now also means something on macOS: with plain Tab, buttons on other slides were skipped regardless of inert. The #15078 test gets the same change; it has not run on macOS yet. --- .../playwright/tests/html-tabsets.spec.ts | 11 ++++--- .../playwright/tests/revealjs.spec.ts | 32 +++++++++---------- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/tests/integration/playwright/tests/html-tabsets.spec.ts b/tests/integration/playwright/tests/html-tabsets.spec.ts index 35dc0479c71..3017ca7d918 100644 --- a/tests/integration/playwright/tests/html-tabsets.spec.ts +++ b/tests/integration/playwright/tests/html-tabsets.spec.ts @@ -1,4 +1,5 @@ import { test, expect } from '@playwright/test'; +import { tabKey } from '../src/utils'; test('Jupyter - Creates working tabsets from for loops', async ({ page }) => { await page.goto('/html/tabsets/jupyter-tabsets.html'); @@ -25,19 +26,19 @@ test.describe('Tabby tabsets (theme: none) keyboard navigation', () => { await expect(page.getByRole('tablist')).toBeAttached(); }); - test('Tab moves from the selected tab into its panel (#730)', async ({ page }) => { + test('Tab moves from the selected tab into its panel (#730)', async ({ page, browserName }) => { await page.getByRole('button', { name: 'Button before the tabset' }).focus(); - await page.keyboard.press('Tab'); + await page.keyboard.press(tabKey(browserName)); await expect(page.getByRole('tab', { name: 'Tab A' })).toBeFocused(); - await page.keyboard.press('Tab'); + await page.keyboard.press(tabKey(browserName)); await expect(page.getByRole('button', { name: 'Button in tab A' })).toBeFocused(); await expect(page.getByRole('tab', { name: 'Tab A' })).toHaveAttribute('aria-selected', 'true'); }); - test('Shift+Tab from the panel returns to the selected tab', async ({ page }) => { + test('Shift+Tab from the panel returns to the selected tab', async ({ page, browserName }) => { await page.getByRole('tab', { name: 'Tab B' }).click(); await page.getByRole('button', { name: 'Button in tab B' }).focus(); - await page.keyboard.press('Shift+Tab'); + await page.keyboard.press(tabKey(browserName, true)); await expect(page.getByRole('tab', { name: 'Tab B' })).toBeFocused(); }); diff --git a/tests/integration/playwright/tests/revealjs.spec.ts b/tests/integration/playwright/tests/revealjs.spec.ts index 6b6dcae3863..8381535e91f 100644 --- a/tests/integration/playwright/tests/revealjs.spec.ts +++ b/tests/integration/playwright/tests/revealjs.spec.ts @@ -113,27 +113,27 @@ test.describe('off-screen slides are not focusable (#14795)', () => { const currentSlideId = (page: Page) => page.evaluate(() => (window as any).Reveal.getCurrentSlide().id); - test('Tab on a slide reaches only that slide (#14795)', async ({ page }) => { + test('Tab on a slide reaches only that slide (#14795)', async ({ page, browserName }) => { await gotoSlide(page, '#/slide-3'); - await page.keyboard.press('Tab'); + await page.keyboard.press(tabKey(browserName)); await expect(slideButton(page, 'slide-3')).toBeFocused(); - await page.keyboard.press('Tab'); + await page.keyboard.press(tabKey(browserName)); await expect(page.locator('.reveal .slides section :focus')).toHaveCount(0); }); - test('Tab after keyboard navigation stays on the new slide', async ({ page }) => { + test('Tab after keyboard navigation stays on the new slide', async ({ page, browserName }) => { await gotoSlide(page, '#/slide-2'); await page.keyboard.press('ArrowRight'); await expect(page).toHaveURL(/#\/slide-3$/); - await page.keyboard.press('Tab'); + await page.keyboard.press(tabKey(browserName)); await expect(slideButton(page, 'slide-3')).toBeFocused(); }); - test('Tab in a vertical stack reaches only the current child', async ({ page }) => { + test('Tab in a vertical stack reaches only the current child', async ({ page, browserName }) => { await gotoSlide(page, '#/stack-b'); - await page.keyboard.press('Tab'); + await page.keyboard.press(tabKey(browserName)); await expect(slideButton(page, 'stack-b')).toBeFocused(); - await page.keyboard.press('Tab'); + await page.keyboard.press(tabKey(browserName)); await expect(focusOutsideCurrentSlide(page)).toHaveCount(0); }); @@ -143,7 +143,7 @@ test.describe('off-screen slides are not focusable (#14795)', () => { await expect(slideButton(page, 'slide-6')).toBeFocused(); }); - test('overview: slides are clickable, inert returns on exit', async ({ page }) => { + test('overview: slides are clickable, inert returns on exit', async ({ page, browserName }) => { await gotoSlide(page, '#/slide-3'); await page.keyboard.press('o'); await expect(page.locator('.reveal.overview')).toBeAttached(); @@ -151,7 +151,7 @@ test.describe('off-screen slides are not focusable (#14795)', () => { await page.locator('section#slide-5').click({ position: { x: 5, y: 5 } }); await expect(page).toHaveURL(/#\/slide-5$/); await expect(page.locator('.reveal.overview')).toHaveCount(0); - await page.keyboard.press('Tab'); + await page.keyboard.press(tabKey(browserName)); await expect(slideButton(page, 'slide-5')).toBeFocused(); }); @@ -162,7 +162,7 @@ test.describe('off-screen slides are not focusable (#14795)', () => { await expect(slideButton(page, 'slide-4')).toBeFocused(); }); - test('scroll view toggle with R: focusable inside, current slide reachable after leaving', async ({ page }) => { + test('scroll view toggle with R: focusable inside, current slide reachable after leaving', async ({ page, browserName }) => { await gotoSlide(page, '#/slide-2'); await page.keyboard.press('R'); await expect.poll(() => isScrollView(page)).toBe(true); @@ -173,11 +173,11 @@ test.describe('off-screen slides are not focusable (#14795)', () => { await expect.poll(() => currentSlideId(page)).toBe('slide-4'); await page.keyboard.press('R'); await expect.poll(() => isScrollView(page)).toBe(false); - await page.keyboard.press('Tab'); + await page.keyboard.press(tabKey(browserName)); await expect(slideButton(page, 'slide-4')).toBeFocused(); }); - test('responsive scroll view below scrollActivationWidth', async ({ page }) => { + test('responsive scroll view below scrollActivationWidth', async ({ page, browserName }) => { await gotoSlide(page, '#/slide-2'); await page.setViewportSize({ width: 400, height: 700 }); await expect.poll(() => isScrollView(page)).toBe(true); @@ -187,7 +187,7 @@ test.describe('off-screen slides are not focusable (#14795)', () => { await expect.poll(() => currentSlideId(page)).toBe('slide-4'); await page.setViewportSize({ width: 1280, height: 720 }); await expect.poll(() => isScrollView(page)).toBe(false); - await page.keyboard.press('Tab'); + await page.keyboard.press(tabKey(browserName)); await expect(slideButton(page, 'slide-4')).toBeFocused(); }); @@ -209,14 +209,14 @@ test.describe('the focus stays in the deck when its slide is left (#15078)', () await expect(page.locator('.reveal.ready')).toBeAttached(); }); - test('moving on from the slide that has the focus hands it to the deck', async ({ page }) => { + test('moving on from the slide that has the focus hands it to the deck', async ({ page, browserName }) => { await slideButton(page, 'slide-2').focus(); await page.keyboard.press('ArrowRight'); await expect(page).toHaveURL(/#\/slide-3$/); await expect(deck(page)).toBeFocused(); await expect(deck(page)).toHaveAttribute('tabindex', '-1'); // From the deck, Tab goes on into the new slide. - await page.keyboard.press('Tab'); + await page.keyboard.press(tabKey(browserName)); await expect(slideButton(page, 'slide-3')).toBeFocused(); }); From adb89ee488ae8dc69a551d72ee9ab1e44495d386 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Tue, 20 Oct 2026 13:57:11 +0200 Subject: [PATCH 2/2] Skip the ArrowDown overlay scroll test on macOS WebKit The axe report overlay ArrowDown test has failed or needed a retry on every macOS Playwright run since that job started. On macOS, WebKit ignores about half of the ArrowDown presses on a focused scroll region: the keydown reaches the element and is not prevented, but nothing scrolls. A plain HTML page with a fixed, focusable overflow box and no Quarto code shows the same rate, so the overlay is not the cause. PageDown scrolled every time on both pages. Skip only the ArrowDown variant, only for WebKit on macOS. PageDown still guards the overlay's keyboard scrolling there, and Linux WebKit, Chromium and Firefox keep the ArrowDown check. The scrollable code block test already skips its arrow-key assertion on WebKit for a similar reason. --- .../playwright/tests/axe-accessibility.spec.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/integration/playwright/tests/axe-accessibility.spec.ts b/tests/integration/playwright/tests/axe-accessibility.spec.ts index f7340378def..18d0e26ee39 100644 --- a/tests/integration/playwright/tests/axe-accessibility.spec.ts +++ b/tests/integration/playwright/tests/axe-accessibility.spec.ts @@ -839,7 +839,14 @@ test.describe('HTML axe — the report overlay scrolls by keyboard (#14680)', () // overlay self-scan (scrollable-region-focusable, line ~388), so only an // actual key press that moves scrollTop catches them. for (const key of ['PageDown', 'ArrowDown']) { - test(`html — focused overlay scrolls on ${key}`, async ({ page }) => { + test(`html — focused overlay scrolls on ${key}`, async ({ page, browserName }) => { + // WebKit on macOS ignores about half of ArrowDown presses on a focused + // scroll region, even a plain HTML one with no Quarto code; PageDown + // scrolls reliably and still covers the overlay. + test.skip( + key === 'ArrowDown' && browserName === 'webkit' && process.platform === 'darwin', + 'WebKit on macOS drops ArrowDown on a focused scroll region', + ); await page.goto('/html/axe-overlay-scroll.html', { waitUntil: 'networkidle' }); const axeReport = page.locator('.quarto-axe-report');