Skip to main content

Layout Robustness & Fragility Detection

Functions for detecting layout fragility, text overflow issues, responsive risks, and performance problems.

These audits detect three categories of layout issues:

  • Overflow - Content spilling visibly outside container bounds
  • Clipping - Content silently cut off by overflow: hidden (content loss without visual indicator)
  • Truncation - Content shortened with ellipsis (intentional but may hide important text)

checkTextFragility

Detect text overflow, truncation, and layout shift risks. Analyzes all text elements on the page for issues that may cause content loss or layout problems at different viewport sizes.

window.__devtool.checkTextFragility()

Returns:

{
issues: [
{
selector: ".card-title",
text: "This is the visible text content...",
longestWord: {
word: "internationalization",
length: 20,
minWidthPx: 185
},
issues: [
{
type: "clipped", // Most dangerous: content silently lost
severity: "error",
message: "Text clipped by overflow:hidden without ellipsis - content is silently lost",
details: {
scrollWidth: 250,
clientWidth: 150,
hiddenContent: "100px",
suggestion: "Add text-overflow: ellipsis, or allow text to wrap, or increase container width"
}
}
],
problematicBreakpoints: [
{
breakpoint: 320,
estimatedWidth: 120,
requiredWidth: 185,
deficit: 65
},
{
breakpoint: 375,
estimatedWidth: 140,
requiredWidth: 185,
deficit: 45
}
]
}
],
summary: {
total: 4,
errors: 2,
warnings: 2,
elementsAnalyzed: 156
},
breakpointsTested: [320, 375, 414, 768, 1024, 1280, 1440, 1920]
}

Issue Types:

TypeSeverityCategoryDescription
clippederrorClippingText cut off by overflow:hidden without ellipsis - silent content loss
vertical-clippederrorClippingText cut off vertically by overflow:hidden - silent content loss
truncatedwarningTruncationText shortened with ellipsis (visible indicator, but content hidden)
horizontal-overflowerrorOverflowText spills outside container horizontally (visible)
vertical-overflowerrorOverflowText spills outside container vertically (visible)
multi-line-auto-heightwarningLayout ShiftMulti-line text with auto height - content changes may cause shift
long-word-no-breakwarningOverflow RiskLong word (>15 chars) without word-break may overflow on narrow screens

Key Features:

  • Clipping detection - Finds text silently cut off by overflow:hidden (the most dangerous issue)
  • Reports the longest word in each element and its minimum pixel width
  • Predicts which breakpoints will cause overflow issues
  • Detects both horizontal and vertical overflow/clipping
  • Identifies layout shift risks from dynamic content

Example:

const fragility = window.__devtool.checkTextFragility()

if (fragility.summary.errors > 0) {
console.log('Critical text issues found!')
fragility.issues
.filter(i => i.issues.some(issue => issue.severity === 'error'))
.forEach(i => {
console.log(`${i.selector}: ${i.text}`)
console.log(` Longest word: "${i.longestWord.word}" needs ${i.longestWord.minWidthPx}px`)
if (i.problematicBreakpoints.length > 0) {
console.log(` Breaks at: ${i.problematicBreakpoints.map(b => b.breakpoint + 'px').join(', ')}`)
}
})
}

checkResponsiveRisk

Detect elements at risk of layout issues at different viewport sizes. Checks for fixed dimensions, small touch targets, horizontal scroll issues, and more.

window.__devtool.checkResponsiveRisk()

Returns:

{
issues: [
{
selector: ".product-card",
tagName: "div",
issues: [
{
type: "fixed-width",
severity: "warning",
message: "Fixed width (400px) may cause horizontal scroll on mobile",
details: {
width: "400px",
breakpointsAffected: [320, 375]
}
}
]
},
{
selector: "button.small-btn",
tagName: "button",
issues: [
{
type: "small-touch-target",
severity: "warning",
message: "Touch target smaller than 44x44px minimum",
details: {
width: "32px",
height: "28px",
recommended: "44x44px minimum"
}
}
]
}
],
summary: {
total: 5,
errors: 1,
warnings: 4,
elementsAnalyzed: 245
},
currentViewport: {
width: 1440,
height: 900
},
breakpointsTested: [320, 375, 414, 768, 1024, 1280, 1440, 1920]
}

Issue Types:

TypeSeverityDescription
fixed-widthwarning/errorFixed pixel width may cause horizontal scroll
min-width-too-largewarning/errormin-width larger than mobile viewports
exceeds-viewporterrorElement currently exceeds viewport width
small-touch-targetwarningInteractive element smaller than 44x44px (Apple HIG)
unintended-horizontal-scrollerrorElement causes horizontal scroll without overflow-x setting
positioned-offscreen-rightwarningAbsolute/fixed element extends past right edge
large-fixed-elementwarningLarge fixed element may obscure content on mobile
small-fontwarningFont size below 12px may be hard to read
extreme-font-sizewarningVery large or small font may cause issues
wide-tableerrorTable wider than viewport
table-not-scrollablewarningWide table without horizontal scroll wrapper

