mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-08 16:39:42 +02:00
test(mobile): read PHONE_MAX as an exclusive bound everywhere, drop the stale 430px baselines
Follow-up to #390. PHONE_MAX had become 599, an inclusive bound, while three of its four consumers still read it as exclusive (width < PHONE_MAX for phone); the one site that switched to <= disagreed with getDeviceType(). It is 600 again with < at every site. The breakpoint table in docs/mobile-testing-report.md says 600, and the three 430px visual baselines are removed: they depict the tablet tier now, and the visual suite recreates a missing baseline on its next run on the machine that owns them. device-matrix.test.ts is also run through Prettier, which the commit hook demanded and the format gate (src/ only) never did. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -156,8 +156,8 @@ There is no dedicated help button in the mobile UI. Help is accessible via:
|
|||||||
|
|
||||||
| Breakpoint | Class | Description |
|
| Breakpoint | Class | Description |
|
||||||
|------------|-------|-------------|
|
|------------|-------|-------------|
|
||||||
| < 430px | `device-mobile` | Phone - most features hidden/simplified |
|
| < 600px | `device-mobile` | Phone - most features hidden/simplified |
|
||||||
| 430-768px | `device-tablet` | Tablet - intermediate layout |
|
| 600-768px | `device-tablet` | Tablet - intermediate layout |
|
||||||
| > 768px | `device-desktop` | Desktop - full features |
|
| > 768px | `device-desktop` | Desktop - full features |
|
||||||
|
|
||||||
Touch devices also get `touch-device` class regardless of screen size.
|
Touch devices also get `touch-device` class regardless of screen size.
|
||||||
|
|||||||
@@ -15,12 +15,7 @@ import {
|
|||||||
getCSSProperty,
|
getCSSProperty,
|
||||||
getCSSNumericValue,
|
getCSSNumericValue,
|
||||||
} from './helpers/assertions.js';
|
} from './helpers/assertions.js';
|
||||||
import {
|
import { REPRESENTATIVE_DEVICES, DEVICE_REGISTRY, type DeviceEntry, type DeviceCategory } from './devices.js';
|
||||||
REPRESENTATIVE_DEVICES,
|
|
||||||
DEVICE_REGISTRY,
|
|
||||||
type DeviceEntry,
|
|
||||||
type DeviceCategory,
|
|
||||||
} from './devices.js';
|
|
||||||
|
|
||||||
const PORT = PORTS.DEVICE_MATRIX;
|
const PORT = PORTS.DEVICE_MATRIX;
|
||||||
const BASE_URL = `http://localhost:${PORT}`;
|
const BASE_URL = `http://localhost:${PORT}`;
|
||||||
@@ -37,10 +32,7 @@ const PHONE_HIDDEN_SELECTORS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
// Visible only on phones
|
// Visible only on phones
|
||||||
const PHONE_ONLY_SELECTORS = [
|
const PHONE_ONLY_SELECTORS = [SELECTORS.SETTINGS_MOBILE, SELECTORS.CASE_MOBILE];
|
||||||
SELECTORS.SETTINGS_MOBILE,
|
|
||||||
SELECTORS.CASE_MOBILE,
|
|
||||||
];
|
|
||||||
|
|
||||||
describe('Device Matrix', () => {
|
describe('Device Matrix', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
@@ -54,98 +46,97 @@ describe('Device Matrix', () => {
|
|||||||
|
|
||||||
// ─── Representative Devices ───────────────────────────────────────────────
|
// ─── Representative Devices ───────────────────────────────────────────────
|
||||||
|
|
||||||
describe.each(
|
describe.each(Object.entries(REPRESENTATIVE_DEVICES) as [DeviceCategory, DeviceEntry][])(
|
||||||
Object.entries(REPRESENTATIVE_DEVICES) as [DeviceCategory, DeviceEntry][],
|
'Representative: %s',
|
||||||
)('Representative: %s', (category, device) => {
|
(category, device) => {
|
||||||
let context: BrowserContext;
|
let context: BrowserContext;
|
||||||
let page: Page;
|
let page: Page;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
({ context, page } = await createDevicePage(device, BASE_URL));
|
({ context, page } = await createDevicePage(device, BASE_URL));
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await context.close();
|
await context.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`has correct device class for ${device.name} (${device.viewport.width}px)`, async () => {
|
it(`has correct device class for ${device.name} (${device.viewport.width}px)`, async () => {
|
||||||
await assertDeviceClasses(page, device.viewport.width);
|
await assertDeviceClasses(page, device.viewport.width);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('no horizontal overflow', async () => {
|
it('no horizontal overflow', async () => {
|
||||||
await assertNoHorizontalOverflow(page);
|
await assertNoHorizontalOverflow(page);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('header positioning matches breakpoint', async () => {
|
it('header positioning matches breakpoint', async () => {
|
||||||
const { width } = device.viewport;
|
const { width } = device.viewport;
|
||||||
const position = await getCSSProperty(page, SELECTORS.HEADER, 'position');
|
const position = await getCSSProperty(page, SELECTORS.HEADER, 'position');
|
||||||
if (width <= BREAKPOINTS.TABLET_MAX) {
|
if (width <= BREAKPOINTS.TABLET_MAX) {
|
||||||
// Phone + tablet (max-width: 768px includes 768): fixed header
|
// Phone + tablet (max-width: 768px includes 768): fixed header
|
||||||
expect(position).toBe('fixed');
|
expect(position).toBe('fixed');
|
||||||
const top = await getCSSProperty(page, SELECTORS.HEADER, 'top');
|
const top = await getCSSProperty(page, SELECTORS.HEADER, 'top');
|
||||||
expect(parseFloat(top)).toBe(0);
|
expect(parseFloat(top)).toBe(0);
|
||||||
} else {
|
} else {
|
||||||
// Desktop: relative header (not fixed)
|
// Desktop: relative header (not fixed)
|
||||||
expect(position).toBe('relative');
|
expect(position).toBe('relative');
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('toolbar positioning matches breakpoint', async () => {
|
|
||||||
const { width } = device.viewport;
|
|
||||||
const position = await getCSSProperty(page, SELECTORS.TOOLBAR, 'position');
|
|
||||||
if (width <= BREAKPOINTS.PHONE_MAX) {
|
|
||||||
// Phone (max-width: 599px): fixed toolbar
|
|
||||||
expect(position).toBe('fixed');
|
|
||||||
} else {
|
|
||||||
// Tablet/desktop: relative toolbar
|
|
||||||
expect(position).toBe('relative');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('correct elements hidden/visible for breakpoint', async () => {
|
|
||||||
const { width } = device.viewport;
|
|
||||||
const isPhone = width < BREAKPOINTS.PHONE_MAX;
|
|
||||||
// Skip strict assertions for devices at the phone/tablet boundary (±10px)
|
|
||||||
const atBoundary = Math.abs(width - BREAKPOINTS.PHONE_MAX) <= 10
|
|
||||||
|| Math.abs(width - BREAKPOINTS.TABLET_MAX) <= 10;
|
|
||||||
|
|
||||||
if (atBoundary) return;
|
|
||||||
|
|
||||||
if (isPhone) {
|
|
||||||
// Phone: certain elements hidden, mobile buttons visible
|
|
||||||
for (const sel of PHONE_HIDDEN_SELECTORS) {
|
|
||||||
await assertHidden(page, sel);
|
|
||||||
}
|
}
|
||||||
for (const sel of PHONE_ONLY_SELECTORS) {
|
});
|
||||||
await assertVisible(page, sel);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Tablet/desktop: phone-hidden elements should be visible, mobile buttons hidden
|
|
||||||
for (const sel of PHONE_ONLY_SELECTORS) {
|
|
||||||
await assertHidden(page, sel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('touch targets pass minimum size', async () => {
|
it('toolbar positioning matches breakpoint', async () => {
|
||||||
const violations = await assertAccessibleTouchTargets(page);
|
const { width } = device.viewport;
|
||||||
// Log violations for debugging but allow a small number
|
const position = await getCSSProperty(page, SELECTORS.TOOLBAR, 'position');
|
||||||
if (violations.length > 0) {
|
if (width < BREAKPOINTS.PHONE_MAX) {
|
||||||
console.warn(
|
// Phone (max-width: 599px): fixed toolbar
|
||||||
`[${device.name}] Touch target violations (${violations.length}):\n` +
|
expect(position).toBe('fixed');
|
||||||
violations.map(v => ` ${v.selector}: ${v.width}x${v.height}px`).join('\n'),
|
} else {
|
||||||
);
|
// Tablet/desktop: relative toolbar
|
||||||
}
|
expect(position).toBe('relative');
|
||||||
// Larger viewports show more UI elements, so allow more violations.
|
}
|
||||||
// Known violators: notification action buttons (26x26), some icon buttons.
|
});
|
||||||
const { width } = device.viewport;
|
|
||||||
// Larger viewports show more elements; scale threshold accordingly
|
it('correct elements hidden/visible for breakpoint', async () => {
|
||||||
const maxViolations = width >= BREAKPOINTS.TABLET_MAX ? 25
|
const { width } = device.viewport;
|
||||||
: width >= BREAKPOINTS.PHONE_MAX ? 20
|
const isPhone = width < BREAKPOINTS.PHONE_MAX;
|
||||||
: 15;
|
// Skip strict assertions for devices at the phone/tablet boundary (±10px)
|
||||||
expect(violations.length).toBeLessThanOrEqual(maxViolations);
|
const atBoundary =
|
||||||
});
|
Math.abs(width - BREAKPOINTS.PHONE_MAX) <= 10 || Math.abs(width - BREAKPOINTS.TABLET_MAX) <= 10;
|
||||||
});
|
|
||||||
|
if (atBoundary) return;
|
||||||
|
|
||||||
|
if (isPhone) {
|
||||||
|
// Phone: certain elements hidden, mobile buttons visible
|
||||||
|
for (const sel of PHONE_HIDDEN_SELECTORS) {
|
||||||
|
await assertHidden(page, sel);
|
||||||
|
}
|
||||||
|
for (const sel of PHONE_ONLY_SELECTORS) {
|
||||||
|
await assertVisible(page, sel);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Tablet/desktop: phone-hidden elements should be visible, mobile buttons hidden
|
||||||
|
for (const sel of PHONE_ONLY_SELECTORS) {
|
||||||
|
await assertHidden(page, sel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('touch targets pass minimum size', async () => {
|
||||||
|
const violations = await assertAccessibleTouchTargets(page);
|
||||||
|
// Log violations for debugging but allow a small number
|
||||||
|
if (violations.length > 0) {
|
||||||
|
console.warn(
|
||||||
|
`[${device.name}] Touch target violations (${violations.length}):\n` +
|
||||||
|
violations.map((v) => ` ${v.selector}: ${v.width}x${v.height}px`).join('\n')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Larger viewports show more UI elements, so allow more violations.
|
||||||
|
// Known violators: notification action buttons (26x26), some icon buttons.
|
||||||
|
const { width } = device.viewport;
|
||||||
|
// Larger viewports show more elements; scale threshold accordingly
|
||||||
|
const maxViolations = width >= BREAKPOINTS.TABLET_MAX ? 25 : width >= BREAKPOINTS.PHONE_MAX ? 20 : 15;
|
||||||
|
expect(violations.length).toBeLessThanOrEqual(maxViolations);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// ─── Full Device Matrix (skip with CI_QUICK=1) ───────────────────────────
|
// ─── Full Device Matrix (skip with CI_QUICK=1) ───────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export const SELECTORS = {
|
|||||||
|
|
||||||
// Device breakpoints (match app.js MobileDetection)
|
// Device breakpoints (match app.js MobileDetection)
|
||||||
export const BREAKPOINTS = {
|
export const BREAKPOINTS = {
|
||||||
PHONE_MAX: 599,
|
PHONE_MAX: 600,
|
||||||
TABLET_MAX: 768,
|
TABLET_MAX: 768,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 56 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 103 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 91 KiB |
Reference in New Issue
Block a user