@tractiontactics/tt-fidelity 0.2.2 → 0.2.3
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/AGENTS.md +1 -0
- package/README.md +9 -6
- package/fixtures/self-test.mjs +1 -1
- package/package.json +1 -1
- package/src/capture.mjs +125 -15
- package/src/constants.mjs +1 -1
package/AGENTS.md
CHANGED
|
@@ -10,6 +10,7 @@ You are measuring a prototype against a TT WP Theme draft. **You are not the gra
|
|
|
10
10
|
4. **One WORK PLAN task**, then re-run with `--round` incremented. Do not batch speculative fixes.
|
|
11
11
|
5. **Never apply a rule site-wide** until `instances_per_page` (in clusters / QUEUE) shows it is a real default. Blanket CSS from a rare seam creates hundreds of false findings.
|
|
12
12
|
6. **Cache:** the tool cache-busts by default. If draft and live disagree, purge host/CDN and re-run — do not “fix” ghosts.
|
|
13
|
+
6b. **Capture prep (0.2.3+):** the tool scrolls the full page, forces lazy images, and unlocks common scroll-reveal (`opacity:0`) before section shots. If prototype PNGs look blank cream with only sticky widgets, you are on an old package — upgrade. Separately, **404 images** on the prototype (or `onerror` hiding media) are real source defects, not capture bugs.
|
|
13
14
|
7. **Escalate** tokens → block Style/Advanced → minimal `PUT /custom-css` → theme change → HTML template. Cite `ttRowId` / `ttBlockIds` when present.
|
|
14
15
|
8. **Default `--scope content`** (page body). Use `--scope chrome` for header/footer. Use `--scope full` only when you intentionally want both (TT draft may then count header/footer rows the prototype does not).
|
|
15
16
|
9. **QUEUE lifetime:** `ttRowId` values persist across *renders* of the same layout JSON, but a **republish / rebuild that regenerates row ids** invalidates the QUEUE. Re-measure after any full layout rewrite.
|
package/README.md
CHANGED
|
@@ -129,12 +129,15 @@ Schema: [`fidelity.schema.json`](./fidelity.schema.json) (`schema_version: 2`).
|
|
|
129
129
|
## How measurement works
|
|
130
130
|
|
|
131
131
|
1. Load both URLs at the same viewport (cache-busted by default).
|
|
132
|
-
2.
|
|
133
|
-
3.
|
|
134
|
-
4.
|
|
135
|
-
5.
|
|
136
|
-
6.
|
|
137
|
-
7.
|
|
132
|
+
2. **Prepare for capture (0.2.3+):** full-page scroll (scroll-reveal / IntersectionObserver), eager lazy images, unlock common `opacity:0` reveal classes, hide sticky tip/chat overlays.
|
|
133
|
+
3. Discover **sections**: draft prefers `.tt-pb-row` / `data-tt-row`; prototype prefers `main > section` (or `body > section`).
|
|
134
|
+
4. Align by document order; count mismatch → structural task.
|
|
135
|
+
5. Screenshot each pair (scroll section into view again), normalize canvas, `pixelmatch` → score + band.
|
|
136
|
+
6. Optionally capture landmark computed styles (explainer).
|
|
137
|
+
7. Cluster failing headings across `--pages` → `QUEUE.md` leverage list.
|
|
138
|
+
8. Emit TT ids (`data-tt-row` / `data-tt-block`) when present for `PUT /pages/{id}/layout`.
|
|
139
|
+
|
|
140
|
+
If prototype shots look like blank cream with only a sticky widget, you are on a pre-0.2.3 build — upgrade. Broken/404 media on the prototype itself will still appear empty.
|
|
138
141
|
|
|
139
142
|
## Agent contract
|
|
140
143
|
|
package/fixtures/self-test.mjs
CHANGED
|
@@ -26,7 +26,7 @@ function assert(cond, msg) {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
async function main() {
|
|
29
|
-
assert(PACKAGE_VERSION === '0.2.
|
|
29
|
+
assert(PACKAGE_VERSION === '0.2.3', 'package version constant is 0.2.3');
|
|
30
30
|
|
|
31
31
|
// Align: unpaired + structural when counts differ
|
|
32
32
|
{
|
package/package.json
CHANGED
package/src/capture.mjs
CHANGED
|
@@ -72,9 +72,9 @@ export async function openPage(browser, {
|
|
|
72
72
|
}
|
|
73
73
|
} catch (e) { /* ignore */ }
|
|
74
74
|
});
|
|
75
|
-
await page.goto(target, { waitUntil: 'networkidle', timeout:
|
|
75
|
+
await page.goto(target, { waitUntil: 'networkidle', timeout: 60000 });
|
|
76
76
|
await page.evaluate(() => document.fonts && document.fonts.ready).catch(() => {});
|
|
77
|
-
await page
|
|
77
|
+
await preparePageForCapture(page);
|
|
78
78
|
|
|
79
79
|
if (menus) {
|
|
80
80
|
for (const s of ['nav li:has(ul)', '.menu-item-has-children', 'nav [aria-haspopup]']) {
|
|
@@ -92,23 +92,133 @@ export async function openPage(browser, {
|
|
|
92
92
|
return { page, context: ctx, cacheHeaders, finalUrl: page.url() };
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Prototypes often use scroll-reveal (opacity:0 until .in) and loading=lazy images.
|
|
97
|
+
* Without a full scroll + image wait, section screenshots are blank cream and the
|
|
98
|
+
* pixel gate is meaningless.
|
|
99
|
+
*/
|
|
100
|
+
export async function preparePageForCapture(page) {
|
|
101
|
+
await page.evaluate(async () => {
|
|
102
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
103
|
+
|
|
104
|
+
// Force-eager lazy images / common data-src patterns.
|
|
105
|
+
for (const img of document.querySelectorAll('img')) {
|
|
106
|
+
try {
|
|
107
|
+
img.loading = 'eager';
|
|
108
|
+
const ds = img.getAttribute('data-src') || img.getAttribute('data-lazy-src');
|
|
109
|
+
if (ds && !img.getAttribute('src')) img.setAttribute('src', ds);
|
|
110
|
+
const dss = img.getAttribute('data-srcset') || img.getAttribute('data-lazy-srcset');
|
|
111
|
+
if (dss && !img.getAttribute('srcset')) img.setAttribute('srcset', dss);
|
|
112
|
+
} catch (e) { /* ignore */ }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Scroll the full document so IntersectionObservers / reveal classes fire.
|
|
116
|
+
const step = Math.max(240, Math.floor(window.innerHeight * 0.7));
|
|
117
|
+
const maxY = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);
|
|
118
|
+
for (let y = 0; y < maxY; y += step) {
|
|
119
|
+
window.scrollTo(0, y);
|
|
120
|
+
await sleep(80);
|
|
121
|
+
}
|
|
122
|
+
window.scrollTo(0, maxY);
|
|
123
|
+
await sleep(150);
|
|
124
|
+
window.scrollTo(0, 0);
|
|
125
|
+
await sleep(200);
|
|
126
|
+
|
|
127
|
+
// If reveal never fired (no IO), force common "in view" classes / opacity.
|
|
128
|
+
for (const el of document.querySelectorAll('section, .sec, .sec-alt, .trust, .lead, .phero, [class*=reveal i], [class*=fade i]')) {
|
|
129
|
+
try {
|
|
130
|
+
const cs = getComputedStyle(el);
|
|
131
|
+
if (parseFloat(cs.opacity) < 0.5) {
|
|
132
|
+
el.classList.add('in', 'is-inview', 'is-visible', 'visible', 'aos-animate');
|
|
133
|
+
el.style.opacity = '1';
|
|
134
|
+
el.style.transform = 'none';
|
|
135
|
+
el.style.visibility = 'visible';
|
|
136
|
+
}
|
|
137
|
+
} catch (e) { /* ignore */ }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Hide sticky tips/chat that paint over section shots (not page content).
|
|
141
|
+
for (const el of document.querySelectorAll(
|
|
142
|
+
'[class*=did-you i], [class*=cookie i], [id*=cookie i], .bc-widget, [class*=chat-widget i], [class*=intercom i]'
|
|
143
|
+
)) {
|
|
144
|
+
try {
|
|
145
|
+
const cs = getComputedStyle(el);
|
|
146
|
+
if (cs.position === 'fixed' || cs.position === 'sticky') {
|
|
147
|
+
el.style.setProperty('display', 'none', 'important');
|
|
148
|
+
}
|
|
149
|
+
} catch (e) { /* ignore */ }
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Wait for images that already have a layout box.
|
|
154
|
+
await page.evaluate(async () => {
|
|
155
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
156
|
+
const deadline = Date.now() + 8000;
|
|
157
|
+
while (Date.now() < deadline) {
|
|
158
|
+
const pending = [...document.images].filter((img) => {
|
|
159
|
+
if (!img.src || img.src.startsWith('data:')) return false;
|
|
160
|
+
const r = img.getBoundingClientRect();
|
|
161
|
+
if (r.width < 2 || r.height < 2) return false;
|
|
162
|
+
return !(img.complete && img.naturalWidth > 0);
|
|
163
|
+
});
|
|
164
|
+
if (!pending.length) break;
|
|
165
|
+
await sleep(150);
|
|
166
|
+
}
|
|
167
|
+
if (document.fonts?.ready) await document.fonts.ready;
|
|
168
|
+
}).catch(() => {});
|
|
169
|
+
|
|
170
|
+
await page.waitForTimeout(200);
|
|
171
|
+
}
|
|
172
|
+
|
|
95
173
|
/**
|
|
96
174
|
* Screenshot a section by clipping from the page (element bounding box).
|
|
97
175
|
*/
|
|
98
176
|
export async function screenshotSection(page, section) {
|
|
99
177
|
const handle = await page.$(section.selector);
|
|
100
|
-
if (
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
178
|
+
if (handle) {
|
|
179
|
+
try {
|
|
180
|
+
await handle.evaluate((el) => {
|
|
181
|
+
el.scrollIntoView({ block: 'center', inline: 'nearest' });
|
|
182
|
+
const cs = getComputedStyle(el);
|
|
183
|
+
if (parseFloat(cs.opacity) < 0.5) {
|
|
184
|
+
el.classList.add('in', 'is-inview', 'is-visible', 'visible', 'aos-animate');
|
|
185
|
+
el.style.opacity = '1';
|
|
186
|
+
el.style.transform = 'none';
|
|
187
|
+
el.style.visibility = 'visible';
|
|
188
|
+
}
|
|
189
|
+
for (const img of el.querySelectorAll('img')) {
|
|
190
|
+
img.loading = 'eager';
|
|
191
|
+
const ds = img.getAttribute('data-src') || img.getAttribute('data-lazy-src');
|
|
192
|
+
if (ds && !img.getAttribute('src')) img.setAttribute('src', ds);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
await page.waitForTimeout(120);
|
|
196
|
+
// Wait briefly for in-section images.
|
|
197
|
+
await handle.evaluate(async (el) => {
|
|
198
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
199
|
+
const deadline = Date.now() + 2500;
|
|
200
|
+
while (Date.now() < deadline) {
|
|
201
|
+
const pending = [...el.querySelectorAll('img')].filter(
|
|
202
|
+
(img) => img.src && !(img.complete && img.naturalWidth > 0)
|
|
203
|
+
);
|
|
204
|
+
if (!pending.length) break;
|
|
205
|
+
await sleep(100);
|
|
206
|
+
}
|
|
207
|
+
}).catch(() => {});
|
|
208
|
+
return await handle.screenshot({ type: 'png', timeout: 20000 });
|
|
209
|
+
} catch (e) {
|
|
210
|
+
/* fall through to clip */
|
|
211
|
+
}
|
|
112
212
|
}
|
|
113
|
-
|
|
213
|
+
const { x, viewportTop, width, height } = section.box || {};
|
|
214
|
+
return page.screenshot({
|
|
215
|
+
type: 'png',
|
|
216
|
+
clip: {
|
|
217
|
+
x: Math.max(0, x || 0),
|
|
218
|
+
y: Math.max(0, viewportTop || 0),
|
|
219
|
+
width: Math.max(1, width || 1),
|
|
220
|
+
height: Math.max(1, Math.min(height || 1, 8000))
|
|
221
|
+
},
|
|
222
|
+
timeout: 15000
|
|
223
|
+
});
|
|
114
224
|
}
|
package/src/constants.mjs
CHANGED