Example:

const risks = window.__devtool.checkResponsiveRisk()

// Check for critical issues
if (risks.summary.errors > 0) {
console.log('Critical responsive issues:')
risks.issues.forEach(el => {
el.issues
.filter(i => i.severity === 'error')
.forEach(issue => {
console.log(`${el.selector}: ${issue.message}`)
})
})
}

// Find small touch targets
const smallTargets = risks.issues.filter(el =>
el.issues.some(i => i.type === 'small-touch-target')
)
console.log(`${smallTargets.length} elements have small touch targets`)

capturePerformanceMetrics

Capture comprehensive performance metrics including CLS, long tasks, and resource timing.

window.__devtool.capturePerformanceMetrics()

Returns:

{
cls: {
score: 0.15,
rating: "needs-improvement", // "good" | "needs-improvement" | "poor"
shifts: [
{
value: 0.08,
startTime: 1523,
sources: [".ad-banner", ".hero-image"]
}
]
},
longTasks: [
{ duration: 85, startTime: 234, name: "self" }
],
resources: {
byType: {
script: { count: 12, totalSize: 450000, totalDuration: 1200 },
img: { count: 25, totalSize: 1200000, totalDuration: 2500 },
css: { count: 5, totalSize: 85000, totalDuration: 300 }
},
largest: [
{ url: "/images/hero.jpg", type: "img", size: 450000, duration: 800 }
],
slowest: [
{ url: "/api/data", type: "fetch", size: 5000, duration: 1200 }
],
renderBlocking: [
{ url: "/styles.css", type: "css", size: 45000, duration: 200 }
]
},
paint: {
firstPaint: 450,
firstContentfulPaint: 620
},
totals: {
pageWeight: 1855000,
resourceCount: 46,
loadTime: 2800,
domContentLoaded: 1200
},
timestamp: 1699999999999
}

CLS Rating Thresholds:

  • good: < 0.1
  • needs-improvement: 0.1 - 0.25
  • poor: > 0.25

Example:

const perf = window.__devtool.capturePerformanceMetrics()

console.log(`Page weight: ${(perf.totals.pageWeight / 1024 / 1024).toFixed(2)} MB`)
console.log(`Load time: ${perf.totals.loadTime}ms`)

if (perf.cls && perf.cls.rating !== 'good') {
console.log('CLS issues:', perf.cls.shifts)
}

E2E Testing Examples

The agnt audit system is tested using Playwright. Here are examples from the test suite:

Running Audits via the Indicator UI

import { test, expect, Page } from '@playwright/test';

// Wait for the __devtool API to be available
async function waitForDevtool(page: Page): Promise<void> {
await page.waitForFunction(
() => {
return (
typeof window.__devtool !== 'undefined' &&
window.__devtool !== null &&
typeof window.__devtool.indicator !== 'undefined'
);
},
{ timeout: 15000 }
);
}

// Run an audit and check results
test('Text Fragility audit detects overflow issues', async ({ page }) => {
await page.goto('http://localhost:12345/layout-issues.html');
await waitForDevtool(page);

// Open the indicator panel
await page.evaluate(() => {
window.__devtool.indicator.togglePanel(true);
});

// Click the Audit dropdown
const auditBtn = page.locator('#__devtool-indicator button').filter({
hasText: 'Audit',
}).first();
await auditBtn.click({ force: true });

// Select Text Fragility audit
await page.waitForSelector('#__devtool-audit-menu');
const menuItem = page.locator('#__devtool-audit-menu button').filter({
hasText: 'Text Fragility',
});
await menuItem.click({ force: true });

// Wait for audit to complete and check results
await page.waitForTimeout(2000);
const result = await page.evaluate(() => {
return window.__devtool.checkTextFragility();
});

expect(result.summary.elementsAnalyzed).toBeGreaterThan(0);
expect(result.breakpointsTested).toContain(320);
});

Test Fixtures for Layout Issues

Create test pages with known issues to validate audits:

<!-- layout-issues.html -->
<!DOCTYPE html>
<html>
<head>
<style>
/* Fixed width that breaks on mobile */
.fixed-width-card {
width: 500px;
border: 1px solid #ccc;
}

/* Text that will overflow */
.overflow-text {
width: 100px;
white-space: nowrap;
overflow: visible;
}

/* Small touch target */
.tiny-button {
width: 20px;
height: 20px;
padding: 0;
}

