test(popover): migrate tests to Playwright (#25176)
Co-authored-by: Liam DeBeasi <liamdebeasi@users.noreply.github.com>
@ -1,61 +0,0 @@
|
||||
import { newE2EPage } from '@stencil/core/testing';
|
||||
|
||||
test('popover - arrow side: top', async () => {
|
||||
await testPopover('top', false);
|
||||
});
|
||||
|
||||
test('popover - arrow side: right', async () => {
|
||||
await testPopover('right', false);
|
||||
});
|
||||
|
||||
test('popover - arrow side: bottom', async () => {
|
||||
await testPopover('bottom', false);
|
||||
});
|
||||
|
||||
test('popover - arrow side: left', async () => {
|
||||
await testPopover('left', false);
|
||||
});
|
||||
|
||||
test('popover - arrow side: start', async () => {
|
||||
await testPopover('start', false);
|
||||
});
|
||||
|
||||
test('popover - arrow side: end', async () => {
|
||||
await testPopover('end', false);
|
||||
});
|
||||
|
||||
test('popover - arrow side: start, rtl', async () => {
|
||||
await testPopover('start', true);
|
||||
});
|
||||
|
||||
test('popover - arrow side: end, rtl', async () => {
|
||||
await testPopover('end', true);
|
||||
});
|
||||
|
||||
const testPopover = async (side: string, isRTL = false) => {
|
||||
const rtl = isRTL ? '&rtl=true' : '';
|
||||
const page = await newE2EPage({ url: `/src/components/popover/test/arrow?ionic:_testing=true${rtl}` });
|
||||
|
||||
const POPOVER_CLASS = `${side}-popover`;
|
||||
const TRIGGER_ID = `${side}-trigger`;
|
||||
const screenshotCompares = [];
|
||||
|
||||
const trigger = await page.find(`#${TRIGGER_ID}`);
|
||||
|
||||
await page.evaluate((POPOVER_TRIGGER_ID) => {
|
||||
const popoverTrigger = document.querySelector(`#${POPOVER_TRIGGER_ID}`);
|
||||
popoverTrigger?.scrollIntoView({ block: 'center' });
|
||||
}, TRIGGER_ID);
|
||||
|
||||
trigger.click();
|
||||
|
||||
await page.waitForSelector(`.${POPOVER_CLASS}`);
|
||||
const popover = await page.find(`.${POPOVER_CLASS}`);
|
||||
await popover.waitForVisible();
|
||||
|
||||
screenshotCompares.push(await page.compareScreenshot());
|
||||
|
||||
for (const screenshotCompare of screenshotCompares) {
|
||||
expect(screenshotCompare).toMatchScreenshot();
|
||||
}
|
||||
};
|
@ -13,10 +13,13 @@
|
||||
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
|
||||
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
|
||||
<style>
|
||||
ion-app > ion-content {
|
||||
--background: #dddddd;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-row-gap: 20px;
|
||||
grid-row-gap: 80px;
|
||||
grid-column-gap: 20px;
|
||||
|
||||
padding: 200px;
|
||||
@ -54,42 +57,48 @@
|
||||
<div class="grid-item">
|
||||
<h2>Top</h2>
|
||||
<ion-button id="top-trigger">Click to Open</ion-button>
|
||||
<ion-popover class="top-popover" trigger="top-trigger" side="top" size="cover">
|
||||
<ion-popover show-backdrop="false" class="top-popover" trigger="top-trigger" side="top" size="cover">
|
||||
<ion-content class="ion-padding"> Hello World </ion-content>
|
||||
</ion-popover>
|
||||
</div>
|
||||
<div class="grid-item">
|
||||
<h2>Right</h2>
|
||||
<ion-button id="right-trigger">Click to Open</ion-button>
|
||||
<ion-popover class="right-popover" trigger="right-trigger" side="right" size="cover">
|
||||
<ion-popover show-backdrop="false" class="right-popover" trigger="right-trigger" side="right" size="cover">
|
||||
<ion-content class="ion-padding"> Hello World </ion-content>
|
||||
</ion-popover>
|
||||
</div>
|
||||
<div class="grid-item">
|
||||
<h2>Bottom</h2>
|
||||
<ion-button id="bottom-trigger">Click to Open</ion-button>
|
||||
<ion-popover class="bottom-popover" trigger="bottom-trigger" side="bottom" size="cover">
|
||||
<ion-popover
|
||||
show-backdrop="false"
|
||||
class="bottom-popover"
|
||||
trigger="bottom-trigger"
|
||||
side="bottom"
|
||||
size="cover"
|
||||
>
|
||||
<ion-content class="ion-padding"> Hello World </ion-content>
|
||||
</ion-popover>
|
||||
</div>
|
||||
<div class="grid-item">
|
||||
<h2>Left</h2>
|
||||
<ion-button id="left-trigger">Click to Open</ion-button>
|
||||
<ion-popover class="left-popover" trigger="left-trigger" side="left" size="cover">
|
||||
<ion-popover show-backdrop="false" class="left-popover" trigger="left-trigger" side="left" size="cover">
|
||||
<ion-content class="ion-padding"> Hello World </ion-content>
|
||||
</ion-popover>
|
||||
</div>
|
||||
<div class="grid-item">
|
||||
<h2>Start</h2>
|
||||
<ion-button id="start-trigger">Click to Open</ion-button>
|
||||
<ion-popover class="start-popover" trigger="start-trigger" side="start" size="cover">
|
||||
<ion-popover show-backdrop="false" class="start-popover" trigger="start-trigger" side="start" size="cover">
|
||||
<ion-content class="ion-padding"> Hello World </ion-content>
|
||||
</ion-popover>
|
||||
</div>
|
||||
<div class="grid-item">
|
||||
<h2>End</h2>
|
||||
<ion-button id="end-trigger">Click to Open</ion-button>
|
||||
<ion-popover class="end-popover" trigger="end-trigger" side="end" size="cover">
|
||||
<ion-popover show-backdrop="false" class="end-popover" trigger="end-trigger" side="end" size="cover">
|
||||
<ion-content class="ion-padding"> Hello World </ion-content>
|
||||
</ion-popover>
|
||||
</div>
|
||||
|
23
core/src/components/popover/test/arrow/popover.e2e.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { test, Viewports } from '@utils/test/playwright';
|
||||
|
||||
import { openPopover } from '../test.utils';
|
||||
|
||||
test.describe('popover: arrow rendering', async () => {
|
||||
/**
|
||||
* The popovers have showBackdrop=false so we can open all of them at once
|
||||
* and massively cut down on screenshots taken. The content has its own
|
||||
* backdrop so you can still see the popovers.
|
||||
*/
|
||||
test('should not have visual regressions', async ({ page }) => {
|
||||
await page.goto('/src/components/popover/test/arrow');
|
||||
await page.setViewportSize(Viewports.tablet.portrait); // avoid extra-long viewport screenshots
|
||||
|
||||
const sides = ['top', 'right', 'bottom', 'left', 'start', 'end'];
|
||||
for (const side of sides) {
|
||||
await openPopover(page, `${side}-trigger`, true);
|
||||
}
|
||||
|
||||
expect(await page.screenshot()).toMatchSnapshot(`popover-arrow-${page.getSnapshotSettings()}.png`);
|
||||
});
|
||||
});
|
After Width: | Height: | Size: 110 KiB |
After Width: | Height: | Size: 56 KiB |
After Width: | Height: | Size: 106 KiB |
After Width: | Height: | Size: 109 KiB |
After Width: | Height: | Size: 55 KiB |
After Width: | Height: | Size: 106 KiB |
After Width: | Height: | Size: 109 KiB |
After Width: | Height: | Size: 53 KiB |
After Width: | Height: | Size: 101 KiB |
After Width: | Height: | Size: 109 KiB |
After Width: | Height: | Size: 53 KiB |
After Width: | Height: | Size: 101 KiB |
@ -20,52 +20,6 @@
|
||||
|
||||
<body>
|
||||
<ion-app>
|
||||
<ion-content class="ion-padding" id="content">
|
||||
<ion-button
|
||||
id="basic-popover"
|
||||
expand="block"
|
||||
onclick="presentPopover({ component: 'profile-page', event: event, htmlAttributes: { 'data-testid': 'basic-popover' } })"
|
||||
>
|
||||
Show Popover</ion-button
|
||||
>
|
||||
<ion-button
|
||||
id="translucent-popover"
|
||||
expand="block"
|
||||
onclick="presentPopover({ component: 'translucent-page', event: event, translucent: true })"
|
||||
>Show Translucent Popover</ion-button
|
||||
>
|
||||
<ion-button
|
||||
id="long-list-popover"
|
||||
expand="block"
|
||||
color="secondary"
|
||||
onclick="presentPopover({ component: 'list-page', event: event })"
|
||||
>Show Long List Popover</ion-button
|
||||
>
|
||||
<ion-button
|
||||
id="no-event-popover"
|
||||
expand="block"
|
||||
color="danger"
|
||||
onclick="presentPopover({ component: 'profile-page' })"
|
||||
>No Event Popover</ion-button
|
||||
>
|
||||
<ion-button
|
||||
id="custom-class-popover"
|
||||
expand="block"
|
||||
color="tertiary"
|
||||
onclick="presentPopover({ component: 'translucent-page', event: event, cssClass: 'my-custom-class' })"
|
||||
>Custom Class Popover</ion-button
|
||||
>
|
||||
<ion-button id="header-popover" expand="block" onclick="presentPopover({ component: 'header-page' })"
|
||||
>Popover With Header</ion-button
|
||||
>
|
||||
<ion-button
|
||||
id="translucent-header-popover"
|
||||
expand="block"
|
||||
onclick="presentPopover({ component: 'translucent-header-page' })"
|
||||
>Popover With Translucent Header</ion-button
|
||||
>
|
||||
</ion-content>
|
||||
|
||||
<ion-content class="ion-padding" id="content">
|
||||
<ion-button
|
||||
id="basic-popover"
|
||||
|
@ -1,142 +1,65 @@
|
||||
import type { E2EPage } from '@stencil/core/testing';
|
||||
import { newE2EPage } from '@stencil/core/testing';
|
||||
import { expect } from '@playwright/test';
|
||||
import type { E2EPage } from '@utils/test/playwright';
|
||||
import { test } from '@utils/test/playwright';
|
||||
|
||||
import { testPopover } from '../test.utils';
|
||||
import { openPopover, screenshotPopover } from '../test.utils';
|
||||
|
||||
const DIRECTORY = 'basic';
|
||||
test.describe('popover: rendering', async () => {
|
||||
test('should not have visual regressions', async ({ page }) => {
|
||||
const buttonIDs = [
|
||||
'basic-popover',
|
||||
'translucent-popover',
|
||||
'long-list-popover',
|
||||
'no-event-popover',
|
||||
'custom-class-popover',
|
||||
'header-popover',
|
||||
'translucent-header-popover',
|
||||
];
|
||||
|
||||
/**
|
||||
* Focusing happens async inside of popover so we need
|
||||
* to wait for the requestAnimationFrame to fire.
|
||||
*/
|
||||
const expectActiveElementTextToEqual = async (page: E2EPage, textValue: string) => {
|
||||
await page.evaluate((text) => document.activeElement!.textContent === text, textValue);
|
||||
};
|
||||
|
||||
const getActiveElementSelectionStart = (page: E2EPage) => {
|
||||
return page.evaluate(() =>
|
||||
document.activeElement instanceof HTMLTextAreaElement ? document.activeElement.selectionStart : null
|
||||
);
|
||||
};
|
||||
|
||||
const getActiveElementScrollTop = (page: E2EPage) => {
|
||||
return page.evaluate(() => {
|
||||
// Returns the closest ion-textarea or active element
|
||||
const target = document.activeElement!.closest('ion-textarea') ?? document.activeElement;
|
||||
return target!.scrollTop;
|
||||
for (const id of buttonIDs) {
|
||||
await screenshotPopover(page, id, 'basic');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
test('popover: basic', async () => {
|
||||
await testPopover(DIRECTORY, '#basic-popover');
|
||||
});
|
||||
|
||||
test('popover: translucent', async () => {
|
||||
await testPopover(DIRECTORY, '#translucent-popover');
|
||||
test.describe('popover: htmlAttributes', async () => {
|
||||
test('should inherit attributes on host', async ({ page }) => {
|
||||
await page.goto('/src/components/popover/test/basic');
|
||||
await openPopover(page, 'basic-popover');
|
||||
|
||||
const alert = page.locator('ion-popover');
|
||||
expect(alert).toHaveAttribute('data-testid', 'basic-popover');
|
||||
});
|
||||
});
|
||||
|
||||
test('popover: long list', async () => {
|
||||
await testPopover(DIRECTORY, '#long-list-popover');
|
||||
});
|
||||
test.describe('popover: focus trap', async () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/src/components/popover/test/basic');
|
||||
});
|
||||
|
||||
test('popover: no event', async () => {
|
||||
await testPopover(DIRECTORY, '#no-event-popover');
|
||||
});
|
||||
|
||||
test('popover: custom class', async () => {
|
||||
await testPopover(DIRECTORY, '#custom-class-popover');
|
||||
});
|
||||
|
||||
test('popover: header', async () => {
|
||||
await testPopover(DIRECTORY, '#header-popover');
|
||||
});
|
||||
|
||||
test('popover: translucent header', async () => {
|
||||
await testPopover(DIRECTORY, '#translucent-header-popover');
|
||||
});
|
||||
|
||||
/**
|
||||
* RTL Tests
|
||||
*/
|
||||
|
||||
test('popover:rtl: basic', async () => {
|
||||
await testPopover(DIRECTORY, '#basic-popover', true, true);
|
||||
});
|
||||
|
||||
test('popover:rtl: translucent', async () => {
|
||||
await testPopover(DIRECTORY, '#translucent-popover', true, true);
|
||||
});
|
||||
|
||||
test('popover:rtl: long list', async () => {
|
||||
await testPopover(DIRECTORY, '#long-list-popover', true, true);
|
||||
});
|
||||
|
||||
test('popover:rtl: no event', async () => {
|
||||
await testPopover(DIRECTORY, '#no-event-popover', true, true);
|
||||
});
|
||||
|
||||
test('popover:rtl: custom class', async () => {
|
||||
await testPopover(DIRECTORY, '#custom-class-popover', true, true);
|
||||
});
|
||||
|
||||
test('popover:rtl: header', async () => {
|
||||
await testPopover(DIRECTORY, '#header-popover', true);
|
||||
});
|
||||
|
||||
test('popover:rtl: translucent header', async () => {
|
||||
await testPopover(DIRECTORY, '#translucent-header-popover', true);
|
||||
});
|
||||
|
||||
test('popover: htmlAttributes', async () => {
|
||||
const page = await newE2EPage({ url: '/src/components/popover/test/basic?ionic:_testing=true' });
|
||||
|
||||
await page.click('#basic-popover');
|
||||
await page.waitForSelector('#basic-popover');
|
||||
|
||||
const alert = await page.find('ion-popover');
|
||||
|
||||
expect(alert).not.toBe(null);
|
||||
await alert.waitForVisible();
|
||||
|
||||
const attribute = await page.evaluate(() => document.querySelector('ion-popover')!.getAttribute('data-testid'));
|
||||
|
||||
expect(attribute).toEqual('basic-popover');
|
||||
});
|
||||
|
||||
describe('popover: focus trap', () => {
|
||||
it('should focus the first ion-item on ArrowDown', async () => {
|
||||
const page = await newE2EPage({ url: '/src/components/popover/test/basic?ionic:_testing=true' });
|
||||
|
||||
await page.click('#basic-popover');
|
||||
|
||||
const popover = await page.find('ion-popover');
|
||||
|
||||
expect(popover).not.toBe(null);
|
||||
await popover.waitForVisible();
|
||||
test('should focus the first ion-item on ArrowDown', async ({ page }) => {
|
||||
await openPopover(page, 'basic-popover');
|
||||
|
||||
await page.keyboard.press('ArrowDown');
|
||||
|
||||
await expectActiveElementTextToEqual(page, 'Item 0');
|
||||
});
|
||||
|
||||
it('should work with ion-item children', async () => {
|
||||
const page = await newE2EPage({ url: '/src/components/popover/test/basic?ionic:_testing=true' });
|
||||
|
||||
await page.click('#basic-popover');
|
||||
await page.waitForSelector('#basic-popover');
|
||||
|
||||
const popover = await page.find('ion-popover');
|
||||
|
||||
expect(popover).not.toBe(null);
|
||||
await popover.waitForVisible();
|
||||
test('should trap focus', async ({ page, browserName }) => {
|
||||
await openPopover(page, 'basic-popover');
|
||||
|
||||
await page.keyboard.press('Tab');
|
||||
|
||||
await expectActiveElementTextToEqual(page, 'Item 0');
|
||||
|
||||
await page.keyboard.down('Shift');
|
||||
if (browserName === 'webkit') {
|
||||
await page.keyboard.down('Alt');
|
||||
}
|
||||
await page.keyboard.press('Tab');
|
||||
await page.keyboard.up('Shift');
|
||||
if (browserName === 'webkit') {
|
||||
await page.keyboard.up('Alt');
|
||||
}
|
||||
|
||||
await expectActiveElementTextToEqual(page, 'Item 3');
|
||||
|
||||
@ -161,19 +84,17 @@ describe('popover: focus trap', () => {
|
||||
await expectActiveElementTextToEqual(page, 'Item 3');
|
||||
});
|
||||
|
||||
it('should not override keyboard interactions for textarea elements', async () => {
|
||||
const page = await newE2EPage({ url: '/src/components/popover/test/basic?ionic:_testing=true' });
|
||||
|
||||
await page.waitForSelector('#popover-with-textarea');
|
||||
await page.click('#popover-with-textarea');
|
||||
|
||||
const popover = await page.find('ion-popover');
|
||||
await popover.waitForVisible();
|
||||
|
||||
await page.waitForFunction('document.activeElement.tagName === "ION-POPOVER"');
|
||||
test('should not override keyboard interactions for textarea elements', async ({ page, browserName }) => {
|
||||
await openPopover(page, 'popover-with-textarea');
|
||||
await page.waitForFunction(() => document.activeElement?.tagName === 'ION-POPOVER');
|
||||
|
||||
await page.keyboard.press('Tab');
|
||||
// Checking within ion-textarea
|
||||
|
||||
// for Firefox, ion-textarea is focused first
|
||||
// need to tab again to get to native input
|
||||
if (browserName === 'firefox') {
|
||||
await page.keyboard.press('Tab');
|
||||
}
|
||||
|
||||
let activeElementTagName = await page.evaluate(() => document.activeElement!.tagName);
|
||||
let scrollTop = null;
|
||||
@ -235,3 +156,27 @@ describe('popover: focus trap', () => {
|
||||
expect(scrollTop).toBeGreaterThanOrEqual(previousScrollTop);
|
||||
});
|
||||
});
|
||||
|
||||
// TODO(FW-1424): convert these to Playwright assertions where possible
|
||||
|
||||
/**
|
||||
* Focusing happens async inside of popover so we need
|
||||
* to wait for the requestAnimationFrame to fire.
|
||||
*/
|
||||
const expectActiveElementTextToEqual = async (page: E2EPage, textValue: string) => {
|
||||
await page.evaluate((text) => document.activeElement!.textContent === text, textValue);
|
||||
};
|
||||
|
||||
const getActiveElementSelectionStart = (page: E2EPage) => {
|
||||
return page.evaluate(() =>
|
||||
document.activeElement instanceof HTMLTextAreaElement ? document.activeElement.selectionStart : null
|
||||
);
|
||||
};
|
||||
|
||||
const getActiveElementScrollTop = (page: E2EPage) => {
|
||||
return page.evaluate(() => {
|
||||
// Returns the closest ion-textarea or active element
|
||||
const target = document.activeElement!.closest('ion-textarea') ?? document.activeElement;
|
||||
return target!.scrollTop;
|
||||
});
|
||||
};
|
After Width: | Height: | Size: 74 KiB |
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 60 KiB |
After Width: | Height: | Size: 74 KiB |
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 60 KiB |
After Width: | Height: | Size: 70 KiB |
After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 58 KiB |
After Width: | Height: | Size: 70 KiB |
After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 58 KiB |
After Width: | Height: | Size: 107 KiB |
After Width: | Height: | Size: 42 KiB |
After Width: | Height: | Size: 95 KiB |
After Width: | Height: | Size: 107 KiB |
After Width: | Height: | Size: 41 KiB |
After Width: | Height: | Size: 95 KiB |
After Width: | Height: | Size: 96 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 82 KiB |
After Width: | Height: | Size: 97 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 82 KiB |
After Width: | Height: | Size: 212 KiB |
After Width: | Height: | Size: 67 KiB |
After Width: | Height: | Size: 162 KiB |
After Width: | Height: | Size: 212 KiB |
After Width: | Height: | Size: 67 KiB |
After Width: | Height: | Size: 162 KiB |
After Width: | Height: | Size: 281 KiB |
After Width: | Height: | Size: 78 KiB |
After Width: | Height: | Size: 193 KiB |
After Width: | Height: | Size: 279 KiB |
After Width: | Height: | Size: 78 KiB |
After Width: | Height: | Size: 193 KiB |
After Width: | Height: | Size: 96 KiB |
After Width: | Height: | Size: 37 KiB |
After Width: | Height: | Size: 80 KiB |
After Width: | Height: | Size: 96 KiB |
After Width: | Height: | Size: 38 KiB |
After Width: | Height: | Size: 80 KiB |
After Width: | Height: | Size: 93 KiB |
After Width: | Height: | Size: 36 KiB |
After Width: | Height: | Size: 75 KiB |
After Width: | Height: | Size: 94 KiB |
After Width: | Height: | Size: 36 KiB |
After Width: | Height: | Size: 75 KiB |
After Width: | Height: | Size: 87 KiB |
After Width: | Height: | Size: 35 KiB |
After Width: | Height: | Size: 68 KiB |
After Width: | Height: | Size: 87 KiB |
After Width: | Height: | Size: 35 KiB |
After Width: | Height: | Size: 68 KiB |
After Width: | Height: | Size: 87 KiB |
After Width: | Height: | Size: 34 KiB |
After Width: | Height: | Size: 70 KiB |
After Width: | Height: | Size: 88 KiB |
After Width: | Height: | Size: 35 KiB |
After Width: | Height: | Size: 70 KiB |
After Width: | Height: | Size: 214 KiB |
After Width: | Height: | Size: 67 KiB |
After Width: | Height: | Size: 163 KiB |
After Width: | Height: | Size: 214 KiB |
After Width: | Height: | Size: 67 KiB |
After Width: | Height: | Size: 163 KiB |
After Width: | Height: | Size: 281 KiB |
After Width: | Height: | Size: 78 KiB |
After Width: | Height: | Size: 193 KiB |
After Width: | Height: | Size: 279 KiB |
After Width: | Height: | Size: 78 KiB |
After Width: | Height: | Size: 193 KiB |
After Width: | Height: | Size: 101 KiB |
After Width: | Height: | Size: 33 KiB |
After Width: | Height: | Size: 88 KiB |
After Width: | Height: | Size: 101 KiB |
After Width: | Height: | Size: 33 KiB |
After Width: | Height: | Size: 88 KiB |
After Width: | Height: | Size: 82 KiB |
After Width: | Height: | Size: 34 KiB |
After Width: | Height: | Size: 70 KiB |
After Width: | Height: | Size: 82 KiB |
After Width: | Height: | Size: 34 KiB |