bricks-builder-mcp 3.9.0 → 3.11.0
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/README.md +16 -2
- package/package.json +1 -1
- package/server.js +788 -215
package/README.md
CHANGED
|
@@ -108,8 +108,22 @@ Claude Code détectera automatiquement le skill au prochain démarrage.
|
|
|
108
108
|
**Éléments**
|
|
109
109
|
- `find_elements`, `get_element`, `update_element` (avec param `label` pour renommer dans le builder), `add_element`, `batch_add`, `delete_element`, `reorder_sections`
|
|
110
110
|
|
|
111
|
-
**⭐ Vérification visuelle**
|
|
112
|
-
- `verify_element` — screenshot crop + report
|
|
111
|
+
**⭐ Vérification visuelle (v3.10+)**
|
|
112
|
+
- `verify_element` — screenshot crop + report. **À utiliser après chaque batch_add significatif.**
|
|
113
|
+
- Compare computed styles vs settings attendus, fonts, erreurs JS, débordement horizontal
|
|
114
|
+
- **v3.10 — Multi-viewport en 1 call** : `viewports: ["desktop", "mobile_portrait"]` → 1 report + 1 screenshot par viewport
|
|
115
|
+
- **v3.10 — Cohérence siblings** : text-align mixé entre frères, jumps font-size anormaux
|
|
116
|
+
- **v3.10 — Containers vides anormaux** : tout bloc ≥ 50×50px sans contenu (attrape les wrappers écrasés par `align-items: stretch`)
|
|
117
|
+
- **v3.10 — Santé des médias** : `naturalWidth > 0` sur les `<img>` (lazy-load cassé), `readyState ≥ 2` sur les `<video>`, alt présents
|
|
118
|
+
- Chaque check porte une `severity` (`critical` / `warning` / `info`) et un `hint` actionnable. Désactivable par catégorie via `checks: { sibling_coherence: false }`.
|
|
119
|
+
|
|
120
|
+
**⭐ Audit page entière (v3.11+)**
|
|
121
|
+
- `audit_page` — fullpage screenshot **avec annotations dessinées** + report consolidé. Complète `verify_element` (qui est ciblé sur 1 élément).
|
|
122
|
+
- Scanne TOUS les éléments Bricks d'une page d'un coup
|
|
123
|
+
- Dessine des cadres colorés (rouge=critical, orange=warning, jaune=info) sur les zones problématiques directement sur le screenshot
|
|
124
|
+
- Multi-viewport en 1 call (par défaut `["desktop", "mobile_portrait"]`)
|
|
125
|
+
- Idéal pour : état initial avant refonte, démo client, audit après une grosse vague de modifs
|
|
126
|
+
- Checks intégrés : containers vides, images cassées + alt manquants, text-align mixé, débordement horizontal global de la page
|
|
113
127
|
|
|
114
128
|
**⭐ Upload optimisé (v3.8+)**
|
|
115
129
|
- `upload_local_file`, `upload_local_files_batch` — l'IA donne juste le path local, le MCP server lit le fichier, conversion WebP automatique (-80 à -95% de poids), renommage SEO via le `title`
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "bricks-builder-mcp",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.11.0",
|
|
5
5
|
"description": "Serveur MCP pour piloter Bricks Builder (WordPress) depuis Claude — édition de pages, gestion d'éléments, réordonnancement des sections, vérification visuelle (verify_element), upload optimisé WebP. Communauté Discord : https://discord.gg/rX22zHRzH",
|
|
6
6
|
"homepage": "https://discord.gg/rX22zHRzH",
|
|
7
7
|
"main": "server.js",
|
package/server.js
CHANGED
|
@@ -214,6 +214,116 @@ function generateHint(prop, expected, got) {
|
|
|
214
214
|
return null;
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
+
// ===== v3.10 — Moteur de checks pluggable =====
|
|
218
|
+
// Chaque check est une fonction Node-side qui reçoit le `audit` collecté
|
|
219
|
+
// dans la page (siblings, media, emptyContainers) et retourne des entrées
|
|
220
|
+
// {ok, severity, label, hint, bbox} à pousser dans le report.
|
|
221
|
+
// Sévérités : 'critical' (bug bloquant), 'warning' (probablement un bug),
|
|
222
|
+
// 'info' (juste à savoir). 'ok' false = compte comme échec dans le score.
|
|
223
|
+
|
|
224
|
+
function buildSiblingCoherenceChecks(audit) {
|
|
225
|
+
const checks = [];
|
|
226
|
+
const siblings = audit.siblings || [];
|
|
227
|
+
if (siblings.length === 0) return checks;
|
|
228
|
+
|
|
229
|
+
const all = [audit.self, ...siblings];
|
|
230
|
+
|
|
231
|
+
// 1) text-align mixé entre frères directs
|
|
232
|
+
// Si un seul frère a un text-align différent, c'est souvent un bug visuel
|
|
233
|
+
// (ex: H2 left + sous-titre center sans intention design).
|
|
234
|
+
const aligns = [...new Set(all.map(s => s['text-align']).filter(Boolean))];
|
|
235
|
+
if (aligns.length > 1) {
|
|
236
|
+
checks.push({
|
|
237
|
+
ok: false,
|
|
238
|
+
severity: 'warning',
|
|
239
|
+
label: `text-align mixé entre frères directs (${aligns.join(', ')})`,
|
|
240
|
+
hint: "Frères directs avec text-align différents — souvent un bug visuel. Vérifie l'intention design ou aligne-les sur la même valeur.",
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// 2) Jumps de font-size > 2.5x entre frères (info seulement, c'est parfois voulu pour H1/sous-titre)
|
|
245
|
+
const withFontSize = all.filter(s => s['font-size-px'] && s['font-size-px'] > 0);
|
|
246
|
+
if (withFontSize.length >= 2) {
|
|
247
|
+
const sizes = withFontSize.map(s => s['font-size-px']);
|
|
248
|
+
const min = Math.min(...sizes);
|
|
249
|
+
const max = Math.max(...sizes);
|
|
250
|
+
if (min > 0 && max / min > 2.5) {
|
|
251
|
+
checks.push({
|
|
252
|
+
ok: true,
|
|
253
|
+
severity: 'info',
|
|
254
|
+
label: `font-size variable entre frères (${Math.round(min)}px → ${Math.round(max)}px)`,
|
|
255
|
+
hint: "Écart > 2.5x entre frères — normal pour H1/sous-titre, à vérifier sinon.",
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return checks;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function buildEmptyContainerChecks(audit) {
|
|
264
|
+
const checks = [];
|
|
265
|
+
const empties = audit.emptyContainers || [];
|
|
266
|
+
if (empties.length === 0) {
|
|
267
|
+
checks.push({ ok: true, label: "Aucun container vide anormal détecté (≥ 50×50px)" });
|
|
268
|
+
return checks;
|
|
269
|
+
}
|
|
270
|
+
const sorted = empties.slice().sort((a, b) => b.area - a.area).slice(0, 5);
|
|
271
|
+
checks.push({
|
|
272
|
+
ok: false,
|
|
273
|
+
severity: 'warning',
|
|
274
|
+
label: `${empties.length} container(s) visible(s) sans contenu (≥ 50×50px)`,
|
|
275
|
+
got: sorted.map(c => `${c.classes[0] || c.id || '?'} ${c.rect.w}×${c.rect.h}`),
|
|
276
|
+
bboxes: sorted.map(c => c.rect),
|
|
277
|
+
hint: "Conteneurs vides détectés — bug fréquent quand align-items: stretch écrase un block-vide à des proportions absurdes (dot 6px qui devient 180px, etc). Vérifie aspect-ratio fixe ou ajoute du contenu.",
|
|
278
|
+
});
|
|
279
|
+
return checks;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function buildMediaHealthChecks(audit) {
|
|
283
|
+
const checks = [];
|
|
284
|
+
const { images = [], videos = [] } = (audit.media || {});
|
|
285
|
+
|
|
286
|
+
if (images.length > 0) {
|
|
287
|
+
const broken = images.filter(i => !i.loaded);
|
|
288
|
+
if (broken.length === 0) {
|
|
289
|
+
checks.push({ ok: true, label: `${images.length} image(s) chargée(s) (naturalWidth > 0)` });
|
|
290
|
+
} else {
|
|
291
|
+
checks.push({
|
|
292
|
+
ok: false,
|
|
293
|
+
severity: 'critical',
|
|
294
|
+
label: `${broken.length}/${images.length} image(s) PAS chargée(s) (naturalWidth = 0)`,
|
|
295
|
+
got: broken.slice(0, 3).map(i => (i.src || '').split('/').pop() || '(no src)'),
|
|
296
|
+
hint: "Image(s) non chargée(s) — vérifie l'URL src, le lazy-loading qui ne se déclenche pas, ou un 404. À éviter en production.",
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
const noAlt = images.filter(i => !i.alt || i.alt.trim().length === 0);
|
|
300
|
+
if (noAlt.length > 0) {
|
|
301
|
+
checks.push({
|
|
302
|
+
ok: false,
|
|
303
|
+
severity: 'warning',
|
|
304
|
+
label: `${noAlt.length}/${images.length} image(s) sans attribut alt`,
|
|
305
|
+
hint: "alt manquant — bloque l'accessibilité et le SEO. Renseigne alt à l'upload via upload_local_file({alt: '...'}).",
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (videos.length > 0) {
|
|
311
|
+
const notLoaded = videos.filter(v => !v.loaded);
|
|
312
|
+
if (notLoaded.length === 0) {
|
|
313
|
+
checks.push({ ok: true, label: `${videos.length} vidéo(s) chargée(s) (readyState ≥ 2)` });
|
|
314
|
+
} else {
|
|
315
|
+
checks.push({
|
|
316
|
+
ok: false,
|
|
317
|
+
severity: 'warning',
|
|
318
|
+
label: `${notLoaded.length}/${videos.length} vidéo(s) pas prête(s) (readyState < 2)`,
|
|
319
|
+
hint: "Vidéo(s) non prête(s) — peut être normal si autoplay désactivé sur mobile. Sinon vérifier src/codec.",
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return checks;
|
|
325
|
+
}
|
|
326
|
+
|
|
217
327
|
// Configuration du fichier de log
|
|
218
328
|
const __filename = fileURLToPath(import.meta.url);
|
|
219
329
|
const __dirname = dirname(__filename);
|
|
@@ -999,7 +1109,7 @@ mcpServer.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
999
1109
|
// ===== v3.6.0 — VERIFY ELEMENT (vérification visuelle + technique) =====
|
|
1000
1110
|
{
|
|
1001
1111
|
name: "verify_element",
|
|
1002
|
-
description: "⭐ APRÈS CHAQUE batch_add OU update_element SIGNIFICATIF, utilise cet outil. Lance un browser headless, navigue sur la page, scroll vers l'élément, prend un screenshot crop et compare les computed styles avec les settings attendus. Retourne {screenshot
|
|
1112
|
+
description: "⭐ APRÈS CHAQUE batch_add OU update_element SIGNIFICATIF, utilise cet outil. Lance un browser headless, navigue sur la page, scroll vers l'élément, prend un screenshot crop et compare les computed styles avec les settings attendus. v3.10 : ajout de checks généralistes (cohérence siblings, containers vides, santé des médias) + multi-viewport en 1 call (param `viewports`). Retourne {screenshot(s) visibles par Claude, report: {score, checks}, computed}. Force l'évolution petit-à-petit-vérifier-petit-à-petit.",
|
|
1003
1113
|
inputSchema: {
|
|
1004
1114
|
type: "object",
|
|
1005
1115
|
properties: {
|
|
@@ -1008,13 +1118,59 @@ mcpServer.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1008
1118
|
viewport: {
|
|
1009
1119
|
type: "string",
|
|
1010
1120
|
enum: ["desktop", "tablet", "mobile_landscape", "mobile_portrait"],
|
|
1011
|
-
description: "Taille d'écran à tester (défaut: desktop)",
|
|
1121
|
+
description: "Taille d'écran à tester (défaut: desktop). Ignoré si `viewports` est fourni.",
|
|
1122
|
+
},
|
|
1123
|
+
viewports: {
|
|
1124
|
+
type: "array",
|
|
1125
|
+
items: { type: "string", enum: ["desktop", "tablet", "mobile_landscape", "mobile_portrait"] },
|
|
1126
|
+
description: "v3.10 : tester plusieurs viewports en 1 call. Renvoie un report + screenshot par viewport. Ex: ['desktop', 'mobile_portrait'].",
|
|
1127
|
+
},
|
|
1128
|
+
checks: {
|
|
1129
|
+
type: "object",
|
|
1130
|
+
description: "v3.10 : activer/désactiver des catégories de checks (toutes activées par défaut).",
|
|
1131
|
+
properties: {
|
|
1132
|
+
expected_styles: { type: "boolean", description: "Compare les computed styles aux settings attendus (défaut: true)" },
|
|
1133
|
+
sibling_coherence: { type: "boolean", description: "Détecte text-align mixé et jumps font-size entre frères directs (défaut: true)" },
|
|
1134
|
+
empty_containers: { type: "boolean", description: "Flag les blocs visibles ≥ 50×50px sans contenu (défaut: true)" },
|
|
1135
|
+
media_health: { type: "boolean", description: "Vérifie naturalWidth > 0 sur les <img>, readyState ≥ 2 sur les <video>, alt présents (défaut: true)" },
|
|
1136
|
+
overflow: { type: "boolean", description: "Détecte les débordements horizontaux (défaut: true)" },
|
|
1137
|
+
console_errors: { type: "boolean", description: "Remonte les erreurs JS console (défaut: true)" },
|
|
1138
|
+
},
|
|
1012
1139
|
},
|
|
1013
1140
|
},
|
|
1014
1141
|
required: ["pageId", "elementId"],
|
|
1015
1142
|
},
|
|
1016
1143
|
},
|
|
1017
1144
|
|
|
1145
|
+
// ===== v3.11 — AUDIT_PAGE (fullpage screenshot + annotations + report consolidé) =====
|
|
1146
|
+
{
|
|
1147
|
+
name: "audit_page",
|
|
1148
|
+
description: "⭐ AUDIT GLOBAL d'une page Bricks en 1 call. Lance un browser headless, charge la page, scanne TOUS les éléments Bricks visibles, dessine des cadres colorés sur le screenshot fullpage là où il y a un problème (rouge=critical, orange=warning, jaune=info), et retourne un report consolidé avec severityCounts. Idéal pour un coup d'œil rapide 'qu'est-ce qui cloche sur cette page ?' avant ou après une refonte. Complète verify_element (qui est ciblé sur 1 élément).",
|
|
1149
|
+
inputSchema: {
|
|
1150
|
+
type: "object",
|
|
1151
|
+
properties: {
|
|
1152
|
+
pageId: { type: "number", description: "L'ID de la page WP à auditer" },
|
|
1153
|
+
viewports: {
|
|
1154
|
+
type: "array",
|
|
1155
|
+
items: { type: "string", enum: ["desktop", "tablet", "mobile_landscape", "mobile_portrait"] },
|
|
1156
|
+
description: "Viewports à tester (défaut: ['desktop', 'mobile_portrait']). Renvoie un screenshot annoté + un report par viewport.",
|
|
1157
|
+
},
|
|
1158
|
+
checks: {
|
|
1159
|
+
type: "object",
|
|
1160
|
+
description: "Activer/désactiver les catégories de checks. Toutes activées par défaut.",
|
|
1161
|
+
properties: {
|
|
1162
|
+
empty_containers: { type: "boolean", description: "Containers Bricks ≥ 50×50px sans contenu (défaut: true)" },
|
|
1163
|
+
media_health: { type: "boolean", description: "Images cassées (naturalWidth=0) + alt manquant (défaut: true)" },
|
|
1164
|
+
sibling_coherence: { type: "boolean", description: "text-align mixé entre frères Bricks (défaut: true)" },
|
|
1165
|
+
page_overflow: { type: "boolean", description: "Débordement horizontal global de la page (défaut: true)" },
|
|
1166
|
+
},
|
|
1167
|
+
},
|
|
1168
|
+
maxAnnotations: { type: "number", description: "Limite d'annotations dessinées sur le screenshot (défaut: 30, pour éviter d'écraser visuellement la page)" },
|
|
1169
|
+
},
|
|
1170
|
+
required: ["pageId"],
|
|
1171
|
+
},
|
|
1172
|
+
},
|
|
1173
|
+
|
|
1018
1174
|
// ===== v3.6.0 — FEEDBACK SYSTEM (missing MCP features) =====
|
|
1019
1175
|
{
|
|
1020
1176
|
name: "report_missing_feature",
|
|
@@ -1656,259 +1812,676 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1656
1812
|
result = await callWordPressAPI("/list-components", "GET");
|
|
1657
1813
|
break;
|
|
1658
1814
|
|
|
1659
|
-
// ===== v3.
|
|
1815
|
+
// ===== v3.10 — VERIFY ELEMENT (multi-viewport + checks pluggables) =====
|
|
1660
1816
|
case "verify_element": {
|
|
1661
1817
|
const pageId = args.pageId;
|
|
1662
1818
|
const elementId = args.elementId;
|
|
1663
|
-
|
|
1819
|
+
// Backward compat : `viewport` (singulier) OU `viewports` (array)
|
|
1820
|
+
const viewportList = (Array.isArray(args.viewports) && args.viewports.length > 0)
|
|
1821
|
+
? args.viewports
|
|
1822
|
+
: [args.viewport || "desktop"];
|
|
1823
|
+
const isMulti = viewportList.length > 1;
|
|
1824
|
+
const checksConfig = Object.assign({
|
|
1825
|
+
expected_styles: true,
|
|
1826
|
+
sibling_coherence: true,
|
|
1827
|
+
empty_containers: true,
|
|
1828
|
+
media_health: true,
|
|
1829
|
+
overflow: true,
|
|
1830
|
+
console_errors: true,
|
|
1831
|
+
}, args.checks || {});
|
|
1664
1832
|
const viewportSizes = {
|
|
1665
1833
|
desktop: { width: 1920, height: 1080 },
|
|
1666
1834
|
tablet: { width: 991, height: 1200 },
|
|
1667
1835
|
mobile_landscape: { width: 767, height: 600 },
|
|
1668
1836
|
mobile_portrait: { width: 478, height: 800 },
|
|
1669
1837
|
};
|
|
1670
|
-
const viewportContext = viewportSizes[viewport] || viewportSizes.desktop;
|
|
1671
1838
|
|
|
1672
|
-
// 1) Récupérer infos plugin (URL, sélecteur, expected styles)
|
|
1839
|
+
// 1) Récupérer infos plugin une seule fois (URL, sélecteur, expected styles)
|
|
1673
1840
|
const info = await callWordPressAPI("/verify-element-info", "POST", { pageId, elementId });
|
|
1674
1841
|
|
|
1675
|
-
// 2)
|
|
1676
|
-
|
|
1677
|
-
try {
|
|
1678
|
-
page = await getNewPage(viewport);
|
|
1679
|
-
} catch (browserErr) {
|
|
1680
|
-
// Erreur Playwright (chromium pas installé etc) — retourner sans crasher
|
|
1681
|
-
result = {
|
|
1682
|
-
success: false,
|
|
1683
|
-
error: browserErr.message,
|
|
1684
|
-
url: info.url,
|
|
1685
|
-
selector: info.selector,
|
|
1686
|
-
hint: "Installation Chromium requise pour verify_element. À défaut, utilise screenshot-website-fast en MCP externe.",
|
|
1687
|
-
};
|
|
1688
|
-
break;
|
|
1689
|
-
}
|
|
1842
|
+
// 2) Boucler sur chaque viewport — chacun = 1 page, 1 screenshot, 1 report
|
|
1843
|
+
const perViewport = [];
|
|
1690
1844
|
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
page.on('console', msg => {
|
|
1694
|
-
if (msg.type() === 'error') consoleErrors.push(msg.text());
|
|
1695
|
-
});
|
|
1696
|
-
page.on('pageerror', err => consoleErrors.push('PageError: ' + err.message));
|
|
1697
|
-
|
|
1698
|
-
let screenshotBase64 = null;
|
|
1699
|
-
let report = { score: '0/0', checks: [] };
|
|
1700
|
-
let computed = null;
|
|
1701
|
-
let loadedFonts = [];
|
|
1845
|
+
for (const viewport of viewportList) {
|
|
1846
|
+
const viewportContext = viewportSizes[viewport] || viewportSizes.desktop;
|
|
1702
1847
|
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
if (!element) {
|
|
1710
|
-
// Diagnostic : qu'a-t-on réellement chargé ?
|
|
1711
|
-
const diagnostics = await page.evaluate(() => ({
|
|
1712
|
-
actualUrl: window.location.href,
|
|
1713
|
-
title: document.title,
|
|
1714
|
-
bodyClass: document.body?.className || '',
|
|
1715
|
-
brxeCount: document.querySelectorAll('[class*="brxe-"]').length,
|
|
1716
|
-
firstBrxe: document.querySelector('[class*="brxe-"]')?.className || null,
|
|
1717
|
-
htmlSnippet: document.documentElement.outerHTML.substring(0, 500),
|
|
1718
|
-
}));
|
|
1719
|
-
result = {
|
|
1848
|
+
let page;
|
|
1849
|
+
try {
|
|
1850
|
+
page = await getNewPage(viewport);
|
|
1851
|
+
} catch (browserErr) {
|
|
1852
|
+
perViewport.push({
|
|
1853
|
+
viewport,
|
|
1720
1854
|
success: false,
|
|
1721
|
-
error:
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
hint: diagnostics.brxeCount === 0
|
|
1726
|
-
? "Aucune classe .brxe-* trouvée — la page n'a probablement pas chargé le contenu Bricks (cert SSL, redirect, 404, page d'erreur)"
|
|
1727
|
-
: `${diagnostics.brxeCount} éléments .brxe-* trouvés mais pas ${info.selector} — l'ID dans la DB ne correspond pas au rendu`,
|
|
1728
|
-
};
|
|
1729
|
-
await page.close();
|
|
1730
|
-
break;
|
|
1855
|
+
error: browserErr.message,
|
|
1856
|
+
hint: "Installation Chromium requise pour verify_element. À défaut, utilise screenshot-website-fast en MCP externe.",
|
|
1857
|
+
});
|
|
1858
|
+
continue;
|
|
1731
1859
|
}
|
|
1732
1860
|
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
// Computed style + dimensions
|
|
1737
|
-
computed = await element.evaluate(el => {
|
|
1738
|
-
const cs = getComputedStyle(el);
|
|
1739
|
-
const rect = el.getBoundingClientRect();
|
|
1740
|
-
// Enfants "réels" = ceux qui ont une classe brxe-{id} ou id brxe-*
|
|
1741
|
-
// Exclut les <video>, <picture> et autres ajoutés par Bricks (bg vidéo, overlay auto, etc.)
|
|
1742
|
-
const realChildren = Array.from(el.children).filter(child => {
|
|
1743
|
-
if (child.id && child.id.startsWith('brxe-')) return true;
|
|
1744
|
-
return Array.from(child.classList).some(cls => cls.startsWith('brxe-'));
|
|
1745
|
-
});
|
|
1746
|
-
const bricksInternalChildren = Array.from(el.children).filter(child => {
|
|
1747
|
-
return !((child.id && child.id.startsWith('brxe-')) ||
|
|
1748
|
-
Array.from(child.classList).some(cls => cls.startsWith('brxe-')));
|
|
1749
|
-
}).map(c => c.tagName.toLowerCase() + (c.className ? '.' + c.className.split(' ').join('.') : ''));
|
|
1750
|
-
return {
|
|
1751
|
-
display: cs.display,
|
|
1752
|
-
'flex-direction': cs.flexDirection,
|
|
1753
|
-
'justify-content': cs.justifyContent,
|
|
1754
|
-
'align-items': cs.alignItems,
|
|
1755
|
-
gap: cs.gap,
|
|
1756
|
-
'column-gap': cs.columnGap,
|
|
1757
|
-
'row-gap': cs.rowGap,
|
|
1758
|
-
width: Math.round(rect.width) + 'px',
|
|
1759
|
-
height: Math.round(rect.height) + 'px',
|
|
1760
|
-
'max-width': cs.maxWidth,
|
|
1761
|
-
'padding-top': cs.paddingTop,
|
|
1762
|
-
'padding-right': cs.paddingRight,
|
|
1763
|
-
'padding-bottom': cs.paddingBottom,
|
|
1764
|
-
'padding-left': cs.paddingLeft,
|
|
1765
|
-
'margin-top': cs.marginTop,
|
|
1766
|
-
'margin-right': cs.marginRight,
|
|
1767
|
-
'margin-bottom': cs.marginBottom,
|
|
1768
|
-
'margin-left': cs.marginLeft,
|
|
1769
|
-
'background-color': cs.backgroundColor,
|
|
1770
|
-
'font-size': cs.fontSize,
|
|
1771
|
-
'font-family': cs.fontFamily,
|
|
1772
|
-
'font-weight': cs.fontWeight,
|
|
1773
|
-
'line-height': cs.lineHeight,
|
|
1774
|
-
color: cs.color,
|
|
1775
|
-
'text-align': cs.textAlign,
|
|
1776
|
-
'border-top-left-radius': cs.borderTopLeftRadius,
|
|
1777
|
-
'border-top-right-radius': cs.borderTopRightRadius,
|
|
1778
|
-
'border-bottom-right-radius': cs.borderBottomRightRadius,
|
|
1779
|
-
'border-bottom-left-radius': cs.borderBottomLeftRadius,
|
|
1780
|
-
visibility: cs.visibility,
|
|
1781
|
-
opacity: cs.opacity,
|
|
1782
|
-
childrenInDom: realChildren.length,
|
|
1783
|
-
childrenTotalDom: el.children.length,
|
|
1784
|
-
bricksInternalChildren: bricksInternalChildren,
|
|
1785
|
-
isVisible: rect.width > 0 && rect.height > 0 && cs.visibility !== 'hidden' && cs.opacity !== '0',
|
|
1786
|
-
hasOverflowX: el.scrollWidth > el.clientWidth,
|
|
1787
|
-
};
|
|
1861
|
+
const consoleErrors = [];
|
|
1862
|
+
page.on('console', msg => {
|
|
1863
|
+
if (msg.type() === 'error') consoleErrors.push(msg.text());
|
|
1788
1864
|
});
|
|
1865
|
+
page.on('pageerror', err => consoleErrors.push('PageError: ' + err.message));
|
|
1789
1866
|
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
document.fonts.forEach(f => { if (f.status === 'loaded') set.add(f.family); });
|
|
1794
|
-
return Array.from(set);
|
|
1795
|
-
});
|
|
1867
|
+
let screenshotBase64 = null;
|
|
1868
|
+
let audit = null;
|
|
1869
|
+
let loadedFonts = [];
|
|
1796
1870
|
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1871
|
+
try {
|
|
1872
|
+
await page.goto(info.url, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
|
1873
|
+
await page.waitForTimeout(2000);
|
|
1874
|
+
|
|
1875
|
+
const element = await page.$(info.selector);
|
|
1876
|
+
if (!element) {
|
|
1877
|
+
const diagnostics = await page.evaluate(() => ({
|
|
1878
|
+
actualUrl: window.location.href,
|
|
1879
|
+
title: document.title,
|
|
1880
|
+
bodyClass: document.body?.className || '',
|
|
1881
|
+
brxeCount: document.querySelectorAll('[class*="brxe-"]').length,
|
|
1882
|
+
firstBrxe: document.querySelector('[class*="brxe-"]')?.className || null,
|
|
1883
|
+
htmlSnippet: document.documentElement.outerHTML.substring(0, 500),
|
|
1884
|
+
}));
|
|
1885
|
+
perViewport.push({
|
|
1886
|
+
viewport,
|
|
1887
|
+
success: false,
|
|
1888
|
+
error: `Élément ${info.selector} introuvable dans le DOM`,
|
|
1889
|
+
diagnostics,
|
|
1890
|
+
hint: diagnostics.brxeCount === 0
|
|
1891
|
+
? "Aucune classe .brxe-* trouvée — la page n'a probablement pas chargé le contenu Bricks (cert SSL, redirect, 404, page d'erreur)"
|
|
1892
|
+
: `${diagnostics.brxeCount} éléments .brxe-* trouvés mais pas ${info.selector} — l'ID dans la DB ne correspond pas au rendu`,
|
|
1893
|
+
});
|
|
1894
|
+
continue;
|
|
1895
|
+
}
|
|
1805
1896
|
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1897
|
+
await element.scrollIntoViewIfNeeded();
|
|
1898
|
+
await page.waitForTimeout(400);
|
|
1899
|
+
|
|
1900
|
+
// === v3.10 : collecte enrichie en 1 evaluate (self + siblings + media + emptyContainers) ===
|
|
1901
|
+
audit = await page.evaluate((selector) => {
|
|
1902
|
+
const el = document.querySelector(selector);
|
|
1903
|
+
if (!el) return null;
|
|
1904
|
+
const cs = getComputedStyle(el);
|
|
1905
|
+
const rect = el.getBoundingClientRect();
|
|
1906
|
+
|
|
1907
|
+
const realChildren = Array.from(el.children).filter(child => {
|
|
1908
|
+
if (child.id && child.id.startsWith('brxe-')) return true;
|
|
1909
|
+
return Array.from(child.classList).some(cls => cls.startsWith('brxe-'));
|
|
1910
|
+
});
|
|
1911
|
+
const bricksInternalChildren = Array.from(el.children).filter(child => {
|
|
1912
|
+
return !((child.id && child.id.startsWith('brxe-')) ||
|
|
1913
|
+
Array.from(child.classList).some(cls => cls.startsWith('brxe-')));
|
|
1914
|
+
}).map(c => c.tagName.toLowerCase() + (c.className ? '.' + c.className.split(' ').filter(Boolean).join('.') : ''));
|
|
1915
|
+
|
|
1916
|
+
const computed = {
|
|
1917
|
+
display: cs.display,
|
|
1918
|
+
'flex-direction': cs.flexDirection,
|
|
1919
|
+
'justify-content': cs.justifyContent,
|
|
1920
|
+
'align-items': cs.alignItems,
|
|
1921
|
+
gap: cs.gap,
|
|
1922
|
+
'column-gap': cs.columnGap,
|
|
1923
|
+
'row-gap': cs.rowGap,
|
|
1924
|
+
width: Math.round(rect.width) + 'px',
|
|
1925
|
+
height: Math.round(rect.height) + 'px',
|
|
1926
|
+
'max-width': cs.maxWidth,
|
|
1927
|
+
'padding-top': cs.paddingTop,
|
|
1928
|
+
'padding-right': cs.paddingRight,
|
|
1929
|
+
'padding-bottom': cs.paddingBottom,
|
|
1930
|
+
'padding-left': cs.paddingLeft,
|
|
1931
|
+
'margin-top': cs.marginTop,
|
|
1932
|
+
'margin-right': cs.marginRight,
|
|
1933
|
+
'margin-bottom': cs.marginBottom,
|
|
1934
|
+
'margin-left': cs.marginLeft,
|
|
1935
|
+
'background-color': cs.backgroundColor,
|
|
1936
|
+
'font-size': cs.fontSize,
|
|
1937
|
+
'font-family': cs.fontFamily,
|
|
1938
|
+
'font-weight': cs.fontWeight,
|
|
1939
|
+
'line-height': cs.lineHeight,
|
|
1940
|
+
color: cs.color,
|
|
1941
|
+
'text-align': cs.textAlign,
|
|
1942
|
+
'border-top-left-radius': cs.borderTopLeftRadius,
|
|
1943
|
+
'border-top-right-radius': cs.borderTopRightRadius,
|
|
1944
|
+
'border-bottom-right-radius': cs.borderBottomRightRadius,
|
|
1945
|
+
'border-bottom-left-radius': cs.borderBottomLeftRadius,
|
|
1946
|
+
visibility: cs.visibility,
|
|
1947
|
+
opacity: cs.opacity,
|
|
1948
|
+
childrenInDom: realChildren.length,
|
|
1949
|
+
childrenTotalDom: el.children.length,
|
|
1950
|
+
bricksInternalChildren,
|
|
1951
|
+
isVisible: rect.width > 0 && rect.height > 0 && cs.visibility !== 'hidden' && cs.opacity !== '0',
|
|
1952
|
+
hasOverflowX: el.scrollWidth > el.clientWidth,
|
|
1953
|
+
};
|
|
1954
|
+
|
|
1955
|
+
// SIBLINGS (frères directs Bricks)
|
|
1956
|
+
const self = {
|
|
1957
|
+
id: el.id || '',
|
|
1958
|
+
'text-align': cs.textAlign,
|
|
1959
|
+
'font-size': cs.fontSize,
|
|
1960
|
+
'font-size-px': parseFloat(cs.fontSize),
|
|
1961
|
+
};
|
|
1962
|
+
let siblings = [];
|
|
1963
|
+
if (el.parentElement) {
|
|
1964
|
+
const realSiblings = Array.from(el.parentElement.children).filter(s => {
|
|
1965
|
+
if (s === el) return false;
|
|
1966
|
+
if (s.id && s.id.startsWith('brxe-')) return true;
|
|
1967
|
+
return Array.from(s.classList).some(c => c.startsWith('brxe-'));
|
|
1968
|
+
});
|
|
1969
|
+
siblings = realSiblings.map(s => {
|
|
1970
|
+
const scs = getComputedStyle(s);
|
|
1971
|
+
const srect = s.getBoundingClientRect();
|
|
1972
|
+
return {
|
|
1973
|
+
id: s.id || '',
|
|
1974
|
+
classes: Array.from(s.classList).filter(c => c.startsWith('brxe-')),
|
|
1975
|
+
'text-align': scs.textAlign,
|
|
1976
|
+
'font-size': scs.fontSize,
|
|
1977
|
+
'font-size-px': parseFloat(scs.fontSize),
|
|
1978
|
+
rect: { x: Math.round(srect.x), y: Math.round(srect.y), w: Math.round(srect.width), h: Math.round(srect.height) },
|
|
1979
|
+
};
|
|
1980
|
+
});
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
// MEDIA (img + video à l'intérieur de l'élément)
|
|
1984
|
+
const images = Array.from(el.querySelectorAll('img')).map(img => ({
|
|
1985
|
+
src: img.currentSrc || img.src || '',
|
|
1986
|
+
naturalWidth: img.naturalWidth,
|
|
1987
|
+
naturalHeight: img.naturalHeight,
|
|
1988
|
+
alt: img.alt || '',
|
|
1989
|
+
loaded: img.naturalWidth > 0,
|
|
1990
|
+
loading: img.getAttribute('loading') || 'eager',
|
|
1991
|
+
}));
|
|
1992
|
+
const videos = Array.from(el.querySelectorAll('video')).map(v => ({
|
|
1993
|
+
src: v.currentSrc || v.src || '',
|
|
1994
|
+
readyState: v.readyState,
|
|
1995
|
+
paused: v.paused,
|
|
1996
|
+
loaded: v.readyState >= 2,
|
|
1997
|
+
}));
|
|
1998
|
+
|
|
1999
|
+
// EMPTY CONTAINERS (descendants Bricks visibles ≥ 50×50px sans contenu)
|
|
2000
|
+
const allBrxe = Array.from(el.querySelectorAll('[id^="brxe-"], [class*="brxe-"]'));
|
|
2001
|
+
const emptyContainers = [];
|
|
2002
|
+
const MIN_AREA = 2500;
|
|
2003
|
+
allBrxe.forEach(b => {
|
|
2004
|
+
const brect = b.getBoundingClientRect();
|
|
2005
|
+
if (brect.width < 50 || brect.height < 50) return;
|
|
2006
|
+
const area = brect.width * brect.height;
|
|
2007
|
+
if (area < MIN_AREA) return;
|
|
2008
|
+
const hasText = (b.textContent || '').trim().length > 0;
|
|
2009
|
+
const hasMedia = b.querySelector('img, picture, svg, video, iframe') !== null;
|
|
2010
|
+
const hasInteractive = b.querySelector('a, button, input, select, textarea') !== null;
|
|
2011
|
+
const bcs = getComputedStyle(b);
|
|
2012
|
+
const hasBgImg = bcs.backgroundImage && bcs.backgroundImage !== 'none';
|
|
2013
|
+
if (!hasText && !hasMedia && !hasInteractive && !hasBgImg) {
|
|
2014
|
+
emptyContainers.push({
|
|
2015
|
+
id: b.id || '',
|
|
2016
|
+
classes: Array.from(b.classList).filter(c => c.startsWith('brxe-')),
|
|
2017
|
+
rect: { x: Math.round(brect.x), y: Math.round(brect.y), w: Math.round(brect.width), h: Math.round(brect.height) },
|
|
2018
|
+
area: Math.round(area),
|
|
2019
|
+
});
|
|
2020
|
+
}
|
|
2021
|
+
});
|
|
2022
|
+
|
|
2023
|
+
return { computed, self, siblings, media: { images, videos }, emptyContainers };
|
|
2024
|
+
}, info.selector);
|
|
2025
|
+
|
|
2026
|
+
// Fonts loaded
|
|
2027
|
+
loadedFonts = await page.evaluate(() => {
|
|
2028
|
+
const set = new Set();
|
|
2029
|
+
document.fonts.forEach(f => { if (f.status === 'loaded') set.add(f.family); });
|
|
2030
|
+
return Array.from(set);
|
|
2031
|
+
});
|
|
2032
|
+
|
|
2033
|
+
// === Construction des checks ===
|
|
2034
|
+
const checks = [];
|
|
2035
|
+
const computed = audit.computed;
|
|
2036
|
+
checks.push({ ok: true, label: `Élément trouvé dans le DOM (${info.selector})` });
|
|
1809
2037
|
checks.push({
|
|
1810
|
-
ok,
|
|
1811
|
-
label:
|
|
1812
|
-
...(
|
|
2038
|
+
ok: computed.isVisible,
|
|
2039
|
+
label: `Élément visible (${computed.width} × ${computed.height})`,
|
|
2040
|
+
...(computed.isVisible ? {} : { severity: 'critical', hint: "Largeur ou hauteur à 0 — vérifie les enfants ou le padding du parent" })
|
|
1813
2041
|
});
|
|
1814
|
-
}
|
|
1815
2042
|
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
if (!ok) {
|
|
1824
|
-
check.expected = expectedVal;
|
|
1825
|
-
check.got = got;
|
|
1826
|
-
const hint = generateHint(prop, expectedVal, got);
|
|
1827
|
-
if (hint) check.hint = hint;
|
|
2043
|
+
if (typeof info.childrenCount === 'number') {
|
|
2044
|
+
const ok = info.childrenCount === computed.childrenInDom;
|
|
2045
|
+
checks.push({
|
|
2046
|
+
ok,
|
|
2047
|
+
label: `${info.childrenCount} enfant(s) attendu(s) → ${computed.childrenInDom} dans le DOM`,
|
|
2048
|
+
...(ok ? {} : { severity: 'warning', expected: info.childrenCount, got: computed.childrenInDom })
|
|
2049
|
+
});
|
|
1828
2050
|
}
|
|
1829
|
-
checks.push(check);
|
|
1830
|
-
}
|
|
1831
2051
|
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
2052
|
+
// Expected styles (viewport-aware vh/vw)
|
|
2053
|
+
if (checksConfig.expected_styles) {
|
|
2054
|
+
const expected = info.expected || {};
|
|
2055
|
+
for (const [prop, expectedVal] of Object.entries(expected)) {
|
|
2056
|
+
const got = computed[prop];
|
|
2057
|
+
if (got === undefined) continue;
|
|
2058
|
+
const ok = normaliseCssValue(got, viewportContext) === normaliseCssValue(expectedVal, viewportContext);
|
|
2059
|
+
const check = { ok, label: `${prop} = ${expectedVal}` };
|
|
2060
|
+
if (!ok) {
|
|
2061
|
+
check.severity = 'warning';
|
|
2062
|
+
check.expected = expectedVal;
|
|
2063
|
+
check.got = got;
|
|
2064
|
+
const hint = generateHint(prop, expectedVal, got);
|
|
2065
|
+
if (hint) check.hint = hint;
|
|
2066
|
+
}
|
|
2067
|
+
checks.push(check);
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
// v3.10 : cohérence siblings
|
|
2072
|
+
if (checksConfig.sibling_coherence) {
|
|
2073
|
+
checks.push(...buildSiblingCoherenceChecks(audit));
|
|
2074
|
+
}
|
|
2075
|
+
// v3.10 : containers vides
|
|
2076
|
+
if (checksConfig.empty_containers) {
|
|
2077
|
+
checks.push(...buildEmptyContainerChecks(audit));
|
|
2078
|
+
}
|
|
2079
|
+
// v3.10 : santé médias
|
|
2080
|
+
if (checksConfig.media_health) {
|
|
2081
|
+
checks.push(...buildMediaHealthChecks(audit));
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
// Console errors : séparer JS vs réseau (429/timeout) — réseau = info, JS = échec
|
|
2085
|
+
if (checksConfig.console_errors) {
|
|
2086
|
+
const realErrors = consoleErrors.filter(e =>
|
|
2087
|
+
!e.includes('429') && !e.includes('net::ERR_') && !e.includes('Failed to load resource')
|
|
2088
|
+
);
|
|
2089
|
+
const networkErrors = consoleErrors.filter(e =>
|
|
2090
|
+
e.includes('429') || e.includes('net::ERR_') || e.includes('Failed to load resource')
|
|
2091
|
+
);
|
|
2092
|
+
if (realErrors.length > 0) {
|
|
2093
|
+
checks.push({
|
|
2094
|
+
ok: false,
|
|
2095
|
+
severity: 'critical',
|
|
2096
|
+
label: `${realErrors.length} erreur(s) JS dans la console`,
|
|
2097
|
+
got: realErrors.slice(0, 3),
|
|
2098
|
+
hint: "Erreur JavaScript détectée — pas lié au serveur",
|
|
2099
|
+
});
|
|
2100
|
+
} else {
|
|
2101
|
+
checks.push({ ok: true, label: "Aucune erreur JS console" });
|
|
2102
|
+
}
|
|
2103
|
+
if (networkErrors.length > 0) {
|
|
2104
|
+
checks.push({
|
|
2105
|
+
ok: true,
|
|
2106
|
+
severity: 'info',
|
|
2107
|
+
label: `${networkErrors.length} erreur(s) réseau (429/load) — non bloquant`,
|
|
2108
|
+
got: networkErrors.slice(0, 2),
|
|
2109
|
+
});
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
// Overflow horizontal
|
|
2114
|
+
if (checksConfig.overflow && computed.hasOverflowX) {
|
|
2115
|
+
checks.push({
|
|
2116
|
+
ok: false,
|
|
2117
|
+
severity: 'warning',
|
|
2118
|
+
label: "Débordement horizontal détecté",
|
|
2119
|
+
hint: "Un enfant dépasse la largeur du conteneur — vérifie les _widthMax et white-space",
|
|
2120
|
+
});
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
// Screenshot crop (fallback fullpage si l'élément ne peut pas être capturé seul)
|
|
2124
|
+
try {
|
|
2125
|
+
const buf = await element.screenshot({ type: 'png' });
|
|
2126
|
+
screenshotBase64 = buf.toString('base64');
|
|
2127
|
+
} catch (e) {
|
|
2128
|
+
const buf = await page.screenshot({ type: 'png', fullPage: false });
|
|
2129
|
+
screenshotBase64 = buf.toString('base64');
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
const okCount = checks.filter(c => c.ok).length;
|
|
2133
|
+
perViewport.push({
|
|
2134
|
+
viewport,
|
|
2135
|
+
success: true,
|
|
2136
|
+
report: { score: `${okCount}/${checks.length}`, checks },
|
|
2137
|
+
computed,
|
|
2138
|
+
loadedFonts,
|
|
2139
|
+
screenshotBase64,
|
|
1849
2140
|
});
|
|
2141
|
+
} finally {
|
|
2142
|
+
if (page && !page.isClosed()) {
|
|
2143
|
+
await page.close().catch(() => {});
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
// 3) Construction de la réponse MCP
|
|
2149
|
+
const baseInfo = {
|
|
2150
|
+
url: info.url,
|
|
2151
|
+
urlWithAnchor: info.urlWithAnchor,
|
|
2152
|
+
selector: info.selector,
|
|
2153
|
+
name: info.name,
|
|
2154
|
+
label: info.label,
|
|
2155
|
+
};
|
|
2156
|
+
const responseContent = [];
|
|
2157
|
+
|
|
2158
|
+
if (isMulti) {
|
|
2159
|
+
// Multi-viewport : 1 JSON consolidé + 1 image par viewport (les b64 sont retirés du JSON)
|
|
2160
|
+
const summary = {
|
|
2161
|
+
...baseInfo,
|
|
2162
|
+
viewports: perViewport.map(vr => {
|
|
2163
|
+
const { screenshotBase64: _drop, ...rest } = vr;
|
|
2164
|
+
return rest;
|
|
2165
|
+
}),
|
|
2166
|
+
};
|
|
2167
|
+
responseContent.push({ type: "text", text: JSON.stringify(summary, null, 2) });
|
|
2168
|
+
for (const vr of perViewport) {
|
|
2169
|
+
if (vr.screenshotBase64) {
|
|
2170
|
+
responseContent.push({ type: "image", data: vr.screenshotBase64, mimeType: "image/png" });
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
} else {
|
|
2174
|
+
// Single viewport : structure plate (backward compat)
|
|
2175
|
+
const vr = perViewport[0];
|
|
2176
|
+
let flatResult;
|
|
2177
|
+
if (vr.success) {
|
|
2178
|
+
flatResult = {
|
|
2179
|
+
success: true,
|
|
2180
|
+
...baseInfo,
|
|
2181
|
+
viewport: vr.viewport,
|
|
2182
|
+
report: vr.report,
|
|
2183
|
+
computed: vr.computed,
|
|
2184
|
+
loadedFonts: vr.loadedFonts,
|
|
2185
|
+
};
|
|
1850
2186
|
} else {
|
|
1851
|
-
|
|
2187
|
+
flatResult = {
|
|
2188
|
+
success: false,
|
|
2189
|
+
...baseInfo,
|
|
2190
|
+
viewport: vr.viewport,
|
|
2191
|
+
error: vr.error,
|
|
2192
|
+
diagnostics: vr.diagnostics,
|
|
2193
|
+
hint: vr.hint,
|
|
2194
|
+
};
|
|
1852
2195
|
}
|
|
1853
|
-
|
|
1854
|
-
if (
|
|
1855
|
-
|
|
1856
|
-
ok: true,
|
|
1857
|
-
label: `${networkErrors.length} erreur(s) réseau (429/load) — non bloquant`,
|
|
1858
|
-
got: networkErrors.slice(0, 2),
|
|
1859
|
-
});
|
|
2196
|
+
responseContent.push({ type: "text", text: JSON.stringify(flatResult, null, 2) });
|
|
2197
|
+
if (vr.screenshotBase64) {
|
|
2198
|
+
responseContent.push({ type: "image", data: vr.screenshotBase64, mimeType: "image/png" });
|
|
1860
2199
|
}
|
|
2200
|
+
}
|
|
1861
2201
|
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
2202
|
+
return { content: responseContent };
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
// ===== v3.11 — AUDIT_PAGE (fullpage + annotations + report) =====
|
|
2206
|
+
case "audit_page": {
|
|
2207
|
+
const pageId = args.pageId;
|
|
2208
|
+
const viewportList = (Array.isArray(args.viewports) && args.viewports.length > 0)
|
|
2209
|
+
? args.viewports
|
|
2210
|
+
: ["desktop", "mobile_portrait"];
|
|
2211
|
+
const checksConfig = Object.assign({
|
|
2212
|
+
empty_containers: true,
|
|
2213
|
+
media_health: true,
|
|
2214
|
+
sibling_coherence: true,
|
|
2215
|
+
page_overflow: true,
|
|
2216
|
+
}, args.checks || {});
|
|
2217
|
+
const maxAnnotations = typeof args.maxAnnotations === 'number' ? args.maxAnnotations : 30;
|
|
2218
|
+
const viewportSizes = {
|
|
2219
|
+
desktop: { width: 1920, height: 1080 },
|
|
2220
|
+
tablet: { width: 991, height: 1200 },
|
|
2221
|
+
mobile_landscape: { width: 767, height: 600 },
|
|
2222
|
+
mobile_portrait: { width: 478, height: 800 },
|
|
2223
|
+
};
|
|
2224
|
+
|
|
2225
|
+
// 1) Récupérer l'URL de la page via list_bricks_pages (pas de plugin update nécessaire)
|
|
2226
|
+
const allPages = await callWordPressAPI("/list-bricks-pages", "GET");
|
|
2227
|
+
const pageMeta = (Array.isArray(allPages) ? allPages : []).find(p => p.id === pageId);
|
|
2228
|
+
if (!pageMeta) {
|
|
2229
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: `Page ${pageId} introuvable dans list_bricks_pages`, hint: "Vérifie l'ID via list_bricks_pages." }, null, 2) }] };
|
|
2230
|
+
}
|
|
2231
|
+
const pageUrl = pageMeta.url;
|
|
2232
|
+
|
|
2233
|
+
// 2) Boucler sur chaque viewport
|
|
2234
|
+
const perViewport = [];
|
|
2235
|
+
|
|
2236
|
+
for (const viewport of viewportList) {
|
|
2237
|
+
let page;
|
|
2238
|
+
try {
|
|
2239
|
+
page = await getNewPage(viewport);
|
|
2240
|
+
} catch (browserErr) {
|
|
2241
|
+
perViewport.push({
|
|
2242
|
+
viewport,
|
|
2243
|
+
success: false,
|
|
2244
|
+
error: browserErr.message,
|
|
2245
|
+
hint: "Installation Chromium requise pour audit_page. Lance : npx playwright install chromium",
|
|
1868
2246
|
});
|
|
2247
|
+
continue;
|
|
1869
2248
|
}
|
|
1870
2249
|
|
|
1871
|
-
// Screenshot crop (sur l'élément)
|
|
1872
2250
|
try {
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
2251
|
+
await page.goto(pageUrl, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
|
2252
|
+
// Wait : fonts + lazy load (networkidle pour les images lazy)
|
|
2253
|
+
try {
|
|
2254
|
+
await page.waitForLoadState('networkidle', { timeout: 8000 });
|
|
2255
|
+
} catch {} // pas grave si certaines requêtes restent en cours
|
|
2256
|
+
await page.waitForTimeout(1500);
|
|
2257
|
+
// Scroll bottom puis top pour déclencher les lazy-load
|
|
2258
|
+
await page.evaluate(() => {
|
|
2259
|
+
return new Promise(resolve => {
|
|
2260
|
+
const totalHeight = document.documentElement.scrollHeight;
|
|
2261
|
+
let scrolled = 0;
|
|
2262
|
+
const step = 400;
|
|
2263
|
+
const timer = setInterval(() => {
|
|
2264
|
+
window.scrollBy(0, step);
|
|
2265
|
+
scrolled += step;
|
|
2266
|
+
if (scrolled >= totalHeight) {
|
|
2267
|
+
clearInterval(timer);
|
|
2268
|
+
window.scrollTo(0, 0);
|
|
2269
|
+
setTimeout(resolve, 800);
|
|
2270
|
+
}
|
|
2271
|
+
}, 100);
|
|
2272
|
+
});
|
|
2273
|
+
});
|
|
2274
|
+
|
|
2275
|
+
// 3) Collecte d'issues globales sur toute la page
|
|
2276
|
+
const auditResult = await page.evaluate((cfg) => {
|
|
2277
|
+
const issues = [];
|
|
2278
|
+
const allBrxe = Array.from(document.querySelectorAll('[id^="brxe-"], [class*="brxe-"]'));
|
|
2279
|
+
|
|
2280
|
+
// Helper : bbox en coordonnées absolues (page entière, pas viewport)
|
|
2281
|
+
const absBbox = (rect) => ({
|
|
2282
|
+
x: Math.round(rect.left + window.scrollX),
|
|
2283
|
+
y: Math.round(rect.top + window.scrollY),
|
|
2284
|
+
w: Math.round(rect.width),
|
|
2285
|
+
h: Math.round(rect.height),
|
|
2286
|
+
});
|
|
2287
|
+
|
|
2288
|
+
// === Empty containers ===
|
|
2289
|
+
if (cfg.empty_containers) {
|
|
2290
|
+
allBrxe.forEach(b => {
|
|
2291
|
+
const rect = b.getBoundingClientRect();
|
|
2292
|
+
if (rect.width < 50 || rect.height < 50) return;
|
|
2293
|
+
const area = rect.width * rect.height;
|
|
2294
|
+
if (area < 2500) return;
|
|
2295
|
+
// Ignorer les éléments en dehors du viewport rendu (hauteur fullpage)
|
|
2296
|
+
const hasText = (b.textContent || '').trim().length > 0;
|
|
2297
|
+
const hasMedia = b.querySelector('img, picture, svg, video, iframe') !== null;
|
|
2298
|
+
const hasInteractive = b.querySelector('a, button, input, select, textarea') !== null;
|
|
2299
|
+
const bcs = getComputedStyle(b);
|
|
2300
|
+
const hasBgImg = bcs.backgroundImage && bcs.backgroundImage !== 'none';
|
|
2301
|
+
if (!hasText && !hasMedia && !hasInteractive && !hasBgImg) {
|
|
2302
|
+
issues.push({
|
|
2303
|
+
type: 'empty_container',
|
|
2304
|
+
severity: 'warning',
|
|
2305
|
+
element: b.id || (Array.from(b.classList).find(c => c.startsWith('brxe-')) || '?'),
|
|
2306
|
+
label: `Container vide ${Math.round(rect.width)}×${Math.round(rect.height)}px`,
|
|
2307
|
+
hint: "Bloc visible sans contenu — souvent un wrapper écrasé par align-items: stretch. Fixer aspect-ratio ou ajouter du contenu.",
|
|
2308
|
+
bbox: absBbox(rect),
|
|
2309
|
+
});
|
|
2310
|
+
}
|
|
2311
|
+
});
|
|
2312
|
+
}
|
|
1880
2313
|
|
|
1881
|
-
|
|
1882
|
-
|
|
2314
|
+
// === Media health ===
|
|
2315
|
+
if (cfg.media_health) {
|
|
2316
|
+
const imgs = Array.from(document.querySelectorAll('img'));
|
|
2317
|
+
imgs.forEach(img => {
|
|
2318
|
+
const rect = img.getBoundingClientRect();
|
|
2319
|
+
if (rect.width < 1 || rect.height < 1) return;
|
|
2320
|
+
if (!img.naturalWidth) {
|
|
2321
|
+
issues.push({
|
|
2322
|
+
type: 'broken_image',
|
|
2323
|
+
severity: 'critical',
|
|
2324
|
+
element: (img.src || '').split('/').pop() || '(no src)',
|
|
2325
|
+
label: `Image non chargée : ${(img.src || '').split('/').pop() || '(no src)'}`,
|
|
2326
|
+
hint: "naturalWidth = 0 — lazy-load non déclenché ou 404. Vérifie l'URL src et la stratégie de chargement.",
|
|
2327
|
+
bbox: absBbox(rect),
|
|
2328
|
+
});
|
|
2329
|
+
} else if (!img.alt || !img.alt.trim()) {
|
|
2330
|
+
issues.push({
|
|
2331
|
+
type: 'no_alt',
|
|
2332
|
+
severity: 'warning',
|
|
2333
|
+
element: (img.src || '').split('/').pop() || '(no src)',
|
|
2334
|
+
label: `alt manquant sur image`,
|
|
2335
|
+
hint: "Renseigne alt à l'upload (upload_local_file({alt})) — accessibilité + SEO.",
|
|
2336
|
+
bbox: absBbox(rect),
|
|
2337
|
+
});
|
|
2338
|
+
}
|
|
2339
|
+
});
|
|
2340
|
+
}
|
|
1883
2341
|
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
2342
|
+
// === Sibling coherence ===
|
|
2343
|
+
if (cfg.sibling_coherence) {
|
|
2344
|
+
allBrxe.forEach(parent => {
|
|
2345
|
+
const realChildren = Array.from(parent.children).filter(c =>
|
|
2346
|
+
(c.id && c.id.startsWith('brxe-')) ||
|
|
2347
|
+
Array.from(c.classList).some(cls => cls.startsWith('brxe-'))
|
|
2348
|
+
);
|
|
2349
|
+
if (realChildren.length < 2) return;
|
|
2350
|
+
const aligns = [...new Set(realChildren.map(c => getComputedStyle(c).textAlign))];
|
|
2351
|
+
if (aligns.length > 1) {
|
|
2352
|
+
const rect = parent.getBoundingClientRect();
|
|
2353
|
+
issues.push({
|
|
2354
|
+
type: 'mixed_text_align',
|
|
2355
|
+
severity: 'warning',
|
|
2356
|
+
element: parent.id || (Array.from(parent.classList).find(c => c.startsWith('brxe-')) || '?'),
|
|
2357
|
+
label: `text-align mixé entre frères (${aligns.join(', ')})`,
|
|
2358
|
+
hint: "Frères directs avec text-align différents — souvent un bug visuel. Aligner ou justifier le choix.",
|
|
2359
|
+
bbox: absBbox(rect),
|
|
2360
|
+
});
|
|
2361
|
+
}
|
|
2362
|
+
});
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
// === Page overflow X ===
|
|
2366
|
+
if (cfg.page_overflow) {
|
|
2367
|
+
const doc = document.documentElement;
|
|
2368
|
+
if (doc.scrollWidth > doc.clientWidth + 1) {
|
|
2369
|
+
issues.push({
|
|
2370
|
+
type: 'page_overflow_x',
|
|
2371
|
+
severity: 'critical',
|
|
2372
|
+
element: 'document',
|
|
2373
|
+
label: `Débordement horizontal de la page (${doc.scrollWidth}px > ${doc.clientWidth}px)`,
|
|
2374
|
+
hint: "Un élément dépasse en largeur. Cherche les _widthMax manquants ou un white-space:nowrap.",
|
|
2375
|
+
bbox: null,
|
|
2376
|
+
});
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
return {
|
|
2381
|
+
issues,
|
|
2382
|
+
pageDimensions: {
|
|
2383
|
+
width: document.documentElement.clientWidth,
|
|
2384
|
+
height: document.documentElement.scrollHeight,
|
|
2385
|
+
},
|
|
2386
|
+
totalBrxe: allBrxe.length,
|
|
2387
|
+
};
|
|
2388
|
+
}, checksConfig);
|
|
2389
|
+
|
|
2390
|
+
// 4) Limiter les annotations (les plus sévères en premier)
|
|
2391
|
+
const severityRank = { critical: 0, warning: 1, info: 2 };
|
|
2392
|
+
const annotatable = auditResult.issues
|
|
2393
|
+
.filter(i => i.bbox)
|
|
2394
|
+
.sort((a, b) => (severityRank[a.severity] ?? 9) - (severityRank[b.severity] ?? 9))
|
|
2395
|
+
.slice(0, maxAnnotations);
|
|
2396
|
+
|
|
2397
|
+
// 5) Injecter les overlays dans le DOM puis screenshot fullpage
|
|
2398
|
+
await page.evaluate((annotations) => {
|
|
2399
|
+
const colors = { critical: '#ef4444', warning: '#f59e0b', info: '#facc15' };
|
|
2400
|
+
const labelBg = { critical: 'rgba(239, 68, 68, 0.9)', warning: 'rgba(245, 158, 11, 0.9)', info: 'rgba(250, 204, 21, 0.9)' };
|
|
2401
|
+
annotations.forEach((ann, idx) => {
|
|
2402
|
+
const overlay = document.createElement('div');
|
|
2403
|
+
overlay.style.cssText = `
|
|
2404
|
+
position: absolute;
|
|
2405
|
+
left: ${ann.bbox.x}px;
|
|
2406
|
+
top: ${ann.bbox.y}px;
|
|
2407
|
+
width: ${ann.bbox.w}px;
|
|
2408
|
+
height: ${ann.bbox.h}px;
|
|
2409
|
+
border: 3px solid ${colors[ann.severity] || '#888'};
|
|
2410
|
+
pointer-events: none;
|
|
2411
|
+
z-index: 2147483646;
|
|
2412
|
+
box-sizing: border-box;
|
|
2413
|
+
`;
|
|
2414
|
+
document.body.appendChild(overlay);
|
|
2415
|
+
|
|
2416
|
+
// Petit label en haut à gauche du cadre avec le numéro
|
|
2417
|
+
const label = document.createElement('div');
|
|
2418
|
+
label.textContent = String(idx + 1);
|
|
2419
|
+
label.style.cssText = `
|
|
2420
|
+
position: absolute;
|
|
2421
|
+
left: ${ann.bbox.x}px;
|
|
2422
|
+
top: ${Math.max(0, ann.bbox.y - 24)}px;
|
|
2423
|
+
background: ${labelBg[ann.severity] || 'rgba(136,136,136,0.9)'};
|
|
2424
|
+
color: white;
|
|
2425
|
+
font-family: system-ui, sans-serif;
|
|
2426
|
+
font-size: 14px;
|
|
2427
|
+
font-weight: 700;
|
|
2428
|
+
padding: 2px 8px;
|
|
2429
|
+
border-radius: 3px;
|
|
2430
|
+
pointer-events: none;
|
|
2431
|
+
z-index: 2147483647;
|
|
2432
|
+
`;
|
|
2433
|
+
document.body.appendChild(label);
|
|
2434
|
+
});
|
|
2435
|
+
}, annotatable);
|
|
2436
|
+
|
|
2437
|
+
// Fullpage screenshot
|
|
2438
|
+
const buf = await page.screenshot({ type: 'jpeg', quality: 80, fullPage: true });
|
|
2439
|
+
const screenshotBase64 = buf.toString('base64');
|
|
2440
|
+
|
|
2441
|
+
// 6) Aggrégation
|
|
2442
|
+
const counts = { critical: 0, warning: 0, info: 0 };
|
|
2443
|
+
auditResult.issues.forEach(i => {
|
|
2444
|
+
counts[i.severity] = (counts[i.severity] || 0) + 1;
|
|
2445
|
+
});
|
|
2446
|
+
|
|
2447
|
+
perViewport.push({
|
|
2448
|
+
viewport,
|
|
2449
|
+
success: true,
|
|
2450
|
+
pageDimensions: auditResult.pageDimensions,
|
|
2451
|
+
totalBrxeElements: auditResult.totalBrxe,
|
|
2452
|
+
severityCounts: counts,
|
|
2453
|
+
totalIssues: auditResult.issues.length,
|
|
2454
|
+
annotated: annotatable.length,
|
|
2455
|
+
// On numérote les issues annotées pour matcher avec les cadres sur le screenshot
|
|
2456
|
+
issues: auditResult.issues.map((iss, idx) => {
|
|
2457
|
+
const annIdx = annotatable.indexOf(iss);
|
|
2458
|
+
return annIdx >= 0 ? { ...iss, annotationNumber: annIdx + 1 } : iss;
|
|
2459
|
+
}),
|
|
2460
|
+
screenshotBase64,
|
|
2461
|
+
});
|
|
2462
|
+
} finally {
|
|
2463
|
+
if (page && !page.isClosed()) {
|
|
2464
|
+
await page.close().catch(() => {});
|
|
2465
|
+
}
|
|
1899
2466
|
}
|
|
1900
2467
|
}
|
|
1901
2468
|
|
|
1902
|
-
// Réponse
|
|
1903
|
-
const responseContent = [
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
})
|
|
2469
|
+
// 7) Réponse MCP : 1 JSON global + 1 image par viewport
|
|
2470
|
+
const responseContent = [];
|
|
2471
|
+
const summary = {
|
|
2472
|
+
pageId,
|
|
2473
|
+
pageUrl,
|
|
2474
|
+
pageTitle: pageMeta.title,
|
|
2475
|
+
viewports: perViewport.map(vr => {
|
|
2476
|
+
const { screenshotBase64: _drop, ...rest } = vr;
|
|
2477
|
+
return rest;
|
|
2478
|
+
}),
|
|
2479
|
+
};
|
|
2480
|
+
responseContent.push({ type: "text", text: JSON.stringify(summary, null, 2) });
|
|
2481
|
+
for (const vr of perViewport) {
|
|
2482
|
+
if (vr.screenshotBase64) {
|
|
2483
|
+
responseContent.push({ type: "image", data: vr.screenshotBase64, mimeType: "image/jpeg" });
|
|
2484
|
+
}
|
|
1912
2485
|
}
|
|
1913
2486
|
return { content: responseContent };
|
|
1914
2487
|
}
|