test(utils): migrate utils e2e tests to playwright (#26377)

This commit is contained in:
Liam DeBeasi
2022-11-30 15:53:25 -05:00
committed by GitHub
parent 13fe669dc1
commit db4cdeb444
36 changed files with 485 additions and 1480 deletions

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@ -37,7 +37,7 @@
])
.onFinish(() => {
const ev = new CustomEvent('ionAnimationFinished');
squareA.dispatchEvent(ev);
window.dispatchEvent(ev);
});
document.querySelector('.play').addEventListener('click', () => {

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

View File

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

View File

@ -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', () => {

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

View File

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

View File

@ -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', () => {

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

View File

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

View File

@ -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', () => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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>

View File

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

View File

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

View File

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

View File

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