@@ -47,6 +47,19 @@ const AXE_REVIEW_ROUTES = [
4747 "/en/trust-center" ,
4848 "/en/install-app" ,
4949] ;
50+ const INPUT_INTENT_AUDIT_ROUTES = [
51+ { route : "/en/qr-code-generator" , requiredIntents : [ "shortText" , "scalar" ] } ,
52+ { route : "/en/base64-encode-decode" , requiredIntents : [ "payload" , "generatedOutput" ] } ,
53+ { route : "/en/regex-tester" , requiredIntents : [ "scalar" , "shortText" , "payload" ] } ,
54+ { route : "/en/json-formatter" , requiredIntents : [ "workbench" , "payload" ] } ,
55+ { route : "/en/csv-json-converter" , requiredIntents : [ "workbench" , "payload" , "generatedOutput" ] } ,
56+ { route : "/en/jwt-decoder" , requiredIntents : [ "workbench" , "payload" , "generatedOutput" ] } ,
57+ { route : "/en/pipeline-builder" , requiredIntents : [ "workbench" , "shortText" , "payload" , "generatedOutput" ] } ,
58+ ] ;
59+ const INPUT_INTENT_AUDIT_VIEWPORTS = [
60+ { width : 390 , height : 844 , mobile : true } ,
61+ { width : 1280 , height : 900 , mobile : false } ,
62+ ] ;
5063const ALL_TOOLS_FILTER_INTERACTION_BUDGET_MS = 2000 ;
5164const ALL_TOOLS_MOBILE_SCROLL_BUDGET_MS = 3500 ;
5265const ALL_TOOLS_MOBILE_MAX_FRAME_DELTA_MS = 500 ;
@@ -194,6 +207,7 @@ function parseArgs(argv) {
194207 includePwa : false ,
195208 firstLoadOnly : false ,
196209 writeFirstLoadArtifacts : false ,
210+ inputIntentsOnly : false ,
197211 } ;
198212
199213 for ( const arg of argv ) {
@@ -217,6 +231,11 @@ function parseArgs(argv) {
217231 continue ;
218232 }
219233
234+ if ( arg === "--input-intents-only" ) {
235+ args . inputIntentsOnly = true ;
236+ continue ;
237+ }
238+
220239 if ( arg . startsWith ( "--port=" ) ) {
221240 const parsed = Number ( arg . slice ( "--port=" . length ) ) ;
222241 if ( Number . isFinite ( parsed ) && parsed > 0 ) {
@@ -1139,6 +1158,93 @@ async function assertMobileReviewMatrix(browser, baseUrl) {
11391158 }
11401159}
11411160
1161+ async function assertInputIntentSizingMatrix ( browser , baseUrl ) {
1162+ for ( const viewport of INPUT_INTENT_AUDIT_VIEWPORTS ) {
1163+ const context = await browser . newContext ( {
1164+ serviceWorkers : "block" ,
1165+ viewport : { width : viewport . width , height : viewport . height } ,
1166+ isMobile : viewport . mobile ,
1167+ } ) ;
1168+
1169+ try {
1170+ for ( const audit of INPUT_INTENT_AUDIT_ROUTES ) {
1171+ const page = await context . newPage ( ) ;
1172+ const routeLabel = `${ audit . route } input intent ${ viewport . width } x${ viewport . height } ` ;
1173+ const runtime = createRuntimeObserver ( page , routeLabel , baseUrl ) ;
1174+
1175+ try {
1176+ await page . goto ( `${ baseUrl } ${ audit . route } ` , { waitUntil : "domcontentloaded" } ) ;
1177+ await page . waitForSelector ( "main" , { timeout : 15_000 } ) ;
1178+ await page . locator ( "[data-input-intent]" ) . first ( ) . waitFor ( { state : "attached" , timeout : 15_000 } ) ;
1179+
1180+ const measurements = await page . evaluate ( ( { mobile } ) => {
1181+ const isVisible = ( element ) => {
1182+ const style = window . getComputedStyle ( element ) ;
1183+ const rect = element . getBoundingClientRect ( ) ;
1184+ return style . display !== "none" && style . visibility !== "hidden" && Number ( style . opacity ) !== 0 && rect . width > 0 && rect . height > 0 ;
1185+ } ;
1186+ const nodes = Array . from ( document . querySelectorAll ( "[data-input-intent]" ) ) . filter ( isVisible ) ;
1187+ const counts = { } ;
1188+ const issues = [ ] ;
1189+
1190+ for ( const element of nodes ) {
1191+ const intent = element . getAttribute ( "data-input-intent" ) ;
1192+ if ( ! intent ) continue ;
1193+ counts [ intent ] = ( counts [ intent ] || 0 ) + 1 ;
1194+
1195+ const rect = element . getBoundingClientRect ( ) ;
1196+ const tag = element . tagName . toLowerCase ( ) ;
1197+ const field = [ "input" , "textarea" , "select" ] . includes ( tag ) ;
1198+ if ( intent === "scalar" && field ) {
1199+ const target = element . closest ( "label" ) || element ;
1200+ const targetRect = target . getBoundingClientRect ( ) ;
1201+ const minimum = mobile ? 44 : 36 ;
1202+ if ( targetRect . height + 0.5 < minimum || targetRect . width + 0.5 < minimum ) {
1203+ issues . push ( `${ intent } ${ tag } : ${ Math . round ( targetRect . width ) } x${ Math . round ( targetRect . height ) } ` ) ;
1204+ }
1205+ }
1206+ if ( intent === "shortText" && tag === "input" && rect . height + 0.5 < ( mobile ? 44 : 44 ) ) {
1207+ issues . push ( `${ intent } input height: ${ Math . round ( rect . height ) } px` ) ;
1208+ }
1209+ if ( intent === "shortText" && tag === "textarea" ) {
1210+ const style = window . getComputedStyle ( element ) ;
1211+ const resize = style . resize ;
1212+ if ( rect . height + 0.5 < 144 ) issues . push ( `${ intent } textarea height: ${ Math . round ( rect . height ) } px (min-height ${ style . minHeight } ; ${ element . className } )` ) ;
1213+ if ( ! resize . includes ( "vertical" ) && resize !== "both" ) issues . push ( `${ intent } textarea resize: ${ resize } ` ) ;
1214+ }
1215+ if ( [ "payload" , "generatedOutput" ] . includes ( intent ) && [ "textarea" , "pre" ] . includes ( tag ) && rect . height + 0.5 < 256 ) {
1216+ issues . push ( `${ intent } ${ tag } height: ${ Math . round ( rect . height ) } px` ) ;
1217+ }
1218+ }
1219+
1220+ return { counts, issues } ;
1221+ } , { mobile : viewport . mobile } ) ;
1222+
1223+ for ( const intent of audit . requiredIntents ) {
1224+ if ( ! measurements . counts [ intent ] ) {
1225+ throw new Error ( `${ routeLabel } did not render required ${ intent } input intent.` ) ;
1226+ }
1227+ }
1228+ if ( measurements . issues . length > 0 ) {
1229+ throw new Error ( `${ routeLabel } input sizing failed:\n- ${ measurements . issues . join ( "\n- " ) } ` ) ;
1230+ }
1231+
1232+ await assertNoHorizontalOverflow ( page , routeLabel ) ;
1233+ const screenshot = await page . screenshot ( { animations : "disabled" , fullPage : false } ) ;
1234+ if ( screenshot . byteLength < 5_000 ) {
1235+ throw new Error ( `${ routeLabel } sizing screenshot was unexpectedly blank (${ screenshot . byteLength } bytes).` ) ;
1236+ }
1237+ runtime . assertClean ( ) ;
1238+ } finally {
1239+ await page . close ( ) ;
1240+ }
1241+ }
1242+ } finally {
1243+ await context . close ( ) ;
1244+ }
1245+ }
1246+ }
1247+
11421248async function assertAxeSeriousCriticalMatrix ( browser , baseUrl ) {
11431249 const context = await browser . newContext ( { serviceWorkers : "block" } ) ;
11441250
@@ -2046,6 +2152,9 @@ async function runSmoke(baseUrl, { writeFirstLoadArtifacts = false } = {}) {
20462152 await assertMobileReviewMatrix ( browser , baseUrl ) ;
20472153 console . log ( "[playwright-smoke] PASS mobile review matrix: no overflow or touch-target regressions" ) ;
20482154
2155+ await assertInputIntentSizingMatrix ( browser , baseUrl ) ;
2156+ console . log ( "[playwright-smoke] PASS input intent sizing matrix: mobile/desktop heights, touch targets, overflow, and screenshots" ) ;
2157+
20492158 await assertAxeSeriousCriticalMatrix ( browser , baseUrl ) ;
20502159 console . log ( "[playwright-smoke] PASS accessibility: no serious or critical axe violations on representative pages" ) ;
20512160 } finally {
@@ -2073,11 +2182,22 @@ async function runPwaSmoke(baseUrl) {
20732182 }
20742183}
20752184
2185+ async function runInputIntentSmoke ( baseUrl ) {
2186+ const browser = await chromium . launch ( { headless : true } ) ;
2187+ try {
2188+ await assertInputIntentSizingMatrix ( browser , baseUrl ) ;
2189+ console . log ( "[playwright-smoke] PASS input intent sizing matrix: mobile/desktop heights, touch targets, overflow, and screenshots" ) ;
2190+ } finally {
2191+ await browser . close ( ) ;
2192+ }
2193+ }
2194+
20762195async function main ( ) {
20772196 const {
20782197 baseUrl,
20792198 firstLoadOnly,
20802199 includePwa,
2200+ inputIntentsOnly,
20812201 port,
20822202 skipServer,
20832203 writeFirstLoadArtifacts,
@@ -2091,15 +2211,19 @@ async function main() {
20912211 console . log ( `[playwright-smoke] Static server ready at ${ baseUrl } ` ) ;
20922212 }
20932213
2094- if ( firstLoadOnly ) {
2214+ if ( inputIntentsOnly ) {
2215+ await runInputIntentSmoke ( baseUrl ) ;
2216+ } else if ( firstLoadOnly ) {
20952217 await runFirstLoadAudit ( baseUrl , { writeArtifacts : writeFirstLoadArtifacts } ) ;
20962218 } else {
20972219 await runSmoke ( baseUrl , { writeFirstLoadArtifacts } ) ;
20982220 }
2099- if ( includePwa && ! firstLoadOnly ) {
2221+ if ( includePwa && ! firstLoadOnly && ! inputIntentsOnly ) {
21002222 await runPwaSmoke ( baseUrl ) ;
21012223 }
2102- console . log ( "[playwright-smoke] PASS: critical routes render and navigate correctly" ) ;
2224+ if ( ! firstLoadOnly && ! inputIntentsOnly ) {
2225+ console . log ( "[playwright-smoke] PASS: critical routes render and navigate correctly" ) ;
2226+ }
21032227 } catch ( error ) {
21042228 console . error ( "[playwright-smoke] FAILED" ) ;
21052229 if ( serverHandle ) {
0 commit comments