- Update dbis_core, cross-chain-pmm-lps, explorer-monorepo, metamask-integration, pr-workspace/chains - Omit embedded publish git dirs and empty placeholders from index Made-with: Cursor
241 lines
7.4 KiB
JavaScript
241 lines
7.4 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* Playwright audit for info.defi-oracle.io (or INFO_SITE_URL).
|
||
* Grades structure, agent surfaces, API reachability from the browser, console/network hygiene, basic perf.
|
||
*
|
||
* Usage:
|
||
* pnpm exec playwright install chromium # once
|
||
* node scripts/verify/playwright-audit-info-defi-oracle.mjs
|
||
* INFO_SITE_URL=http://192.168.11.218 node scripts/verify/playwright-audit-info-defi-oracle.mjs
|
||
*/
|
||
import { chromium } from 'playwright';
|
||
|
||
const BASE = (process.env.INFO_SITE_URL || 'https://info.defi-oracle.io').replace(/\/$/, '');
|
||
const ROUTES = [
|
||
'/',
|
||
'/tokens',
|
||
'/pools',
|
||
'/swap',
|
||
'/routing',
|
||
'/governance',
|
||
'/ecosystem',
|
||
'/documentation',
|
||
'/solacenet',
|
||
'/agents',
|
||
'/disclosures',
|
||
];
|
||
|
||
function clamp(n, lo, hi) {
|
||
return Math.max(lo, Math.min(hi, n));
|
||
}
|
||
|
||
function scoreFromRatio(ok, total) {
|
||
if (total === 0) return 10;
|
||
return clamp((ok / total) * 10, 0, 10);
|
||
}
|
||
|
||
async function collectPageMetrics(page) {
|
||
return page.evaluate(() => {
|
||
const h1s = [...document.querySelectorAll('h1')].map((el) => el.textContent?.trim() || '');
|
||
const nav = document.querySelector('nav');
|
||
const main = document.querySelector('main') || document.querySelector('[role="main"]');
|
||
const title = document.title || '';
|
||
const metaDesc = document.querySelector('meta[name="description"]')?.getAttribute('content') || '';
|
||
const jsonLd = [...document.querySelectorAll('script[type="application/ld+json"]')].length;
|
||
const navLinks = nav ? nav.querySelectorAll('a').length : 0;
|
||
let ttfbMs = null;
|
||
let domCompleteMs = null;
|
||
const navEntry = performance.getEntriesByType('navigation')[0];
|
||
if (navEntry) {
|
||
ttfbMs = Math.round(navEntry.responseStart);
|
||
domCompleteMs = Math.round(navEntry.domComplete);
|
||
}
|
||
return {
|
||
title,
|
||
metaDesc,
|
||
jsonLdScripts: jsonLd,
|
||
h1Count: h1s.length,
|
||
h1Texts: h1s.slice(0, 3),
|
||
hasMain: !!main,
|
||
navLinkCount: navLinks,
|
||
ttfbMs,
|
||
domCompleteMs,
|
||
};
|
||
});
|
||
}
|
||
|
||
async function main() {
|
||
const browser = await chromium.launch({ headless: true });
|
||
const context = await browser.newContext({
|
||
viewport: { width: 1280, height: 800 },
|
||
ignoreHTTPSErrors: true,
|
||
});
|
||
const page = await context.newPage();
|
||
|
||
const consoleErrors = [];
|
||
const consoleWarnings = [];
|
||
page.on('console', (msg) => {
|
||
const t = msg.type();
|
||
const text = msg.text();
|
||
if (t === 'error') consoleErrors.push(text);
|
||
if (t === 'warning') consoleWarnings.push(text);
|
||
});
|
||
|
||
const failedRequests = [];
|
||
page.on('requestfailed', (req) => {
|
||
failedRequests.push({ url: req.url(), failure: req.failure()?.errorText || 'unknown' });
|
||
});
|
||
|
||
const pages = [];
|
||
for (const path of ROUTES) {
|
||
const url = `${BASE}${path === '/' ? '/' : path}`;
|
||
const res = await page.goto(url, { waitUntil: 'load', timeout: 60_000 });
|
||
const status = res?.status() ?? 0;
|
||
await page.waitForTimeout(1500);
|
||
const metrics = await collectPageMetrics(page);
|
||
const bodySample = await page.evaluate(() => document.body?.innerText?.slice(0, 2000) || '');
|
||
pages.push({ path, url, status, metrics, bodySampleLength: bodySample.length });
|
||
}
|
||
|
||
const staticChecks = [];
|
||
for (const p of ['/llms.txt', '/robots.txt', '/sitemap.xml', '/agent-hints.json']) {
|
||
const r = await page.request.get(`${BASE}${p}`);
|
||
staticChecks.push({
|
||
path: p,
|
||
status: r.status(),
|
||
ok: r.ok(),
|
||
contentType: r.headers()['content-type'] || '',
|
||
});
|
||
}
|
||
|
||
let tokenAggProbe = { ok: false, status: 0, snippet: '' };
|
||
try {
|
||
const taUrl = `${BASE}/token-aggregation/api/v1/networks?refresh=1`;
|
||
const tr = await page.request.get(taUrl);
|
||
tokenAggProbe.status = tr.status();
|
||
tokenAggProbe.ok = tr.ok();
|
||
const ct = (tr.headers()['content-type'] || '').toLowerCase();
|
||
if (ct.includes('json')) {
|
||
const j = await tr.json().catch(() => null);
|
||
tokenAggProbe.snippet = j && typeof j === 'object' ? JSON.stringify(j).slice(0, 120) : '';
|
||
} else {
|
||
tokenAggProbe.snippet = (await tr.text()).slice(0, 80);
|
||
}
|
||
} catch (e) {
|
||
tokenAggProbe.error = String(e);
|
||
}
|
||
|
||
await browser.close();
|
||
|
||
const routesOk = pages.filter((p) => p.status >= 200 && p.status < 400).length;
|
||
const staticOk = staticChecks.filter((s) => s.ok).length;
|
||
const h1Ok = pages.filter((p) => p.metrics.h1Count === 1).length;
|
||
const mainOk = pages.filter((p) => p.metrics.hasMain).length;
|
||
|
||
const uniqueConsoleBuckets = new Set(
|
||
consoleErrors.map((t) => {
|
||
if (t.includes('CORS policy')) return 'cors-blocked-cross-origin-api';
|
||
if (t.includes('502')) return 'http-502';
|
||
if (t.includes('429')) return 'http-429';
|
||
if (t.includes('Failed to load resource')) return 'failed-resource-generic';
|
||
return t.slice(0, 100);
|
||
}),
|
||
);
|
||
const distinctConsoleIssues = uniqueConsoleBuckets.size;
|
||
|
||
const scores = {
|
||
availability: scoreFromRatio(routesOk, pages.length),
|
||
staticAgentSurfaces: scoreFromRatio(staticOk, staticChecks.length),
|
||
documentStructure: clamp(
|
||
(scoreFromRatio(h1Ok, pages.length) * 0.5 + scoreFromRatio(mainOk, pages.length) * 0.5),
|
||
0,
|
||
10,
|
||
),
|
||
tokenAggregationApi: tokenAggProbe.ok ? 10 : tokenAggProbe.status === 429 ? 4 : 2,
|
||
consoleHygiene:
|
||
consoleErrors.length === 0 ? 10 : clamp(10 - distinctConsoleIssues * 2.5, 0, 10),
|
||
networkHygiene:
|
||
failedRequests.length === 0 ? 10 : clamp(10 - Math.min(failedRequests.length, 8) * 1.1, 0, 10),
|
||
homeMetaAndSeo: (() => {
|
||
const home = pages[0];
|
||
let s = 0;
|
||
if (home?.metrics.title?.length > 10) s += 3;
|
||
if (home?.metrics.metaDesc?.length > 20) s += 3;
|
||
if (home?.metrics.jsonLdScripts > 0) s += 4;
|
||
return clamp(s, 0, 10);
|
||
})(),
|
||
performanceHint: (() => {
|
||
const ttfb = pages[0]?.metrics?.ttfbMs;
|
||
if (ttfb == null || Number.isNaN(ttfb)) return 7;
|
||
if (ttfb < 300) return 10;
|
||
if (ttfb < 800) return 8;
|
||
if (ttfb < 1800) return 6;
|
||
return 4;
|
||
})(),
|
||
};
|
||
|
||
const weights = {
|
||
availability: 0.18,
|
||
staticAgentSurfaces: 0.12,
|
||
documentStructure: 0.12,
|
||
tokenAggregationApi: 0.18,
|
||
consoleHygiene: 0.1,
|
||
networkHygiene: 0.1,
|
||
homeMetaAndSeo: 0.1,
|
||
performanceHint: 0.1,
|
||
};
|
||
|
||
let overall = 0;
|
||
for (const [k, w] of Object.entries(weights)) {
|
||
overall += scores[k] * w;
|
||
}
|
||
overall = Math.round(overall * 10) / 10;
|
||
|
||
const report = {
|
||
baseUrl: BASE,
|
||
generatedAt: new Date().toISOString(),
|
||
overallScore: overall,
|
||
scores,
|
||
weights,
|
||
pages: pages.map((p) => ({
|
||
path: p.path,
|
||
url: p.url,
|
||
status: p.status,
|
||
metrics: p.metrics,
|
||
bodyTextChars: p.bodySampleLength,
|
||
})),
|
||
staticChecks,
|
||
tokenAggregationProbe: tokenAggProbe,
|
||
consoleErrors,
|
||
distinctConsoleIssues,
|
||
consoleWarnings: consoleWarnings.slice(0, 20),
|
||
failedRequests: failedRequests.slice(0, 30),
|
||
rubricNotes: [
|
||
'Scores are heuristic (0–10 per axis); overall is weighted sum.',
|
||
'tokenAggregationApi uses same-origin /token-aggregation from BASE (expects JSON 200).',
|
||
'Exactly one h1 per route is preferred for documentStructure.',
|
||
'performanceHint uses Navigation Timing responseStart (TTFB) on home only.',
|
||
],
|
||
};
|
||
|
||
const grade =
|
||
overall >= 9
|
||
? 'A'
|
||
: overall >= 8
|
||
? 'B'
|
||
: overall >= 7
|
||
? 'C'
|
||
: overall >= 6
|
||
? 'D'
|
||
: 'F';
|
||
|
||
report.letterGrade = grade;
|
||
|
||
console.log(JSON.stringify(report, null, 2));
|
||
}
|
||
|
||
main().catch((e) => {
|
||
console.error(e);
|
||
process.exit(1);
|
||
});
|