test: add e2e suite for core comparison flows
Adds 17 new specs across six files plus the supporting POM infrastructure: - fixtures with auto-opened comparison + typography helpers - TypographyMenu POM reading values from ComboControl aria-labels - ComparisonPage extended with side selection, font picking, slider-value reader, FontFaceSet inspection, storage snapshot - compare-flow: A/B selection, aria-pressed state, storage write - preview-text: input binding + slider character rendering - slider: keyboard ARIA contract (Arrow / Shift+Arrow / Page / Home / End) - font-loading: FontFaceSet contains selected, excludes unrelated - persistence: font selection + typography settings survive reload - typography: symmetric increase/decrease for all four controls
This commit is contained in:
@@ -7,19 +7,37 @@ import { BasePage } from './base-page';
|
||||
/**
|
||||
* Page object for the root comparison view. Encapsulates locators for the
|
||||
* primary controls so tests don't hardcode aria-labels or DOM structure.
|
||||
*
|
||||
* Selection flow: clicking a font row assigns it to whichever side
|
||||
* (`A` = "Left Font" / Primary, `B` = "Right Font" / Secondary) is currently
|
||||
* active in the Sidebar — there's no per-row A/B toggle.
|
||||
*/
|
||||
export class ComparisonPage extends BasePage {
|
||||
readonly searchInput: Locator;
|
||||
readonly previewInput: Locator;
|
||||
readonly slider: Locator;
|
||||
readonly primarySideButton: Locator;
|
||||
readonly secondarySideButton: Locator;
|
||||
readonly primaryFont: Locator;
|
||||
readonly secondaryFont: Locator;
|
||||
readonly fontList: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page);
|
||||
this.searchInput = page.getByRole('textbox', { name: 'Search typefaces' });
|
||||
this.previewInput = page.getByRole('textbox', { name: 'Preview text' });
|
||||
this.slider = page.getByRole('slider', { name: 'Font comparison slider' });
|
||||
// ARIA-controls couples the side toggle to the font display it targets — copy-independent.
|
||||
this.primarySideButton = page.locator('[aria-controls="primary-font"]');
|
||||
this.secondarySideButton = page.locator('[aria-controls="secondary-font"]');
|
||||
this.primaryFont = page.locator('#primary-font');
|
||||
this.secondaryFont = page.locator('#secondary-font');
|
||||
this.fontList = page.locator('[data-font-list]');
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the root page and wait for the main controls to be interactable.
|
||||
* Uses lg+ viewport for the preview input to be visible.
|
||||
*/
|
||||
async open() {
|
||||
await this.goto('/');
|
||||
@@ -33,4 +51,94 @@ export class ComparisonPage extends BasePage {
|
||||
async setPreviewText(text: string) {
|
||||
await this.previewInput.fill(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch which side the next font click will assign to.
|
||||
*/
|
||||
async selectSide(side: 'A' | 'B') {
|
||||
const button = side === 'A' ? this.primarySideButton : this.secondarySideButton;
|
||||
await button.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Read which side is currently active from `aria-pressed`.
|
||||
* Falls back to A when neither button reports pressed (initial state in some flows).
|
||||
*/
|
||||
async activeSide(): Promise<'A' | 'B' | null> {
|
||||
const [primaryPressed, secondaryPressed] = await Promise.all([
|
||||
this.primarySideButton.getAttribute('aria-pressed'),
|
||||
this.secondarySideButton.getAttribute('aria-pressed'),
|
||||
]);
|
||||
if (primaryPressed === 'true') {
|
||||
return 'A';
|
||||
}
|
||||
if (secondaryPressed === 'true') {
|
||||
return 'B';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for a font and click the matching list row. The row's accessible
|
||||
* name is the font name itself (rendered by FontApplicator).
|
||||
*/
|
||||
async pickFont(name: string) {
|
||||
await this.searchFor(name);
|
||||
const row = this.fontList.getByRole('button', { name, exact: true });
|
||||
await row.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign fontA to side A and fontB to side B in one call.
|
||||
*/
|
||||
async pickPair(fontA: string, fontB: string) {
|
||||
await this.selectSide('A');
|
||||
await this.pickFont(fontA);
|
||||
await this.selectSide('B');
|
||||
await this.pickFont(fontB);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read aria-valuenow off the comparison slider.
|
||||
*/
|
||||
async sliderValue(): Promise<number> {
|
||||
const value = await this.slider.getAttribute('aria-valuenow');
|
||||
return Number(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Snapshot the glyphdiff:* localStorage entries.
|
||||
*/
|
||||
async readStorage(): Promise<Record<string, string | null>> {
|
||||
return await this.page.evaluate(() => {
|
||||
const out: Record<string, string | null> = {};
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i)!;
|
||||
if (key.startsWith('glyphdiff:')) {
|
||||
out[key] = localStorage.getItem(key);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the document.fonts FontFaceSet contains a fully-loaded face for
|
||||
* the named family. Counts only faces registered via the FontFace API —
|
||||
* system-installed fallbacks (which `document.fonts.check` honours) are
|
||||
* excluded, so a `false` here is meaningful in negative assertions.
|
||||
*/
|
||||
async fontLoaded(name: string): Promise<boolean> {
|
||||
return await this.page.evaluate(target => {
|
||||
for (const face of document.fonts) {
|
||||
// FontFace.family is wrapped in quotes only if the literal was;
|
||||
// strip any surrounding quotes before comparing.
|
||||
const family = face.family.replace(/^["']|["']$/g, '');
|
||||
if (family === target && face.status === 'loaded') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}, name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import type {
|
||||
Locator,
|
||||
Page,
|
||||
} from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Typography settings menu — desktop layout exposes inline ComboControls with
|
||||
* increase/decrease buttons. The current value is encoded in the trigger
|
||||
* button's aria-label as `${controlLabel}: ${value}` (e.g. "Size: 24").
|
||||
*/
|
||||
export type TypographyControl = 'size' | 'weight' | 'leading' | 'tracking';
|
||||
|
||||
const LABELS: Record<TypographyControl, { increase: string; decrease: string; trigger: string }> = {
|
||||
size: {
|
||||
increase: 'Increase Font Size',
|
||||
decrease: 'Decrease Font Size',
|
||||
trigger: 'Size',
|
||||
},
|
||||
weight: {
|
||||
increase: 'Increase Font Weight',
|
||||
decrease: 'Decrease Font Weight',
|
||||
trigger: 'Weight',
|
||||
},
|
||||
leading: {
|
||||
increase: 'Increase Line Height',
|
||||
decrease: 'Decrease Line Height',
|
||||
trigger: 'Leading',
|
||||
},
|
||||
tracking: {
|
||||
increase: 'Increase Letter Spacing',
|
||||
decrease: 'Decrease Letter Spacing',
|
||||
trigger: 'Tracking',
|
||||
},
|
||||
};
|
||||
|
||||
export class TypographyMenu {
|
||||
constructor(private readonly page: Page) {}
|
||||
|
||||
increase(control: TypographyControl): Locator {
|
||||
return this.page.getByRole('button', { name: LABELS[control].increase });
|
||||
}
|
||||
|
||||
decrease(control: TypographyControl): Locator {
|
||||
return this.page.getByRole('button', { name: LABELS[control].decrease });
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger button whose aria-label encodes the current value, e.g. "Size: 24".
|
||||
*/
|
||||
trigger(control: TypographyControl): Locator {
|
||||
return this.page.getByRole('button', { name: new RegExp(`^${LABELS[control].trigger}:\\s`) });
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the numeric value out of the trigger button's aria-label.
|
||||
* Returns null if the label can't be read yet.
|
||||
*/
|
||||
async readValue(control: TypographyControl): Promise<number | null> {
|
||||
const label = await this.trigger(control).getAttribute('aria-label');
|
||||
if (!label) {
|
||||
return null;
|
||||
}
|
||||
const match = label.match(/:\s*(-?\d+(?:\.\d+)?)/);
|
||||
return match ? Number(match[1]) : null;
|
||||
}
|
||||
|
||||
async bump(control: TypographyControl, direction: 'up' | 'down', times = 1) {
|
||||
const button = direction === 'up' ? this.increase(control) : this.decrease(control);
|
||||
for (let i = 0; i < times; i++) {
|
||||
await button.click();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user