Files
Brandy Smith 05026c5a48 test(angular): migrate to playwright (#30594)
- Migrates Angular test app tests from Cypress to Playwright
- Resolves test TODOs with Angular 18
- Resolves nav TODO by adding in standalone components
- Updates browserslist to remove warnings

---------

Co-authored-by: Brandy Smith <6577830+brandyscarney@users.noreply.github.com>
2025-08-05 13:58:46 +00:00

162 lines
5.7 KiB
TypeScript

import { expect, type Page, type Locator } from '@playwright/test';
export async function testStack(page: Page, selector: string, expectedStack: string[]) {
const elements = page.locator(`${selector} > *`);
const count = await elements.count();
// Get the actual stack (tag names of child elements), filtering out non-components
const actualStack: string[] = [];
for (let i = 0; i < count; i++) {
const tagName = await elements.nth(i).evaluate(el => el.tagName.toLowerCase());
// Filter out non-component elements like 'slot', 'div', etc.
if (tagName.includes('-') || tagName.startsWith('app-') || tagName.startsWith('ion-')) {
actualStack.push(tagName);
}
}
// Compare the actual stack with the expected stack
expect(actualStack).toEqual(expectedStack);
}
export async function testLifeCycle(page: Page, selector: string, expectedCounts: Record<string, number>) {
await expect(page.locator(`${selector} #ngOnInit`)).toHaveText('1');
await expect(page.locator(`${selector} #ionViewWillEnter`)).toHaveText(expectedCounts.ionViewWillEnter.toString());
await expect(page.locator(`${selector} #ionViewDidEnter`)).toHaveText(expectedCounts.ionViewDidEnter.toString());
await expect(page.locator(`${selector} #ionViewWillLeave`)).toHaveText(expectedCounts.ionViewWillLeave.toString());
await expect(page.locator(`${selector} #ionViewDidLeave`)).toHaveText(expectedCounts.ionViewDidLeave.toString());
}
export async function ionPageVisible(page: Page, selector: string) {
await expect(page.locator(selector)).toBeVisible();
await expect(page.locator(selector)).toHaveClass(/ion-page/);
await expect(page.locator(selector)).not.toHaveClass(/ion-page-hidden/);
await expect(page.locator(selector)).not.toHaveClass(/ion-page-invisible/);
await expect(page.locator(selector)).not.toHaveAttribute('aria-hidden', 'true');
}
export async function ionPageHidden(page: Page, selector: string) {
await expect(page.locator(selector)).toHaveClass(/ion-page-hidden/);
await expect(page.locator(selector)).toHaveAttribute('aria-hidden', 'true');
}
export async function ionPageDoesNotExist(page: Page, selector: string) {
await expect(page.locator(selector)).toHaveCount(0);
}
export async function ionTabClick(page: Page, tabName: string) {
await page.locator(`ion-tab-button`).filter({ hasText: tabName }).click();
}
export async function testTabTitle(page: Page, title: string) {
const tab = await getSelectedTab(page);
await expect(tab.locator('ion-title')).toHaveText(title);
return getSelectedTab(page);
}
export async function getSelectedTab(page: Page): Promise<Locator> {
// Wait for tab navigation to stabilize
await page.waitForTimeout(200);
// Look for tabs that are visible and have content
const tabs = page.locator('ion-tabs ion-router-outlet > *:not(.ion-page-hidden)');
const count = await tabs.count();
// If there's only one tab, return it
if (count === 1) {
return tabs.first();
}
// If there are multiple tabs, find the one that's actually visible and has content
for (let i = 0; i < count; i++) {
const tab = tabs.nth(i);
const isVisible = await tab.isVisible();
if (isVisible) {
return tab;
}
}
// Fallback to the first tab if none are explicitly visible
return tabs.first();
}
export async function testState(page: Page, count: number, tab: string) {
await expect(page.locator('#tabs-state')).toHaveText(`${count}.${tab}`);
}
export async function testUrlContains(page: Page, urlFragment: string) {
// Escape special regex characters in the URL fragment
const escapedFragment = urlFragment.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
await expect(page).toHaveURL(new RegExp(escapedFragment));
}
export async function testUrlEquals(page: Page, url: string) {
await expect(page).toHaveURL(url);
}
export async function testForward(page: Page) {
// Wait for navigation to complete
await page.waitForTimeout(100);
await testStack(page, 'ion-router-outlet', ['app-router-link', 'app-router-link-page']);
await testLifeCycle(page, 'app-router-link-page', {
ionViewWillEnter: 1,
ionViewDidEnter: 1,
ionViewWillLeave: 0,
ionViewDidLeave: 0,
});
await expect(page.locator('app-router-link-page #canGoBack')).toHaveText('true');
await page.goBack();
await page.waitForTimeout(100);
await testStack(page, 'ion-router-outlet', ['app-router-link']);
await testLifeCycle(page, 'app-router-link', {
ionViewWillEnter: 2,
ionViewDidEnter: 2,
ionViewWillLeave: 1,
ionViewDidLeave: 1,
});
}
export async function testRoot(page: Page) {
await testStack(page, 'ion-router-outlet', ['app-router-link-page']);
await testLifeCycle(page, 'app-router-link-page', {
ionViewWillEnter: 1,
ionViewDidEnter: 1,
ionViewWillLeave: 0,
ionViewDidLeave: 0,
});
await expect(page.locator('app-router-link-page #canGoBack')).toHaveText('false');
await page.goBack();
await page.waitForTimeout(100);
await testStack(page, 'ion-router-outlet', ['app-router-link']);
await testLifeCycle(page, 'app-router-link', {
ionViewWillEnter: 1,
ionViewDidEnter: 1,
ionViewWillLeave: 0,
ionViewDidLeave: 0,
});
}
export async function testBack(page: Page) {
await testStack(page, 'ion-router-outlet', ['app-router-link-page']);
await testLifeCycle(page, 'app-router-link-page', {
ionViewWillEnter: 1,
ionViewDidEnter: 1,
ionViewWillLeave: 0,
ionViewDidLeave: 0,
});
await expect(page.locator('app-router-link-page #canGoBack')).toHaveText('false');
await page.goBack();
await page.waitForTimeout(100);
await testStack(page, 'ion-router-outlet', ['app-router-link']);
await testLifeCycle(page, 'app-router-link', {
ionViewWillEnter: 1,
ionViewDidEnter: 1,
ionViewWillLeave: 0,
ionViewDidLeave: 0,
});
}