autokap 1.1.7 → 1.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser-pool.js +10 -0
- package/dist/browser.js +136 -33
- package/package.json +1 -1
package/dist/browser-pool.js
CHANGED
|
@@ -6,6 +6,16 @@ export const CHROMIUM_ARGS = [
|
|
|
6
6
|
'--no-sandbox',
|
|
7
7
|
'--disable-setuid-sandbox',
|
|
8
8
|
'--disable-dev-shm-usage',
|
|
9
|
+
// Disable font hinting to match macOS Core Text subpixel positioning.
|
|
10
|
+
// Linux Chromium defaults to "medium" hinting which pixel-snaps glyph
|
|
11
|
+
// advance widths — this widens text horizontally and shifts wrap points
|
|
12
|
+
// vs a Mac developer's local view. Validated via Docker repro on
|
|
13
|
+
// autokap.app: with default hinting "Vos screenshots en autopilote"
|
|
14
|
+
// wraps as "...screenshots en / autopilote", on Mac and with
|
|
15
|
+
// hinting=none it wraps as "...screenshots / en autopilote" (identical
|
|
16
|
+
// to local). Trade-off: minor blurriness at <12px sizes; acceptable for
|
|
17
|
+
// marketing screenshots where layout fidelity matters more.
|
|
18
|
+
'--font-render-hinting=none',
|
|
9
19
|
] : []),
|
|
10
20
|
// No GPU on headless servers
|
|
11
21
|
'--disable-gpu',
|
package/dist/browser.js
CHANGED
|
@@ -120,6 +120,40 @@ async function withHelperTimeout(label, timeoutMs, work) {
|
|
|
120
120
|
function isLikelyFontUrl(url) {
|
|
121
121
|
return /\.(?:woff2?|ttf|otf)(?:[?#]|$)/i.test(url);
|
|
122
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* Strip C0/C1 control characters (except TAB/LF/CR) and DEL from text nodes
|
|
125
|
+
* before screenshot. macOS Core Text renders these as zero-width/invisible by
|
|
126
|
+
* default; Linux Skia + most webfonts render them as `.notdef` tofu boxes
|
|
127
|
+
* because the font's unicode-range claims coverage of U+0000-007F but the
|
|
128
|
+
* actual font file has no glyph for control characters. Examples seen in
|
|
129
|
+
* customer sites: `\x1B[32m...\x1B[0m` ANSI escapes embedded in mock terminal
|
|
130
|
+
* output, BOM-like markers in CMS-pasted text, etc.
|
|
131
|
+
*
|
|
132
|
+
* This normalizes Linux capture output to match what a developer sees on
|
|
133
|
+
* their Mac. Idempotent — safe to run once per screenshot.
|
|
134
|
+
*
|
|
135
|
+
* Preserves: U+0009 TAB, U+000A LF, U+000D CR (semantic whitespace).
|
|
136
|
+
* Strips: U+0000-0008, U+000B, U+000C, U+000E-001F, U+007F (DEL), U+0080-009F.
|
|
137
|
+
*/
|
|
138
|
+
async function stripInvisibleControlChars(page) {
|
|
139
|
+
await page.evaluate(() => {
|
|
140
|
+
const RE = new RegExp('[\\u0000-\\u0008\\u000B\\u000C\\u000E-\\u001F\\u007F-\\u009F]', 'g');
|
|
141
|
+
const walk = (node) => {
|
|
142
|
+
if (node.nodeType === 3) {
|
|
143
|
+
const original = node.nodeValue ?? '';
|
|
144
|
+
if (original.length > 0 && RE.test(original)) {
|
|
145
|
+
node.nodeValue = original.replace(RE, '');
|
|
146
|
+
}
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (node.nodeType !== 1)
|
|
150
|
+
return;
|
|
151
|
+
for (const child of Array.from(node.childNodes))
|
|
152
|
+
walk(child);
|
|
153
|
+
};
|
|
154
|
+
walk(document.body);
|
|
155
|
+
}).catch(() => { });
|
|
156
|
+
}
|
|
123
157
|
/**
|
|
124
158
|
* In-page metric probe gate: measure the width of a sample string rendered with
|
|
125
159
|
* the inherited font-family vs a deliberately-divergent baseline (monospace by
|
|
@@ -327,45 +361,109 @@ async function logFontDiagnostics(page, stage) {
|
|
|
327
361
|
}
|
|
328
362
|
async function collectPlatformFontDiagnostics(page) {
|
|
329
363
|
try {
|
|
364
|
+
// Step 1: in-page, find diverse visible text-bearing elements and compute
|
|
365
|
+
// a path-based CSS selector that uniquely identifies each. We sample
|
|
366
|
+
// across many tag types so badges/pills/labels (where font bugs hide) are
|
|
367
|
+
// covered — the previous version only captured h1/h2/p/button/a/body and
|
|
368
|
+
// missed all <span> content like the AssistantBadges in the hero.
|
|
369
|
+
const candidates = await page.evaluate(() => {
|
|
370
|
+
const computeSelector = (el) => {
|
|
371
|
+
const parts = [];
|
|
372
|
+
let cur = el;
|
|
373
|
+
let safety = 8;
|
|
374
|
+
while (cur && cur.tagName !== 'BODY' && cur.tagName !== 'HTML' && safety-- > 0) {
|
|
375
|
+
const tag = cur.tagName.toLowerCase();
|
|
376
|
+
const parent = cur.parentElement;
|
|
377
|
+
if (!parent)
|
|
378
|
+
break;
|
|
379
|
+
const sameTagSiblings = Array.from(parent.children).filter((c) => c.tagName === cur.tagName);
|
|
380
|
+
if (sameTagSiblings.length === 1) {
|
|
381
|
+
parts.unshift(tag);
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
const idx = sameTagSiblings.indexOf(cur) + 1;
|
|
385
|
+
parts.unshift(`${tag}:nth-of-type(${idx})`);
|
|
386
|
+
}
|
|
387
|
+
cur = parent;
|
|
388
|
+
}
|
|
389
|
+
return ['body', ...parts].join(' > ');
|
|
390
|
+
};
|
|
391
|
+
const TAGS = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'a', 'button', 'label', 'li', 'td', 'span', 'code', 'pre', 'kbd', 'small'];
|
|
392
|
+
const out = [];
|
|
393
|
+
const seen = new Set();
|
|
394
|
+
for (const tag of TAGS) {
|
|
395
|
+
if (out.length >= 25)
|
|
396
|
+
break;
|
|
397
|
+
const elements = Array.from(document.querySelectorAll(tag));
|
|
398
|
+
let perTagCount = 0;
|
|
399
|
+
for (const el of elements) {
|
|
400
|
+
if (perTagCount >= 3 || out.length >= 25)
|
|
401
|
+
break;
|
|
402
|
+
const text = (el.textContent ?? '').replace(/\s+/g, ' ').trim();
|
|
403
|
+
if (text.length < 3)
|
|
404
|
+
continue;
|
|
405
|
+
const rect = el.getBoundingClientRect();
|
|
406
|
+
if (rect.width < 4 || rect.height < 4)
|
|
407
|
+
continue;
|
|
408
|
+
const style = getComputedStyle(el);
|
|
409
|
+
if (style.visibility === 'hidden' || style.display === 'none')
|
|
410
|
+
continue;
|
|
411
|
+
const selector = computeSelector(el);
|
|
412
|
+
if (seen.has(selector))
|
|
413
|
+
continue;
|
|
414
|
+
seen.add(selector);
|
|
415
|
+
out.push({
|
|
416
|
+
selector,
|
|
417
|
+
tag,
|
|
418
|
+
classes: el.className.toString().slice(0, 120),
|
|
419
|
+
text: text.slice(0, 60),
|
|
420
|
+
fontFamily: style.fontFamily,
|
|
421
|
+
fontSize: style.fontSize,
|
|
422
|
+
fontWeight: style.fontWeight,
|
|
423
|
+
});
|
|
424
|
+
perTagCount++;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return out;
|
|
428
|
+
});
|
|
330
429
|
const client = await page.context().newCDPSession(page);
|
|
331
430
|
try {
|
|
332
431
|
await client.send('DOM.enable');
|
|
333
432
|
await client.send('CSS.enable');
|
|
334
|
-
const documentResponse = await client.send('DOM.getDocument', { depth: 1, pierce: true });
|
|
335
|
-
const selectors = [
|
|
336
|
-
'body',
|
|
337
|
-
'h1',
|
|
338
|
-
'h2',
|
|
339
|
-
'p',
|
|
340
|
-
'button',
|
|
341
|
-
'a',
|
|
342
|
-
'[data-ak]',
|
|
343
|
-
'span',
|
|
344
|
-
];
|
|
433
|
+
const documentResponse = await client.send('DOM.getDocument', { depth: -1, pierce: true });
|
|
345
434
|
const diagnostics = [];
|
|
346
|
-
for (const
|
|
347
|
-
|
|
348
|
-
nodeId: documentResponse.root.nodeId,
|
|
349
|
-
selector,
|
|
350
|
-
});
|
|
351
|
-
if (!queryResponse.nodeId)
|
|
352
|
-
continue;
|
|
353
|
-
const text = await page.locator(selector).first().evaluate((node) => (node.textContent ?? '').replace(/\s+/g, ' ').trim().slice(0, 80)).catch(() => '');
|
|
354
|
-
const fontsResponse = await client.send('CSS.getPlatformFontsForNode', {
|
|
355
|
-
nodeId: queryResponse.nodeId,
|
|
356
|
-
});
|
|
357
|
-
diagnostics.push({
|
|
358
|
-
selector,
|
|
359
|
-
text,
|
|
360
|
-
fonts: (fontsResponse.fonts ?? []).map((font) => ({
|
|
361
|
-
familyName: font.familyName,
|
|
362
|
-
postScriptName: font.postScriptName,
|
|
363
|
-
isCustomFont: font.isCustomFont,
|
|
364
|
-
glyphCount: font.glyphCount,
|
|
365
|
-
})),
|
|
366
|
-
});
|
|
367
|
-
if (diagnostics.length >= 6)
|
|
435
|
+
for (const candidate of candidates) {
|
|
436
|
+
if (diagnostics.length >= 20)
|
|
368
437
|
break;
|
|
438
|
+
try {
|
|
439
|
+
const queryResponse = await client.send('DOM.querySelector', {
|
|
440
|
+
nodeId: documentResponse.root.nodeId,
|
|
441
|
+
selector: candidate.selector,
|
|
442
|
+
});
|
|
443
|
+
if (!queryResponse.nodeId)
|
|
444
|
+
continue;
|
|
445
|
+
const fontsResponse = await client.send('CSS.getPlatformFontsForNode', {
|
|
446
|
+
nodeId: queryResponse.nodeId,
|
|
447
|
+
});
|
|
448
|
+
diagnostics.push({
|
|
449
|
+
selector: candidate.selector,
|
|
450
|
+
tag: candidate.tag,
|
|
451
|
+
classes: candidate.classes,
|
|
452
|
+
text: candidate.text,
|
|
453
|
+
fontFamily: candidate.fontFamily,
|
|
454
|
+
fontSize: candidate.fontSize,
|
|
455
|
+
fontWeight: candidate.fontWeight,
|
|
456
|
+
fonts: (fontsResponse.fonts ?? []).map((font) => ({
|
|
457
|
+
familyName: font.familyName,
|
|
458
|
+
postScriptName: font.postScriptName,
|
|
459
|
+
isCustomFont: font.isCustomFont,
|
|
460
|
+
glyphCount: font.glyphCount,
|
|
461
|
+
})),
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
catch {
|
|
465
|
+
// Skip nodes that fail to resolve — page may be re-rendering.
|
|
466
|
+
}
|
|
369
467
|
}
|
|
370
468
|
return diagnostics;
|
|
371
469
|
}
|
|
@@ -1086,6 +1184,11 @@ export class Browser {
|
|
|
1086
1184
|
await page.mouse.move(0, 0);
|
|
1087
1185
|
await ensureCaptureHideStyles(page);
|
|
1088
1186
|
await this.waitForFontsBeforeScreenshot(page);
|
|
1187
|
+
// Strip C0/C1 control chars from text nodes — Linux Skia renders them as
|
|
1188
|
+
// .notdef tofu, macOS Core Text renders them invisibly. Normalize Linux
|
|
1189
|
+
// capture to match Mac (validated via Docker repro on autokap.app's
|
|
1190
|
+
// terminal block where ANSI escape literals were producing tofu boxes).
|
|
1191
|
+
await stripInvisibleControlChars(page);
|
|
1089
1192
|
await logFontDiagnostics(page, 'before screenshot');
|
|
1090
1193
|
const screenshot = Buffer.from(await page.screenshot({ type: 'png', fullPage: false }));
|
|
1091
1194
|
await logCaptureRenderDiagnostics(page, screenshot, this.options, 'after screenshot');
|