mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
test(radio): enable keyboard tests (#27262)
Issue number: Internal --------- <!-- Please refer to our contributing documentation for any questions on submitting a pull request, or let us know here if you need any help: https://ionicframework.com/docs/building/contributing --> <!-- Some docs updates need to be made in the `ionic-docs` repo, in a separate PR. See https://github.com/ionic-team/ionic-framework/blob/main/.github/CONTRIBUTING.md#modifying-documentation for details. --> <!-- Please do not submit updates to dependencies unless it fixes an issue. --> <!-- Please try to limit your pull request to one type (bugfix, feature, etc). Submit multiple pull requests if needed. --> ## What is the current behavior? <!-- Please describe the current behavior that you are modifying. --> `Tab` keyboard navigation tests are disabled for `ion-radio` because they were extremely flaky in CI. ## What is the new behavior? <!-- Please describe the behavior or changes that are being added by this PR. --> - Re-enables `ion-radio` `Tab` keyboard navigation tests - Ports a slimmed down version of Wordpress' playwright utilities for using modifier keys with `Tab` keyboard navigation on environments that do not support "natural tab navigation" ## Does this introduce a breaking change? - [ ] Yes - [x] No <!-- If this introduces a breaking change, please describe the impact and migration path for existing applications below. --> ## Other information <!-- Any other information that is important to this PR such as screenshots of how the component looks before and after the change. --> This utility should likely be used in all places where we are using the `Alt+Tab` logic for webkit-based browsers today. If/when the changes are agreed upon, I can follow-up with PR that adds this behavior to those tests.
This commit is contained in:
@@ -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(`
|
||||
<ion-app>
|
||||
<ion-content>
|
||||
<ion-list>
|
||||
<ion-radio-group id="first-group" value="huey">
|
||||
<ion-item>
|
||||
<ion-radio value="huey">Huey</ion-radio>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-radio value="dewey">Dewey</ion-radio>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-radio value="fooey" disabled>Fooey</ion-radio>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-radio value="louie">Louie</ion-radio>
|
||||
</ion-item>
|
||||
</ion-radio-group>
|
||||
</ion-list>
|
||||
<ion-list>
|
||||
<ion-radio-group id="second-group" value="huey">
|
||||
<ion-item>
|
||||
<ion-radio value="huey">Huey</ion-radio>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-radio value="dewey">Dewey</ion-radio>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-radio value="fooey" disabled>Fooey</ion-radio>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-radio value="louie">Louie</ion-radio>
|
||||
</ion-item>
|
||||
</ion-radio-group>
|
||||
</ion-list>
|
||||
</ion-content>
|
||||
</ion-app>
|
||||
`);
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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<CustomFixtures>({
|
||||
base.skip(base.info().project.metadata.mode === mode, reason);
|
||||
},
|
||||
},
|
||||
pageUtils: async ({ page }, use) => {
|
||||
await use(new PageUtils({ page }));
|
||||
},
|
||||
});
|
||||
|
||||
154
core/src/utils/test/press-keys.ts
Normal file
154
core/src/utils/test/press-keys.ts
Normal file
@@ -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(`<button>1</button><button>2</button>`);
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user