24f084ae77
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
145 lines
5.0 KiB
TypeScript
145 lines
5.0 KiB
TypeScript
import type {
|
|
Locator,
|
|
Page,
|
|
} from '@playwright/test';
|
|
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('/');
|
|
await this.searchInput.waitFor({ state: 'visible' });
|
|
}
|
|
|
|
async searchFor(query: string) {
|
|
await this.searchInput.fill(query);
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|