diff --git a/core/src/components/radio/test/a11y/radio.e2e-legacy.ts b/core/src/components/radio/test/a11y/radio.e2e-legacy.ts
index a31e238dd5..e12e1c417a 100644
--- a/core/src/components/radio/test/a11y/radio.e2e-legacy.ts
+++ b/core/src/components/radio/test/a11y/radio.e2e-legacy.ts
@@ -14,15 +14,70 @@ test.describe('radio: a11y', () => {
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});
+});
- // TODO FW-3747
- test.skip('using arrow keys should move between enabled radios within group', async ({ page, browserName }) => {
- const tabKey = browserName === 'webkit' ? 'Alt+Tab' : 'Tab';
- await page.goto(`/src/components/radio/test/a11y`);
+// TODO: FW-4155 - Enable tests once tab behavior is fixed for modern syntax.
+test.describe.skip('radio: keyboard navigation', () => {
+ test.beforeEach(async ({ page, skip }) => {
+ skip.rtl();
+ await page.setContent(`
+
+
+
+
+
+ Huey
+
+
+ Dewey
+
+
+ Fooey
+
+
+ Louie
+
+
+
+
+
+
+ Huey
+
+
+ Dewey
+
+
+ Fooey
+
+
+ Louie
+
+
+
+
+
+ `);
+ });
+
+ test('tabbing should switch between radio groups', async ({ page, pageUtils }) => {
+ const firstGroupRadios = page.locator('#first-group ion-radio');
+ const secondGroupRadios = page.locator('#second-group ion-radio');
+
+ await pageUtils.pressKeys('Tab');
+ await expect(firstGroupRadios.nth(0)).toBeFocused();
+
+ await pageUtils.pressKeys('Tab');
+ await expect(secondGroupRadios.nth(0)).toBeFocused();
+
+ await pageUtils.pressKeys('shift+Tab');
+ await expect(firstGroupRadios.nth(0)).toBeFocused();
+ });
+ test('using arrow keys should move between enabled radios within group', async ({ page, pageUtils }) => {
const firstGroupRadios = page.locator('#first-group ion-radio');
- await page.keyboard.press(tabKey);
+ await pageUtils.pressKeys('Tab');
await expect(firstGroupRadios.nth(0)).toBeFocused();
await page.keyboard.press('ArrowDown');
diff --git a/core/src/components/radio/test/legacy/a11y/radio.e2e-legacy.ts b/core/src/components/radio/test/legacy/a11y/radio.e2e-legacy.ts
index 7e0a2739d1..db922441c2 100644
--- a/core/src/components/radio/test/legacy/a11y/radio.e2e-legacy.ts
+++ b/core/src/components/radio/test/legacy/a11y/radio.e2e-legacy.ts
@@ -1,34 +1,31 @@
import { expect } from '@playwright/test';
import { test } from '@utils/test/playwright';
-// TODO FW-3747
-test.describe.skip('radio: a11y', () => {
+test.describe('radio: a11y', () => {
test.beforeEach(({ skip }) => {
skip.rtl();
});
- test('tabbing should switch between radio groups', async ({ page, browserName }) => {
- const tabKey = browserName === 'webkit' ? 'Alt+Tab' : 'Tab';
+ test('tabbing should switch between radio groups', async ({ page, pageUtils }) => {
await page.goto(`/src/components/radio/test/legacy/a11y`);
const firstGroupRadios = page.locator('#first-group ion-radio');
const secondGroupRadios = page.locator('#second-group ion-radio');
- await page.keyboard.press(tabKey);
+ await pageUtils.pressKeys('Tab');
await expect(firstGroupRadios.nth(0)).toBeFocused();
- await page.keyboard.press(tabKey);
+ await pageUtils.pressKeys('Tab');
await expect(secondGroupRadios.nth(0)).toBeFocused();
- await page.keyboard.press(`Shift+${tabKey}`);
+ await pageUtils.pressKeys('shift+Tab');
await expect(firstGroupRadios.nth(0)).toBeFocused();
});
- test('using arrow keys should move between enabled radios within group', async ({ page, browserName }) => {
- const tabKey = browserName === 'webkit' ? 'Alt+Tab' : 'Tab';
+ test('using arrow keys should move between enabled radios within group', async ({ page, pageUtils }) => {
await page.goto(`/src/components/radio/test/legacy/a11y`);
const firstGroupRadios = page.locator('#first-group ion-radio');
- await page.keyboard.press(tabKey);
+ await pageUtils.pressKeys('Tab');
await expect(firstGroupRadios.nth(0)).toBeFocused();
await page.keyboard.press('ArrowDown');
diff --git a/core/src/utils/test/playwright/playwright-page.ts b/core/src/utils/test/playwright/playwright-page.ts
index 224beac78d..bbab9b9b0c 100644
--- a/core/src/utils/test/playwright/playwright-page.ts
+++ b/core/src/utils/test/playwright/playwright-page.ts
@@ -7,6 +7,8 @@ import type {
} from '@playwright/test';
import { test as base } from '@playwright/test';
+import { PageUtils } from '../press-keys';
+
import { initPageEvents } from './page/event-spy';
import {
getSnapshotSettings,
@@ -36,6 +38,7 @@ type CustomTestArgs = PlaywrightTestArgs &
type CustomFixtures = {
page: E2EPage;
skip: E2ESkip;
+ pageUtils: PageUtils;
};
/**
@@ -91,4 +94,7 @@ export const test = base.extend({
base.skip(base.info().project.metadata.mode === mode, reason);
},
},
+ pageUtils: async ({ page }, use) => {
+ await use(new PageUtils({ page }));
+ },
});
diff --git a/core/src/utils/test/press-keys.ts b/core/src/utils/test/press-keys.ts
new file mode 100644
index 0000000000..fbee9d410f
--- /dev/null
+++ b/core/src/utils/test/press-keys.ts
@@ -0,0 +1,154 @@
+import type { Browser, BrowserContext, Page } from '@playwright/test';
+
+/**
+ * The purpose of this utility is to provide a way to press keys in a way that
+ * is consistent across browsers and platforms. Playwright does not automatically
+ * normalize key presses, so we need to do it ourselves.
+ *
+ * In certain environments, such as Webkit on macOS, the browser will not focus
+ * the correct element in the DOM when the tab key is pressed.
+ * This utility will detect if the browser has natural tab navigation and
+ * will use the appropriate key combination to simulate a tab press.
+ * The utility will normalize key presses for other combinations as well.
+ */
+
+const SHIFT = 'shift';
+const CTRL = 'ctrl';
+const ALT = 'alt';
+const COMMAND = 'meta';
+
+/**
+ * Source: https://github.com/WordPress/gutenberg/blob/f0d0d569a06c42833670c9b5285d04a63968a220/packages/e2e-test-utils-playwright/src/page-utils/press-keys.ts
+ * Slimmed down version of WordPress' pressKeys utility.
+ */
+export class PageUtils {
+ browser: Browser;
+ page: Page;
+ context: BrowserContext;
+
+ constructor({ page }: { page: Page }) {
+ this.page = page;
+ this.context = page.context();
+ this.browser = this.context.browser()!;
+ }
+
+ pressKeys: typeof pressKeys = pressKeys.bind(this);
+}
+
+const baseModifiers = {
+ primary: (_isApple: any) => (_isApple() ? [COMMAND] : [CTRL]),
+ primaryShift: (_isApple: any) => (_isApple() ? [SHIFT, COMMAND] : [CTRL, SHIFT]),
+ primaryAlt: (_isApple: any) => (_isApple() ? [ALT, COMMAND] : [CTRL, ALT]),
+ secondary: (_isApple: any) => (_isApple() ? [SHIFT, ALT, COMMAND] : [CTRL, SHIFT, ALT]),
+ access: (_isApple: any) => (_isApple() ? [CTRL, ALT] : [SHIFT, ALT]),
+ ctrl: () => [CTRL],
+ alt: () => [ALT],
+ ctrlShift: () => [CTRL, SHIFT],
+ shift: () => [SHIFT],
+ shiftAlt: () => [SHIFT, ALT],
+ undefined: () => [],
+};
+
+const isAppleOS = () => process.platform === 'darwin';
+const isWebkit = (page: Page) => page.context().browser()!.browserType().name() === 'webkit';
+const browserCache = new WeakMap();
+
+/**
+ * Detects if the browser has natural tab navigation.
+ * Natural tab navigation means that the browser will focus the next element
+ * in the DOM when the tab key is pressed.
+ */
+const getHasNaturalTabNavigation = async (page: Page) => {
+ if (!isAppleOS() || !isWebkit(page)) {
+ return true;
+ }
+ if (browserCache.has(page.context().browser()!)) {
+ return browserCache.get(page.context().browser()!);
+ }
+ const testPage = await page.context().newPage();
+ await testPage.setContent(``);
+ await testPage.getByText('1').focus();
+ await testPage.keyboard.press('Tab');
+ const featureDetected = await testPage.getByText('2').evaluate((node) => node === document.activeElement);
+ browserCache.set(page.context().browser()!, featureDetected);
+ await testPage.close();
+ return featureDetected;
+};
+
+type Options = {
+ /**
+ * Number of times to press the key.
+ */
+ times?: number;
+ /**
+ * Delay between each key press in milliseconds.
+ */
+ delay?: number;
+};
+
+const modifiers = {
+ ...baseModifiers,
+ shiftAlt: (_isApple: () => boolean) => (_isApple() ? [SHIFT, ALT] : [SHIFT, CTRL]),
+};
+
+/**
+ * Presses a key combination.
+ * @param key - Key combination to press.
+ * @param options - Options for the key press.
+ * @example
+ * ```ts
+ * await pressKeys('a');
+ * await pressKeys('a', { times: 2 });
+ * await pressKeys('a', { delay: 100 });
+ * await pressKeys('Shift+Tab');
+ * ```
+ */
+export async function pressKeys(this: PageUtils, key: string, { times, ...pressOptions }: Options = {}) {
+ const hasNaturalTabNavigation = await getHasNaturalTabNavigation(this.page);
+ /**
+ * Split the key combination into individual keys and map each key to its
+ * corresponding modifier.
+ */
+ const keys = key.split('+').flatMap((keyCode) => {
+ /**
+ * If the key is a modifier, we need to map it to the correct modifier for
+ * the current platform.
+ */
+ if (keyCode in modifiers) {
+ return modifiers[keyCode as keyof typeof modifiers](isAppleOS).map((modifier) =>
+ modifier === CTRL ? 'Control' : capitalCase(modifier)
+ );
+ } else if (keyCode === 'Tab' && !hasNaturalTabNavigation) {
+ /**
+ * If the key is the tab key and the browser does not have natural tab
+ * navigation, we need to simulate the tab key press by pressing the Alt key
+ * and the Tab key.
+ */
+ return ['Alt', 'Tab'];
+ }
+ // If the key is not a modifier, we can just return the key.
+ return keyCode;
+ });
+ const normalizedKeys = keys.join('+');
+ const command = () => this.page.keyboard.press(normalizedKeys);
+
+ times = times ?? 1;
+ for (let i = 0; i < times; i += 1) {
+ await command();
+
+ if (times > 1 && pressOptions.delay !== undefined) {
+ /**
+ * If we are pressing the key multiple times, we need to wait for the
+ * delay between each key press.
+ */
+ await this.page.waitForTimeout(pressOptions.delay);
+ }
+ }
+}
+
+/**
+ * Capitalizes the first letter of a string.
+ */
+function capitalCase(string: string) {
+ return string.charAt(0).toUpperCase() + string.slice(1);
+}