From 929d1783c0ec9f082783cc2fc00d5b2a78f0069e Mon Sep 17 00:00:00 2001 From: Narendra Singh Kushwah <158804600+kmjtechno@users.noreply.github.com> Date: Tue, 10 Nov 2026 14:15:28 +0530 Subject: [PATCH 1/4] fix(browser): bounded mobile lazy-image scroll before strict pending-image QA --- scripts/browser-public-qa.mjs | 49 +++++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/scripts/browser-public-qa.mjs b/scripts/browser-public-qa.mjs index 853dade54..635260349 100644 --- a/scripts/browser-public-qa.mjs +++ b/scripts/browser-public-qa.mjs @@ -50,6 +50,52 @@ export function assess(sample) { const digest = (buf) => createHash("sha256").update(buf).digest("hex"); const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); +// Fixed browser-internal QA expression. Chromium may not request below-fold +// native lazy images simply because a test flips loading="eager" once. +// The mobile homepage is much taller than a desktop viewport, so explicitly +// traverse a bounded distance before settling. Never classify pending assets +// as PASS, navigate to arbitrary URLs, or inject browser-side user inputs. +export const IMAGE_SETTLE_EXPRESSION = String.raw`(async()=>{ + const pause=ms=>new Promise(resolve=>setTimeout(resolve,ms)); + const html=document.documentElement; + const previous=html.style.scrollBehavior; + const promote=()=>{ + for(const image of document.images)if(image.loading==='lazy')image.loading='eager'; + }; + html.style.scrollBehavior='auto'; + try{ + const MAX_SCROLL_PX=24000; + const SCROLL_STEP_PX=600; + const MAX_SCROLL_STEPS=41; + for(let n=0;n=height)break; + } + promote(); + const images=[...document.images]; + await Promise.race([ + Promise.all(images.map(image=>image.complete?Promise.resolve(): + new Promise(resolve=>{ + image.addEventListener('load',resolve,{once:true}); + image.addEventListener('error',resolve,{once:true}); + }) + )), + pause(4000) + ]); + await document.fonts.ready; + return true; + }finally{ + window.scrollTo(0,0); + html.style.scrollBehavior=previous; + } +})()`; + // A load event can precede client hydration. Permit a tightly bounded retry // only when text is initially blank; persistent blanks remain QA failures. export async function measureAfterTextReady( @@ -313,8 +359,7 @@ export async function publicBrowserQa(targetName, outputDir) { { awaitPromise: true, returnByValue: true, - expression: - "(async()=>{const images=[...document.images];for(const image of images)if(image.loading==='lazy')image.loading='eager';await Promise.race([Promise.all(images.map(image=>image.complete?Promise.resolve():new Promise(resolve=>{image.addEventListener('load',resolve,{once:true});image.addEventListener('error',resolve,{once:true})}))),new Promise(resolve=>setTimeout(resolve,4000))]);await document.fonts.ready;return true})()", + expression: IMAGE_SETTLE_EXPRESSION, }, sessionId, ); From 4389540caf26285f090552ca456d19a9655b6813 Mon Sep 17 00:00:00 2001 From: Narendra Singh Kushwah <158804600+kmjtechno@users.noreply.github.com> Date: Tue, 10 Nov 2026 14:16:24 +0530 Subject: [PATCH 2/4] test(browser): VM-simulate mobile offscreen lazy images, late hydration and bounded timeout --- tests/browser-public-qa.test.js | 100 ++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/tests/browser-public-qa.test.js b/tests/browser-public-qa.test.js index 5918b8791..abf4d3b30 100644 --- a/tests/browser-public-qa.test.js +++ b/tests/browser-public-qa.test.js @@ -3,6 +3,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import test from "node:test"; +import vm from "node:vm"; import { TARGETS, VIEWPORTS, @@ -12,6 +13,7 @@ import { createChromePrivateEnvironment, measureAfterTextReady, measureAfterImagesReady, + IMAGE_SETTLE_EXPRESSION, } from "../scripts/browser-public-qa.mjs"; test("browser test destinations and viewports are fixed", () => { @@ -202,6 +204,104 @@ test("browser compatibility profile uses captured commands without widening exec assert.match(src, /timeoutMs: 120000/); }); + +test("mobile lazy-image traversal reaches below fold and restores scroll state", async () => { + const positions = []; + const image = { + loading: "lazy", + complete: false, + naturalWidth: 0, + listeners: {}, + addEventListener(type, listener) { + this.listeners[type] = listener; + }, + }; + const second = { + loading: "lazy", + complete: false, + naturalWidth: 0, + listeners: {}, + addEventListener(type, listener) { + this.listeners[type] = listener; + }, + }; + const html = { scrollHeight: 3200, style: { scrollBehavior: "smooth" } }; + const document = { + documentElement: html, + body: { scrollHeight: 3200 }, + fonts: { ready: Promise.resolve() }, + images: [image], + }; + const window = { + scrollTo(_x, y) { + positions.push(y); + if (y >= 600) { + image.complete = true; + image.naturalWidth = 240; + image.listeners.load?.(); + } + if (y >= 1200 && !document.images.includes(second)) + document.images.push(second); + if (y >= 1800) { + second.complete = true; + second.naturalWidth = 280; + second.listeners.load?.(); + } + }, + }; + const done = await vm.runInNewContext( + IMAGE_SETTLE_EXPRESSION, + { document, window, setTimeout: (callback) => { callback(); return 0; } }, + ); + assert.equal(done, true); + assert.ok(positions.includes(600)); + assert.ok(positions.includes(1800)); + assert.equal(positions.at(-1), 0); + assert.equal(html.style.scrollBehavior, "smooth"); + assert.equal(document.images.length, 2); + assert.ok(document.images.every((i) => i.complete && i.naturalWidth > 0)); + assert.ok(document.images.every((i) => i.loading === "eager")); +}); + +test("lazy image traversal is capped and never turns unresolved images into PASS", async () => { + const positions = []; + const pending = { + loading: "lazy", complete: false, naturalWidth: 0, + addEventListener() {}, + }; + const html = { scrollHeight: 1000000000, style: { scrollBehavior: "auto" } }; + const document = { + documentElement: html, + body: { scrollHeight: 1000000000 }, + fonts: { ready: Promise.resolve() }, + images: [pending], + }; + const window = { + scrollTo(_x, y) { positions.push(y); }, + }; + assert.equal( + await vm.runInNewContext(IMAGE_SETTLE_EXPRESSION, { + document, window, setTimeout: (callback) => { callback(); return 0; }, + }), + true, + ); + assert.ok(positions.length <= 42); + assert.ok(positions.every((n) => n >= 0 && n <= 24000)); + assert.equal(positions.at(-1), 0); + assert.equal(pending.complete, false); + const sample = { + actualUrl: "https://kmjtechno.com/", + expectedUrl: "https://kmjtechno.com/", + httpStatus: 200, bodyLength: 600, + overflowPx: 0, exceptions: 0, + brokenImages: 1, pendingImages: 1, failedImages: 0, + }; + assert.deepEqual(assess(sample), ["PENDING_IMAGES_TIMEOUT"]); + const source = fs.readFileSync("scripts/browser-public-qa.mjs", "utf8"); + assert.match(source, /expression: IMAGE_SETTLE_EXPRESSION/); + assert.doesNotMatch(source, /--no-sandbox|--remote-debugging-port/); +}); + test("mobile lazy images settle within bounded retry without suppressing actual failures", async () => { let now = 0; let checks = 0; From 953fe58075c884247ce4f9bf8d35fb43b657b8db Mon Sep 17 00:00:00 2001 From: Narendra Singh Kushwah <158804600+kmjtechno@users.noreply.github.com> Date: Tue, 10 Nov 2026 14:17:03 +0530 Subject: [PATCH 3/4] docs(browser): disclose bounded below-fold image traversal and strict fail outcomes --- docs/BROWSER-LIVE-QA.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/BROWSER-LIVE-QA.md b/docs/BROWSER-LIVE-QA.md index 0ccf4e0f3..dab111ee6 100644 --- a/docs/BROWSER-LIVE-QA.md +++ b/docs/BROWSER-LIVE-QA.md @@ -17,9 +17,14 @@ Each run captures public pages at 1440, 768, and 390-pixel widths (nine screenshots). It checks actual navigation, HTTP 200, nonblank text, broken images, uncaught JS exceptions, and horizontal overflow. Incomplete or undecodable images fail closed rather than being counted as healthy. The isolated browser -first requests deferred lazy images and waits up to four seconds for load/error -before measurement; images still pending afterward are recorded as failures. Mobile lazy-image -loading has a second bounded 3-second settle phase for _pending-only_ images; +first traverses the current document in a fixed, bounded scroll (at most 24,000 +pixels in 600-pixel increments), promoting native lazy images to eager as they +enter view; this also notices images inserted by late hydration. It returns to +the original page top and restores scroll behavior. The traversal is bounded +to 41 steps with a 60-millisecond delay per step. It then waits up to four +seconds for load/error before measurement. Images still pending afterward +remain failures. Mobile lazy-image loading has a second bounded 3-second +settle phase for _pending-only_ images; hard-failed or undecodable images still fail immediately. Each sample now reports safe aggregate `pendingImages` and `failedImages` counts (no URLs, DOM content, cookies or image bytes). The initial and second windows are From ed5e4255cdef2ad74a29b4efa63e2bff6f6cc1cd Mon Sep 17 00:00:00 2001 From: Narendra Singh Kushwah <158804600+kmjtechno@users.noreply.github.com> Date: Tue, 10 Nov 2026 14:19:46 +0530 Subject: [PATCH 4/4] style(browser): match Prettier for VM lazy-image regression tests --- tests/browser-public-qa.test.js | 38 +++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/tests/browser-public-qa.test.js b/tests/browser-public-qa.test.js index abf4d3b30..9263e8176 100644 --- a/tests/browser-public-qa.test.js +++ b/tests/browser-public-qa.test.js @@ -204,7 +204,6 @@ test("browser compatibility profile uses captured commands without widening exec assert.match(src, /timeoutMs: 120000/); }); - test("mobile lazy-image traversal reaches below fold and restores scroll state", async () => { const positions = []; const image = { @@ -249,10 +248,14 @@ test("mobile lazy-image traversal reaches below fold and restores scroll state", } }, }; - const done = await vm.runInNewContext( - IMAGE_SETTLE_EXPRESSION, - { document, window, setTimeout: (callback) => { callback(); return 0; } }, - ); + const done = await vm.runInNewContext(IMAGE_SETTLE_EXPRESSION, { + document, + window, + setTimeout: (callback) => { + callback(); + return 0; + }, + }); assert.equal(done, true); assert.ok(positions.includes(600)); assert.ok(positions.includes(1800)); @@ -266,7 +269,9 @@ test("mobile lazy-image traversal reaches below fold and restores scroll state", test("lazy image traversal is capped and never turns unresolved images into PASS", async () => { const positions = []; const pending = { - loading: "lazy", complete: false, naturalWidth: 0, + loading: "lazy", + complete: false, + naturalWidth: 0, addEventListener() {}, }; const html = { scrollHeight: 1000000000, style: { scrollBehavior: "auto" } }; @@ -277,11 +282,18 @@ test("lazy image traversal is capped and never turns unresolved images into PASS images: [pending], }; const window = { - scrollTo(_x, y) { positions.push(y); }, + scrollTo(_x, y) { + positions.push(y); + }, }; assert.equal( await vm.runInNewContext(IMAGE_SETTLE_EXPRESSION, { - document, window, setTimeout: (callback) => { callback(); return 0; }, + document, + window, + setTimeout: (callback) => { + callback(); + return 0; + }, }), true, ); @@ -292,9 +304,13 @@ test("lazy image traversal is capped and never turns unresolved images into PASS const sample = { actualUrl: "https://kmjtechno.com/", expectedUrl: "https://kmjtechno.com/", - httpStatus: 200, bodyLength: 600, - overflowPx: 0, exceptions: 0, - brokenImages: 1, pendingImages: 1, failedImages: 0, + httpStatus: 200, + bodyLength: 600, + overflowPx: 0, + exceptions: 0, + brokenImages: 1, + pendingImages: 1, + failedImages: 0, }; assert.deepEqual(assess(sample), ["PENDING_IMAGES_TIMEOUT"]); const source = fs.readFileSync("scripts/browser-public-qa.mjs", "utf8");