test(utils): migrate to generators (#27383)

Issue number: N/A

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying. -->

Animation, framework delegate, and overlay tests are using legacy syntax

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

- Animation, framework delegate, and overlay tests are using generator
syntax


513e850842

The overlay focus tests do not vary across modes, so I removed the extra
checks.

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->
This commit is contained in:
Liam DeBeasi
2023-05-04 11:49:46 -04:00
committed by GitHub
parent 1113bd8f4a
commit 14e000db76
15 changed files with 609 additions and 617 deletions

View File

@ -1,43 +0,0 @@
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

@ -0,0 +1,38 @@
import type { E2EPage } from '@utils/test/playwright';
import { configs, test } from '@utils/test/playwright';
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('animation: animationbuilder'), async () => {
test('backwards-compatibility animation', async ({ page }) => {
await page.goto('/src/utils/animation/test/animationbuilder', config);
await testNavigation(page);
});
test('ios-transition web', async ({ page }) => {
await page.goto('/src/utils/animation/test/animationbuilder', config);
await testNavigation(page);
});
test('ios-transition css', async ({ page }) => {
await page.goto('/src/utils/animation/test/animationbuilder?ionic:_forceCSSAnimations=true', config);
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,27 +0,0 @@
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

@ -0,0 +1,24 @@
import { configs, test } from '@utils/test/playwright';
import type { E2EPage } from '@utils/test/playwright';
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('animation: basic'), async () => {
test(`should resolve using web animations`, async ({ page }) => {
await page.goto('/src/utils/animation/test/basic', config);
await testPage(page);
});
test(`should resolve using css animations`, async ({ page }) => {
await page.goto('/src/utils/animation/test/basic?ionic:_forceCSSAnimations=true', config);
await testPage(page);
});
});
});
const testPage = async (page: E2EPage) => {
const ionAnimationFinished = await page.spyOnEvent('ionAnimationFinished');
await page.click('.play');
await ionAnimationFinished.next();
};

View File

@ -1,21 +1,18 @@
import { expect } from '@playwright/test';
import { test } from '@utils/test/playwright';
import { configs, 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');
});
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('animation: display'), async () => {
test(`should resolve using web animations`, async ({ page }) => {
await page.goto('/src/utils/animation/test/display', config);
await testDisplay(page);
});
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);
test(`should resolve using css animations`, async ({ page }) => {
await page.goto('/src/utils/animation/test/display?ionic:_forceCSSAnimations=true', config);
await testDisplay(page);
});
});
});

View File

@ -1,21 +1,18 @@
import { expect } from '@playwright/test';
import { test } from '@utils/test/playwright';
import { configs, 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');
});
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('animation: hooks'), async () => {
test(`should fire hooks using web animations`, async ({ page }) => {
await page.goto('/src/utils/animation/test/hooks', config);
await testHooks(page);
});
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);
test(`should fire hooks using css animations`, async ({ page }) => {
await page.goto('/src/utils/animation/test/hooks?ionic:_forceCSSAnimations=true', config);
await testHooks(page);
});
});
});

View File

@ -1,24 +1,21 @@
import { expect } from '@playwright/test';
import { test } from '@utils/test/playwright';
import { configs, 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');
});
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('animation: multiple'), async () => {
test(`should resolve grouped animations using web animations`, async ({ page }) => {
await page.goto('/src/utils/animation/test/multiple', config);
await testMultiple(page);
});
test(`should resolve grouped animations using web animations`, async ({ page }) => {
await page.goto('/src/utils/animation/test/multiple');
await testMultiple(page);
});
/**
* CSS animations will occasionally resolve out of order, so we skip for now
*/
test.skip(`should resolve grouped animations using css animations`, async ({ page }) => {
await page.goto('/src/utils/animation/test/multiple?ionic:_forceCSSAnimations=true');
await testMultiple(page);
/**
* CSS animations will occasionally resolve out of order, so we skip for now
*/
test.skip(`should resolve grouped animations using css animations`, async ({ page }) => {
await page.goto('/src/utils/animation/test/multiple?ionic:_forceCSSAnimations=true', config);
await testMultiple(page);
});
});
});

View File

