Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions demo/e2e/alert-focus.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ for (const appearance of ['light', 'dark'] as const) {
await page.emulateMedia({ colorScheme: appearance });
await page.setViewportSize({ width: 411, height: 891 });
await page.goto('/main/index/alert?type=preferred');
await page.locator('html').evaluate((root, appearance) => {
root.classList.toggle('ion-palette-dark', appearance === 'dark');
root.style.colorScheme = appearance;
}, appearance);
await expect(page.locator('html')).toHaveCSS('color-scheme', appearance);
await expect
.poll(() => page.locator('html').evaluate((root) => root.classList.contains('ion-palette-dark')))
.toBe(appearance === 'dark');
const alert = page.locator('ion-alert');
const wrapper = alert.locator('.alert-wrapper');
await expect(wrapper).toBeFocused();
Expand Down
24 changes: 24 additions & 0 deletions demo/e2e/android-settings-regressions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ for (const mode of ['light', 'dark'] as const) {
await page.emulateMedia({ colorScheme: mode });
await page.setViewportSize({ width: 411, height: 900 });
await page.goto('/main/index/item-list');
await page.locator('html').evaluate((root, appearance) => {
root.classList.toggle('ion-palette-dark', appearance === 'dark');
root.style.colorScheme = appearance;
}, mode);
await expect(page.locator('html')).toHaveCSS('color-scheme', mode);
await expect.poll(() => page.locator('html').evaluate((root) => root.classList.contains('ion-palette-dark'))).toBe(mode === 'dark');
const label = page.locator('app-item-list ion-list.list-inset').nth(2).locator('ion-item').last().locator('ion-label');
const lines = await label.evaluate((element) => {
const range = document.createRange();
Expand All @@ -54,6 +60,12 @@ for (const mode of ['light', 'dark'] as const) {
.toBe('radial-gradient(20px, rgba(0, 0, 0, 0) 99%, rgba(0, 0, 0, 0) 100%)');

await page.goto('/main/index/accordion');
await page.locator('html').evaluate((root, appearance) => {
root.classList.toggle('ion-palette-dark', appearance === 'dark');
root.style.colorScheme = appearance;
}, mode);
await expect(page.locator('html')).toHaveCSS('color-scheme', mode);
await expect.poll(() => page.locator('html').evaluate((root) => root.classList.contains('ion-palette-dark'))).toBe(mode === 'dark');
const accordions = page.locator('app-accordion ion-accordion');
await expect(accordions.first()).toHaveClass(/hydrated/);
expect(await accordions.first().evaluate((element) => getComputedStyle(element).marginBottom)).toBe('2px');
Expand All @@ -67,6 +79,12 @@ for (const mode of ['light', 'dark'] as const) {
test(`${mode}: toggles are icon-free unless Ionic on/off labels are explicitly enabled`, async ({ page }) => {
await page.emulateMedia({ colorScheme: mode });
await page.goto('/main/index/toggle');
await page.locator('html').evaluate((root, appearance) => {
root.classList.toggle('ion-palette-dark', appearance === 'dark');
root.style.colorScheme = appearance;
}, mode);
await expect(page.locator('html')).toHaveCSS('color-scheme', mode);
await expect.poll(() => page.locator('html').evaluate((root) => root.classList.contains('ion-palette-dark'))).toBe(mode === 'dark');
for (const control of await page.locator('app-toggle ion-toggle:not([enable-on-off-labels])').all()) {
await expect(control).toHaveClass(/hydrated/);
const handle = control.locator('[part~="handle"]');
Expand Down Expand Up @@ -98,6 +116,12 @@ for (const mode of ['light', 'dark'] as const) {
test(`${mode}: range cutout follows its Ionic container`, async ({ page }) => {
await page.emulateMedia({ colorScheme: mode });
await page.goto('/main/index/range');
await page.locator('html').evaluate((root, appearance) => {
root.classList.toggle('ion-palette-dark', appearance === 'dark');
root.style.colorScheme = appearance;
}, mode);
await expect(page.locator('html')).toHaveCSS('color-scheme', mode);
await expect.poll(() => page.locator('html').evaluate((root) => root.classList.contains('ion-palette-dark'))).toBe(mode === 'dark');
const range = page.locator('app-range ion-range').nth(3);
await expect(range).toHaveClass(/hydrated/);
await range.evaluate((element) => {
Expand Down
10 changes: 5 additions & 5 deletions demo/e2e/android17-geometry.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect, test } from '@playwright/test';
import { mockDeviceColors, expectDeviceColors } from './device-colors';
import { mockDeviceColors, gotoDevicePage, expectDeviceColors } from './device-colors';
import { arrowBackSharp } from 'ionicons/icons';

test('CSS font default respects Ionic application overrides and iOS mode', async ({ page }) => {
Expand Down Expand Up @@ -44,7 +44,7 @@ for (const mode of ['light', 'dark'] as const) {
test(`${mode}: measured Android control geometry and Ionic overrides`, async ({ page }, testInfo) => {
await mockDeviceColors(page, mode);
await page.setViewportSize({ width: 412, height: 915 });
await page.goto('/main/index/toggle');
await gotoDevicePage(page, '/main/index/toggle', mode);
await expectDeviceColors(page, mode);
const toggle = page.locator('app-toggle ion-toggle').nth(2);
const track = toggle.locator('[part~="track"]');
Expand All @@ -60,7 +60,7 @@ for (const mode of ['light', 'dark'] as const) {
await toggle.evaluate((element) => element.classList.add('md3-disabled'));
await expect(track).not.toHaveCSS('height', '28px');

await page.goto('/main/index/item-list');
await gotoDevicePage(page, '/main/index/item-list', mode);
const title = page.locator('app-item-list > ion-header ion-title');
await expect(title).toHaveCSS('font-size', '22px');
await expect(title).toHaveCSS('padding-inline-start', '12px');
Expand Down Expand Up @@ -105,12 +105,12 @@ for (const mode of ['light', 'dark'] as const) {
const itemBox = await twoLineItem.boundingBox();
expect(itemBox!.height).toBeCloseTo(twoLineBox!.height + noteBox!.height + 32, 1);

await page.goto('/main/index/radio');
await gotoDevicePage(page, '/main/index/radio', mode);
const radio = page.locator('app-radio ion-radio').first();
await expect(radio.locator('[part~="container"]')).toHaveCSS('width', '20px');
await expect(radio.locator('..')).toHaveCSS('height', '72px');

await page.goto('/main/index/range');
await gotoDevicePage(page, '/main/index/range', mode);
const range = page.locator('app-range ion-range').first();
await expect(range.locator('[part~="bar"]')).toHaveCSS('height', '16px');
await expect(range.locator('[part~="knob"]').first()).toHaveCSS('width', '4px');
Expand Down
19 changes: 19 additions & 0 deletions demo/e2e/device-colors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,25 @@ export const mockDeviceColors = async (page: Page, mode: 'light' | 'dark') => {
);
};

/** Select appearance through the settings UI before opening a device-color test route. */
export const gotoDevicePage = async (page: Page, path: string, mode: 'light' | 'dark') => {
await page.goto('/main/index', { waitUntil: 'networkidle' });
const dark = page.getByRole('switch', { name: 'Dark Mode', exact: true });
if (mode === 'dark') {
await dark.click();
await expect(dark).toBeChecked();
} else await expect(dark).not.toBeChecked();
await expectDeviceColors(page, mode);
// Use same-document browser navigation so the selected settings survive route changes.
await page.evaluate((path) => {
history.pushState(null, '', path);
window.dispatchEvent(new PopStateEvent('popstate'));
}, path);
await expect(page.locator('index-page')).toBeHidden();
await expect(page.locator('ion-content[role="main"]:visible').first()).toBeVisible();
await page.waitForLoadState('networkidle');
};

export const expectDeviceColors = async (page: Page, mode: 'light' | 'dark') => {
const root = page.locator('html');
await expect
Expand Down
49 changes: 40 additions & 9 deletions demo/e2e/dynamic-colors.spec.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,52 @@
import { expect, test } from '@playwright/test';
import { mockDeviceColors, expectDeviceColors } from './device-colors';

test('system/light/dark appearance follows device only in system mode', async ({ page }) => {
test('unsupported Material You is disabled while dark mode remains usable', async ({ page }) => {
await page.emulateMedia({ colorScheme: 'dark' });
await page.goto('/');
const root = page.locator('html');
const dark = page.getByRole('switch', { name: 'Dark Mode', exact: true });
const material = page.getByRole('switch', { name: /^Material You/ });
await expect(dark).not.toBeChecked();
await expect(material).not.toBeChecked();
await expect(material).toBeDisabled();
await dark.click();
await expect(root).toHaveClass(/ion-palette-dark/);
await page.getByRole('button', { name: 'Light', exact: true }).click();
await expect(material).not.toBeChecked();
await expect(dark).toBeChecked();
await expect(root).toHaveClass(/ion-palette-dark/);
await dark.click();
await expect(root).not.toHaveClass(/ion-palette-dark/);
await page.emulateMedia({ colorScheme: 'light' });
await page.emulateMedia({ colorScheme: 'dark' });
await expect(material).not.toBeChecked();
await expect(material).toBeDisabled();
await expect(root).not.toHaveClass(/ion-palette-dark/);
await page.getByRole('button', { name: 'Dark', exact: true }).click();
await expect(root).toHaveClass(/ion-palette-dark/);
await page.emulateMedia({ colorScheme: 'light' });
await expect(root).toHaveClass(/ion-palette-dark/);
await page.getByRole('button', { name: 'System', exact: true }).click();
await expect(root).not.toHaveClass(/ion-palette-dark/);
await page.emulateMedia({ colorScheme: 'dark' });
await expect(root).not.toHaveClass(/ion-palette-dark/);
});

test('supported device toggles independently restore and reapply the selected palette', async ({ page }) => {
await mockDeviceColors(page, 'dark');
await page.goto('/main/index');
const dark = page.getByRole('switch', { name: 'Dark Mode', exact: true });
const material = page.getByRole('switch', { name: 'Material You', exact: true });
const root = page.locator('html');
await expect(material).toBeEnabled();
await expect(material).toBeChecked();
await expectDeviceColors(page, 'light');
await dark.click();
await expect(dark).toBeChecked();
await expectDeviceColors(page, 'dark');
await material.click();
await expect(material).not.toBeChecked();
await expect.poll(() => root.evaluate((el) => (el as HTMLElement).style.getPropertyValue('--md-sys-color-primary'))).toBe('');
await expect(root).toHaveClass(/ion-palette-dark/);
await expect(dark).toBeChecked();
await material.click();
await expect(material).toBeChecked();
await expectDeviceColors(page, 'dark');
await dark.click();
await expect(dark).not.toBeChecked();
await expectDeviceColors(page, 'light');
await expect(material).toBeChecked();
});
6 changes: 6 additions & 0 deletions demo/e2e/ionic-color-inheritance.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ for (const mode of ['light', 'dark'] as const) {
await page.setViewportSize({ width, height: width === 411 ? 891 : 800 });
await page.emulateMedia({ colorScheme: mode });
await page.goto('/main/index/color-palette');
await page.locator('html').evaluate((root, appearance) => {
root.classList.toggle('ion-palette-dark', appearance === 'dark');
root.style.colorScheme = appearance;
}, mode);
await expect(page.locator('html')).toHaveCSS('color-scheme', mode);
await expect.poll(() => page.locator('html').evaluate((root) => root.classList.contains('ion-palette-dark'))).toBe(mode === 'dark');
await expect(page.getByTestId('palette-card')).toHaveClass(/hydrated/);
const result = await page.evaluate(async (mode) => {
const panelRole = mode === 'light' ? '--md-sys-color-surface-container-lowest' : '--md-sys-color-surface-container-high';
Expand Down
12 changes: 12 additions & 0 deletions demo/e2e/ionic-contracts.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ for (const mode of ['light', 'dark'] as const) {
test(`${mode}: MD parents retain a child's explicit iOS mode`, async ({ page }) => {
await page.emulateMedia({ colorScheme: mode });
await page.goto('/main/index/color-palette');
await page.locator('html').evaluate((root, appearance) => {
root.classList.toggle('ion-palette-dark', appearance === 'dark');
root.style.colorScheme = appearance;
}, mode);
await expect(page.locator('html')).toHaveCSS('color-scheme', mode);
await expect.poll(() => page.locator('html').evaluate((root) => root.classList.contains('ion-palette-dark'))).toBe(mode === 'dark');
const card = page.getByTestId('palette-mixed-mode');
await expect(card).toHaveClass(/hydrated/);
await card.evaluate((element) => {
Expand All @@ -24,6 +30,12 @@ for (const mode of ['light', 'dark'] as const) {
test(`${mode}: tab icon opt-out retains normal Ionic inheritance`, async ({ page }) => {
await page.emulateMedia({ colorScheme: mode });
await page.goto('/main/index/tabs');
await page.locator('html').evaluate((root, appearance) => {
root.classList.toggle('ion-palette-dark', appearance === 'dark');
root.style.colorScheme = appearance;
}, mode);
await expect(page.locator('html')).toHaveCSS('color-scheme', mode);
await expect.poll(() => page.locator('html').evaluate((root) => root.classList.contains('ion-palette-dark'))).toBe(mode === 'dark');
const tab = page.locator('app-tabs ion-tab-button').first();
await expect(tab).toHaveClass(/hydrated/);
await tab.evaluate((element) => {
Expand Down
4 changes: 2 additions & 2 deletions demo/e2e/material-foregrounds.spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { expect, test } from '@playwright/test';
import { mockDeviceColors, expectDeviceColors, expectDeviceColorPairs } from './device-colors';
import { mockDeviceColors, gotoDevicePage, expectDeviceColors, expectDeviceColorPairs } from './device-colors';
import scheme from './fixtures/android17-device-colors.json';

for (const mode of ['light', 'dark'] as const) {
test(`${mode}: explicit Ionic colors render native foreground pairs`, async ({ page }, testInfo) => {
await mockDeviceColors(page, mode);
await page.setViewportSize({ width: 800, height: 1000 });
await page.goto('/main/index/color-palette');
await gotoDevicePage(page, '/main/index/color-palette', mode);
await expectDeviceColors(page, mode);
await expectDeviceColorPairs(page, mode);
await page.evaluate(() => {
Expand Down
32 changes: 31 additions & 1 deletion demo/e2e/md3-color-roles.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ for (const mode of ['light', 'dark'] as const) {
test(`${mode}: Ionic-only palette, optional pairs, explicit colors and opt-out`, async ({ page }) => {
await page.emulateMedia({ colorScheme: mode });
await page.goto('/main/index/color-palette');
await page.locator('html').evaluate((root, appearance) => {
root.classList.toggle('ion-palette-dark', appearance === 'dark');
root.style.colorScheme = appearance;
}, mode);
await expect(page.locator('html')).toHaveCSS('color-scheme', mode);
await expect.poll(() => page.locator('html').evaluate((root) => root.classList.contains('ion-palette-dark'))).toBe(mode === 'dark');
await expect(page.getByTestId('palette-tonal')).toBeVisible();
await expect(page.getByTestId('palette-tonal')).toHaveClass(/hydrated/);
const ids = [
Expand Down Expand Up @@ -97,11 +103,23 @@ for (const mode of ['light', 'dark'] as const) {
test(`${mode}: Android Settings surfaces, groups, back button and icon-free switches`, async ({ page }) => {
await page.emulateMedia({ colorScheme: mode });
await page.goto('/main/index/color-palette');
await page.locator('html').evaluate((root, appearance) => {
root.classList.toggle('ion-palette-dark', appearance === 'dark');
root.style.colorScheme = appearance;
}, mode);
await expect(page.locator('html')).toHaveCSS('color-scheme', mode);
await expect.poll(() => page.locator('html').evaluate((root) => root.classList.contains('ion-palette-dark'))).toBe(mode === 'dark');
const title = page.locator('app-color-palette ion-card-title').first();
await expect(title).toHaveClass(/hydrated/);
await title.evaluate((element) => element.setAttribute('color', 'danger'));
await expect(title).toHaveClass(/ion-color-danger/);
expect((await colors(title)).foreground).toEqual((await colors(page.getByTestId('palette-danger'))).background);
await expect
.poll(
async () =>
JSON.stringify((await colors(title)).foreground) ===
JSON.stringify((await colors(page.getByTestId('palette-danger'))).background),
)
.toBe(true);
await title.evaluate((element) => {
element.removeAttribute('color');
(element as HTMLElement).style.setProperty('--color', '#123456');
Expand All @@ -117,6 +135,12 @@ for (const mode of ['light', 'dark'] as const) {
expect(backStyle.background).toContain('20px');

await page.goto('/main/index/item-list');
await page.locator('html').evaluate((root, appearance) => {
root.classList.toggle('ion-palette-dark', appearance === 'dark');
root.style.colorScheme = appearance;
}, mode);
await expect(page.locator('html')).toHaveCSS('color-scheme', mode);
await expect.poll(() => page.locator('html').evaluate((root) => root.classList.contains('ion-palette-dark'))).toBe(mode === 'dark');
const list = page.locator('app-item-list ion-list.list-inset').nth(1);
await expect(list).toBeVisible();
expect(await list.evaluate((element) => getComputedStyle(element).backgroundColor)).toBe('rgba(0, 0, 0, 0)');
Expand All @@ -136,6 +160,12 @@ for (const mode of ['light', 'dark'] as const) {
expect(await item.evaluate((element) => getComputedStyle(element).marginBottom)).toBe('0px');

await page.goto('/main/index/toggle');
await page.locator('html').evaluate((root, appearance) => {
root.classList.toggle('ion-palette-dark', appearance === 'dark');
root.style.colorScheme = appearance;
}, mode);
await expect(page.locator('html')).toHaveCSS('color-scheme', mode);
await expect.poll(() => page.locator('html').evaluate((root) => root.classList.contains('ion-palette-dark'))).toBe(mode === 'dark');
const toggle = page.locator('app-toggle ion-toggle').first();
await expect(toggle).toHaveClass(/hydrated/);
const glyph = () =>
Expand Down
8 changes: 4 additions & 4 deletions demo/e2e/neutral-palette.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { expect, test } from '@playwright/test';
import { mockDeviceColors, expectDeviceColors } from './device-colors';
import { mockDeviceColors, gotoDevicePage, expectDeviceColors } from './device-colors';
import scheme from './fixtures/android17-device-colors.json';

for (const mode of ['light', 'dark'] as const) {
test(`${mode}: white base and named neutral colors preserve their order`, async ({ page }) => {
await mockDeviceColors(page, mode);
await page.goto('/main/index/card');
await gotoDevicePage(page, '/main/index/card', mode);
await expectDeviceColors(page, mode);
const roles = scheme[mode];
const rgb = (hex: string) => `rgb(${[1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16)).join(', ')})`;
Expand Down Expand Up @@ -46,7 +46,7 @@ for (const mode of ['light', 'dark'] as const) {
]) {
test(`${mode}: ${route} preserves the default overlay surface`, async ({ page }) => {
await mockDeviceColors(page, mode);
await page.goto('/main/index/' + (route.startsWith('modal') ? 'modal' : route));
await gotoDevicePage(page, '/main/index/' + (route.startsWith('modal') ? 'modal' : route), mode);
if (route.startsWith('modal'))
await page
.locator('app-modal ion-item')
Expand All @@ -69,7 +69,7 @@ for (const mode of ['light', 'dark'] as const) {

test('dark device search fields remain distinct from toolbar panels', async ({ page }) => {
await mockDeviceColors(page, 'dark');
await page.goto('/main/index/searchbar');
await gotoDevicePage(page, '/main/index/searchbar', 'dark');
await expectDeviceColors(page, 'dark');
const searchbars = page.locator('app-searchbar ion-toolbar:not(.ion-color) ion-searchbar');
await expect(searchbars.first()).toBeVisible();
Expand Down
Loading
Loading