test(menu): migrate to playwright (#26187)

This commit is contained in:
Brandy Carney
2022-11-14 10:46:52 -05:00
committed by GitHub
parent 9216744205
commit 2c36049a7f
48 changed files with 246 additions and 341 deletions

View File

@@ -1,15 +0,0 @@
import { AxePuppeteer } from '@axe-core/puppeteer';
import { newE2EPage } from '@stencil/core/testing';
test('menu: axe', async () => {
const page = await newE2EPage({
url: '/src/components/menu/test/a11y?ionic:_testing=true',
});
const menu = await page.find('ion-menu');
await menu.callMethod('open');
await menu.waitForVisible();
const results = await new AxePuppeteer(page).analyze();
expect(results.violations.length).toEqual(0);
});

View File

@@ -2,18 +2,28 @@
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Segment - a11y</title>
<title>Menu - a11y</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0" />
<link href="../../../../../css/core.css" rel="stylesheet" />
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
<script src="../../../../../scripts/testing/scripts.js"></script>
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<script type="module">
import { menuController } from '../../../../dist/ionic/index.esm.js';
window.menuController = menuController;
</script>
</head>
<body>
<main>
<h1>Menu</h1>
<div class="ion-page" id="main-content">
<ion-content class="ion-padding">
<h1>Menu</h1>
<ion-button id="open-menu" expand="block" onclick="openMenu()">Open Menu</ion-button>
</ion-content>
</div>
<ion-menu menu-id="menu" content-id="main-content">
<ion-header>
<ion-toolbar>
@@ -21,6 +31,7 @@
</ion-toolbar>
</ion-header>
<ion-content>
<h1 class="ion-margin">Open Menu</h1>
<ion-list>
<ion-item>
<ion-button>Button</ion-button>
@@ -35,5 +46,12 @@
</ion-content>
</ion-menu>
</main>
<script>
async function openMenu() {
await menuController.enable(true, 'menu');
await menuController.open('menu');
}
</script>
</body>
</html>

View File

@@ -0,0 +1,28 @@
import AxeBuilder from '@axe-core/playwright';
import { expect } from '@playwright/test';
import { test } from '@utils/test/playwright';
test.describe('menu: a11y', () => {
test.beforeEach(async ({ skip }) => {
skip.rtl();
skip.mode('md');
});
test('menu should not have accessibility violations', async ({ page }) => {
await page.goto(`/src/components/menu/test/a11y`);
const menu = page.locator('ion-menu');
const button = page.locator('#open-menu');
await button.click();
await page.waitForSelector('ion-menu', { state: 'visible' });
await expect(menu).toHaveAttribute('role', 'navigation');
const heading = page.locator('ion-menu h1');
await expect(heading).toHaveText('Open Menu');
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});
});

View File

@@ -1,81 +0,0 @@
import { newE2EPage } from '@stencil/core/testing';
import { testMenu } from '../test.utils';
const DIRECTORY = 'basic';
const getActiveElementID = async (page) => {
const activeElement = await page.evaluateHandle(() => document.activeElement);
return page.evaluate((el) => el?.id, activeElement);
};
test('menu: start menu', async () => {
await testMenu(DIRECTORY, '#start-menu', 'first');
});
test('menu: start custom menu', async () => {
await testMenu(DIRECTORY, '#custom-menu', 'custom');
});
test('menu: end menu', async () => {
await testMenu(DIRECTORY, '#end-menu');
});
test('menu: focus trap', async () => {
const page = await newE2EPage({ url: '/src/components/menu/test/basic?ionic:_testing=true' });
await page.click('#open-first');
const menu = await page.find('#start-menu');
await menu.waitForVisible();
let activeElID = await getActiveElementID(page);
expect(activeElID).toEqual('start-menu');
await page.keyboard.press('Tab');
activeElID = await getActiveElementID(page);
expect(activeElID).toEqual('start-menu-button');
// do it again to make sure focus stays inside menu
await page.keyboard.press('Tab');
activeElID = await getActiveElementID(page);
expect(activeElID).toEqual('start-menu-button');
});
test('menu: preserve scroll position', async () => {
const page = await newE2EPage({ url: '/src/components/menu/test/basic?ionic:_testing=true' });
await page.click('#open-first');
const menu = await page.find('#start-menu');
await menu.waitForVisible();
await page.$eval('#start-menu ion-content', (menuContentEl: any) => {
return menuContentEl.scrollToPoint(0, 200);
});
await menu.callMethod('close');
await page.click('#open-first');
await menu.waitForVisible();
const scrollTop = await page.$eval('#start-menu ion-content', async (menuContentEl: any) => {
const contentScrollEl = await menuContentEl.getScrollElement();
return contentScrollEl.scrollTop;
});
expect(scrollTop).toEqual(200);
});
/**
* RTL Tests
*/
test('menu:rtl: start menu', async () => {
await testMenu(DIRECTORY, '#start-menu', 'first', true);
});
test('menu:rtl: start custom menu', async () => {
await testMenu(DIRECTORY, '#custom-menu', 'custom', true);
});
test('menu:rtl: end menu', async () => {
await testMenu(DIRECTORY, '#end-menu', '', true);
});

View File

@@ -12,12 +12,6 @@
<script src="../../../../../scripts/testing/scripts.js"></script>
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<script>
const forceCSSAnimations = new URLSearchParams(window.location.search).get('ionic:_forceCSSAnimations');
if (forceCSSAnimations) {
window.Animation = null;
}
</script>
<script type="module">
import { menuController } from '../../../../dist/ionic/index.esm.js';
window.menuController = menuController;
@@ -28,8 +22,8 @@
<ion-app>
<ion-menu
side="start"
menu-id="first"
id="start-menu"
menu-id="start-menu"
content-id="main"
class="menu-part"
aria-label="start menu"
@@ -68,7 +62,7 @@
</ion-content>
</ion-menu>
<ion-menu side="start" menu-id="custom" class="my-custom-menu" id="custom-menu" content-id="main">
<ion-menu side="start" menu-id="custom-menu" content-id="main" class="my-custom-menu">
<ion-header>
<ion-toolbar color="tertiary">
<ion-title>Custom Menu</ion-title>
@@ -85,7 +79,7 @@
</ion-content>
</ion-menu>
<ion-menu side="end" type="push" id="end-menu" content-id="main">
<ion-menu side="end" menu-id="end-menu" content-id="main" type="push">
<ion-header>
<ion-toolbar color="danger">
<ion-title>End Menu</ion-title>
@@ -109,9 +103,9 @@
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-button expand="block" id="open-first" onclick="openFirst()">Open Start Menu</ion-button>
<ion-button expand="block" onclick="openEnd()">Open End Menu</ion-button>
<ion-button expand="block" onclick="openCustom()">Open Custom Menu</ion-button>
<ion-button expand="block" id="open-start" onclick="openStart()">Open Start Menu</ion-button>
<ion-button expand="block" id="open-end" onclick="openEnd()">Open End Menu</ion-button>
<ion-button expand="block" id="open-custom" onclick="openCustom()">Open Custom Menu</ion-button>
</ion-content>
</div>
</ion-app>
@@ -133,18 +127,21 @@
</style>
<script>
async function openFirst() {
await menuController.enable(true, 'first');
await menuController.open('first');
async function openStart() {
// Open the menu by menu-id
await menuController.enable(true, 'start-menu');
await menuController.open('start-menu');
}
async function openEnd() {
// Open the menu by side
await menuController.open('end');
}
async function openCustom() {
await menuController.enable(true, 'custom');
await menuController.open('custom');
// Open the menu by menu-id
await menuController.enable(true, 'custom-menu');
await menuController.open('custom-menu');
}
</script>
</body>

View File

@@ -0,0 +1,93 @@
import type { Locator } from '@playwright/test';
import { expect } from '@playwright/test';
import type { E2EPage } from '@utils/test/playwright';
import { test } from '@utils/test/playwright';
test.describe('menu: basic', () => {
test.beforeEach(async ({ page }) => {
await page.goto(`/src/components/menu/test/basic`);
});
test('should open selected menu by side', async ({ page }) => {
const startMenu = page.locator('[menu-id="start-menu"]');
const customMenu = page.locator('[menu-id="custom-menu"]');
const endMenu = page.locator('[menu-id="end-menu"]');
await testMenu(page, startMenu, 'start');
await testMenu(page, customMenu, 'custom');
await testMenu(page, endMenu, 'end');
});
test('should trap focus', async ({ page, skip, browserName }) => {
skip.rtl('Trapping focus is not dependent on document direction');
skip.browser('firefox', 'Firefox incorrectly allows keyboard focus to move to ion-content');
const ionDidOpen = await page.spyOnEvent('ionDidOpen');
await page.click('#open-start');
await ionDidOpen.next();
const button = await page.locator('#start-menu-button');
if (browserName === 'webkit') {
await page.keyboard.down('Alt');
}
await page.keyboard.press('Tab');
await expect(button).toBeFocused();
await page.keyboard.press('Tab');
if (browserName === 'webkit') {
await page.keyboard.up('Alt');
}
await expect(button).toBeFocused();
});
test('should preserve scroll position', async ({ page, skip }) => {
skip.rtl('Scroll position is not dependent on document direction');
skip.browser('firefox', 'Firefox does not preserve scroll position');
const ionDidOpen = await page.spyOnEvent('ionDidOpen');
await page.click('#open-start');
await ionDidOpen.next();
await page.locator('#start-menu ion-content').evaluate(async (el: HTMLIonContentElement) => {
await el.scrollToPoint(0, 200);
});
await page.locator('#start-menu').evaluate(async (el: HTMLIonMenuElement) => {
await el.close();
});
await page.click('#open-start');
await ionDidOpen.next();
const scrollTop = await page.locator('#start-menu ion-content').evaluate(async (el: HTMLIonContentElement) => {
const contentScrollEl = await el.getScrollElement();
return contentScrollEl.scrollTop;
});
await expect(scrollTop).toBe(200);
});
});
async function testMenu(page: E2EPage, menu: Locator, menuId: string) {
const ionDidOpen = await page.spyOnEvent('ionDidOpen');
const ionDidClose = await page.spyOnEvent('ionDidClose');
await page.click(`#open-${menuId}`);
await ionDidOpen.next();
await expect(menu).toHaveClass(/show-menu/);
expect(await page.screenshot()).toMatchSnapshot(`menu-basic-${menuId}-${page.getSnapshotSettings()}.png`);
await menu.evaluate(async (el: HTMLIonMenuElement) => {
await el.close();
});
await ionDidClose.next();
}

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

View File

@@ -1,90 +0,0 @@
import { newE2EPage } from '@stencil/core/testing';
const getActiveElementID = async (page) => {
const activeElement = await page.evaluateHandle(() => document.activeElement);
return page.evaluate((el) => el?.id, activeElement);
};
test('menu: focus trap with overlays', async () => {
const page = await newE2EPage({
url: '/src/components/menu/test/focus-trap?ionic:_testing=true',
});
const ionDidOpen = await page.spyOnEvent('ionDidOpen');
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss');
const menu = await page.find('ion-menu');
await menu.callMethod('open');
await ionDidOpen.next();
expect(await getActiveElementID(page)).toEqual('menu');
const openModal = await page.find('#open-modal-button');
await openModal.click();
await ionModalDidPresent.next();
expect(await getActiveElementID(page)).toEqual('modal-element');
const modal = await page.find('ion-modal');
await modal.callMethod('dismiss');
await ionModalDidDismiss.next();
expect(await getActiveElementID(page)).toEqual('open-modal-button');
});
test('menu: focus trap with content inside overlays', async () => {
const page = await newE2EPage({
url: '/src/components/menu/test/focus-trap?ionic:_testing=true',
});
const ionDidOpen = await page.spyOnEvent('ionDidOpen');
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
const menu = await page.find('ion-menu');
await menu.callMethod('open');
await ionDidOpen.next();
expect(await getActiveElementID(page)).toEqual('menu');
const openModal = await page.find('#open-modal-button');
await openModal.click();
await ionModalDidPresent.next();
const button = await page.find('#other-button');
await button.click();
expect(await getActiveElementID(page)).toEqual('other-button');
});
test('menu: should work with swipe gestures after modal is dismissed', async () => {
const page = await newE2EPage({
url: '/src/components/menu/test/focus-trap?ionic:_testing=true',
});
const ionDidOpen = await page.spyOnEvent('ionDidOpen');
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss');
const menu = await page.find('ion-menu');
await menu.callMethod('open');
await ionDidOpen.next();
const openModal = await page.find('#open-modal-button');
await openModal.click();
await ionModalDidPresent.next();
const modal = await page.find('ion-modal');
await modal.callMethod('dismiss');
await ionModalDidDismiss.next();
await page.mouse.move(30, 168);
await page.mouse.down();
await page.mouse.move(384, 168);
await page.mouse.up();
await page.waitForChanges();
expect(menu.classList.contains('show-menu')).toBeTruthy();
});

View File

@@ -14,7 +14,7 @@
</head>
<body>
<ion-app>
<ion-menu content-id="main" id="menu">
<ion-menu id="menu" content-id="main">
<ion-header>
<ion-toolbar>
<ion-title>Menu</ion-title>
@@ -24,7 +24,7 @@
<ion-item>
<ion-button id="open-modal-button">Open Modal</ion-button>
</ion-item>
<ion-modal id="modal-element" trigger="open-modal-button">
<ion-modal id="modal" trigger="open-modal-button">
<ion-content class="ion-padding">
Modal content
<ion-button onclick="dismissModal()">Dismiss Modal</ion-button>

View File

@@ -0,0 +1,89 @@
import { expect } from '@playwright/test';
import { test } from '@utils/test/playwright';
test.describe('menu: focus trap', () => {
test.beforeEach(async ({ page, skip }) => {
await page.goto(`/src/components/menu/test/focus-trap`);
skip.rtl('Trapping focus is not dependent on document direction');
skip.browser('firefox', 'Firefox incorrectly allows keyboard focus to move to ion-content');
});
test('should trap focus with overlays', async ({ page, browserName }) => {
const ionDidOpen = await page.spyOnEvent('ionDidOpen');
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss');
await page.click('#open-menu-button');
await ionDidOpen.next();
const menu = await page.locator('#menu');
await expect(menu).toBeFocused();
const openModalButton = await page.locator('#open-modal-button');
await openModalButton.click();
await ionModalDidPresent.next();
const modal = await page.locator('#modal');
await expect(modal).toBeFocused();
await modal.evaluate(async (el: HTMLIonModalElement) => {
await el.dismiss();
});
await ionModalDidDismiss.next();
// Safari focuses the body after the modal is dismissed
if (browserName !== 'webkit') {
await expect(openModalButton).toBeFocused();
}
});
test('should trap focus with content inside overlays', async ({ page }) => {
const ionDidOpen = await page.spyOnEvent('ionDidOpen');
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
await page.click('#open-menu-button');
await ionDidOpen.next();
const menu = await page.locator('#menu');
await expect(menu).toBeFocused();
await page.click('#open-modal-button');
await ionModalDidPresent.next();
const modal = await page.locator('#modal');
await expect(modal).toBeFocused();
});
test('should work with swipe gestures after modal is dismissed', async ({ page }) => {
const ionDidOpen = await page.spyOnEvent('ionDidOpen');
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss');
await page.click('#open-menu-button');
await ionDidOpen.next();
const menu = await page.locator('#menu');
await expect(menu).toBeFocused();
await page.click('#open-modal-button');
await ionModalDidPresent.next();
const modal = await page.locator('#modal');
await modal.evaluate(async (el: HTMLIonModalElement) => {
await el.dismiss();
});
await ionModalDidDismiss.next();
await page.mouse.move(30, 168);
await page.mouse.down();
await page.mouse.move(384, 168);
await page.mouse.up();
await page.waitForChanges();
await expect(menu).toHaveClass(/show-menu/);
});
});

View File

@@ -1,11 +0,0 @@
import { testMenu } from '../test.utils';
const DIRECTORY = 'standalone';
test('menu: start standalone', async () => {
await testMenu(DIRECTORY, '#start-menu');
});
test('menu: end standalone', async () => {
await testMenu(DIRECTORY, '#end-menu');
});

View File

@@ -1,85 +0,0 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Menu - Standalone</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
/>
<link href="../../../../../css/core.css" rel="stylesheet" />
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
<script src="../../../../../scripts/testing/scripts.js"></script>
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<script type="module">
import { menuController } from '../../../../dist/ionic/index.esm.js';
window.menuController = menuController;
</script>
<style>
.ion-page ion-content {
--padding-top: 16px;
--padding-end: 16px;
--padding-bottom: 16px;
--padding-start: 16px;
}
</style>
</head>
<body>
<ion-menu side="start" id="start-menu" content-id="main">
<ion-header>
<ion-toolbar>
<ion-title>Start Menu</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list>
<ion-item>Menu Item</ion-item>
<ion-item>Menu Item</ion-item>
<ion-item>Menu Item</ion-item>
<ion-item>Menu Item</ion-item>
<ion-item>Menu Item</ion-item>
</ion-list>
</ion-content>
</ion-menu>
<ion-menu side="end" type="push" id="end-menu" content-id="main">
<ion-header>
<ion-toolbar>
<ion-title>End Menu</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list>
<ion-item>Menu Item</ion-item>
<ion-item>Menu Item</ion-item>
<ion-item>Menu Item</ion-item>
<ion-item>Menu Item</ion-item>
<ion-item>Menu Item</ion-item>
</ion-list>
</ion-content>
</ion-menu>
<div class="ion-page" id="main">
<ion-header>
<ion-toolbar>
<ion-title>Menu - Standalone</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-button expand="block" onclick="openStart()">Open Start Menu</ion-button>
<ion-button expand="block" onclick="openEnd()">Open End Menu</ion-button>
</ion-content>
</div>
<script>
function openStart() {
menuController.open('start');
}
function openEnd() {
menuController.open('end');
}
</script>
</body>
</html>

View File

@@ -1,38 +0,0 @@
import { newE2EPage } from '@stencil/core/testing';
import { generateE2EUrl } from '@utils/test';
import { menuController } from '../../../utils/menu-controller';
export const testMenu = async (type: string, selector: string, menuId = '', rtl = false) => {
try {
const pageUrl = generateE2EUrl('menu', type, rtl);
const page = await newE2EPage({
url: pageUrl,
});
const screenshotCompares = [];
if (menuId.length > 0) {
await menuController.enable(true, menuId);
}
const menu = await page.find(selector);
await menu.callMethod('open');
await page.waitForTimeout(1000);
screenshotCompares.push(await page.compareScreenshot());
await menu.callMethod('close');
await page.waitForTimeout(250);
screenshotCompares.push(await page.compareScreenshot('dismiss'));
for (const screenshotCompare of screenshotCompares) {
expect(screenshotCompare).toMatchScreenshot();
}
} catch (err) {
throw err;
}
};