@ -1,180 +0,0 @@
import { expect } from '@playwright/test';
import type { Locator } from '@playwright/test';
import { KeyboardResize } from '@utils/native/keyboard';
import type { E2EPage } from '@utils/test/playwright';
import { test } from '@utils/test/playwright';
const getScrollPosition = async (contentEl: Locator) => {
return await contentEl.evaluate(async (el: HTMLIonContentElement) => {
const scrollEl = await el.getScrollElement();
return scrollEl.scrollTop;
});
};
// TODO FW-3427
test.describe.skip('scroll-assist', () => {
let scrollAssistFixture: ScrollAssistFixture;
test.beforeEach(async ({ page, skip }) => {
test.slow();
skip.rtl();
skip.mode('md', 'Scroll utils are only needed on iOS mode');
skip.browser('firefox');
skip.browser('chromium');
scrollAssistFixture = new ScrollAssistFixture(page);
});
test.describe('scroll-assist: basic functionality', () => {
test.beforeEach(async () => {
await scrollAssistFixture.goto();
});
test('should not activate when input is above the keyboard', async () => {
await scrollAssistFixture.expectNotToHaveScrollAssist(
'#input-above-keyboard',
'#input-above-keyboard input:not(.cloned-input)'
);
});
test('should activate when input is below the keyboard', async () => {
await scrollAssistFixture.expectToHaveScrollAssist(
'#input-below-keyboard',
'#input-below-keyboard input:not(.cloned-input)'
);
});
test('should activate even when not explicitly tapping input', async () => {
await scrollAssistFixture.expectToHaveScrollAssist(
'#item-below-keyboard ion-label',
'#input-below-keyboard input:not(.cloned-input)'
);
});
});
test.describe('scroll-assist: scroll-padding', () => {
test.describe('scroll-padding: browser/cordova', () => {
test.beforeEach(async () => {
await scrollAssistFixture.goto();
});
test('should add scroll padding for an input at the bottom of the scroll container', async () => {
await scrollAssistFixture.expectToHaveScrollPadding(
'#input-outside-viewport',
'#input-outside-viewport input:not(.cloned-input)'
);
});
test('should keep scroll padding even when switching between inputs', async () => {
await scrollAssistFixture.expectToHaveScrollPadding(
'#input-outside-viewport',
'#input-outside-viewport input:not(.cloned-input)'
);
await scrollAssistFixture.expectToHaveScrollPadding(
'#textarea-outside-viewport',
'#textarea-outside-viewport textarea:not(.cloned-input)'
);
});
});
test.describe('scroll-padding: webview resizing', () => {
test('should add scroll padding when webview resizing is "none"', async () => {
await scrollAssistFixture.goto(KeyboardResize.None);
await scrollAssistFixture.expectToHaveScrollPadding(
'#input-outside-viewport',
'#input-outside-viewport input:not(.cloned-input)'
);
});
test('should not add scroll padding when webview resizing is "body"', async () => {
await scrollAssistFixture.goto(KeyboardResize.Body);
await scrollAssistFixture.expectNotToHaveScrollPadding(
'#input-outside-viewport',
'#input-outside-viewport input:not(.cloned-input)'
);
});
test('should not add scroll padding when webview resizing is "ionic"', async () => {
await scrollAssistFixture.goto(KeyboardResize.Ionic);
await scrollAssistFixture.expectNotToHaveScrollPadding(
'#input-outside-viewport',
'#input-outside-viewport input:not(.cloned-input)'
);
});
test('should not add scroll padding when webview resizing is "native"', async () => {
await scrollAssistFixture.goto(KeyboardResize.Native);
await scrollAssistFixture.expectNotToHaveScrollPadding(
'#input-outside-viewport',
'#input-outside-viewport input:not(.cloned-input)'
);
});
});
});
});
class ScrollAssistFixture {
readonly page: E2EPage;
private content!: Locator;
constructor(page: E2EPage) {
this.page = page;
}
async goto(resizeMode?: KeyboardResize) {
let url = `/src/utils/input-shims/hacks/test`;
if (resizeMode !== undefined) {
url += `?resizeMode=${resizeMode}`;
}
await this.page.goto(url);
this.content = this.page.locator('ion-content');
}
private async focusInput(interactiveSelector: string, inputSelector: string) {
const { page } = this;
const interactive = page.locator(interactiveSelector);
const input = page.locator(inputSelector);
await interactive.click({ force: true });
await expect(input).toBeFocused();
await page.waitForChanges();
}
private getScrollPosition() {
const { content } = this;
return getScrollPosition(content);
}
async expectNotToHaveScrollAssist(interactiveSelector: string, inputSelector: string) {
await expect(await this.getScrollPosition()).toBe(0);
await this.focusInput(interactiveSelector, inputSelector);
await expect(await this.getScrollPosition()).toBe(0);
}
async expectToHaveScrollAssist(interactiveSelector: string, inputSelector: string) {
await expect(await this.getScrollPosition()).toBe(0);
await this.focusInput(interactiveSelector, inputSelector);
await expect(await this.getScrollPosition()).not.toBe(0);
}
async expectToHaveScrollPadding(interactiveSelector: string, inputSelector: string) {
const { content } = this;
await this.focusInput(interactiveSelector, inputSelector);
await expect(content).not.toHaveCSS('--keyboard-offset', '0px');
}
async expectNotToHaveScrollPadding(interactiveSelector: string, inputSelector: string) {
const { content } = this;
await this.focusInput(interactiveSelector, inputSelector);
await expect(content).toHaveCSS('--keyboard-offset', '0px');
}
}

