mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-20 12:29:55 +08:00
test(utils): migrate utils e2e tests to playwright (#26377)
This commit is contained in:
56
core/src/components/accordion/test/accordion.e2e.ts
Normal file
56
core/src/components/accordion/test/accordion.e2e.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { test } from '@utils/test/playwright';
|
||||
|
||||
test.describe('accordion: states', () => {
|
||||
test.beforeEach(({ skip }) => {
|
||||
skip.rtl();
|
||||
skip.mode('ios');
|
||||
});
|
||||
test('should properly set readonly on child accordions', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<ion-accordion-group animated="false">
|
||||
<ion-accordion>
|
||||
<ion-item slot="header">Label</ion-item>
|
||||
<div slot="content">Content</div>
|
||||
</ion-accordion>
|
||||
</ion-accordion-group>
|
||||
`);
|
||||
|
||||
const accordionGroup = page.locator('ion-accordion-group');
|
||||
const accordion = page.locator('ion-accordion');
|
||||
|
||||
expect(accordion).toHaveJSProperty('readonly', false);
|
||||
|
||||
await accordionGroup.evaluate((el: HTMLIonAccordionGroupElement) => {
|
||||
el.readonly = true;
|
||||
});
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
expect(accordion).toHaveJSProperty('readonly', true);
|
||||
});
|
||||
|
||||
test('should properly set disabled on child accordions', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<ion-accordion-group animated="false">
|
||||
<ion-accordion>
|
||||
<ion-item slot="header">Label</ion-item>
|
||||
<div slot="content">Content</div>
|
||||
</ion-accordion>
|
||||
</ion-accordion-group>
|
||||
`);
|
||||
|
||||
const accordionGroup = page.locator('ion-accordion-group');
|
||||
const accordion = page.locator('ion-accordion');
|
||||
|
||||
expect(accordion).toHaveJSProperty('disabled', false);
|
||||
|
||||
await accordionGroup.evaluate((el: HTMLIonAccordionGroupElement) => {
|
||||
el.disabled = true;
|
||||
});
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
expect(accordion).toHaveJSProperty('disabled', true);
|
||||
});
|
||||
});
|
@ -1,55 +0,0 @@
|
||||
import { newE2EPage } from '@stencil/core/testing';
|
||||
|
||||
test('should properly set readonly on child accordions', async () => {
|
||||
const page = await newE2EPage({
|
||||
html: `
|
||||
<ion-accordion-group animated="false">
|
||||
<ion-accordion>
|
||||
<ion-item slot="header">Label</ion-item>
|
||||
<div slot="content">Content</div>
|
||||
</ion-accordion>
|
||||
</ion-accordion-group>
|
||||
`,
|
||||
});
|
||||
|
||||
const accordion = await page.find('ion-accordion');
|
||||
const value = await accordion.getProperty('readonly');
|
||||
|
||||
expect(value).toBe(false);
|
||||
|
||||
await page.$eval('ion-accordion-group', (el: HTMLIonAccordionGroupElement) => {
|
||||
el.readonly = true;
|
||||
});
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
const valueAgain = await accordion.getProperty('readonly');
|
||||
expect(valueAgain).toBe(true);
|
||||
});
|
||||
|
||||
test('should properly set disabled on child accordions', async () => {
|
||||
const page = await newE2EPage({
|
||||
html: `
|
||||
<ion-accordion-group animated="false">
|
||||
<ion-accordion>
|
||||
<ion-item slot="header">Label</ion-item>
|
||||
<div slot="content">Content</div>
|
||||
</ion-accordion>
|
||||
</ion-accordion-group>
|
||||
`,
|
||||
});
|
||||
|
||||
const accordion = await page.find('ion-accordion');
|
||||
const value = await accordion.getProperty('disabled');
|
||||
|
||||
expect(value).toBe(false);
|
||||
|
||||
await page.$eval('ion-accordion-group', (el: HTMLIonAccordionGroupElement) => {
|
||||
el.disabled = true;
|
||||
});
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
const valueAgain = await accordion.getProperty('disabled');
|
||||
expect(valueAgain).toBe(true);
|
||||
});
|
@ -1,10 +0,0 @@
|
||||
import { newE2EPage } from '@stencil/core/testing';
|
||||
|
||||
test('label: basic', async () => {
|
||||
const page = await newE2EPage({
|
||||
url: '/src/components/label/test/basic?ionic:_testing=true',
|
||||
});
|
||||
|
||||
const compare = await page.compareScreenshot();
|
||||
expect(compare).toMatchScreenshot();
|
||||
});
|
@ -1,38 +0,0 @@
|
||||
import { newE2EPage } from '@stencil/core/testing';
|
||||
import { generateE2EUrl } from '@utils/test';
|
||||
|
||||
export const testLoading = async (type: string, selector: string, rtl = false) => {
|
||||
try {
|
||||
const pageUrl = generateE2EUrl('loading', type, rtl);
|
||||
|
||||
const page = await newE2EPage({
|
||||
url: pageUrl,
|
||||
});
|
||||
|
||||
const screenshotCompares = [];
|
||||
|
||||
await page.click(selector);
|
||||
await page.waitForSelector(selector);
|
||||
|
||||
let loading = await page.find('ion-loading');
|
||||
expect(loading).not.toBeNull();
|
||||
|
||||
await loading.waitForVisible();
|
||||
|
||||
screenshotCompares.push(await page.compareScreenshot());
|
||||
|
||||
await loading.callMethod('dismiss');
|
||||
await loading.waitForNotVisible();
|
||||
|
||||
screenshotCompares.push(await page.compareScreenshot('dismiss'));
|
||||
|
||||
loading = await page.find('ion-loading');
|
||||
expect(loading).toBeNull();
|
||||
|
||||
for (const screenshotCompare of screenshotCompares) {
|
||||
expect(screenshotCompare).toMatchScreenshot();
|
||||
}
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
};
|
@ -1,10 +0,0 @@
|
||||
import { newE2EPage } from '@stencil/core/testing';
|
||||
|
||||
test('slides: basic', async () => {
|
||||
const page = await newE2EPage({
|
||||
url: '/src/components/slides/test/basic?ionic:_testing=true',
|
||||
});
|
||||
|
||||
const compare = await page.compareScreenshot();
|
||||
expect(compare).toMatchScreenshot();
|
||||
});
|
@ -1,10 +0,0 @@
|
||||
import { newE2EPage } from '@stencil/core/testing';
|
||||
|
||||
test('slides: image', async () => {
|
||||
const page = await newE2EPage({
|
||||
url: '/src/components/slides/test/image?ionic:_testing=true',
|
||||
});
|
||||
|
||||
const compare = await page.compareScreenshot();
|
||||
expect(compare).toMatchScreenshot();
|
||||
});
|
@ -1,33 +0,0 @@
|
||||
import { newE2EPage } from '@stencil/core/testing';
|
||||
|
||||
test('slides: prevent-default', async () => {
|
||||
// For this specific test, _testing=false to import tap-click in app.tsx
|
||||
const page = await newE2EPage({
|
||||
url: '/src/components/slides/test/prevent-default?ionic:_testing=false',
|
||||
});
|
||||
|
||||
const screenshotCompares = [];
|
||||
|
||||
screenshotCompares.push(await page.compareScreenshot());
|
||||
|
||||
const scroller = await page.find('#scrollDownButton');
|
||||
const button = await page.find('#changeBackgroundButton');
|
||||
const contentWithBackground = await page.find('#contentWithBackground');
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await scroller.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
screenshotCompares.push(await page.compareScreenshot('scroll down button'));
|
||||
|
||||
await button.click();
|
||||
|
||||
screenshotCompares.push(await page.compareScreenshot('change background'));
|
||||
|
||||
expect(contentWithBackground).toHaveClasses(['blueBackground']);
|
||||
|
||||
for (const screenshotCompare of screenshotCompares) {
|
||||
expect(screenshotCompare).toMatchScreenshot();
|
||||
}
|
||||
});
|
@ -1,10 +0,0 @@
|
||||
import { newE2EPage } from '@stencil/core/testing';
|
||||
|
||||
test('slides: vertical', async () => {
|
||||
const page = await newE2EPage({
|
||||
url: '/src/components/slides/test/vertical?ionic:_testing=true',
|
||||
});
|
||||
|
||||
const compare = await page.compareScreenshot();
|
||||
expect(compare).toMatchScreenshot();
|
||||
});
|
@ -1,42 +0,0 @@
|
||||
import { newE2EPage } from '@stencil/core/testing';
|
||||
import { generateE2EUrl } from '@utils/test';
|
||||
|
||||
export const testToast = async (type: string, selector: string, rtl = false) => {
|
||||
try {
|
||||
const pageUrl = generateE2EUrl('toast', type, rtl);
|
||||
|
||||
const page = await newE2EPage({
|
||||
url: pageUrl,
|
||||
});
|
||||
|
||||
const screenshotCompares = [];
|
||||
|
||||
const button = await page.find(selector);
|
||||
await button.waitForVisible();
|
||||
await button.click();
|
||||
|
||||
await page.waitForTimeout(250);
|
||||
|
||||
let toast = await page.find('ion-toast');
|
||||
await toast.waitForVisible();
|
||||
|
||||
expect(toast).not.toBe(null);
|
||||
await toast.waitForVisible();
|
||||
|
||||
screenshotCompares.push(await page.compareScreenshot());
|
||||
|
||||
await toast.callMethod('dismiss');
|
||||
await toast.waitForNotVisible();
|
||||
|
||||
screenshotCompares.push(await page.compareScreenshot('dismiss'));
|
||||
|
||||
toast = await page.find('ion-toast');
|
||||
expect(toast).toBe(null);
|
||||
|
||||
for (const screenshotCompare of screenshotCompares) {
|
||||
expect(screenshotCompare).toMatchScreenshot();
|
||||
}
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
};
|
@ -1,11 +0,0 @@
|
||||
import { newE2EPage } from '@stencil/core/testing';
|
||||
|
||||
test('virtual-scroll: basic', async () => {
|
||||
const page = await newE2EPage({
|
||||
url: '/src/components/virtual-scroll/test/basic?ionic:_testing=true',
|
||||
});
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
const compare = await page.compareScreenshot();
|
||||
expect(compare).toMatchScreenshot();
|
||||
});
|
@ -1,11 +0,0 @@
|
||||
import { newE2EPage } from '@stencil/core/testing';
|
||||
|
||||
test('virtual-scroll: cards', async () => {
|
||||
const page = await newE2EPage({
|
||||
url: '/src/components/virtual-scroll/test/cards?ionic:_testing=true',
|
||||
});
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
const compare = await page.compareScreenshot();
|
||||
expect(compare).toMatchScreenshot();
|
||||
});
|
@ -1,10 +0,0 @@
|
||||
import { newE2EPage } from '@stencil/core/testing';
|
||||
|
||||
test('themes: css-variables', async () => {
|
||||
const page = await newE2EPage({
|
||||
url: '/src/themes/test/css-variables?ionic:_testing=true',
|
||||
});
|
||||
|
||||
const compare = await page.compareScreenshot();
|
||||
expect(compare).toMatchScreenshot();
|
||||
});
|
@ -0,0 +1,43 @@
|
||||
import type { E2EPage } from '@utils/test/playwright';
|
||||
import { test } from '@utils/test/playwright';
|
||||
|
||||
test.describe('animation: animationbuilder', async () => {
|
||||
test.beforeEach(({ skip }) => {
|
||||
skip.rtl();
|
||||
});
|
||||
test('backwards-compatibility animation', async ({ page }) => {
|
||||
await page.goto('/src/utils/animation/test/animationbuilder');
|
||||
await testNavigation(page);
|
||||
});
|
||||
|
||||
test('ios-transition web', async ({ page, skip }) => {
|
||||
skip.mode('md');
|
||||
|
||||
await page.goto('/src/utils/animation/test/animationbuilder');
|
||||
await testNavigation(page);
|
||||
});
|
||||
|
||||
test('ios-transition css', async ({ page, skip }) => {
|
||||
skip.mode('md');
|
||||
|
||||
await page.goto('/src/utils/animation/test/animationbuilder?ionic:_forceCSSAnimations=true');
|
||||
await testNavigation(page);
|
||||
});
|
||||
});
|
||||
|
||||
const testNavigation = async (page: E2EPage) => {
|
||||
const ionRouteDidChange = await page.spyOnEvent('ionRouteDidChange');
|
||||
|
||||
await page.click('page-root ion-button.next');
|
||||
await ionRouteDidChange.next();
|
||||
page.click('page-one ion-button.next');
|
||||
await ionRouteDidChange.next();
|
||||
page.click('page-two ion-button.next');
|
||||
await ionRouteDidChange.next();
|
||||
page.click('page-three ion-back-button');
|
||||
await ionRouteDidChange.next();
|
||||
page.click('page-two ion-back-button');
|
||||
await ionRouteDidChange.next();
|
||||
page.click('page-one ion-back-button');
|
||||
await ionRouteDidChange.next();
|
||||
};
|
@ -1,45 +0,0 @@
|
||||
import type { E2EPage } from '@stencil/core/testing';
|
||||
import { newE2EPage } from '@stencil/core/testing';
|
||||
|
||||
test('animation:backwards-compatibility animation', async () => {
|
||||
const page = await newE2EPage({ url: '/src/utils/animation/test/animationbuilder' });
|
||||
await testNavigation(page);
|
||||
});
|
||||
|
||||
test('animation:ios-transition web', async () => {
|
||||
const page = await newE2EPage({ url: '/src/utils/animation/test/animationbuilder?ionic:mode=ios' });
|
||||
await testNavigation(page);
|
||||
});
|
||||
|
||||
test('animation:ios-transition css', async () => {
|
||||
const page = await newE2EPage({
|
||||
url: '/src/utils/animation/test/animationbuilder?ionic:mode=ios&ionic:_forceCSSAnimations=true',
|
||||
});
|
||||
await testNavigation(page);
|
||||
});
|
||||
|
||||
const testNavigation = async (page: E2EPage) => {
|
||||
const screenshotCompares = [];
|
||||
const ionRouteDidChange = await page.spyOnEvent('ionRouteDidChange');
|
||||
|
||||
screenshotCompares.push(await page.compareScreenshot());
|
||||
|
||||
await page.click('page-root ion-button.next');
|
||||
await ionRouteDidChange.next();
|
||||
page.click('page-one ion-button.next');
|
||||
await ionRouteDidChange.next();
|
||||
page.click('page-two ion-button.next');
|
||||
await ionRouteDidChange.next();
|
||||
page.click('page-three ion-back-button');
|
||||
await ionRouteDidChange.next();
|
||||
page.click('page-two ion-back-button');
|
||||
await ionRouteDidChange.next();
|
||||
page.click('page-one ion-back-button');
|
||||
await ionRouteDidChange.next();
|
||||
|
||||
screenshotCompares.push(await page.compareScreenshot('end navigation'));
|
||||
|
||||
for (const screenshotCompare of screenshotCompares) {
|
||||
expect(screenshotCompare).toMatchScreenshot();
|
||||
}
|
||||
};
|
27
core/src/utils/animation/test/basic/animation.e2e.ts
Normal file
27
core/src/utils/animation/test/basic/animation.e2e.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { test } from '@utils/test/playwright';
|
||||
import type { E2EPage } from '@utils/test/playwright';
|
||||
|
||||
test.describe('animation: basic', async () => {
|
||||
test.beforeEach(({ skip }) => {
|
||||
skip.rtl();
|
||||
skip.mode('ios');
|
||||
});
|
||||
|
||||
test(`should resolve using web animations`, async ({ page }) => {
|
||||
await page.goto('/src/utils/animation/test/basic');
|
||||
await testPage(page);
|
||||
});
|
||||
|
||||
test(`should resolve using css animations`, async ({ page }) => {
|
||||
await page.goto('/src/utils/animation/test/basic?ionic:_forceCSSAnimations=true');
|
||||
await testPage(page);
|
||||
});
|
||||
});
|
||||
|
||||
const testPage = async (page: E2EPage) => {
|
||||
const ionAnimationFinished = await page.spyOnEvent('ionAnimationFinished');
|
||||
|
||||
await page.click('.play');
|
||||
|
||||
await ionAnimationFinished.next();
|
||||
};
|
@ -1,59 +0,0 @@
|
||||
import { newE2EPage } from '@stencil/core/testing';
|
||||
|
||||
import { listenForEvent, waitForFunctionTestContext } from '../../../test/utils';
|
||||
|
||||
test(`animation:web: basic`, async () => {
|
||||
const page = await newE2EPage({ url: '/src/utils/animation/test/basic' });
|
||||
const screenshotCompares = [];
|
||||
|
||||
screenshotCompares.push(await page.compareScreenshot());
|
||||
|
||||
const ANIMATION_FINISHED = 'onIonAnimationFinished';
|
||||
const animationFinishedCount: any = { count: 0 };
|
||||
await page.exposeFunction(ANIMATION_FINISHED, () => {
|
||||
animationFinishedCount.count += 1;
|
||||
});
|
||||
|
||||
const square = await page.$('.square-a');
|
||||
await listenForEvent(page, 'ionAnimationFinished', square, ANIMATION_FINISHED);
|
||||
|
||||
await page.click('.play');
|
||||
await page.waitForSelector('.play');
|
||||
|
||||
await waitForFunctionTestContext(
|
||||
(payload: any) => {
|
||||
return payload.animationFinishedCount.count === 1;
|
||||
},
|
||||
{ animationFinishedCount }
|
||||
);
|
||||
|
||||
screenshotCompares.push(await page.compareScreenshot('end animation'));
|
||||
});
|
||||
|
||||
test(`animation:css: basic`, async () => {
|
||||
const page = await newE2EPage({ url: '/src/utils/animation/test/basic?ionic:_forceCSSAnimations=true' });
|
||||
const screenshotCompares = [];
|
||||
|
||||
screenshotCompares.push(await page.compareScreenshot());
|
||||
|
||||
const ANIMATION_FINISHED = 'onIonAnimationFinished';
|
||||
const animationFinishedCount: any = { count: 0 };
|
||||
await page.exposeFunction(ANIMATION_FINISHED, () => {
|
||||
animationFinishedCount.count += 1;
|
||||
});
|
||||
|
||||
const square = await page.$('.square-a');
|
||||
await listenForEvent(page, 'ionAnimationFinished', square, ANIMATION_FINISHED);
|
||||
|
||||
await page.click('.play');
|
||||
await page.waitForSelector('.play');
|
||||
|
||||
await waitForFunctionTestContext(
|
||||
(payload: any) => {
|
||||
return payload.animationFinishedCount.count === 1;
|
||||
},
|
||||
{ animationFinishedCount }
|
||||
);
|
||||
|
||||
screenshotCompares.push(await page.compareScreenshot('end animation'));
|
||||
});
|
@ -37,7 +37,7 @@
|
||||
])
|
||||
.onFinish(() => {
|
||||
const ev = new CustomEvent('ionAnimationFinished');
|
||||
squareA.dispatchEvent(ev);
|
||||
window.dispatchEvent(ev);
|
||||
});
|
||||
|
||||
document.querySelector('.play').addEventListener('click', () => {
|
||||
|
37
core/src/utils/animation/test/display/animation.e2e.ts
Normal file
37
core/src/utils/animation/test/display/animation.e2e.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { test } from '@utils/test/playwright';
|
||||
import type { E2EPage } from '@utils/test/playwright';
|
||||
|
||||
test.describe('animation: display', async () => {
|
||||
test.beforeEach(({ skip }) => {
|
||||
skip.rtl();
|
||||
skip.mode('ios');
|
||||
});
|
||||
|
||||
test(`should resolve using web animations`, async ({ page }) => {
|
||||
await page.goto('/src/utils/animation/test/display');
|
||||
await testDisplay(page);
|
||||
});
|
||||
|
||||
test(`should resolve using css animations`, async ({ page }) => {
|
||||
await page.goto('/src/utils/animation/test/display?ionic:_forceCSSAnimations=true');
|
||||
await testDisplay(page);
|
||||
});
|
||||
});
|
||||
|
||||
const testDisplay = async (page: E2EPage) => {
|
||||
const ionAnimationFinished = await page.spyOnEvent('ionAnimationFinished');
|
||||
|
||||
await page.click('.play');
|
||||
|
||||
await ionAnimationFinished.next();
|
||||
await expect(ionAnimationFinished).toHaveReceivedEventDetail('AnimationBFinished');
|
||||
|
||||
await ionAnimationFinished.next();
|
||||
await expect(ionAnimationFinished).toHaveReceivedEventDetail('AnimationAFinished');
|
||||
|
||||
await ionAnimationFinished.next();
|
||||
await expect(ionAnimationFinished).toHaveReceivedEventDetail('AnimationRootFinished');
|
||||
|
||||
await expect(ionAnimationFinished).toHaveReceivedEventTimes(3);
|
||||
};
|
@ -1,42 +0,0 @@
|
||||
import { newE2EPage } from '@stencil/core/testing';
|
||||
|
||||
import { listenForEvent, waitForFunctionTestContext } from '../../../test/utils';
|
||||
|
||||
test(`animation:web: display`, async () => {
|
||||
const page = await newE2EPage({ url: '/src/utils/animation/test/display' });
|
||||
await runTest(page);
|
||||
});
|
||||
|
||||
test(`animation:css: display`, async () => {
|
||||
const page = await newE2EPage({ url: '/src/utils/animation/test/display?ionic:_forceCSSAnimations=true' });
|
||||
await runTest(page);
|
||||
});
|
||||
|
||||
const runTest = async (page: any) => {
|
||||
const screenshotCompares = [];
|
||||
|
||||
screenshotCompares.push(await page.compareScreenshot());
|
||||
|
||||
const ANIMATION_FINISHED = 'onIonAnimationFinished';
|
||||
const animationStatus = [];
|
||||
await page.exposeFunction(ANIMATION_FINISHED, (ev: any) => {
|
||||
animationStatus.push(ev.detail);
|
||||
});
|
||||
|
||||
const squareA = await page.$('.square-a');
|
||||
await listenForEvent(page, 'ionAnimationFinished', squareA, ANIMATION_FINISHED);
|
||||
|
||||
await page.click('.play');
|
||||
await page.waitForSelector('.play');
|
||||
|
||||
await waitForFunctionTestContext(
|
||||
(payload: any) => {
|
||||
return (
|
||||
payload.animationStatus.join(', ') ===
|
||||
['AnimationBFinished', 'AnimationAFinished', 'AnimationRootFinished'].join(', ')
|
||||
);
|
||||
},
|
||||
{ animationStatus }
|
||||
);
|
||||
screenshotCompares.push(await page.compareScreenshot('end animation'));
|
||||
};
|
@ -43,7 +43,7 @@
|
||||
])
|
||||
.onFinish(() => {
|
||||
const ev = new CustomEvent('ionAnimationFinished', { detail: 'AnimationAFinished' });
|
||||
squareA.dispatchEvent(ev);
|
||||
window.dispatchEvent(ev);
|
||||
});
|
||||
|
||||
animationB
|
||||
@ -58,12 +58,12 @@
|
||||
])
|
||||
.onFinish(() => {
|
||||
const ev = new CustomEvent('ionAnimationFinished', { detail: 'AnimationBFinished' });
|
||||
squareA.dispatchEvent(ev);
|
||||
window.dispatchEvent(ev);
|
||||
});
|
||||
|
||||
rootAnimation.addAnimation([animationA, animationB]).onFinish(() => {
|
||||
const ev = new CustomEvent('ionAnimationFinished', { detail: 'AnimationRootFinished' });
|
||||
squareA.dispatchEvent(ev);
|
||||
window.dispatchEvent(ev);
|
||||
});
|
||||
|
||||
document.querySelector('.play').addEventListener('click', () => {
|
||||
|
63
core/src/utils/animation/test/hooks/animation.e2e.ts
Normal file
63
core/src/utils/animation/test/hooks/animation.e2e.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { test } from '@utils/test/playwright';
|
||||
import type { E2EPage } from '@utils/test/playwright';
|
||||
|
||||
test.describe('animation: hooks', async () => {
|
||||
test.beforeEach(({ skip }) => {
|
||||
skip.rtl();
|
||||
skip.mode('ios');
|
||||
});
|
||||
|
||||
test(`should fire hooks using web animations`, async ({ page }) => {
|
||||
await page.goto('/src/utils/animation/test/hooks');
|
||||
await testHooks(page);
|
||||
});
|
||||
|
||||
test(`should fire hooks using css animations`, async ({ page }) => {
|
||||
await page.goto('/src/utils/animation/test/hooks?ionic:_forceCSSAnimations=true');
|
||||
await testHooks(page);
|
||||
});
|
||||
});
|
||||
|
||||
const testHooks = async (page: E2EPage) => {
|
||||
const square = page.locator('.square-a');
|
||||
const ionAnimationFinished = await page.spyOnEvent('ionAnimationFinished');
|
||||
const beforeRead = await page.spyOnEvent('beforeRead');
|
||||
const beforeWrite = await page.spyOnEvent('beforeWrite');
|
||||
const afterRead = await page.spyOnEvent('afterRead');
|
||||
const afterWrite = await page.spyOnEvent('afterWrite');
|
||||
|
||||
// Test initial classes
|
||||
await expect(square).toHaveClass(/hello-world/);
|
||||
await expect(square).not.toHaveClass(/test-class/);
|
||||
|
||||
// Test initial styles
|
||||
await expect(square).toHaveCSS('padding-bottom', '20px');
|
||||
await expect(square).toHaveCSS('color', 'rgb(0, 0, 0)');
|
||||
|
||||
await page.click('.play');
|
||||
|
||||
// Test beforeRemoveClass and beforeAddClass
|
||||
await expect(square).not.toHaveClass(/hello-world/);
|
||||
await expect(square).toHaveClass(/test-class/);
|
||||
|
||||
// Test beforeStyles and beforeClearStyles
|
||||
await expect(square).toHaveCSS('padding-bottom', '0px');
|
||||
await expect(square).toHaveCSS('color', 'rgb(128, 0, 128)');
|
||||
|
||||
await beforeRead.next();
|
||||
await beforeWrite.next();
|
||||
|
||||
await ionAnimationFinished.next();
|
||||
|
||||
await afterRead.next();
|
||||
await afterWrite.next();
|
||||
|
||||
// Test afterRemoveClass and afterAddClass
|
||||
await expect(square).toHaveClass(/hello-world/);
|
||||
await expect(square).not.toHaveClass(/test-class/);
|
||||
|
||||
// Test afterStyles and afterClearStyles
|
||||
await expect(square).toHaveCSS('padding-bottom', '20px');
|
||||
await expect(square).toHaveCSS('color', 'rgb(0, 0, 0)');
|
||||
};
|
@ -1,146 +0,0 @@
|
||||
import { newE2EPage } from '@stencil/core/testing';
|
||||
|
||||
import { listenForEvent, waitForFunctionTestContext } from '../../../test/utils';
|
||||
|
||||
test(`animation:web: hooks`, async () => {
|
||||
const page = await newE2EPage({ url: '/src/utils/animation/test/hooks' });
|
||||
const screenshotCompares = [];
|
||||
|
||||
screenshotCompares.push(await page.compareScreenshot());
|
||||
|
||||
const square = await page.$('.square-a');
|
||||
|
||||
const styles = await getStyles(page, '.square-a');
|
||||
expect(styles.paddingBottom).toEqual('20px');
|
||||
expect(styles.color).toEqual('rgb(0, 0, 0)');
|
||||
|
||||
const classList = await getClassList(square);
|
||||
expect(classList.includes('hello-world')).toEqual(true);
|
||||
expect(classList.includes('test-class')).toEqual(false);
|
||||
|
||||
await waitForEventToBeCalled('afterWrite', page, square, async () => {
|
||||
await waitForEventToBeCalled('afterRead', page, square, async () => {
|
||||
await waitForEventToBeCalled('ionAnimationFinished', page, square, async () => {
|
||||
await waitForEventToBeCalled('beforeWrite', page, square, async () => {
|
||||
await waitForEventToBeCalled('beforeRead', page, square, async () => {
|
||||
await page.click('.play');
|
||||
await page.waitForSelector('.play');
|
||||
|
||||
// Test beforeRemoveClass and beforeAddClass
|
||||
const webClassListAgain = await getClassList(square);
|
||||
expect(webClassListAgain.includes('hello-world')).toEqual(false);
|
||||
expect(webClassListAgain.includes('test-class')).toEqual(true);
|
||||
|
||||
// Test beforeStyles and beforeClearStyles
|
||||
const webStylesAgain = await getStyles(page, '.square-a');
|
||||
expect(webStylesAgain.paddingBottom).toEqual('0px');
|
||||
expect(webStylesAgain.color).toEqual('rgb(128, 0, 128)');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Test afterRemoveClass and afterAddClass
|
||||
const classListAgain = await getClassList(square);
|
||||
expect(classListAgain.includes('hello-world')).toEqual(true);
|
||||
expect(classListAgain.includes('test-class')).toEqual(false);
|
||||
|
||||
// Test afterStyles and afterClearStyles
|
||||
const stylesAgain = await getStyles(page, '.square-a');
|
||||
expect(stylesAgain.paddingBottom).toEqual('20px');
|
||||
expect(stylesAgain.color).toEqual('rgb(0, 0, 0)');
|
||||
|
||||
screenshotCompares.push(await page.compareScreenshot('end animation'));
|
||||
});
|
||||
|
||||
test(`animation:css: hooks`, async () => {
|
||||
const page = await newE2EPage({ url: '/src/utils/animation/test/hooks?ionic:_forceCSSAnimations=true' });
|
||||
const screenshotCompares = [];
|
||||
|
||||
screenshotCompares.push(await page.compareScreenshot());
|
||||
|
||||
const square = await page.$('.square-a');
|
||||
|
||||
const styles = await getStyles(page, '.square-a');
|
||||
expect(styles.paddingBottom).toEqual('20px');
|
||||
expect(styles.color).toEqual('rgb(0, 0, 0)');
|
||||
|
||||
const classList = await getClassList(square);
|
||||
expect(classList.includes('hello-world')).toEqual(true);
|
||||
expect(classList.includes('test-class')).toEqual(false);
|
||||
|
||||
await waitForEventToBeCalled('afterWrite', page, square, async () => {
|
||||
await waitForEventToBeCalled('afterRead', page, square, async () => {
|
||||
await waitForEventToBeCalled('ionAnimationFinished', page, square, async () => {
|
||||
await waitForEventToBeCalled('beforeWrite', page, square, async () => {
|
||||
await waitForEventToBeCalled('beforeRead', page, square, async () => {
|
||||
await page.click('.play');
|
||||
await page.waitForSelector('.play');
|
||||
|
||||
// Test beforeRemoveClass and beforeAddClass
|
||||
const cssClassListAgain = await getClassList(square);
|
||||
expect(cssClassListAgain.includes('hello-world')).toEqual(false);
|
||||
expect(cssClassListAgain.includes('test-class')).toEqual(true);
|
||||
|
||||
// Test beforeStyles and beforeClearStyles
|
||||
const cssStylesAgain = await getStyles(page, '.square-a');
|
||||
expect(cssStylesAgain.paddingBottom).toEqual('0px');
|
||||
expect(cssStylesAgain.color).toEqual('rgb(128, 0, 128)');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Test afterRemoveClass and afterAddClass
|
||||
const classListAgain = await getClassList(square);
|
||||
expect(classListAgain.includes('hello-world')).toEqual(true);
|
||||
expect(classListAgain.includes('test-class')).toEqual(false);
|
||||
|
||||
// Test afterStyles and afterClearStyles
|
||||
const stylesAgain = await getStyles(page, '.square-a');
|
||||
expect(stylesAgain.paddingBottom).toEqual('20px');
|
||||
expect(stylesAgain.color).toEqual('rgb(0, 0, 0)');
|
||||
|
||||
screenshotCompares.push(await page.compareScreenshot('end animation'));
|
||||
});
|
||||
|
||||
const waitForEventToBeCalled = async (eventName: string, page: any, el: HTMLElement, fn: any, num = 1) => {
|
||||
const EVENT_FIRED = `on${eventName}`;
|
||||
const eventFiredCount: any = { count: 0 };
|
||||
await page.exposeFunction(EVENT_FIRED, () => {
|
||||
eventFiredCount.count += 1;
|
||||
});
|
||||
|
||||
await listenForEvent(page, eventName, el, EVENT_FIRED);
|
||||
|
||||
if (fn) {
|
||||
await fn();
|
||||
}
|
||||
|
||||
await waitForFunctionTestContext(
|
||||
(payload: any) => {
|
||||
return payload.eventFiredCount.count === payload.num;
|
||||
},
|
||||
{ eventFiredCount, num }
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = async (page: any, selector: string) => {
|
||||
return page.evaluate(
|
||||
(payload: any) => {
|
||||
const el = document.querySelector(payload.selector);
|
||||
|
||||
return JSON.parse(JSON.stringify(getComputedStyle(el)));
|
||||
},
|
||||
{ selector }
|
||||
);
|
||||
};
|
||||
|
||||
const getClassList = async (el: HTMLElement) => {
|
||||
const classListObject = await el.getProperty('classList');
|
||||
const jsonValue = await classListObject.jsonValue();
|
||||
|
||||
return Object.values(jsonValue);
|
||||
};
|
@ -34,19 +34,19 @@
|
||||
.beforeClearStyles(['padding-bottom'])
|
||||
.beforeAddRead(() => {
|
||||
const ev = new CustomEvent('beforeRead');
|
||||
squareA.dispatchEvent(ev);
|
||||
window.dispatchEvent(ev);
|
||||
})
|
||||
.beforeAddWrite(() => {
|
||||
const ev = new CustomEvent('beforeWrite');
|
||||
squareA.dispatchEvent(ev);
|
||||
window.dispatchEvent(ev);
|
||||
})
|
||||
.afterAddRead(() => {
|
||||
const ev = new CustomEvent('afterRead');
|
||||
squareA.dispatchEvent(ev);
|
||||
window.dispatchEvent(ev);
|
||||
})
|
||||
.afterAddWrite(() => {
|
||||
const ev = new CustomEvent('afterWrite');
|
||||
squareA.dispatchEvent(ev);
|
||||
window.dispatchEvent(ev);
|
||||
})
|
||||
.afterAddClass(['hello-world'])
|
||||
.afterRemoveClass(['test-class'])
|
||||
@ -60,7 +60,7 @@
|
||||
])
|
||||
.onFinish(() => {
|
||||
const ev = new CustomEvent('ionAnimationFinished');
|
||||
squareA.dispatchEvent(ev);
|
||||
window.dispatchEvent(ev);
|
||||
});
|
||||
|
||||
document.querySelector('.play').addEventListener('click', () => {
|
||||
|
46
core/src/utils/animation/test/multiple/animation.e2e.ts
Normal file
46
core/src/utils/animation/test/multiple/animation.e2e.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { test } from '@utils/test/playwright';
|
||||
import type { E2EPage } from '@utils/test/playwright';
|
||||
|
||||
test.describe('animation: multiple', async () => {
|
||||
test.beforeEach(({ skip }) => {
|
||||
skip.rtl();
|
||||
skip.mode('ios');
|
||||
});
|
||||
|
||||
test(`should resolve grouped animations using web animations`, async ({ page }) => {
|
||||
await page.goto('/src/utils/animation/test/multiple');
|
||||
await testMultiple(page);
|
||||
});
|
||||
|
||||
test(`should resolve grouped animations using css animations`, async ({ page }) => {
|
||||
await page.goto('/src/utils/animation/test/multiple?ionic:_forceCSSAnimations=true');
|
||||
await testMultiple(page);
|
||||
});
|
||||
});
|
||||
|
||||
const testMultiple = async (page: E2EPage) => {
|
||||
const ionAnimationFinished = await page.spyOnEvent('ionAnimationFinished');
|
||||
|
||||
await page.click('.play');
|
||||
|
||||
await ionAnimationFinished.next();
|
||||
await expect(ionAnimationFinished).toHaveReceivedEventDetail('AnimationCSubBFinished');
|
||||
|
||||
await ionAnimationFinished.next();
|
||||
await expect(ionAnimationFinished).toHaveReceivedEventDetail('AnimationBFinished');
|
||||
|
||||
await ionAnimationFinished.next();
|
||||
await expect(ionAnimationFinished).toHaveReceivedEventDetail('AnimationCSubAFinished');
|
||||
|
||||
await ionAnimationFinished.next();
|
||||
await expect(ionAnimationFinished).toHaveReceivedEventDetail('AnimationCFinished');
|
||||
|
||||
await ionAnimationFinished.next();
|
||||
await expect(ionAnimationFinished).toHaveReceivedEventDetail('AnimationAFinished');
|
||||
|
||||
await ionAnimationFinished.next();
|
||||
await expect(ionAnimationFinished).toHaveReceivedEventDetail('AnimationRootFinished');
|
||||
|
||||
await expect(ionAnimationFinished).toHaveReceivedEventTimes(6);
|
||||
};
|
@ -1,77 +0,0 @@
|
||||
import { newE2EPage } from '@stencil/core/testing';
|
||||
|
||||
import { listenForEvent, waitForFunctionTestContext } from '../../../test/utils';
|
||||
|
||||
test(`animation:web: multiple`, async () => {
|
||||
const page = await newE2EPage({ url: '/src/utils/animation/test/multiple' });
|
||||
const screenshotCompares = [];
|
||||
|
||||
screenshotCompares.push(await page.compareScreenshot());
|
||||
|
||||
const ANIMATION_FINISHED = 'onIonAnimationFinished';
|
||||
const animationStatus = [];
|
||||
await page.exposeFunction(ANIMATION_FINISHED, (ev: any) => {
|
||||
animationStatus.push(ev.detail);
|
||||
});
|
||||
|
||||
const squareA = await page.$('.square-a');
|
||||
await listenForEvent(page, 'ionAnimationFinished', squareA, ANIMATION_FINISHED);
|
||||
|
||||
await page.click('.play');
|
||||
await page.waitForSelector('.play');
|
||||
|
||||
await waitForFunctionTestContext(
|
||||
(payload: any) => {
|
||||
return (
|
||||
payload.animationStatus.join(', ') ===
|
||||
[
|
||||
'AnimationCSubBFinished',
|
||||
'AnimationBFinished',
|
||||
'AnimationCSubAFinished',
|
||||
'AnimationCFinished',
|
||||
'AnimationAFinished',
|
||||
'AnimationRootFinished',
|
||||
].join(', ')
|
||||
);
|
||||
},
|
||||
{ animationStatus }
|
||||
);
|
||||
screenshotCompares.push(await page.compareScreenshot('end animation'));
|
||||
});
|
||||
|
||||
test(`animation:css: multiple`, async () => {
|
||||
const page = await newE2EPage({ url: '/src/utils/animation/test/multiple?ionic:_forceCSSAnimations=true' });
|
||||
const screenshotCompares = [];
|
||||
|
||||
screenshotCompares.push(await page.compareScreenshot());
|
||||
|
||||
const ANIMATION_FINISHED = 'onIonAnimationFinished';
|
||||
const animationStatus = [];
|
||||
await page.exposeFunction(ANIMATION_FINISHED, (ev: any) => {
|
||||
animationStatus.push(ev.detail);
|
||||
});
|
||||
|
||||
const squareA = await page.$('.square-a');
|
||||
await listenForEvent(page, 'ionAnimationFinished', squareA, ANIMATION_FINISHED);
|
||||
|
||||
await page.click('.play');
|
||||
await page.waitForSelector('.play');
|
||||
|
||||
await waitForFunctionTestContext(
|
||||
(payload: any) => {
|
||||
return (
|
||||
payload.animationStatus.join(', ') ===
|
||||
[
|
||||
'AnimationCSubBFinished',
|
||||
'AnimationBFinished',
|
||||
'AnimationCSubAFinished',
|
||||
'AnimationCFinished',
|
||||
'AnimationAFinished',
|
||||
'AnimationRootFinished',
|
||||
].join(', ')
|
||||
);
|
||||
},
|
||||
{ animationStatus }
|
||||
);
|
||||
screenshotCompares.push(await page.compareScreenshot('end animation'));
|
||||
});
|
@ -56,7 +56,7 @@
|
||||
})
|
||||
.onFinish(() => {
|
||||
const ev = new CustomEvent('ionAnimationFinished', { detail: 'AnimationAFinished' });
|
||||
squareA.dispatchEvent(ev);
|
||||
window.dispatchEvent(ev);
|
||||
});
|
||||
|
||||
animationB
|
||||
@ -78,7 +78,7 @@
|
||||
})
|
||||
.onFinish(() => {
|
||||
const ev = new CustomEvent('ionAnimationFinished', { detail: 'AnimationBFinished' });
|
||||
squareA.dispatchEvent(ev);
|
||||
window.dispatchEvent(ev);
|
||||
});
|
||||
|
||||
animationC
|
||||
@ -100,7 +100,7 @@
|
||||
})
|
||||
.onFinish(() => {
|
||||
const ev = new CustomEvent('ionAnimationFinished', { detail: 'AnimationCFinished' });
|
||||
squareA.dispatchEvent(ev);
|
||||
window.dispatchEvent(ev);
|
||||
});
|
||||
|
||||
animationCSubA
|
||||
@ -110,12 +110,12 @@
|
||||
.fromTo('color', 'red', 'blue')
|
||||
.onFinish(() => {
|
||||
const ev = new CustomEvent('ionAnimationFinished', { detail: 'AnimationCSubAFinished' });
|
||||
squareA.dispatchEvent(ev);
|
||||
window.dispatchEvent(ev);
|
||||
});
|
||||
|
||||
animationCSubB.addElement(squareCSubText).onFinish(() => {
|
||||
const ev = new CustomEvent('ionAnimationFinished', { detail: 'AnimationCSubBFinished' });
|
||||
squareA.dispatchEvent(ev);
|
||||
window.dispatchEvent(ev);
|
||||
});
|
||||
|
||||
animationC.addAnimation([animationCSubA, animationCSubB]);
|
||||
@ -125,7 +125,7 @@
|
||||
.fill('none')
|
||||
.onFinish(() => {
|
||||
const ev = new CustomEvent('ionAnimationFinished', { detail: 'AnimationRootFinished' });
|
||||
squareA.dispatchEvent(ev);
|
||||
window.dispatchEvent(ev);
|
||||
});
|
||||
|
||||
document.querySelector('.play').addEventListener('click', () => {
|
||||
|
@ -1,36 +0,0 @@
|
||||
import { newE2EPage } from '@stencil/core/testing';
|
||||
import { dragElementBy } from '@utils/test';
|
||||
|
||||
test('swipe to go back should complete', async () => {
|
||||
const page = await newE2EPage({ url: '/src/utils/gesture/test?ionic:mode=ios' });
|
||||
|
||||
const nav = await page.find('ion-nav');
|
||||
const ionNavDidChange = await nav.spyOnEvent('ionNavDidChange');
|
||||
|
||||
await page.click('.next');
|
||||
await ionNavDidChange.next();
|
||||
|
||||
const content = await page.$('.page-two-content');
|
||||
|
||||
const width = await page.evaluate(() => window.innerWidth);
|
||||
await dragElementBy(content, page, width, 0, { x: 25, y: 100 });
|
||||
|
||||
await ionNavDidChange.next();
|
||||
});
|
||||
|
||||
test('swipe to go back should complete in rtl', async () => {
|
||||
const page = await newE2EPage({ url: '/src/utils/gesture/test?rtl=true&ionic:mode=ios' });
|
||||
|
||||
const nav = await page.find('ion-nav');
|
||||
const ionNavDidChange = await nav.spyOnEvent('ionNavDidChange');
|
||||
|
||||
await page.click('.next');
|
||||
await ionNavDidChange.next();
|
||||
|
||||
const width = await page.evaluate(() => window.innerWidth);
|
||||
|
||||
const content = await page.$('.page-two-content');
|
||||
await dragElementBy(content, page, -width, 0, { x: width - 25, y: 100 });
|
||||
|
||||
await ionNavDidChange.next();
|
||||
});
|
@ -1,34 +0,0 @@
|
||||
import { newE2EPage } from '@stencil/core/testing';
|
||||
|
||||
test('framework-delegate: should present modal already at ion-app root', async () => {
|
||||
const page = await newE2EPage({ url: '/src/utils/test/framework-delegate?ionic:_testing=true' });
|
||||
|
||||
const button = await page.find('#button-inline-root');
|
||||
await button.click();
|
||||
|
||||
const modal = await page.find('#inline-root');
|
||||
expect(modal).not.toBe(null);
|
||||
await modal.waitForVisible();
|
||||
});
|
||||
|
||||
test('framework-delegate: should present modal in content', async () => {
|
||||
const page = await newE2EPage({ url: '/src/utils/test/framework-delegate?ionic:_testing=true' });
|
||||
|
||||
const button = await page.find('#button-inline-content');
|
||||
await button.click();
|
||||
|
||||
const modal = await page.find('#inline-content');
|
||||
expect(modal).not.toBe(null);
|
||||
await modal.waitForVisible();
|
||||
});
|
||||
|
||||
test('framework-delegate: should present modal via controller', async () => {
|
||||
const page = await newE2EPage({ url: '/src/utils/test/framework-delegate?ionic:_testing=true' });
|
||||
|
||||
const button = await page.find('#button-controller');
|
||||
await button.click();
|
||||
|
||||
const modal = await page.find('#controller');
|
||||
expect(modal).not.toBe(null);
|
||||
await modal.waitForVisible();
|
||||
});
|
@ -0,0 +1,40 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { test } from '@utils/test/playwright';
|
||||
|
||||
test.describe('framework-delegate', () => {
|
||||
test.beforeEach(async ({ page, skip }) => {
|
||||
skip.rtl();
|
||||
skip.mode('ios');
|
||||
|
||||
await page.goto('/src/utils/test/framework-delegate');
|
||||
});
|
||||
test('should present modal already at ion-app root', async ({ page }) => {
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
|
||||
await page.click('#button-inline-root');
|
||||
|
||||
const modal = page.locator('#inline-root');
|
||||
await ionModalDidPresent.next();
|
||||
await expect(modal).toBeVisible();
|
||||
});
|
||||
|
||||
test('should present modal in content', async ({ page }) => {
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
|
||||
await page.click('#button-inline-content');
|
||||
|
||||
const modal = page.locator('#inline-content');
|
||||
await ionModalDidPresent.next();
|
||||
await expect(modal).toBeVisible();
|
||||
});
|
||||
|
||||
test('should present modal via controller', async ({ page }) => {
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
|
||||
await page.click('#button-controller');
|
||||
|
||||
const modal = page.locator('#controller');
|
||||
await ionModalDidPresent.next();
|
||||
await expect(modal).toBeVisible();
|
||||
});
|
||||
});
|
@ -9,6 +9,7 @@
|
||||
/>
|
||||
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
|
||||
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
|
||||
<script src="../../../../../scripts/testing/scripts.js"></script>
|
||||
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
|
||||
<script type="module">
|
||||
import { modalController, createAnimation } from '../../../../../dist/ionic/index.esm.js';
|
||||
|
@ -1,169 +0,0 @@
|
||||
import { newE2EPage } from '@stencil/core/testing';
|
||||
|
||||
import { checkComponentModeClasses, checkModeClasses } from '../utils';
|
||||
|
||||
// This test is to loop through all components that should have
|
||||
// specific classes added and test them
|
||||
test('component: modes', async () => {
|
||||
const page = await newE2EPage({
|
||||
url: '/src/utils/test/modes?ionic:_testing=true',
|
||||
});
|
||||
|
||||
// First test: .button class
|
||||
// ----------------------------------------------------------------
|
||||
// components that need to have the `button` class
|
||||
// for use in styling by other components (`ion-buttons`)
|
||||
// e.g. <ion-back-button class="button">
|
||||
let tags = ['ion-button', 'ion-back-button', 'ion-menu-button'];
|
||||
|
||||
for (const tag of tags) {
|
||||
const el = await page.find(tag);
|
||||
expect(el).toHaveClass('button');
|
||||
}
|
||||
|
||||
// Second test: .item class
|
||||
// ----------------------------------------------------------------
|
||||
// components that need to have the `item` class
|
||||
// for use in styling by other components
|
||||
// e.g. <ion-item-divider class="item">
|
||||
tags = ['ion-item', 'ion-item-divider', 'ion-item-group'];
|
||||
|
||||
for (const tag of tags) {
|
||||
const el = await page.find(tag);
|
||||
expect(el).toHaveClass('item');
|
||||
}
|
||||
|
||||
// Third test: .{component}-{mode} class
|
||||
// ----------------------------------------------------------------
|
||||
// components that need to have their tag name
|
||||
// + mode as a class for internal styling
|
||||
// e.g. <ion-card-content class="card-content-md">
|
||||
tags = [
|
||||
'ion-card-content',
|
||||
'ion-footer',
|
||||
'ion-header',
|
||||
'ion-infinite-scroll-content',
|
||||
'ion-item-group',
|
||||
'ion-item-options',
|
||||
'ion-list',
|
||||
'ion-picker',
|
||||
'ion-refresher',
|
||||
'ion-slides',
|
||||
'ion-split-pane',
|
||||
];
|
||||
|
||||
const globalMode = await page.evaluate(() => document.documentElement.getAttribute('mode'));
|
||||
for (const tag of tags) {
|
||||
const el = await page.find(tag);
|
||||
await checkComponentModeClasses(el, globalMode!);
|
||||
}
|
||||
|
||||
// Fourth test: .{mode} class
|
||||
// ----------------------------------------------------------------
|
||||
// components that need to have the mode class
|
||||
// added for external / user styling
|
||||
// e.g. <ion-badge class="md">
|
||||
tags = [
|
||||
'ion-action-sheet',
|
||||
'ion-alert',
|
||||
'ion-app',
|
||||
'ion-avatar',
|
||||
'ion-back-button',
|
||||
'ion-backdrop',
|
||||
'ion-badge',
|
||||
'ion-button',
|
||||
'ion-buttons',
|
||||
'ion-card-content',
|
||||
'ion-card-header',
|
||||
'ion-card-subtitle',
|
||||
'ion-card-title',
|
||||
'ion-card',
|
||||
'ion-checkbox',
|
||||
'ion-chip',
|
||||
'ion-col',
|
||||
'ion-content',
|
||||
'ion-datetime',
|
||||
'ion-fab',
|
||||
'ion-fab-button',
|
||||
'ion-fab-list',
|
||||
'ion-footer',
|
||||
'ion-grid',
|
||||
'ion-header',
|
||||
'ion-icon',
|
||||
'ion-img',
|
||||
'ion-infinite-scroll',
|
||||
'ion-infinite-scroll-content',
|
||||
'ion-input',
|
||||
'ion-item',
|
||||
'ion-item-divider',
|
||||
'ion-item-group',
|
||||
'ion-item-option',
|
||||
'ion-item-options',
|
||||
'ion-item-sliding',
|
||||
'ion-label',
|
||||
'ion-list',
|
||||
'ion-list-header',
|
||||
'ion-loading',
|
||||
'ion-modal',
|
||||
'ion-menu',
|
||||
'ion-menu-button',
|
||||
'ion-menu-toggle',
|
||||
'ion-note',
|
||||
'ion-picker',
|
||||
'ion-picker-column',
|
||||
'ion-popover',
|
||||
'ion-progress-bar',
|
||||
'ion-radio',
|
||||
'ion-radio-group',
|
||||
'ion-range',
|
||||
'ion-refresher',
|
||||
'ion-refresher-content',
|
||||
'ion-reorder',
|
||||
'ion-reorder-group',
|
||||
'ion-ripple-effect',
|
||||
'ion-router-link',
|
||||
'ion-row',
|
||||
'ion-searchbar',
|
||||
'ion-segment',
|
||||
'ion-segment-button',
|
||||
'ion-select',
|
||||
'ion-select-option',
|
||||
'ion-select-popover',
|
||||
'ion-skeleton-text',
|
||||
'ion-slide',
|
||||
'ion-slides',
|
||||
'ion-spinner',
|
||||
'ion-split-pane',
|
||||
'ion-tab-bar',
|
||||
'ion-tab-button',
|
||||
'ion-text',
|
||||
'ion-textarea',
|
||||
'ion-thumbnail',
|
||||
'ion-title',
|
||||
'ion-toast',
|
||||
'ion-toggle',
|
||||
'ion-toolbar',
|
||||
];
|
||||
|
||||
for (const tag of tags) {
|
||||
await page.waitForSelector(tag);
|
||||
const el = await page.find(tag);
|
||||
await checkModeClasses(el, globalMode!);
|
||||
}
|
||||
|
||||
// Fifth test: {mode} attribute on non-ionic ancestor element
|
||||
// ----------------------------------------------------------------
|
||||
// non-ionic ancestor components with a mode attribute
|
||||
// e.g. <p mode="foo">
|
||||
const ancestorTags = ['p[mode]'];
|
||||
const childTag = 'ion-label';
|
||||
|
||||
for (const tag of ancestorTags) {
|
||||
await page.waitForSelector(tag);
|
||||
const ancestor = await page.find(tag);
|
||||
const mode = ancestor.getAttribute('mode');
|
||||
const expectedMode = ['ios', 'md'].includes(mode) ? mode : globalMode!;
|
||||
const el = await ancestor.find(childTag);
|
||||
await checkModeClasses(el, expectedMode);
|
||||
}
|
||||
});
|
@ -1,191 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Components - Modes</title>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
<link href="../../../../../css/ionic.bundle.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>
|
||||
</head>
|
||||
|
||||
<body onLoad="load()">
|
||||
<ion-app>
|
||||
<ion-split-pane content-id="main">
|
||||
<ion-menu content-id="main"> </ion-menu>
|
||||
|
||||
<div class="ion-page" id="main">
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Components: Modes</ion-title>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button></ion-back-button>
|
||||
<ion-button></ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content>
|
||||
<ion-action-sheet></ion-action-sheet>
|
||||
<ion-alert></ion-alert>
|
||||
<ion-avatar></ion-avatar>
|
||||
<ion-backdrop></ion-backdrop>
|
||||
<ion-badge></ion-badge>
|
||||
<ion-button></ion-button>
|
||||
<ion-card>
|
||||
<ion-card-header>
|
||||
<ion-card-subtitle></ion-card-subtitle>
|
||||
<ion-card-title></ion-card-title>
|
||||
</ion-card-header>
|
||||
<ion-card-content></ion-card-content>
|
||||
</ion-card>
|
||||
|
||||
<ion-checkbox></ion-checkbox>
|
||||
<ion-chip></ion-chip>
|
||||
<ion-col></ion-col>
|
||||
<ion-datetime></ion-datetime>
|
||||
<ion-fab-button></ion-fab-button>
|
||||
<ion-fab-list></ion-fab-list>
|
||||
<ion-fab></ion-fab>
|
||||
<ion-footer></ion-footer>
|
||||
<ion-grid></ion-grid>
|
||||
<ion-icon name="star"></ion-icon>
|
||||
<ion-img></ion-img>
|
||||
<ion-infinite-scroll></ion-infinite-scroll>
|
||||
<ion-infinite-scroll-content></ion-infinite-scroll-content>
|
||||
<ion-input></ion-input>
|
||||
<ion-item-divider></ion-item-divider>
|
||||
<ion-item-group></ion-item-group>
|
||||
<ion-item-option></ion-item-option>
|
||||
<ion-item-options></ion-item-options>
|
||||
<ion-item-sliding></ion-item-sliding>
|
||||
<ion-item></ion-item>
|
||||
<ion-label></ion-label>
|
||||
<ion-list></ion-list>
|
||||
<ion-list-header></ion-list-header>
|
||||
<ion-loading></ion-loading>
|
||||
<ion-menu-button></ion-menu-button>
|
||||
<ion-menu-toggle></ion-menu-toggle>
|
||||
<ion-modal></ion-modal>
|
||||
<ion-note></ion-note>
|
||||
<ion-picker></ion-picker>
|
||||
<ion-popover></ion-popover>
|
||||
<ion-progress-bar></ion-progress-bar>
|
||||
<ion-radio-group></ion-radio-group>
|
||||
<ion-radio></ion-radio>
|
||||
<ion-range></ion-range>
|
||||
<ion-refresher></ion-refresher>
|
||||
<ion-refresher-content></ion-refresher-content>
|
||||
<ion-reorder-group></ion-reorder-group>
|
||||
<ion-reorder></ion-reorder>
|
||||
<ion-ripple-effect></ion-ripple-effect>
|
||||
<ion-router-link></ion-router-link>
|
||||
<ion-row></ion-row>
|
||||
<ion-searchbar></ion-searchbar>
|
||||
<ion-segment-button></ion-segment-button>
|
||||
<ion-segment></ion-segment>
|
||||
<ion-select-option></ion-select-option>
|
||||
<ion-select-popover></ion-select-popover>
|
||||
<ion-select></ion-select>
|
||||
<ion-skeleton-text></ion-skeleton-text>
|
||||
<ion-slide></ion-slide>
|
||||
<ion-slides></ion-slides>
|
||||
<ion-spinner></ion-spinner>
|
||||
<ion-tab-bar></ion-tab-bar>
|
||||
<ion-tab-button></ion-tab-button>
|
||||
<ion-text></ion-text>
|
||||
<ion-textarea></ion-textarea>
|
||||
<ion-thumbnail></ion-thumbnail>
|
||||
<ion-title></ion-title>
|
||||
<ion-toast></ion-toast>
|
||||
<ion-toggle></ion-toggle>
|
||||
<ion-toolbar></ion-toolbar>
|
||||
<p mode="ios">
|
||||
<ion-label></ion-label>
|
||||
</p>
|
||||
<p mode="md">
|
||||
<ion-label></ion-label>
|
||||
</p>
|
||||
<p mode="foo">
|
||||
<ion-label></ion-label>
|
||||
</p>
|
||||
</ion-content>
|
||||
</div>
|
||||
</ion-split-pane>
|
||||
</ion-app>
|
||||
|
||||
<script>
|
||||
async function load() {
|
||||
const actionSheet = document.querySelector('ion-action-sheet');
|
||||
|
||||
actionSheet.buttons = [
|
||||
{
|
||||
text: 'Delete',
|
||||
role: 'destructive',
|
||||
icon: 'trash',
|
||||
handler: () => {
|
||||
console.log('Delete clicked');
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Share',
|
||||
icon: 'share',
|
||||
handler: () => {
|
||||
console.log('Share clicked');
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Cancel',
|
||||
icon: 'close',
|
||||
role: 'cancel',
|
||||
handler: () => {
|
||||
console.log('Cancel clicked');
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
await actionSheet.present();
|
||||
|
||||
const picker = document.querySelector('ion-picker');
|
||||
if (picker.componentOnReady) {
|
||||
await picker.componentOnReady();
|
||||
}
|
||||
|
||||
picker.buttons = [{ text: 'Cancel', role: 'cancel' }, { text: 'Done' }];
|
||||
|
||||
picker.columns = [
|
||||
{
|
||||
name: 'month',
|
||||
align: 'left',
|
||||
selectedIndex: 0,
|
||||
options: [
|
||||
{
|
||||
text: 'June',
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
text: 'July',
|
||||
value: 1,
|
||||
},
|
||||
],
|
||||
prevSelected: 0,
|
||||
selectedIndex: 0,
|
||||
optionsWidth: '68px',
|
||||
columnWidth: '200px',
|
||||
prefixWidth: '100px',
|
||||
suffixWidth: '100px',
|
||||
prefix: undefined,
|
||||
suffix: undefined,
|
||||
},
|
||||
];
|
||||
|
||||
await picker.present();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -1,178 +0,0 @@
|
||||
import { newE2EPage } from '@stencil/core/testing';
|
||||
|
||||
import { getActiveElementParent } from '../utils';
|
||||
|
||||
test('overlays: hardware back button: should dismiss a presented overlay', async () => {
|
||||
const page = await newE2EPage({ url: '/src/utils/test/overlays?ionic:_testing=true' });
|
||||
|
||||
const createAndPresentButton = await page.find('#create-and-present');
|
||||
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss');
|
||||
|
||||
await createAndPresentButton.click();
|
||||
const modal = await page.find('ion-modal');
|
||||
expect(modal).not.toBe(null);
|
||||
|
||||
await ionModalDidPresent.next();
|
||||
|
||||
const simulateButton = await modal.find('#modal-simulate');
|
||||
expect(simulateButton).not.toBe(null);
|
||||
|
||||
await simulateButton.click();
|
||||
|
||||
await ionModalDidDismiss.next();
|
||||
|
||||
await page.waitForSelector('ion-modal', { hidden: true });
|
||||
});
|
||||
|
||||
test('overlays: hardware back button: should dismiss the presented overlay, even though another hidden modal was added last', async () => {
|
||||
const page = await newE2EPage({ url: '/src/utils/test/overlays?ionic:_testing=true' });
|
||||
|
||||
const createAndPresentButton = await page.find('#create-and-present');
|
||||
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
|
||||
await createAndPresentButton.click();
|
||||
const modal = await page.find('ion-modal');
|
||||
expect(modal).not.toBe(null);
|
||||
|
||||
await ionModalDidPresent.next();
|
||||
|
||||
const createButton = await page.find('#modal-create');
|
||||
await createButton.click();
|
||||
|
||||
const modals = await page.$$('ion-modal');
|
||||
expect(modals.length).toEqual(2);
|
||||
|
||||
expect(await modals[0].evaluate((node) => node.classList.contains('overlay-hidden'))).toEqual(false);
|
||||
expect(await modals[1].evaluate((node) => node.classList.contains('overlay-hidden'))).toEqual(true);
|
||||
|
||||
const simulateButton = await modal.find('#modal-simulate');
|
||||
expect(simulateButton).not.toBe(null);
|
||||
|
||||
await simulateButton.click();
|
||||
|
||||
expect(await modals[0].evaluate((node) => node.classList.contains('overlay-hidden'))).toEqual(true);
|
||||
expect(await modals[1].evaluate((node) => node.classList.contains('overlay-hidden'))).toEqual(true);
|
||||
});
|
||||
|
||||
test('overlays: Esc: should dismiss a presented overlay', async () => {
|
||||
const page = await newE2EPage({ url: '/src/utils/test/overlays?ionic:_testing=true' });
|
||||
|
||||
const createAndPresentButton = await page.find('#create-and-present');
|
||||
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss');
|
||||
|
||||
await createAndPresentButton.click();
|
||||
const modal = await page.find('ion-modal');
|
||||
expect(modal).not.toBe(null);
|
||||
|
||||
await ionModalDidPresent.next();
|
||||
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
await ionModalDidDismiss.next();
|
||||
|
||||
await page.waitForSelector('ion-modal', { hidden: true });
|
||||
});
|
||||
|
||||
test('overlays: Esc: should dismiss the presented overlay, even though another hidden modal was added last', async () => {
|
||||
const page = await newE2EPage({ url: '/src/utils/test/overlays?ionic:_testing=true' });
|
||||
|
||||
const createAndPresentButton = await page.find('#create-and-present');
|
||||
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
|
||||
await createAndPresentButton.click();
|
||||
const modal = await page.find('ion-modal');
|
||||
expect(modal).not.toBe(null);
|
||||
|
||||
await ionModalDidPresent.next();
|
||||
|
||||
const createButton = await page.find('#modal-create');
|
||||
await createButton.click();
|
||||
|
||||
const modals = await page.$$('ion-modal');
|
||||
expect(modals.length).toEqual(2);
|
||||
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
await page.waitForSelector('ion-modal#ion-overlay-1', { hidden: true });
|
||||
});
|
||||
|
||||
test('overlays: Nested: should dismiss the top overlay', async () => {
|
||||
const page = await newE2EPage({ url: '/src/utils/test/overlays?ionic:_testing=true' });
|
||||
|
||||
const createNestedButton = await page.find('#create-nested');
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
|
||||
await createNestedButton.click();
|
||||
|
||||
await ionModalDidPresent.next();
|
||||
|
||||
const modal = await page.find('ion-modal');
|
||||
expect(modal).not.toBe(null);
|
||||
|
||||
const dismissNestedOverlayButton = await page.find('#dismiss-modal-nested-overlay');
|
||||
const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss');
|
||||
|
||||
await dismissNestedOverlayButton.click();
|
||||
|
||||
await ionModalDidDismiss.next();
|
||||
|
||||
const modals = await page.$$('ion-modal');
|
||||
expect(modals.length).toEqual(0);
|
||||
});
|
||||
|
||||
test('toast should not cause focus trapping', async () => {
|
||||
const page = await newE2EPage({ url: '/src/utils/test/overlays?ionic:_testing=true' });
|
||||
const ionToastDidPresent = await page.spyOnEvent('ionToastDidPresent');
|
||||
|
||||
await page.click('#create-and-present-toast');
|
||||
await ionToastDidPresent.next();
|
||||
|
||||
await page.click('#root-input');
|
||||
|
||||
const parentEl = await getActiveElementParent(page);
|
||||
expect(parentEl.id).toEqual('root-input');
|
||||
});
|
||||
|
||||
test('toast should not cause focus trapping even when opened from a focus trapping overlay', async () => {
|
||||
const page = await newE2EPage({ url: '/src/utils/test/overlays?ionic:_testing=true' });
|
||||
const ionToastDidPresent = await page.spyOnEvent('ionToastDidPresent');
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
|
||||
await page.click('#create-and-present');
|
||||
await ionModalDidPresent.next();
|
||||
|
||||
await page.click('#modal-toast');
|
||||
await ionToastDidPresent.next();
|
||||
|
||||
await page.click('.modal-input');
|
||||
|
||||
const parentEl = await getActiveElementParent(page);
|
||||
expect(parentEl.className).toContain('modal-input-0');
|
||||
});
|
||||
|
||||
test('focus trapping should only run on the top-most overlay', async () => {
|
||||
const page = await newE2EPage({ url: '/src/utils/test/overlays?ionic:_testing=true' });
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
|
||||
await page.click('#create-and-present');
|
||||
await ionModalDidPresent.next();
|
||||
|
||||
await page.click('.modal-0 .modal-input');
|
||||
|
||||
const parentEl = await getActiveElementParent(page);
|
||||
expect(parentEl.className).toContain('modal-input-0');
|
||||
|
||||
await page.click('#modal-create-and-present');
|
||||
await ionModalDidPresent.next();
|
||||
|
||||
await page.click('.modal-1 .modal-input');
|
||||
|
||||
const parentElAgain = await getActiveElementParent(page);
|
||||
expect(parentElAgain.className).toContain('modal-input-1');
|
||||
});
|
@ -9,6 +9,7 @@
|
||||
/>
|
||||
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
|
||||
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
|
||||
<script src="../../../../../scripts/testing/scripts.js"></script>
|
||||
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
|
||||
<script type="module">
|
||||
import { modalController, toastController, createAnimation } from '../../../../../dist/ionic/index.esm.js';
|
||||
|
@ -1,6 +1,109 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { test } from '@utils/test/playwright';
|
||||
|
||||
test.describe('overlays: dismiss', () => {
|
||||
test.beforeEach(async ({ page, skip }) => {
|
||||
skip.rtl();
|
||||
|
||||
await page.goto('/src/utils/test/overlays');
|
||||
});
|
||||
test('hardware back button: should dismiss a presented overlay', async ({ page }) => {
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss');
|
||||
|
||||
await page.click('#create-and-present');
|
||||
|
||||
await ionModalDidPresent.next();
|
||||
|
||||
await page.click('#modal-simulate');
|
||||
|
||||
await ionModalDidDismiss.next();
|
||||
});
|
||||
|
||||
test('hardware back button: should dismiss the presented overlay, even though another hidden modal was added last', async ({
|
||||
page,
|
||||
}) => {
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss');
|
||||
|
||||
await page.click('#create-and-present');
|
||||
|
||||
await ionModalDidPresent.next();
|
||||
|
||||
await page.click('#modal-create');
|
||||
|
||||
const modals = page.locator('ion-modal');
|
||||
await expect(await modals.count()).toEqual(2);
|
||||
|
||||
await expect(await modals.nth(0)).not.toHaveClass(/overlay-hidden/);
|
||||
await expect(await modals.nth(1)).toHaveClass(/overlay-hidden/);
|
||||
|
||||
await page.click('#modal-simulate');
|
||||
|
||||
await ionModalDidDismiss.next();
|
||||
|
||||
await expect(await modals.count()).toEqual(1);
|
||||
await expect(await modals.nth(0)).toHaveClass(/overlay-hidden/);
|
||||
});
|
||||
|
||||
test('Esc: should dismiss a presented overlay', async ({ page }) => {
|
||||
const createAndPresentButton = page.locator('#create-and-present');
|
||||
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss');
|
||||
|
||||
await createAndPresentButton.click();
|
||||
|
||||
await ionModalDidPresent.next();
|
||||
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
await ionModalDidDismiss.next();
|
||||
});
|
||||
|
||||
test('Esc: should dismiss the presented overlay, even though another hidden modal was added last', async ({
|
||||
page,
|
||||
}) => {
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss');
|
||||
|
||||
await page.click('#create-and-present');
|
||||
|
||||
await ionModalDidPresent.next();
|
||||
|
||||
await page.click('#modal-create');
|
||||
|
||||
const modals = page.locator('ion-modal');
|
||||
await expect(await modals.count()).toEqual(2);
|
||||
|
||||
await expect(await modals.nth(0)).not.toHaveClass(/overlay-hidden/);
|
||||
await expect(await modals.nth(1)).toHaveClass(/overlay-hidden/);
|
||||
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
await ionModalDidDismiss.next();
|
||||
|
||||
await expect(await modals.count()).toEqual(1);
|
||||
await expect(await modals.nth(0)).toHaveClass(/overlay-hidden/);
|
||||
});
|
||||
|
||||
test('overlays: Nested: should dismiss the top overlay', async ({ page }) => {
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss');
|
||||
|
||||
await page.click('#create-nested');
|
||||
|
||||
await ionModalDidPresent.next();
|
||||
|
||||
await page.click('#dismiss-modal-nested-overlay');
|
||||
|
||||
await ionModalDidDismiss.next();
|
||||
|
||||
const modals = page.locator('ion-modal');
|
||||
expect(await modals.count()).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('overlays: focus', () => {
|
||||
test.beforeEach(({ skip }) => {
|
||||
skip.rtl();
|
||||
@ -116,4 +219,57 @@ test.describe('overlays: focus', () => {
|
||||
await page.keyboard.press(tabKey);
|
||||
await expect(disabledFalseButton).toBeFocused();
|
||||
});
|
||||
|
||||
test('toast should not cause focus trapping', async ({ page }) => {
|
||||
await page.goto('/src/utils/test/overlays');
|
||||
const ionToastDidPresent = await page.spyOnEvent('ionToastDidPresent');
|
||||
|
||||
await page.click('#create-and-present-toast');
|
||||
await ionToastDidPresent.next();
|
||||
|
||||
const input = page.locator('#root-input input');
|
||||
await input.click();
|
||||
|
||||
await expect(input).toBeFocused();
|
||||
});
|
||||
|
||||
test('toast should not cause focus trapping even when opened from a focus trapping overlay', async ({ page }) => {
|
||||
await page.goto('/src/utils/test/overlays');
|
||||
|
||||
const ionToastDidPresent = await page.spyOnEvent('ionToastDidPresent');
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
|
||||
await page.click('#create-and-present');
|
||||
await ionModalDidPresent.next();
|
||||
|
||||
await page.click('#modal-toast');
|
||||
await ionToastDidPresent.next();
|
||||
|
||||
const modalInput = page.locator('.modal-input input');
|
||||
await modalInput.click();
|
||||
|
||||
await expect(modalInput).toBeFocused();
|
||||
});
|
||||
|
||||
test('focus trapping should only run on the top-most overlay', async ({ page }) => {
|
||||
await page.goto('/src/utils/test/overlays');
|
||||
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
|
||||
await page.click('#create-and-present');
|
||||
await ionModalDidPresent.next();
|
||||
|
||||
const modalInputZero = page.locator('.modal-0 .modal-input input');
|
||||
await modalInputZero.click();
|
||||
|
||||
await expect(modalInputZero).toBeFocused();
|
||||
|
||||
await page.click('#modal-create-and-present');
|
||||
await ionModalDidPresent.next();
|
||||
|
||||
const modalInputOne = page.locator('.modal-1 .modal-input input');
|
||||
await modalInputOne.click();
|
||||
|
||||
await expect(modalInputOne).toBeFocused();
|
||||
});
|
||||
});
|
||||
|
@ -1,248 +0,0 @@
|
||||
import type { E2EElement, E2EPage } from '@stencil/core/testing';
|
||||
import type { ElementHandle, SerializableOrJSHandle } from 'puppeteer';
|
||||
|
||||
/**
|
||||
* page.evaluate can only return a serializable value,
|
||||
* so it is not possible to return the full element.
|
||||
* Instead, we return an object with some common
|
||||
* properties that you may want to access in a test.
|
||||
*/
|
||||
|
||||
const getSerialElement = async (page: E2EPage, element: SerializableOrJSHandle) => {
|
||||
return page.evaluate((el) => {
|
||||
const { className, tagName, id } = el;
|
||||
return {
|
||||
className,
|
||||
tagName,
|
||||
id,
|
||||
};
|
||||
}, element);
|
||||
};
|
||||
|
||||
export const getActiveElementParent = async (page: E2EPage) => {
|
||||
const activeElement = await page.evaluateHandle(() => document.activeElement!.parentElement);
|
||||
return getSerialElement(page, activeElement);
|
||||
};
|
||||
|
||||
export const getActiveElement = async (page: E2EPage) => {
|
||||
const activeElement = await page.evaluateHandle(() => document.activeElement);
|
||||
return getSerialElement(page, activeElement);
|
||||
};
|
||||
|
||||
export const generateE2EUrl = (component: string, type: string, rtl = false): string => {
|
||||
let url = `/src/components/${component}/test/${type}?ionic:_testing=true`;
|
||||
if (rtl) {
|
||||
url = `${url}&rtl=true`;
|
||||
}
|
||||
|
||||
return url;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the value of a property on an element
|
||||
*/
|
||||
export const getElementProperty = async (element: any, property: string): Promise<string> => {
|
||||
const getProperty = await element.getProperty(property);
|
||||
if (!getProperty) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return getProperty.jsonValue();
|
||||
};
|
||||
|
||||
/**
|
||||
* Listens for an event and fires a callback
|
||||
* @param page - The Puppeteer `page` object
|
||||
* @param eventType: string - The event name to listen for. ex: `ionPickerColChange`
|
||||
* @param element: HTMLElement - An HTML element
|
||||
* @param callbackName: string - The name of the callback function to
|
||||
* call when the event is fired.
|
||||
*
|
||||
* Note: The callback function must be added using
|
||||
* page.exposeFunction prior to calling this function.
|
||||
*/
|
||||
export const listenForEvent = async (
|
||||
page: any,
|
||||
eventType: string,
|
||||
element: any,
|
||||
callbackName: string
|
||||
): Promise<any> => {
|
||||
try {
|
||||
return await page.evaluate(
|
||||
(scopeEventType: string, scopeElement: any, scopeCallbackName: string) => {
|
||||
scopeElement.addEventListener(scopeEventType, (e: any) => {
|
||||
(window as any)[scopeCallbackName]({ detail: e.detail });
|
||||
});
|
||||
},
|
||||
eventType,
|
||||
element,
|
||||
callbackName
|
||||
);
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Drags an element by (x, y) pixels
|
||||
* @param element: HTMLElement - The HTML Element to drag
|
||||
* @param page - The Puppeteer 'page' object
|
||||
* @param x: number - Amount to drag `element` by on the x-axis
|
||||
* @param y: number - Amount to drag `element` by on the y-axis
|
||||
* @param startCoordinates (optional) - Coordinates of where to start the drag
|
||||
* gesture. If not provided, the drag gesture will start in the middle of the
|
||||
* element.
|
||||
*/
|
||||
export const dragElementBy = async (
|
||||
element: ElementHandle<Element>,
|
||||
page: any,
|
||||
x = 0,
|
||||
y = 0,
|
||||
startCoordinates?: { x: number; y: number }
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const boundingBox = (await element.boundingBox())!;
|
||||
|
||||
const startX = startCoordinates?.x === undefined ? boundingBox.x + boundingBox.width / 2 : startCoordinates.x;
|
||||
const startY = startCoordinates?.y === undefined ? boundingBox.y + boundingBox.height / 2 : startCoordinates.y;
|
||||
|
||||
const midX = startX + x / 2;
|
||||
const midY = startY + y / 2;
|
||||
|
||||
const endX = startX + x;
|
||||
const endY = startY + y;
|
||||
|
||||
await page.mouse.move(startX, startY);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(midX, midY);
|
||||
await page.mouse.move(endX, endY);
|
||||
await page.mouse.up();
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Wait for a function to return true
|
||||
* This method runs in the context of the
|
||||
* test whereas page.waitForFunction runs in
|
||||
* the context of the browser
|
||||
* @param fn - The function to run
|
||||
* @param params: any - Any parameters that the fn needs
|
||||
* @param interval: number - Interval to run setInterval on
|
||||
*/
|
||||
export const waitForFunctionTestContext = async (fn: any, params: any, interval = 16): Promise<any> => {
|
||||
return new Promise<void>((resolve) => {
|
||||
const intervalId = setInterval(() => {
|
||||
if (fn(params)) {
|
||||
clearInterval(intervalId);
|
||||
return resolve();
|
||||
}
|
||||
}, interval);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Pierce through shadow roots
|
||||
* https://github.com/GoogleChrome/puppeteer/issues/858#issuecomment-359763824
|
||||
*/
|
||||
export const queryDeep = async (page: E2EPage, ...selectors: string[]): Promise<ElementHandle> => {
|
||||
const shadowSelectorFn = (el: Element, selector: string): Element | null =>
|
||||
el?.shadowRoot && el.shadowRoot.querySelector(selector);
|
||||
|
||||
// eslint-disable-next-line no-async-promise-executor
|
||||
return new Promise(async (resolve) => {
|
||||
const [firstSelector, ...restSelectors] = selectors;
|
||||
let parentElement = await page.$(firstSelector);
|
||||
|
||||
for (const selector of restSelectors) {
|
||||
parentElement = (await page.evaluateHandle(shadowSelectorFn, parentElement, selector)) as any;
|
||||
}
|
||||
|
||||
if (parentElement) {
|
||||
resolve(parentElement);
|
||||
}
|
||||
});
|
||||
};
|
||||
/**
|
||||
* Given an element and optional selector, use the selector if
|
||||
* it exists or get the node name of that element if not. Combine
|
||||
* with the current mode to verify the correct classes exist.
|
||||
*
|
||||
* @param el: E2EElement - The element to verify classes on
|
||||
* @param selector: string - A selector to use instead of the element tag name
|
||||
* @param globalMode: string - the global mode as a fallback
|
||||
*
|
||||
* Examples:
|
||||
* await checkComponentModeClasses(await page.find('ion-card-content'), globalMode)
|
||||
* => expect(el).toHaveClass(`card-content-{mode}`);
|
||||
*
|
||||
* await checkComponentModeClasses(await page.find('ion-card-content'), globalMode, 'some-class')
|
||||
* => expect(el).toHaveClass(`some-class-{mode}`);
|
||||
*/
|
||||
export const checkComponentModeClasses = async (el: E2EElement, globalMode: string, selector?: string) => {
|
||||
// If passed a selector to use, use that, else grab the nodeName
|
||||
// of the element and remove the ion prefix to get the class selector
|
||||
const component = selector !== undefined ? selector : el.nodeName.toLowerCase().replace('ion-', '');
|
||||
|
||||
const mode = (await el.getProperty('mode')) || globalMode;
|
||||
|
||||
expect(el).toHaveClass(`${component}-${mode}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Given an element, get the mode and verify it exists as a class
|
||||
*
|
||||
* @param el: E2EElement - the element to verify the mode class on
|
||||
* @param globalMode: string - the global mode as a fallback
|
||||
*/
|
||||
export const checkModeClasses = async (el: E2EElement, globalMode: string) => {
|
||||
const mode = (await el.getProperty('mode')) || globalMode;
|
||||
expect(el).toHaveClass(`${mode}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Scrolls to a specific x/y coordinate within a scroll container. Supports custom
|
||||
* method for `ion-content` implementations.
|
||||
*
|
||||
* @param page The Puppeteer page object
|
||||
* @param selector The element to scroll within.
|
||||
* @param x The x coordinate to scroll to.
|
||||
* @param y The y coordinate to scroll to.
|
||||
*/
|
||||
export const scrollTo = async (page: E2EPage, selector: string, x: number, y: number) => {
|
||||
await page.evaluate(async (selector) => {
|
||||
const el = document.querySelector<HTMLElement>(selector);
|
||||
if (el) {
|
||||
if (el.tagName === 'ION-CONTENT') {
|
||||
await (el as any).scrollToPoint(x, y);
|
||||
} else {
|
||||
el.scroll(x, y);
|
||||
}
|
||||
} else {
|
||||
console.error(`Unable to find element with selector: ${selector}`);
|
||||
}
|
||||
}, selector);
|
||||
};
|
||||
|
||||
/**
|
||||
* Scrolls to the bottom of a scroll container. Supports custom method for
|
||||
* `ion-content` implementations.
|
||||
*
|
||||
* @param page The Puppeteer page object
|
||||
* @param selector The element to scroll within.
|
||||
*/
|
||||
export const scrollToBottom = async (page: E2EPage, selector: string) => {
|
||||
await page.evaluate(async (elSelector) => {
|
||||
const el = document.querySelector<HTMLElement>(elSelector);
|
||||
if (el) {
|
||||
if (el.tagName === 'ION-CONTENT') {
|
||||
await (el as any).scrollToBottom();
|
||||
} else {
|
||||
el.scrollTop = el.scrollHeight;
|
||||
}
|
||||
} else {
|
||||
console.error(`Unable to find element with selector: ${elSelector}`);
|
||||
}
|
||||
}, selector);
|
||||
};
|
Reference in New Issue
Block a user