Skip to content

Commit 91ebecc

Browse files
trialclaude
andcommitted
test(browser): measure the paint, not just the markup — served at another URL the rewritten stars fill and the absolute ones do not; v1.40.0
The PR claimed an absolute self-reference loses its fill at another URL as a hypothesis. Measured with Chrome 148: at a second same-origin URL, `url(<render URL>#id)` and the path-absolute spelling paint nothing (an external paint server), the browser fetches the render URL trying to resolve it, and `url(#id)` paints. The new test renders a page, serves the snapshot at another URL and reads the pixel; a control with the absolute reference restored must read unfilled. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent b748691 commit 91ebecc

2 files changed

Lines changed: 57 additions & 3 deletions

File tree

‎packages/browser/README.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -831,8 +831,9 @@ route whose cache key drops the query, and (plugin ≥ 0.100.0, `entityServe`) e
831831
the same entity. Anything a script built from `location` names the render URL. Links to it are
832832
harmless, but a CSS reference is not: a reviews widget that fills its rating stars with
833833
`fill="url(<page URL>#rating_star_filled)"` names a gradient in the same document only while the
834-
snapshot is served at that exact URL. Anywhere else the reference is external, and the stars lose
835-
their fill.
834+
snapshot is served at that exact URL. Anywhere else the reference is external. Measured in Chrome 148,
835+
an external paint server paints nothing, so the stars lose their fill, and the browser also fetches the
836+
render URL while trying to resolve it.
836837

837838
So the serialized snapshot rewrites every CSS `url(<this page's URL>#id)` (absolute, scheme-relative
838839
or path-absolute; any quoting) to `url(#id)`. A fragment-only `url()` always refers to the current

‎packages/browser/test/selfReferences.test.ts‎

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ let base = '';
8383

8484
const FIXTURE = `<!doctype html><html><head><title>stars</title></head><body>
8585
<svg width="0" height="0"><defs><linearGradient id="star_filled"><stop offset="1" stop-color="#fc0"/></linearGradient></defs></svg>
86-
<svg id="stars" width="20" height="20"></svg>
86+
<svg id="stars" width="20" height="20" style="display:block"></svg>
8787
<script>
8888
document.getElementById('stars').innerHTML =
8989
'<path d="M0 0h20v20H0z" fill="url(' + location.href.split('#')[0] + '#star_filled)"></path>';
@@ -109,3 +109,56 @@ test('a rendered snapshot carries the fragment-only reference a script built fro
109109
assert.match(result.html!, /fill="url\(#star_filled\)"/);
110110
assert.doesNotMatch(result.html!, /url\(http/, 'no absolute self-reference survives');
111111
});
112+
113+
// ── the reason, measured: what each spelling PAINTS when the snapshot is served at another URL ─────────
114+
115+
const paintOf = async (url: string, selector: string): Promise<'filled' | 'unfilled'> => {
116+
const puppeteer = (await import('puppeteer')).default;
117+
const browser = await puppeteer.launch({ headless: true });
118+
try {
119+
const page = await browser.newPage();
120+
await page.goto(url, { waitUntil: 'networkidle0' });
121+
const png = await (await page.$(selector))!.screenshot({ encoding: 'base64' });
122+
// Decoded in the same tab: a second tab would leave this one in the background, where screenshots hang.
123+
const [r, g, b] = await page.evaluate(async (b64: string) => {
124+
const img = new Image();
125+
img.src = `data:image/png;base64,${b64}`;
126+
await img.decode();
127+
const canvas = document.createElement('canvas');
128+
canvas.width = img.width;
129+
canvas.height = img.height;
130+
const ctx = canvas.getContext('2d')!;
131+
ctx.drawImage(img, 0, 0);
132+
return [...ctx.getImageData(img.width >> 1, img.height >> 1, 1, 1).data.slice(0, 3)];
133+
}, png);
134+
// The gradient is #fc0 (255, 204, 0); the page behind it is white.
135+
return r > 200 && g > 150 && b < 80 ? 'filled' : 'unfilled';
136+
} finally {
137+
await browser.close();
138+
}
139+
};
140+
141+
test('served at ANOTHER URL, the rewritten snapshot still paints its stars — the absolute one does not', async () => {
142+
const rendered = await renderOnce({ url: `${base}/product/prd-1/slug.jsp`, config: { scroll: { enabled: false } } });
143+
const snapshot = rendered.html!.replace(/<script[\s\S]*?<\/script>/g, '');
144+
const absolute = snapshot.replace('url(#star_filled)', `url(${base}/product/prd-1/slug.jsp#star_filled)`);
145+
assert.notEqual(absolute, snapshot, 'the control restores the absolute reference');
146+
const served = new Map([
147+
['/product/prd-1/OTHER-SPELLING.jsp', snapshot],
148+
['/product/prd-1/control.jsp', absolute],
149+
]);
150+
const other = http.createServer((req, res) => {
151+
res.setHeader('content-type', 'text/html; charset=utf-8');
152+
res.end(served.get(req.url ?? '') ?? FIXTURE);
153+
});
154+
await new Promise<void>((resolve) => other.listen(0, '127.0.0.1', resolve));
155+
const at = (path: string) => `http://127.0.0.1:${(other.address() as AddressInfo).port}${path}`;
156+
try {
157+
assert.equal(await paintOf(at('/product/prd-1/OTHER-SPELLING.jsp'), '#stars'), 'filled');
158+
// Measured with Chrome 148: an absolute self-reference at another URL is an external paint server,
159+
// which paints nothing (the browser even fetches that URL trying to resolve it).
160+
assert.equal(await paintOf(at('/product/prd-1/control.jsp'), '#stars'), 'unfilled');
161+
} finally {
162+
other.close();
163+
}
164+
});

0 commit comments

Comments
 (0)