View File

@ -0,0 +1,180 @@
import { expect } from '@playwright/test';
import type { Locator } from '@playwright/test';
import { KeyboardResize } from '@utils/native/keyboard';
import type { E2EPage, E2EPageOptions } from '@utils/test/playwright';
import { configs, test } from '@utils/test/playwright';
const getScrollPosition = async (contentEl: Locator) => {
return await contentEl.evaluate(async (el: HTMLIonContentElement) => {
const scrollEl = await el.getScrollElement();
return scrollEl.scrollTop;
});
};
// TODO FW-3427
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe.skip(title('scroll-assist'), () => {
let scrollAssistFixture: ScrollAssistFixture;
test.beforeEach(async ({ page, skip }) => {
test.slow();
skip.browser('firefox');
skip.browser('chromium');
scrollAssistFixture = new ScrollAssistFixture(page);
});
test.describe('scroll-assist: basic functionality', () => {
test.beforeEach(async () => {
await scrollAssistFixture.goto(config);
});
test('should not activate when input is above the keyboard', async () => {
await scrollAssistFixture.expectNotToHaveScrollAssist(
'#input-above-keyboard',
'#input-above-keyboard input:not(.cloned-input)'
);
});
test('should activate when input is below the keyboard', async () => {
await scrollAssistFixture.expectToHaveScrollAssist(
'#input-below-keyboard',
'#input-below-keyboard input:not(.cloned-input)'
);
});
test('should activate even when not explicitly tapping input', async () => {
await scrollAssistFixture.expectToHaveScrollAssist(
'#item-below-keyboard ion-label',
'#input-below-keyboard input:not(.cloned-input)'
);
});
});
test.describe('scroll-assist: scroll-padding', () => {
test.describe('scroll-padding: browser/cordova', () => {
test.beforeEach(async () => {
await scrollAssistFixture.goto(config);
});
test('should add scroll padding for an input at the bottom of the scroll container', async () => {
await scrollAssistFixture.expectToHaveScrollPadding(
'#input-outside-viewport',
'#input-outside-viewport input:not(.cloned-input)'
);
});
test('should keep scroll padding even when switching between inputs', async () => {
await scrollAssistFixture.expectToHaveScrollPadding(
'#input-outside-viewport',
'#input-outside-viewport input:not(.cloned-input)'
);
await scrollAssistFixture.expectToHaveScrollPadding(
'#textarea-outside-viewport',
'#textarea-outside-viewport textarea:not(.cloned-input)'
);
});
});
test.describe('scroll-padding: webview resizing', () => {
test('should add scroll padding when webview resizing is "none"', async () => {
await scrollAssistFixture.goto(config, KeyboardResize.None);
await scrollAssistFixture.expectToHaveScrollPadding(
'#input-outside-viewport',
'#input-outside-viewport input:not(.cloned-input)'
);
});
test('should not add scroll padding when webview resizing is "body"', async () => {
await scrollAssistFixture.goto(config, KeyboardResize.Body);
await scrollAssistFixture.expectNotToHaveScrollPadding(
'#input-outside-viewport',
'#input-outside-viewport input:not(.cloned-input)'
);
});
test('should not add scroll padding when webview resizing is "ionic"', async () => {
await scrollAssistFixture.goto(config, KeyboardResize.Ionic);
await scrollAssistFixture.expectNotToHaveScrollPadding(
'#input-outside-viewport',
'#input-outside-viewport input:not(.cloned-input)'
);
});
test('should not add scroll padding when webview resizing is "native"', async () => {
await scrollAssistFixture.goto(config, KeyboardResize.Native);
await scrollAssistFixture.expectNotToHaveScrollPadding(
'#input-outside-viewport',
'#input-outside-viewport input:not(.cloned-input)'
);
});
});
});
});
});
class ScrollAssistFixture {
readonly page: E2EPage;
private content!: Locator;
constructor(page: E2EPage) {
this.page = page;
}
async goto(config: E2EPageOptions, resizeMode?: KeyboardResize) {
let url = `/src/utils/input-shims/hacks/test`;
if (resizeMode !== undefined) {
url += `?resizeMode=${resizeMode}`;
}
await this.page.goto(url, config);
this.content = this.page.locator('ion-content');
}
private async focusInput(interactiveSelector: string, inputSelector: string) {
const { page } = this;
const interactive = page.locator(interactiveSelector);
const input = page.locator(inputSelector);
await interactive.click({ force: true });
await expect(input).toBeFocused();
await page.waitForChanges();
}
private getScrollPosition() {
const { content } = this;
return getScrollPosition(content);
}
async expectNotToHaveScrollAssist(interactiveSelector: string, inputSelector: string) {
await expect(await this.getScrollPosition()).toBe(0);
await this.focusInput(interactiveSelector, inputSelector);
await expect(await this.getScrollPosition()).toBe(0);
}
async expectToHaveScrollAssist(interactiveSelector: string, inputSelector: string) {
await expect(await this.getScrollPosition()).toBe(0);
await this.focusInput(interactiveSelector, inputSelector);
await expect(await this.getScrollPosition()).not.toBe(0);
}
async expectToHaveScrollPadding(interactiveSelector: string, inputSelector: string) {
const { content } = this;
await this.focusInput(interactiveSelector, inputSelector);
await expect(content).not.toHaveCSS('--keyboard-offset', '0px');
}
async expectNotToHaveScrollPadding(interactiveSelector: string, inputSelector: string) {
const { content } = this;
await this.focusInput(interactiveSelector, inputSelector);
await expect(content).toHaveCSS('--keyboard-offset', '0px');
}
}

