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:
Sean Perkins
2023-04-28 13:28:47 -04:00
committed by GitHub
parent fe86d9a586
commit 0bb84bb1c0
4 changed files with 227 additions and 15 deletions

View File

@@ -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');

View File

@@ -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');

View File

@@ -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 }));
},
});

View 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);
}