/* Long word without word-break */
.long-word-container {
width: 150px;
}
</style>
</head>
<body>
<div class="fixed-width-card">
<p>This card has a fixed width that will cause issues on mobile.</p>
</div>

<div class="overflow-text">
This text will overflow its container because of nowrap.
</div>

<button class="tiny-button">X</button>

<div class="long-word-container">
<p>Contains supercalifragilisticexpialidocious which is very long.</p>
</div>
</body>
</html>

Smoke Test for All Audits

test('All audits complete without errors', async ({ page }) => {
await page.goto('http://localhost:12345/clean-baseline.html');
await waitForDevtool(page);

const audits = [
'Full Page Audit',
'Accessibility',
'Security',
'SEO / Meta',
'Layout Issues',
'Text Fragility',
'Responsive Risk',
'Last Click Context',
'Recent DOM Changes',
'Browser State',
'Network/Resources',
'DOM Complexity',
'CSS Quality',
];

for (const label of audits) {
const result = await runAudit(page, label);
expect.soft(result.success, `${label} should appear in results`).toBe(true);
expect.soft(result.hasError, `${label} should not error`).toBe(false);
}
});

Common Patterns

Pre-Deploy Quality Check

async function preDeployCheck() {
const textFragility = window.__devtool.checkTextFragility()
const responsiveRisk = window.__devtool.checkResponsiveRisk()

const issues = []

// No text overflow errors
if (textFragility.summary.errors > 0) {
issues.push(`${textFragility.summary.errors} text overflow errors`)
}

// No viewport overflow
const viewportOverflows = responsiveRisk.issues.filter(el =>
el.issues.some(i => i.type === 'exceeds-viewport')
)
if (viewportOverflows.length > 0) {
issues.push(`${viewportOverflows.length} elements overflow viewport`)
}

// No critical touch target issues
const tinyTargets = responsiveRisk.issues.filter(el =>
el.issues.some(i => i.type === 'small-touch-target')
)
if (tinyTargets.length > 5) {
issues.push(`${tinyTargets.length} small touch targets`)
}

return {
pass: issues.length === 0,
issues: issues,
textFragility: textFragility.summary,
responsiveRisk: responsiveRisk.summary
}
}

Mobile Compatibility Check

function checkMobileReady() {
const risks = window.__devtool.checkResponsiveRisk()

// Find issues that affect mobile breakpoints (320-414px)
const mobileIssues = risks.issues.filter(el =>
el.issues.some(i =>
i.details?.breakpointsAffected?.some(bp => bp <= 414)
)
)

console.log(`Mobile issues: ${mobileIssues.length}`)
mobileIssues.forEach(el => {
console.log(` ${el.selector}:`)
el.issues.forEach(i => console.log(` - ${i.message}`))
})

return {
mobileReady: mobileIssues.length === 0,
issues: mobileIssues
}
}

Content Loss Detection

function findContentLoss() {
const fragility = window.__devtool.checkTextFragility()

// Find elements losing content - prioritize silent clipping (worst)
const contentLoss = fragility.issues.filter(el =>
el.issues.some(i =>
i.type === 'clipped' || // Silent horizontal clipping (worst)
i.type === 'vertical-clipped' || // Silent vertical clipping (worst)
i.type === 'truncated' || // Visible truncation with ellipsis
i.type === 'horizontal-overflow' ||
i.type === 'vertical-overflow'
)
)

// Report silent clipping first (most dangerous)
const silentClipping = contentLoss.filter(el =>
el.issues.some(i => i.type === 'clipped' || i.type === 'vertical-clipped')
)

if (silentClipping.length > 0) {
console.log('CRITICAL: Silent content clipping detected!')
silentClipping.forEach(el => {
console.log(` ${el.selector}: "${el.text}"`)
const clip = el.issues.find(i => i.type === 'clipped' || i.type === 'vertical-clipped')
console.log(` Hidden: ${clip.details.hiddenContent}`)
console.log(` Fix: ${clip.details.suggestion}`)
})
}

contentLoss.forEach(el => {
console.log(`Content lost at: ${el.selector}`)
console.log(` Text: "${el.text}"`)
console.log(` Longest word: "${el.longestWord.word}" (${el.longestWord.length} chars)`)
console.log(` Min width needed: ${el.longestWord.minWidthPx}px`)
if (el.problematicBreakpoints.length > 0) {
console.log(` Problematic at: ${el.problematicBreakpoints.map(b => b.breakpoint + 'px').join(', ')}`)
}
})

return contentLoss
}

Performance Notes

  • checkTextFragility and checkResponsiveRisk scan all elements - may be slow on large pages
  • Both functions are synchronous and block the main thread
  • Use for diagnostic purposes, not in production hot paths
  • Consider running audits after page load completes
  • The indicator UI runs audits asynchronously and displays results as attachment chips

See Also