View File

@ -1,28 +0,0 @@
import { expect } from '@playwright/test';
import { test } from '@utils/test/playwright';
// TODO FW-3010
test.describe.skip('tap click utility', () => {
test.beforeEach(({ skip }) => {
skip.rtl();
skip.mode('ios');
});
test('it should apply activated class when clicking element', async ({ page }) => {
await page.setContent(`
<ion-app>
<button class="ion-activatable ion-activatable-instant">Click Me</button>
</ion-app>
`);
const button = page.locator('button');
const box = await button.boundingBox()!;
if (box) {
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
await page.mouse.down();
await page.waitForChanges();
}
await expect(button).toHaveClass(/ion-activated/);
});
});

View File

@ -0,0 +1,30 @@
import { expect } from '@playwright/test';
import { configs, test } from '@utils/test/playwright';
// TODO FW-3010
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe.skip(title('tap click utility'), () => {
test('it should apply activated class when clicking element', async ({ page }) => {
await page.setContent(
`
<ion-app>
<button class="ion-activatable ion-activatable-instant">Click Me</button>
</ion-app>
`,
config
);
const button = page.locator('button');
const box = await button.boundingBox()!;
if (box) {
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
await page.mouse.down();
await page.waitForChanges();
}
await expect(button).toHaveClass(/ion-activated/);
});
});
});

View File

@ -1,40 +0,0 @@
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

