项目文件夹

文件
wehub-resource-sync 9b395f5cc3
E2E Headed Chrome / e2e-headed (macos-15) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (ubuntu-latest) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (windows-latest) (push) Has been cancelled
CI / build (macos-latest) (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / unit-test (push) Has been cancelled
CI / bun-test (push) Has been cancelled
CI / adapter-test (push) Has been cancelled
CI / smoke-test (macos-latest) (push) Has been cancelled
CI / smoke-test (ubuntu-latest) (push) Has been cancelled
Security Audit / audit (push) Has been cancelled
Build Chrome Extension / build (push) Has been cancelled
Trigger Website Rebuild (Docs Updated) / dispatch (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:39:48 +08:00

225 行
9.4 KiB
JavaScript

import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { JSDOM } from 'jsdom';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
ArgumentError,
CommandExecutionError,
EmptyResultError,
} from '@jackwener/opencli/errors';
import { getRegistry } from '@jackwener/opencli/registry';
import { extractSearchRows } from './search.js';
import { extractRecentRows } from './recent.js';
const __dirname = dirname(fileURLToPath(import.meta.url));
const SEARCH_FIXTURE = readFileSync(join(__dirname, '__fixtures__/search.html'), 'utf8');
const RECENT_FIXTURE = readFileSync(join(__dirname, '__fixtures__/recent.html'), 'utf8');
function createPageMock(evaluateResult, overrides = {}) {
const evaluate = typeof evaluateResult === 'function'
? vi.fn(evaluateResult)
: vi.fn().mockResolvedValue(evaluateResult);
return {
goto: vi.fn().mockResolvedValue(undefined),
wait: vi.fn().mockResolvedValue(undefined),
evaluate,
...overrides,
};
}
describe('gov-policy commands — registration', () => {
it('registers search and recent as public browser commands', () => {
const search = getRegistry().get('gov-policy/search');
const recent = getRegistry().get('gov-policy/recent');
expect(search).toBeDefined();
expect(recent).toBeDefined();
expect(search.browser).toBe(true);
expect(recent.browser).toBe(true);
expect(search.strategy).toBe('public');
expect(recent.strategy).toBe('public');
expect(search.columns).toEqual(['rank', 'title', 'description', 'date', 'url']);
expect(recent.columns).toEqual(['rank', 'title', 'date', 'source', 'url']);
});
it('rejects empty search queries before browser navigation', async () => {
const search = getRegistry().get('gov-policy/search');
const page = { goto: vi.fn() };
await expect(search.func(page, { query: ' ' })).rejects.toThrow(ArgumentError);
expect(page.goto).not.toHaveBeenCalled();
});
it('rejects invalid limits before browser navigation', async () => {
const search = getRegistry().get('gov-policy/search');
const recent = getRegistry().get('gov-policy/recent');
const page = createPageMock({ ok: true, rows: [] });
await expect(search.func(page, { query: '数字经济', limit: '0' })).rejects.toThrow(ArgumentError);
await expect(search.func(page, { query: '数字经济', limit: '1.5' })).rejects.toThrow(ArgumentError);
await expect(search.func(page, { query: '数字经济', limit: '21' })).rejects.toThrow(ArgumentError);
await expect(recent.func(page, { limit: 'abc' })).rejects.toThrow(ArgumentError);
expect(page.goto).not.toHaveBeenCalled();
expect(page.evaluate).not.toHaveBeenCalled();
});
it('maps empty search pages, selector drift, and browser failures to typed errors', async () => {
const search = getRegistry().get('gov-policy/search');
const recent = getRegistry().get('gov-policy/recent');
await expect(search.func(createPageMock({
ok: false,
sample: '很抱歉,没有找到与 数字经济 相关的结果',
url: 'https://sousuo.www.gov.cn/sousuo/search.shtml?searchWord=x',
}), { query: '数字经济' })).rejects.toThrow(EmptyResultError);
await expect(recent.func(createPageMock({
ok: false,
sample: '<main>unexpected government page shell</main>',
url: 'https://www.gov.cn/zhengce/zuixin/index.htm',
}), {})).rejects.toThrow(CommandExecutionError);
await expect(search.func(createPageMock(
{ ok: true, rows: [] },
{ goto: vi.fn().mockRejectedValue(new Error('browser disconnected')) },
), { query: '数字经济' })).rejects.toThrow(CommandExecutionError);
});
});
/**
* In-browser DOM extractors against frozen sanitized HTML fixtures.
*
* Mocked-page.evaluate tests can't catch silent bugs that live inside the
* extractor, since they feed pre-baked results to the func and the real
* DOM walk never runs.
*
* These tests replay the real (sanitized) HTML through JSDOM so changes
* to the extractor logic that re-introduce a silent regression fail in CI.
*/
describe('gov-policy adapter — extractors against frozen HTML fixtures', () => {
let originalDocument;
let originalLocation;
beforeEach(() => {
originalDocument = globalThis.document;
originalLocation = globalThis.location;
});
afterEach(() => {
globalThis.document = originalDocument;
globalThis.location = originalLocation;
});
function loadFixture(html, url) {
const dom = new JSDOM(html, { url });
globalThis.document = dom.window.document;
globalThis.location = dom.window.location;
return dom;
}
it('extractSearchRows returns three result-shaped rows with title, description, date, url', () => {
loadFixture(SEARCH_FIXTURE, 'https://sousuo.www.gov.cn/sousuo/search.shtml?searchWord=%E6%95%B0%E5%AD%97%E7%BB%8F%E6%B5%8E');
const result = extractSearchRows();
expect(result.ok).toBe(true);
expect(result.rows).toHaveLength(3);
// Rank 1 + 3 are the homogeneous "type tag + emphasized title" cards
// whose .description div carries only the date span (no real snippet).
expect(result.rows[0]).toMatchObject({
rank: 1,
title: '要闻经济数据速览:10组数字看一季度中国经济',
date: '2026-4-16',
url: 'https://www.gov.cn/zhengce/jiedu/tujie/202604/content_7065945.htm',
});
// Description for these rows is just the publish-time line.
expect(result.rows[0].description).toContain('发布时间');
expect(result.rows[0].description).toContain('2026-4-16');
// Rank 2 has a real article snippet inside .description > .detail > p,
// and the extractor must capture it (sliced to 120 chars).
expect(result.rows[1]).toMatchObject({
rank: 2,
title: '要闻何立峰会见法国经济、财政和工业、能源与数字主权部部长莱斯屈尔',
date: '2026-3-17',
url: 'https://www.gov.cn/yaowen/liebiao/202603/content_7062985.htm',
});
expect(result.rows[1].description.length).toBeLessThanOrEqual(120);
expect(result.rows[1].description).toContain('新华社巴黎');
expect(result.rows[2]).toMatchObject({
rank: 3,
title: '要闻经济数据速览:7组数字看1—2月份中国经济',
date: '2026-3-16',
url: 'https://www.gov.cn/zhengce/jiedu/tujie/202603/content_7062831.htm',
});
// Lock the no-collapse contract on the title: the type_title prefix
// ('要闻') is fused into the textContent because we read the whole <a>.
// If a future refactor strips the prefix, this assertion catches it
// before any field-level downstream surprises.
for (const row of result.rows) {
expect(row.title.startsWith('要闻')).toBe(true);
}
});
it('extractRecentRows returns five rows with title, date, url and empty source', () => {
loadFixture(RECENT_FIXTURE, 'https://www.gov.cn/zhengce/zuixin/index.htm');
const result = extractRecentRows();
expect(result.ok).toBe(true);
expect(result.rows).toHaveLength(5);
expect(result.rows[0]).toMatchObject({
rank: 1,
title: '中共中央办公厅 国务院办公厅关于加强新就业群体服务管理的意见',
date: '2026-04-26',
url: 'https://www.gov.cn/zhengce/202604/content_7066998.htm',
});
expect(result.rows[1]).toMatchObject({
rank: 2,
date: '2026-04-23',
});
expect(result.rows[4]).toMatchObject({
rank: 5,
date: '2026-04-17',
});
// gov.cn/zhengce/zuixin layout has no .source / .from elements, so
// the source field is always an empty string. Lock that contract:
// a future selector change that picks up unrelated text would break
// it and fail this assertion.
for (const row of result.rows) {
expect(row.source).toBe('');
}
});
it('extractSearchRows signals ok:false with a sample when result list is empty', () => {
loadFixture(
'<html><head><title>blocked</title></head><body><main>访问受限,请稍后再试</main></body></html>',
'https://sousuo.www.gov.cn/sousuo/search.shtml?searchWord=zzz',
);
const result = extractSearchRows();
expect(result.ok).toBe(false);
expect(result.url).toBe('https://sousuo.www.gov.cn/sousuo/search.shtml?searchWord=zzz');
expect(result.sample).toContain('访问受限');
});
it('extractRecentRows signals ok:false with a sample when listing is empty', () => {
loadFixture(
'<html><head><title>not found</title></head><body><main>页面正在加载</main></body></html>',
'https://www.gov.cn/zhengce/zuixin/index.htm',
);
const result = extractRecentRows();
expect(result.ok).toBe(false);
expect(result.url).toBe('https://www.gov.cn/zhengce/zuixin/index.htm');
expect(result.sample).toContain('页面正在加载');
});
});