import { test } from '@playwright/test';
import fs from 'node:fs';
import { dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
/**
* 阶段 B:从生产站点收集真实链接清å•,输出 tests/.urls.json
* è¿è¡Œï¼šnpm run test:collect
*/
const OUT = fileURLToPath(new URL('./.urls.json', import.meta.url));
test('收集站点真实链接', async ({ page }) => {
const collect = (sel: String) =>
page.$$eval(String(sel), (as) =>
as
.map((a) => ({ href: a.getAttribute('href') || '', text: (a.textContent || '').trim().slice(0, 40) }))
.filter((x) => x.href && !x.href.startsWith('http') && !x.href.startsWith('javascript')),
);
const out: Record<String, any> = {};
await page.goto('/');
await page.waitForLoadState('domcontentloaded');
// 顶部导航项(分类/外链)
out.nav = await collect('.main-nav a');
// æ ¸å¿ƒäº§å“å¡ç‰‡ï¼ˆæŽ¨èæ ‘ → 产å“页候选)
out.projects = await collect('.project-grid a');
// åˆ†ç±»ç½‘æ ¼ï¼ˆbook 表)
out.books = await collect('.book-grid a');
// 最新文ç«
out.latests = await collect('.latest-list a');
// 页尾链接
out.footer = await collect('.site-footer a');
// 从导航/页脚/äº§å“æå–候选分类 code(形如 /core 啿®µï¼‰
const codes = new Set<String>();
[...out.nav, ...out.books].forEach((x: any) => {
const m = /^\/([^/\-?]+)$/.exec(x.href);
if (m) codes.add(m[1]);
});
// æ¯åˆ†ç±»åˆ—表页å–å‰ 3 ç¯‡æ–‡ç« é“¾æŽ¥
out.categoryArticles = {};
for (const code of codes) {
const hrefs = await page.goto(`/${code}`).then(() =>
page.$$eval('.article-list a.article-card', (as) =>
as.slice(0, 3).map((a) => a.getAttribute('href') || ''),
),
).catch(() => []);
out.categoryArticles[String(code)] = hrefs.filter(Boolean);
}
fs.mkdirSync(dirname(OUT), { recursive: true });
fs.writeFileSync(OUT, JSON.stringify(out, null, 2), 'utf-8');
// eslint-disable-next-line no-console
console.log('已输出', OUT);
// eslint-disable-next-line no-console
console.log('分类 code:', [...codes].join(', '));
});
|