@ -0,0 +1,39 @@
import { expect } from '@playwright/test';
import { configs, test } from '@utils/test/playwright';
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('framework-delegate'), () => {
test.beforeEach(async ({ page }) => {
await page.goto('/src/utils/test/framework-delegate', config);
});
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

@ -1,254 +0,0 @@
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);
});
});
// TODO FW-3536
test.describe.skip('overlays: focus', () => {
test.beforeEach(({ skip }) => {
skip.rtl();
});
test('should not select a hidden focusable element', async ({ page, browserName }) => {
await page.setContent(`
<style>
[hidden] {
display: none;
}
</style>
<ion-button id="open-modal">Show Modal</ion-button>
<ion-modal trigger="open-modal">
<ion-content>
<ion-button hidden id="hidden">Hidden Button</ion-button>
<ion-button id="visible">Visible Button</ion-button>
</ion-content>
</ion-modal>
`);
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
const presentButton = page.locator('ion-button#open-modal');
const visibleButton = page.locator('ion-button#visible');
const tabKey = browserName === 'webkit' ? 'Alt+Tab' : 'Tab';
await presentButton.click();
await ionModalDidPresent.next();
await page.keyboard.press(tabKey);
await expect(visibleButton).toBeFocused();
await page.keyboard.press(tabKey);
await expect(visibleButton).toBeFocused();
});
test('should not select a disabled focusable element', async ({ page, browserName }) => {
await page.setContent(`
<ion-button id="open-modal">Show Modal</ion-button>
<ion-modal trigger="open-modal">
<ion-content>
<ion-button disabled="true" id="disabled">Button</ion-button>
<ion-button id="active">Active Button</ion-button>
</ion-content>
</ion-modal>
`);
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
const presentButton = page.locator('ion-button#open-modal');
const activeButton = page.locator('ion-button#active');
const tabKey = browserName === 'webkit' ? 'Alt+Tab' : 'Tab';
await presentButton.click();
await ionModalDidPresent.next();
await page.keyboard.press(tabKey);
await expect(activeButton).toBeFocused();
await page.keyboard.press(tabKey);
await expect(activeButton).toBeFocused();
});
test('should select a focusable element with disabled="false"', async ({ page, browserName }) => {
await page.setContent(`
<ion-button id="open-modal">Show Modal</ion-button>
<ion-modal trigger="open-modal">
<ion-content>
<ion-button disabled="false" id="disabled-false">Button</ion-button>
<ion-button id="active">Active Button</ion-button>
</ion-content>
</ion-modal>
`);
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
const presentButton = page.locator('ion-button#open-modal');
const disabledFalseButton = page.locator('ion-button#disabled-false');
const activeButton = page.locator('ion-button#active');
const tabKey = browserName === 'webkit' ? 'Alt+Tab' : 'Tab';
await presentButton.click();
await ionModalDidPresent.next();
await page.keyboard.press(tabKey);
await expect(disabledFalseButton).toBeFocused();
await page.keyboard.press(tabKey);
await expect(activeButton).toBeFocused();
// Loop back to beginning of overlay
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

@ -0,0 +1,262 @@
import { expect } from '@playwright/test';
import { configs, test } from '@utils/test/playwright';
/**
* This behavior does not vary across modes/directions.
*/
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('overlays: dismiss'), () => {
test.beforeEach(async ({ page }) => {
await page.goto('/src/utils/test/overlays', config);
});
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);
});
});
// TODO FW-3536
test.describe.skip(title('overlays: focus'), () => {
test('should not select a hidden focusable element', async ({ page, browserName }) => {
await page.setContent(
`
<style>
[hidden] {
display: none;
}
</style>
<ion-button id="open-modal">Show Modal</ion-button>
<ion-modal trigger="open-modal">
<ion-content>
<ion-button hidden id="hidden">Hidden Button</ion-button>
<ion-button id="visible">Visible Button</ion-button>
</ion-content>
</ion-modal>
`,
config
);
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
const presentButton = page.locator('ion-button#open-modal');
const visibleButton = page.locator('ion-button#visible');
const tabKey = browserName === 'webkit' ? 'Alt+Tab' : 'Tab';
await presentButton.click();
await ionModalDidPresent.next();
await page.keyboard.press(tabKey);
await expect(visibleButton).toBeFocused();
await page.keyboard.press(tabKey);
await expect(visibleButton).toBeFocused();
});
test('should not select a disabled focusable element', async ({ page, browserName }) => {
await page.setContent(
`
<ion-button id="open-modal">Show Modal</ion-button>
<ion-modal trigger="open-modal">
<ion-content>
<ion-button disabled="true" id="disabled">Button</ion-button>
<ion-button id="active">Active Button</ion-button>
</ion-content>
</ion-modal>
`,
config
);
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
const presentButton = page.locator('ion-button#open-modal');
const activeButton = page.locator('ion-button#active');
const tabKey = browserName === 'webkit' ? 'Alt+Tab' : 'Tab';
await presentButton.click();
await ionModalDidPresent.next();
await page.keyboard.press(tabKey);
await expect(activeButton).toBeFocused();
await page.keyboard.press(tabKey);
await expect(activeButton).toBeFocused();
});
test('should select a focusable element with disabled="false"', async ({ page, browserName }) => {
await page.setContent(
`
<ion-button id="open-modal">Show Modal</ion-button>
<ion-modal trigger="open-modal">
<ion-content>
<ion-button disabled="false" id="disabled-false">Button</ion-button>
<ion-button id="active">Active Button</ion-button>
</ion-content>
</ion-modal>
`,
config
);
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
const presentButton = page.locator('ion-button#open-modal');
const disabledFalseButton = page.locator('ion-button#disabled-false');
const activeButton = page.locator('ion-button#active');
const tabKey = browserName === 'webkit' ? 'Alt+Tab' : 'Tab';
await presentButton.click();
await ionModalDidPresent.next();
await page.keyboard.press(tabKey);
await expect(disabledFalseButton).toBeFocused();
await page.keyboard.press(tabKey);
await expect(activeButton).toBeFocused();
// Loop back to beginning of overlay
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', config);
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', config);
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', config);
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();
});
});
});