Cognitive Load Analyzer — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited Cognitive Load Analyzer (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
You are an expert QA engineer specializing in cognitive load assessment, usability heuristic evaluation, and information architecture analysis. When asked to evaluate interface complexity, measure decision overload, audit visual hierarchy, or analyze task completion paths in a web application, follow these comprehensive instructions to systematically identify and quantify sources of unnecessary cognitive burden.
Organize your cognitive load analysis suite with this directory structure:
tests/
cognitive-load/
information-density.spec.ts
choice-overload.spec.ts
navigation-complexity.spec.ts
form-complexity.spec.ts
visual-hierarchy.spec.ts
consistency-audit.spec.ts
task-completion-paths.spec.ts
fixtures/
cognitive-page.fixture.ts
helpers/
density-calculator.ts
choice-counter.ts
hierarchy-analyzer.ts
consistency-checker.ts
task-path-tracer.ts
cognitive-score.ts
reports/
cognitive-load-report.json
cognitive-load-report.html
thresholds/
cognitive-thresholds.json
playwright.config.tsEach spec file measures a different dimension of cognitive load. The helpers directory contains the measurement algorithms. Thresholds define the acceptable limits for each metric.
Information density measures how much content is presented per unit of viewport area. High density overwhelms users; low density wastes space and requires excessive scrolling.
// helpers/density-calculator.ts
import { Page } from '@playwright/test';
export interface DensityMetrics {
totalTextElements: number;
totalWordCount: number;
totalInteractiveElements: number;
totalImages: number;
viewportArea: number;
visibleContentArea: number;
textDensity: number; // words per 1000px of viewport height
interactiveDensity: number; // interactive elements per viewport
informationUnits: number; // distinct information groups
densityScore: number; // 0-100 composite score
}
export class DensityCalculator {
async calculate(page: Page): Promise<DensityMetrics> {
const viewport = page.viewportSize();
if (!viewport) throw new Error('No viewport size available');
const viewportArea = viewport.width * viewport.height;
const measurements = await page.evaluate(() => {
// Count visible text elements
const textSelectors = 'p, h1, h2, h3, h4, h5, h6, span, li, td, th, label, a, button';
const textElements = document.querySelectorAll(textSelectors);
let totalWords = 0;
let visibleTextElements = 0;
textElements.forEach((el) => {
const style = window.getComputedStyle(el);
if (style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0') {
const text = el.textContent?.trim();
if (text && text.length > 0) {
visibleTextElements++;
totalWords += text.split(/\s+/).filter((w) => w.length > 0).length;
}
}
});
// Count interactive elements visible in the viewport
const interactiveSelectors = [
'a[href]', 'button', 'input', 'select', 'textarea',
'[role="button"]', '[role="link"]', '[role="tab"]',
'[role="menuitem"]', '[onclick]', '[tabindex]:not([tabindex="-1"])',
];
const interactiveElements = document.querySelectorAll(interactiveSelectors.join(', '));
let visibleInteractive = 0;
interactiveElements.forEach((el) => {
const rect = el.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0 && rect.top < window.innerHeight && rect.bottom > 0) {
visibleInteractive++;
}
});
// Count images in viewport
const images = document.querySelectorAll('img, svg, [role="img"]');
let visibleImages = 0;
images.forEach((el) => {
const rect = el.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0 && rect.top < window.innerHeight) {
visibleImages++;
}
});
// Calculate visible content area
const bodyRect = document.body.getBoundingClientRect();
const visibleContentArea = Math.min(bodyRect.height, window.innerHeight) * bodyRect.width;
// Count distinct information groups (sections, cards, panels)
const groupSelectors = 'section, article, .card, .panel, [role="region"], [role="group"], fieldset';
const groups = document.querySelectorAll(groupSelectors);
let visibleGroups = 0;
groups.forEach((el) => {
const rect = el.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0 && rect.top < window.innerHeight) {
visibleGroups++;
}
});
return {
visibleTextElements,
totalWords,
visibleInteractive,
visibleImages,
visibleContentArea,
visibleGroups,
};
});
const viewportHeightK = viewport.height / 1000;
const textDensity = measurements.totalWords / viewportHeightK;
const interactiveDensity = measurements.visibleInteractive;
// Composite density score (0-100, lower is less dense)
let densityScore = 50;
if (textDensity > 500) densityScore += 15;
if (textDensity > 800) densityScore += 15;
if (textDensity < 100) densityScore -= 10;
if (interactiveDensity > 20) densityScore += 10;
if (interactiveDensity > 40) densityScore += 10;
if (measurements.visibleGroups > 8) densityScore += 10;
densityScore = Math.max(0, Math.min(100, densityScore));
return {
totalTextElements: measurements.visibleTextElements,
totalWordCount: measurements.totalWords,
totalInteractiveElements: measurements.visibleInteractive,
totalImages: measurements.visibleImages,
viewportArea,
visibleContentArea: measurements.visibleContentArea,
textDensity,
interactiveDensity,
informationUnits: measurements.visibleGroups,
densityScore,
};
}
}Choice overload occurs when users face too many options of similar visual weight, causing decision paralysis.
// helpers/choice-counter.ts
import { Page } from '@playwright/test';
export interface ChoiceMetrics {
navigationItemCount: number;
formFieldCount: number;
callToActionCount: number;
filterOptionCount: number;
tabCount: number;
cardChoiceCount: number;
totalDecisionPoints: number;
choiceOverloadScore: number; // 0-100, higher = more overload
issues: string[];
}
export class ChoiceCounter {
async count(page: Page): Promise<ChoiceMetrics> {
const metrics = await page.evaluate(() => {
const issues: string[] = [];
// Count top-level navigation items
const navItems = document.querySelectorAll(
'nav a, nav button, [role="navigation"] a, [role="navigation"] button'
);
const visibleNavItems = Array.from(navItems).filter((el) => {
const rect = el.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
});
const navigationItemCount = visibleNavItems.length;
if (navigationItemCount > 7) {
issues.push(`Navigation has ${navigationItemCount} items (recommended: 5-7)`);
}
// Count visible form fields
const formFields = document.querySelectorAll(
'input:not([type="hidden"]), select, textarea, [role="combobox"]'
);
const visibleFormFields = Array.from(formFields).filter((el) => {
const rect = el.getBoundingClientRect();
return rect.width > 0 && rect.height > 0 && rect.top < window.innerHeight;
});
const formFieldCount = visibleFormFields.length;
if (formFieldCount > 7) {
issues.push(`${formFieldCount} form fields visible (recommended: 3-5 per step)`);
}
// Count call-to-action buttons
const ctaButtons = document.querySelectorAll(
'button[type="submit"], .btn-primary, .cta, [data-testid*="cta"]'
);
const visibleCTAs = Array.from(ctaButtons).filter((el) => {
const rect = el.getBoundingClientRect();
return rect.width > 0 && rect.height > 0 && rect.top < window.innerHeight;
});
const callToActionCount = visibleCTAs.length;
if (callToActionCount > 2) {
issues.push(`${callToActionCount} CTAs competing for attention (recommended: 1-2)`);
}
// Count filter options
const filterElements = document.querySelectorAll(
'[data-testid*="filter"], .filter-option, .facet'
);
const filterOptionCount = filterElements.length;
if (filterOptionCount > 10) {
issues.push(`${filterOptionCount} filter options visible (use progressive disclosure)`);
}
// Count tabs
const tabs = document.querySelectorAll('[role="tab"], .tab, .nav-tab');
const tabCount = tabs.length;
if (tabCount > 6) {
issues.push(`${tabCount} tabs visible (recommended: 4-6, use overflow for more)`);
}
// Count choice cards in viewport
const cards = document.querySelectorAll('.card, article, .product-card, .pricing-card');
const visibleCards = Array.from(cards).filter((el) => {
const rect = el.getBoundingClientRect();
return rect.width > 0 && rect.height > 0 && rect.top < window.innerHeight;
});
const cardChoiceCount = visibleCards.length;
if (cardChoiceCount > 9) {
issues.push(`${cardChoiceCount} choice cards in viewport (recommended: 6-9)`);
}
return {
navigationItemCount,
formFieldCount,
callToActionCount,
filterOptionCount,
tabCount,
cardChoiceCount,
issues,
};
});
const totalDecisionPoints =
metrics.navigationItemCount +
metrics.formFieldCount +
metrics.callToActionCount +
metrics.tabCount +
metrics.cardChoiceCount;
let overloadScore = 0;
if (totalDecisionPoints > 10) overloadScore += 15;
if (totalDecisionPoints > 20) overloadScore += 15;
if (totalDecisionPoints > 30) overloadScore += 20;
if (metrics.navigationItemCount > 7) overloadScore += 10;
if (metrics.formFieldCount > 7) overloadScore += 10;
if (metrics.callToActionCount > 2) overloadScore += 10;
if (metrics.cardChoiceCount > 9) overloadScore += 10;
if (metrics.tabCount > 6) overloadScore += 10;
overloadScore = Math.min(100, overloadScore);
return {
...metrics,
totalDecisionPoints,
choiceOverloadScore: overloadScore,
};
}
}Visual hierarchy determines the order in which users process page information. A clear hierarchy guides the eye efficiently; a flat hierarchy forces exhaustive scanning.
// helpers/hierarchy-analyzer.ts
import { Page } from '@playwright/test';
export interface HierarchyMetrics {
headingLevels: Array<{ level: number; count: number; text: string[] }>;
headingHierarchyValid: boolean;
fontSizeVariations: number;
whitespaceRatio: number;
primaryActionClear: boolean;
visualWeightDistribution: 'balanced' | 'top-heavy' | 'flat' | 'clear-hierarchy';
hierarchyScore: number; // 0-100, higher is better
issues: string[];
}
export class HierarchyAnalyzer {
async analyze(page: Page): Promise<HierarchyMetrics> {
const data = await page.evaluate(() => {
const issues: string[] = [];
// Analyze heading hierarchy
const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6');
const levelMap = new Map<number, string[]>();
headings.forEach((h) => {
const level = parseInt(h.tagName.charAt(1));
if (!levelMap.has(level)) levelMap.set(level, []);
levelMap.get(level)?.push(h.textContent?.trim().substring(0, 50) || '');
});
const headingLevels = Array.from(levelMap.entries()).map(([level, texts]) => ({
level,
count: texts.length,
text: texts,
}));
// Check for skipped heading levels
const sortedLevels = Array.from(levelMap.keys()).sort();
let hierarchyValid = true;
for (let i = 1; i < sortedLevels.length; i++) {
if (sortedLevels[i] - sortedLevels[i - 1] > 1) {
hierarchyValid = false;
issues.push(`Heading hierarchy skips from h${sortedLevels[i - 1]} to h${sortedLevels[i]}`);
}
}
const h1Count = levelMap.get(1)?.length || 0;
if (h1Count === 0) {
issues.push('Page has no h1 heading');
hierarchyValid = false;
}
if (h1Count > 1) {
issues.push(`Page has ${h1Count} h1 headings (recommended: exactly 1)`);
}
// Count distinct font sizes
const allElements = document.querySelectorAll('body *');
const fontSizes = new Set<string>();
allElements.forEach((el) => {
const style = window.getComputedStyle(el);
if (style.display !== 'none' && el.textContent?.trim()) {
fontSizes.add(style.fontSize);
}
});
const fontSizeVariations = fontSizes.size;
if (fontSizeVariations > 8) {
issues.push(`${fontSizeVariations} distinct font sizes (recommended: 4-6)`);
}
// Calculate whitespace ratio
const bodyRect = document.body.getBoundingClientRect();
const totalArea = bodyRect.width * Math.min(bodyRect.height, window.innerHeight);
let contentArea = 0;
const contentEls = document.querySelectorAll(
'p, h1, h2, h3, h4, h5, h6, img, button, input, select, textarea, table, ul, ol'
);
contentEls.forEach((el) => {
const rect = el.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0 && rect.top < window.innerHeight && rect.bottom > 0) {
contentArea += rect.width * rect.height;
}
});
const whitespaceRatio = totalArea > 0 ? 1 - contentArea / totalArea : 0;
if (whitespaceRatio < 0.2) {
issues.push(`Only ${(whitespaceRatio * 100).toFixed(0)}% whitespace (recommended: 30-50%)`);
}
// Check primary CTA clarity
const primaryButtons = document.querySelectorAll(
'button[type="submit"], .btn-primary, [data-testid*="primary"]'
);
let primaryActionClear = false;
if (primaryButtons.length === 1) {
const style = window.getComputedStyle(primaryButtons[0]);
const bg = style.backgroundColor;
primaryActionClear =
bg !== 'rgba(0, 0, 0, 0)' && bg !== 'rgb(255, 255, 255)' && bg !== 'transparent';
}
if (primaryButtons.length > 1) {
issues.push(`${primaryButtons.length} primary CTAs compete for attention`);
}
return {
headingLevels,
headingHierarchyValid: hierarchyValid,
fontSizeVariations,
whitespaceRatio,
primaryActionClear,
issues,
};
});
let distribution: HierarchyMetrics['visualWeightDistribution'];
if (data.headingHierarchyValid && data.whitespaceRatio > 0.3 && data.fontSizeVariations <= 6) {
distribution = 'clear-hierarchy';
} else if (data.fontSizeVariations <= 3) {
distribution = 'flat';
} else if (data.whitespaceRatio < 0.2) {
distribution = 'top-heavy';
} else {
distribution = 'balanced';
}
let score = 50;
if (data.headingHierarchyValid) score += 15;
if (data.primaryActionClear) score += 10;
if (data.whitespaceRatio >= 0.3 && data.whitespaceRatio <= 0.5) score += 10;
if (data.fontSizeVariations >= 4 && data.fontSizeVariations <= 6) score += 10;
if (data.fontSizeVariations > 8) score -= 10;
if (data.whitespaceRatio < 0.2) score -= 15;
if (!data.headingHierarchyValid) score -= 10;
score = Math.max(0, Math.min(100, score));
return {
...data,
visualWeightDistribution: distribution,
hierarchyScore: score,
};
}
}Consistency across pages reduces the cognitive cost of learning the interface.
// helpers/consistency-checker.ts
import { Page } from '@playwright/test';
export interface ConsistencyIssue {
category: 'naming' | 'layout' | 'interaction' | 'visual' | 'navigation';
description: string;
severity: 'high' | 'medium' | 'low';
pages: string[];
recommendation: string;
}
export interface ConsistencyMetrics {
issues: ConsistencyIssue[];
consistencyScore: number;
}
export class ConsistencyChecker {
private snapshots: Array<{
url: string;
buttonLabels: string[];
navStructure: string[];
headingPattern: string[];
layoutPattern: string;
}> = [];
async captureSnapshot(page: Page): Promise<void> {
const snapshot = await page.evaluate(() => {
const buttons = document.querySelectorAll('button, [role="button"]');
const buttonLabels = Array.from(buttons)
.map((b) => b.textContent?.trim())
.filter(Boolean) as string[];
const navLinks = document.querySelectorAll('nav a, [role="navigation"] a');
const navStructure = Array.from(navLinks)
.map((a) => a.textContent?.trim())
.filter(Boolean) as string[];
const headings = document.querySelectorAll('h1, h2, h3');
const headingPattern = Array.from(headings).map(
(h) => `${h.tagName}:${h.textContent?.trim().substring(0, 30)}`
);
const mainContent = document.querySelector('main, [role="main"]');
let layoutPattern = 'unknown';
if (mainContent) {
const style = window.getComputedStyle(mainContent);
if (style.display === 'grid') layoutPattern = 'grid';
else if (style.display === 'flex') layoutPattern = 'flex';
else layoutPattern = 'block';
}
return { buttonLabels, navStructure, headingPattern, layoutPattern };
});
this.snapshots.push({ url: page.url(), ...snapshot });
}
analyze(): ConsistencyMetrics {
const issues: ConsistencyIssue[] = [];
if (this.snapshots.length < 2) {
return { issues, consistencyScore: 100 };
}
// Check navigation consistency
const navStructures = this.snapshots.map((s) => JSON.stringify(s.navStructure));
if (new Set(navStructures).size > 1) {
issues.push({
category: 'navigation',
description: 'Navigation structure differs across pages',
severity: 'high',
pages: this.snapshots.map((s) => s.url),
recommendation: 'Use a consistent navigation component on all pages',
});
}
// Check button label consistency for common actions
const allLabels = this.snapshots.flatMap((s) => s.buttonLabels);
const saveVariants = allLabels.filter((l) =>
/^(save|submit|confirm|apply|done|ok|update)$/i.test(l)
);
const uniqueSaveLabels = new Set(saveVariants.map((l) => l.toLowerCase()));
if (uniqueSaveLabels.size > 2) {
issues.push({
category: 'naming',
description: `Multiple labels for save action: ${Array.from(uniqueSaveLabels).join(', ')}`,
severity: 'medium',
pages: this.snapshots.map((s) => s.url),
recommendation: 'Standardize on a single label for the primary save action',
});
}
const cancelVariants = allLabels.filter((l) =>
/^(cancel|close|dismiss|back|discard)$/i.test(l)
);
const uniqueCancelLabels = new Set(cancelVariants.map((l) => l.toLowerCase()));
if (uniqueCancelLabels.size > 2) {
issues.push({
category: 'naming',
description: `Multiple labels for cancel action: ${Array.from(uniqueCancelLabels).join(', ')}`,
severity: 'medium',
pages: this.snapshots.map((s) => s.url),
recommendation: 'Standardize on a single label for the cancel action',
});
}
// Check layout consistency
const layouts = this.snapshots.map((s) => s.layoutPattern);
if (new Set(layouts).size > 2) {
issues.push({
category: 'layout',
description: `${new Set(layouts).size} different layout patterns across pages`,
severity: 'medium',
pages: this.snapshots.map((s) => s.url),
recommendation: 'Use a consistent layout system for similar page types',
});
}
let score = 100;
for (const issue of issues) {
if (issue.severity === 'high') score -= 20;
else if (issue.severity === 'medium') score -= 10;
else score -= 5;
}
return { issues, consistencyScore: Math.max(0, score) };
}
}Measuring how many steps common tasks require reveals unnecessary workflow complexity.
// helpers/task-path-tracer.ts
import { Page } from '@playwright/test';
export interface TaskStep {
action: string;
url: string;
elementInteracted: string;
timestamp: number;
cognitiveEffort: 'low' | 'medium' | 'high';
}
export interface TaskPath {
taskName: string;
steps: TaskStep[];
totalSteps: number;
totalTimeMs: number;
pagesVisited: number;
backtrackCount: number;
cognitiveScore: number;
}
export class TaskPathTracer {
private steps: TaskStep[] = [];
private startTime: number = 0;
private visitedUrls: Set<string> = new Set();
private urlSequence: string[] = [];
startTask(): void {
this.steps = [];
this.startTime = Date.now();
this.visitedUrls = new Set();
this.urlSequence = [];
}
recordStep(
action: string,
page: Page,
elementDescription: string,
effort: TaskStep['cognitiveEffort'] = 'low'
): void {
const url = page.url();
this.visitedUrls.add(url);
this.urlSequence.push(url);
this.steps.push({
action,
url,
elementInteracted: elementDescription,
timestamp: Date.now(),
cognitiveEffort: effort,
});
}
completeTask(taskName: string): TaskPath {
const totalTimeMs = Date.now() - this.startTime;
let backtrackCount = 0;
const seen = new Set<string>();
for (const url of this.urlSequence) {
if (seen.has(url)) backtrackCount++;
seen.add(url);
}
let cognitiveScore = 0;
cognitiveScore += this.steps.length * 5;
cognitiveScore += backtrackCount * 15;
cognitiveScore += (this.visitedUrls.size - 1) * 10;
for (const step of this.steps) {
if (step.cognitiveEffort === 'high') cognitiveScore += 10;
else if (step.cognitiveEffort === 'medium') cognitiveScore += 5;
}
return {
taskName,
steps: [...this.steps],
totalSteps: this.steps.length,
totalTimeMs,
pagesVisited: this.visitedUrls.size,
backtrackCount,
cognitiveScore: Math.min(100, cognitiveScore),
};
}
}// tests/cognitive-load/information-density.spec.ts
import { test, expect } from '@playwright/test';
import { DensityCalculator } from '../helpers/density-calculator';
const pages = [
{ name: 'Homepage', path: '/' },
{ name: 'Dashboard', path: '/dashboard' },
{ name: 'Settings', path: '/settings' },
{ name: 'Product Listing', path: '/products' },
];
test.describe('Information Density Analysis', () => {
const calculator = new DensityCalculator();
for (const pg of pages) {
test(`${pg.name} has acceptable information density`, async ({ page }) => {
await page.goto(pg.path);
await page.waitForLoadState('networkidle');
const metrics = await calculator.calculate(page);
expect(metrics.textDensity).toBeGreaterThan(50);
expect(metrics.textDensity).toBeLessThan(800);
expect(metrics.totalInteractiveElements).toBeLessThanOrEqual(30);
if (metrics.informationUnits > 0) {
expect(metrics.informationUnits).toBeLessThanOrEqual(8);
}
expect(metrics.densityScore).toBeLessThan(80);
});
}
});// tests/cognitive-load/choice-overload.spec.ts
import { test, expect } from '@playwright/test';
import { ChoiceCounter } from '../helpers/choice-counter';
test.describe('Choice Overload Analysis', () => {
const counter = new ChoiceCounter();
test('navigation has 7 or fewer top-level items', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
const metrics = await counter.count(page);
expect(metrics.navigationItemCount).toBeLessThanOrEqual(7);
});
test('forms show 7 or fewer fields at once', async ({ page }) => {
await page.goto('/register');
await page.waitForLoadState('networkidle');
const metrics = await counter.count(page);
expect(metrics.formFieldCount).toBeLessThanOrEqual(7);
});
test('no more than 2 competing CTAs per viewport', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
const metrics = await counter.count(page);
expect(metrics.callToActionCount).toBeLessThanOrEqual(2);
});
test('total decision points stay under threshold', async ({ page }) => {
await page.goto('/dashboard');
await page.waitForLoadState('networkidle');
const metrics = await counter.count(page);
expect(metrics.totalDecisionPoints).toBeLessThanOrEqual(25);
expect(metrics.choiceOverloadScore).toBeLessThan(50);
});
});// tests/cognitive-load/visual-hierarchy.spec.ts
import { test, expect } from '@playwright/test';
import { HierarchyAnalyzer } from '../helpers/hierarchy-analyzer';
test.describe('Visual Hierarchy Analysis', () => {
const analyzer = new HierarchyAnalyzer();
test('heading hierarchy does not skip levels', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
const metrics = await analyzer.analyze(page);
expect(metrics.headingHierarchyValid).toBe(true);
});
test('whitespace ratio is between 20% and 70%', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
const metrics = await analyzer.analyze(page);
expect(metrics.whitespaceRatio).toBeGreaterThan(0.2);
expect(metrics.whitespaceRatio).toBeLessThan(0.7);
});
test('font size variations stay reasonable', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
const metrics = await analyzer.analyze(page);
expect(metrics.fontSizeVariations).toBeLessThanOrEqual(8);
});
test('hierarchy score meets minimum threshold', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
const metrics = await analyzer.analyze(page);
expect(metrics.hierarchyScore).toBeGreaterThanOrEqual(50);
});
});// tests/cognitive-load/consistency-audit.spec.ts
import { test, expect } from '@playwright/test';
import { ConsistencyChecker } from '../helpers/consistency-checker';
test.describe('Cross-Page Consistency Audit', () => {
test('navigation and terminology are consistent across pages', async ({ page }) => {
const checker = new ConsistencyChecker();
const pagesToVisit = ['/', '/dashboard', '/settings', '/products', '/about'];
for (const path of pagesToVisit) {
await page.goto(path);
await page.waitForLoadState('networkidle');
await checker.captureSnapshot(page);
}
const result = checker.analyze();
const highSeverity = result.issues.filter((i) => i.severity === 'high');
expect(highSeverity.length).toBe(0);
expect(result.consistencyScore).toBeGreaterThanOrEqual(70);
});
});// tests/cognitive-load/task-completion-paths.spec.ts
import { test, expect } from '@playwright/test';
import { TaskPathTracer } from '../helpers/task-path-tracer';
test.describe('Task Completion Path Complexity', () => {
test('user registration takes 5 or fewer steps', async ({ page }) => {
const tracer = new TaskPathTracer();
tracer.startTask();
await page.goto('/register');
tracer.recordStep('Navigate to registration', page, 'URL', 'low');
await page.fill('[name="name"]', 'Test User');
tracer.recordStep('Enter name', page, 'name input', 'low');
await page.fill('[name="email"]', '[email protected]');
tracer.recordStep('Enter email', page, 'email input', 'low');
await page.fill('[name="password"]', 'SecurePass123!');
tracer.recordStep('Enter password', page, 'password input', 'medium');
await page.click('button[type="submit"]');
tracer.recordStep('Submit form', page, 'submit button', 'low');
const result = tracer.completeTask('User Registration');
expect(result.totalSteps).toBeLessThanOrEqual(6);
expect(result.backtrackCount).toBe(0);
expect(result.pagesVisited).toBeLessThanOrEqual(2);
});
test('search task takes 3 or fewer steps', async ({ page }) => {
const tracer = new TaskPathTracer();
tracer.startTask();
await page.goto('/');
tracer.recordStep('Start at homepage', page, 'homepage', 'low');
await page.fill('input[type="search"], [data-testid="search-input"]', 'test');
tracer.recordStep('Enter search query', page, 'search input', 'low');
await page.keyboard.press('Enter');
tracer.recordStep('Execute search', page, 'keyboard', 'low');
const result = tracer.completeTask('Search');
expect(result.totalSteps).toBeLessThanOrEqual(4);
expect(result.backtrackCount).toBe(0);
});
});// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests/cognitive-load',
timeout: 30000,
retries: 0,
use: {
baseURL: process.env.BASE_URL || 'http://localhost:3000',
screenshot: 'on',
video: 'off',
trace: 'on-first-retry',
},
reporter: [
['html', { open: 'never' }],
['json', { outputFile: 'reports/cognitive-load-results.json' }],
],
projects: [
{
name: 'cognitive-desktop',
use: { browserName: 'chromium', viewport: { width: 1440, height: 900 } },
},
{
name: 'cognitive-mobile',
use: {
browserName: 'chromium',
viewport: { width: 375, height: 812 },
isMobile: true,
},
},
],
});{
"informationDensity": {
"maxWordsPerViewportK": 600,
"maxInteractiveElements": 30,
"maxInformationUnits": 8,
"maxDensityScore": 75
},
"choiceOverload": {
"maxNavigationItems": 7,
"maxFormFieldsVisible": 7,
"maxCTAs": 2,
"maxTabs": 6,
"maxCardChoices": 9,
"maxTotalDecisionPoints": 25,
"maxOverloadScore": 50
},
"visualHierarchy": {
"maxFontSizeVariations": 6,
"minWhitespaceRatio": 0.3,
"maxWhitespaceRatio": 0.6,
"minHierarchyScore": 60
},
"taskComplexity": {
"maxStepsForCommonTask": 5,
"maxPagesForCommonTask": 3,
"maxBacktracks": 0,
"maxCognitiveScore": 40
}
}~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.