mirror of
https://github.com/DIYgod/RSSHub.git
synced 2026-08-29 01:53:47 +08:00
chore: fix ci
This commit is contained in:
@@ -1,251 +0,0 @@
|
||||
import type { Context } from 'hono';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import type { Data } from '@/types';
|
||||
|
||||
import { route } from './routes/gov/zhengce/govall';
|
||||
|
||||
const apiUrl = 'https://sousuoht.www.gov.cn/athena/forward/2B22E8E39E850E17F95A016A74FCB6B673336FA8B6FEC0E2955907EF9AEE06BE';
|
||||
const articleUrl = 'http://www.gov.cn/gongbao/content/2009/content_1322126.htm';
|
||||
|
||||
describe('gov.cn information search route', () => {
|
||||
it('supports the documented legacy advanced-search parameters', async () => {
|
||||
const { default: server } = await import('@/setup.test');
|
||||
let requestBody: any;
|
||||
|
||||
server.use(
|
||||
http.get('http://sousuo.gov.cn/list.htm', () => HttpResponse.text('<html></html>')),
|
||||
http.post(apiUrl, async ({ request }) => {
|
||||
requestBody = await request.json();
|
||||
|
||||
expect(request.headers.get('athenaAppKey')).toBeTruthy();
|
||||
expect(request.headers.get('athenaAppName')).toBe(encodeURIComponent('国网搜索'));
|
||||
|
||||
return HttpResponse.json({
|
||||
resultCode: {
|
||||
code: 200,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
middle: {
|
||||
list: [
|
||||
{
|
||||
title: '中华人民共和国国务院令(第<em>555</em>号)<br> 流动人口计划生育工作条例',
|
||||
title_no_tag: '中华人民共和国国务院令(第555号)<br> 流动人口计划生育工作条例',
|
||||
url: articleUrl,
|
||||
summary: '搜索接口摘要',
|
||||
time: '2009-05-30 23:59:59',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}),
|
||||
http.get(articleUrl, () => HttpResponse.text('<div id="UCAP-CONTENT"><p>文章全文</p></div>'))
|
||||
);
|
||||
|
||||
const ctx = {
|
||||
req: {
|
||||
param: (name: string) => (name === 'advance' ? 'orpro=555¬pro=2&search_field=title' : undefined),
|
||||
},
|
||||
} as unknown as Context;
|
||||
const result = (await route.handler(ctx)) as Data;
|
||||
|
||||
expect(requestBody).toMatchObject({
|
||||
code: '17da70961a7',
|
||||
dataTypeId: '107',
|
||||
orderBy: 'time',
|
||||
searchBy: 'title',
|
||||
pageNo: 1,
|
||||
pageSize: 20,
|
||||
isDefaultAdvanced: 1,
|
||||
isAdvancedSearch: 1,
|
||||
advancedFilters: [
|
||||
{
|
||||
fieldName: 'containsAll',
|
||||
searchWord: [],
|
||||
},
|
||||
{
|
||||
fieldName: 'containsOne',
|
||||
searchWord: ['555'],
|
||||
},
|
||||
{
|
||||
fieldName: 'none',
|
||||
searchWord: ['2'],
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(result.link).toMatch(/^https:\/\/sousuo\.www\.gov\.cn\/sousuo\/search\.shtml\?/);
|
||||
expect(result.item).toEqual([
|
||||
{
|
||||
title: '中华人民共和国国务院令(第555号) 流动人口计划生育工作条例',
|
||||
link: articleUrl,
|
||||
description: '<p>文章全文</p>',
|
||||
pubDate: new Date('2009-05-30T15:59:59.000Z'),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('maps legacy keyword and date filters to the Athena request', async () => {
|
||||
const { default: server } = await import('@/setup.test');
|
||||
let requestBody: any;
|
||||
|
||||
server.use(
|
||||
http.post(apiUrl, async ({ request }) => {
|
||||
requestBody = await request.json();
|
||||
|
||||
return HttpResponse.json({
|
||||
resultCode: {
|
||||
code: 200,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
middle: {
|
||||
list: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
const ctx = {
|
||||
req: {
|
||||
param: (name: string) =>
|
||||
name === 'advance'
|
||||
? 'allpro=%E5%8C%BB%E7%96%97+%E4%BF%9D%E9%9A%9C&inpro=%E5%AE%8C%E6%95%B4+%E7%9F%AD%E8%AF%AD&orpro=%E5%8C%BB%E4%BF%9D+%E7%A4%BE%E4%BF%9D¬pro=%E6%95%99%E8%82%B2&searchfield=content&pubmintimeYear=2009&pubmintimeMonth=5&pubmaxtimeYear=2009&pubmaxtimeMonth=5'
|
||||
: undefined,
|
||||
},
|
||||
} as unknown as Context;
|
||||
await route.handler(ctx);
|
||||
|
||||
expect(requestBody).toMatchObject({
|
||||
searchBy: 'all',
|
||||
granularity: 'CUSTOM',
|
||||
beginDateTime: Date.UTC(2009, 4, 1) - 8 * 60 * 60 * 1000,
|
||||
endDateTime: Date.UTC(2009, 5, 1) - 8 * 60 * 60 * 1000 - 1,
|
||||
advancedFilters: [
|
||||
{
|
||||
fieldName: 'containsAll',
|
||||
searchWord: ['医疗', '保障', '完整 短语'],
|
||||
},
|
||||
{
|
||||
fieldName: 'containsOne',
|
||||
searchWord: ['医保', '社保'],
|
||||
},
|
||||
{
|
||||
fieldName: 'none',
|
||||
searchWord: ['教育'],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('requests the latest items when advanced-search parameters are omitted', async () => {
|
||||
const { default: server } = await import('@/setup.test');
|
||||
let requestBody: any;
|
||||
|
||||
server.use(
|
||||
http.post(apiUrl, async ({ request }) => {
|
||||
requestBody = await request.json();
|
||||
|
||||
return HttpResponse.json({
|
||||
resultCode: {
|
||||
code: 200,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
middle: {
|
||||
list: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
const ctx = {
|
||||
req: {
|
||||
param: () => {},
|
||||
},
|
||||
} as unknown as Context;
|
||||
const result = (await route.handler(ctx)) as Data;
|
||||
|
||||
expect(requestBody).toMatchObject({
|
||||
allData: true,
|
||||
pageNo: 1,
|
||||
pageSize: 20,
|
||||
searchBy: 'all',
|
||||
});
|
||||
expect(requestBody).not.toHaveProperty('advancedFilters');
|
||||
expect(result.link).toContain('allData=true');
|
||||
});
|
||||
|
||||
it('uses API content when an article page cannot be fetched', async () => {
|
||||
const { default: server } = await import('@/setup.test');
|
||||
const unavailableArticleUrl = 'https://www.gov.cn/test/unavailable-article.htm';
|
||||
|
||||
server.use(
|
||||
http.post(apiUrl, () =>
|
||||
HttpResponse.json({
|
||||
resultCode: {
|
||||
code: 200,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
middle: {
|
||||
list: [
|
||||
{
|
||||
title: '<em>测试</em><br/>标题',
|
||||
url: unavailableArticleUrl,
|
||||
content: '接口正文',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
),
|
||||
http.get(unavailableArticleUrl, () => new HttpResponse(null, { status: 500 }))
|
||||
);
|
||||
|
||||
const ctx = {
|
||||
req: {
|
||||
param: () => {},
|
||||
},
|
||||
} as unknown as Context;
|
||||
const result = (await route.handler(ctx)) as Data;
|
||||
|
||||
expect(result.item).toEqual([
|
||||
{
|
||||
title: '测试 标题',
|
||||
link: unavailableArticleUrl,
|
||||
description: '接口正文',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('reports an actionable error when the search API fails', async () => {
|
||||
const { default: server } = await import('@/setup.test');
|
||||
|
||||
server.use(
|
||||
http.post(apiUrl, () =>
|
||||
HttpResponse.json({
|
||||
resultCode: {
|
||||
code: 1000,
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const ctx = {
|
||||
req: {
|
||||
param: () => {},
|
||||
},
|
||||
} as unknown as Context;
|
||||
|
||||
await expect(route.handler(ctx)).rejects.toThrow('中国政府网搜索接口请求失败,错误代码:1000');
|
||||
});
|
||||
});
|
||||
@@ -1,202 +0,0 @@
|
||||
import type { Context } from 'hono';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { route } from '@/routes/people';
|
||||
import type { Data } from '@/types';
|
||||
|
||||
const rootUrl = 'http://politics.people.com.cn';
|
||||
const currentUrl = `${rootUrl}/GB/1024`;
|
||||
const articleUrl = `${rootUrl}/n1/2026/0801/c1001-40771961.html`;
|
||||
|
||||
function createCtx(site: string, category = '') {
|
||||
return {
|
||||
req: {
|
||||
param: () => ({ site, category }),
|
||||
query: (name: string) => (name === 'limit' ? '1' : undefined),
|
||||
},
|
||||
} as unknown as Context;
|
||||
}
|
||||
|
||||
describe('GET /people/:site?/:category?', () => {
|
||||
it('uses a maintained politics page when the channel homepage is forbidden', async () => {
|
||||
const { default: server } = await import('@/setup.test');
|
||||
|
||||
server.use(
|
||||
http.get(`${rootUrl}/GB/`, () => HttpResponse.text('Forbidden', { status: 403 })),
|
||||
http.get(currentUrl, () =>
|
||||
HttpResponse.html(`
|
||||
<html>
|
||||
<head><meta charset="utf-8"><title>高层动态--时政--人民网</title></head>
|
||||
<body>
|
||||
<div class="jsnew_line">
|
||||
<a href="/n1/2026/0801/c1001-40771961.html">时政即时新闻</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
),
|
||||
http.get(articleUrl, () =>
|
||||
HttpResponse.html(`
|
||||
<html>
|
||||
<body>
|
||||
<b id="newstime">2026年08月01日10:30</b>
|
||||
<div id="rm_txt_zw"><p>时政正文</p></div>
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
)
|
||||
);
|
||||
|
||||
const feed = (await route.handler(createCtx('politics'))) as Data;
|
||||
|
||||
expect(feed.title).toBe('高层动态--时政--人民网');
|
||||
expect(feed.link).toBe(currentUrl);
|
||||
expect(feed.item).toHaveLength(1);
|
||||
expect(feed.item?.[0]).toMatchObject({
|
||||
title: '时政即时新闻',
|
||||
link: articleUrl,
|
||||
description: '<p>时政正文</p>',
|
||||
});
|
||||
expect(new Date(feed.item?.[0].pubDate ?? '').toISOString()).toBe('2026-08-01T02:30:00.000Z');
|
||||
});
|
||||
|
||||
it('maps the retired society category to the current channel homepage', async () => {
|
||||
const { default: server } = await import('@/setup.test');
|
||||
const societyRootUrl = 'http://society.people.com.cn/';
|
||||
const societyArticleUrl = `${societyRootUrl}n1/2026/0803/c1008-40772964.html`;
|
||||
|
||||
server.use(
|
||||
http.get(`${societyRootUrl}GB/1008`, () => HttpResponse.text('Retired category must not be used', { status: 500 })),
|
||||
http.get(societyRootUrl, () =>
|
||||
HttpResponse.html(`
|
||||
<html>
|
||||
<head><meta charset="utf-8"><title>社会·法治--人民网</title></head>
|
||||
<body>
|
||||
<div class="jsnew_line">
|
||||
<a href="/n1/2026/0803/c1008-40772964.html">社会即时新闻</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
),
|
||||
http.get(societyArticleUrl, () =>
|
||||
HttpResponse.html(`
|
||||
<html>
|
||||
<body>
|
||||
<b id="newstime">2026年08月03日09:15</b>
|
||||
<div class="rm_txt_con"><p>社会正文</p></div>
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
)
|
||||
);
|
||||
|
||||
const feed = (await route.handler(createCtx('society', '1008'))) as Data;
|
||||
|
||||
expect(feed.title).toBe('社会·法治--人民网');
|
||||
expect(feed.link).toBe(societyRootUrl);
|
||||
expect(feed.item).toHaveLength(1);
|
||||
expect(feed.item?.[0]).toMatchObject({
|
||||
title: '社会即时新闻',
|
||||
link: societyArticleUrl,
|
||||
description: '<p>社会正文</p>',
|
||||
});
|
||||
expect(new Date(feed.item?.[0].pubDate ?? '').toISOString()).toBe('2026-08-03T01:15:00.000Z');
|
||||
});
|
||||
|
||||
it('follows an official channel migration and resolves article links against its destination', async () => {
|
||||
const { default: server } = await import('@/setup.test');
|
||||
const legalRootUrl = 'http://legal.people.com.cn/';
|
||||
const societyRootUrl = 'http://society.people.com.cn/';
|
||||
const articleUrl = `${societyRootUrl}n1/2026/0803/c1008-40772812.html`;
|
||||
|
||||
server.use(
|
||||
http.get(`${legalRootUrl}GB/`, () => HttpResponse.text('Legacy path must not be used', { status: 500 })),
|
||||
http.get(legalRootUrl, () =>
|
||||
HttpResponse.html(`
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="refresh" content="0;url=${societyRootUrl}">
|
||||
</head>
|
||||
</html>
|
||||
`)
|
||||
),
|
||||
http.get(societyRootUrl, () =>
|
||||
HttpResponse.html(`
|
||||
<html>
|
||||
<head><meta charset="utf-8"><title>社会·法治--人民网</title></head>
|
||||
<body>
|
||||
<div class="jsnew_line">
|
||||
<a href="/n1/2026/0803/c1008-40772812.html">法治即时新闻</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
),
|
||||
http.get(articleUrl, () =>
|
||||
HttpResponse.html(`
|
||||
<html>
|
||||
<body>
|
||||
<b id="newstime">2026年08月03日10:20</b>
|
||||
<div id="rm_txt_zw"><p>法治正文</p></div>
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
)
|
||||
);
|
||||
|
||||
const feed = (await route.handler(createCtx('legal'))) as Data;
|
||||
|
||||
expect(feed.title).toBe('社会·法治--人民网');
|
||||
expect(feed.link).toBe(societyRootUrl);
|
||||
expect(feed.item).toHaveLength(1);
|
||||
expect(feed.item?.[0]).toMatchObject({
|
||||
title: '法治即时新闻',
|
||||
link: articleUrl,
|
||||
description: '<p>法治正文</p>',
|
||||
});
|
||||
expect(new Date(feed.item?.[0].pubDate ?? '').toISOString()).toBe('2026-08-03T02:20:00.000Z');
|
||||
});
|
||||
|
||||
it.each(['ftp://legal.people.com.cn/file', 'http://['])('ignores an unsafe or malformed channel migration to %s', async (redirectTarget) => {
|
||||
const { default: server } = await import('@/setup.test');
|
||||
const legalRootUrl = 'http://legal.people.com.cn/';
|
||||
const legalArticleUrl = `${legalRootUrl}n1/2026/0803/c1008-40772809.html`;
|
||||
|
||||
server.use(
|
||||
http.get(legalRootUrl, () =>
|
||||
HttpResponse.html(`
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="refresh" content="0;url=${redirectTarget}">
|
||||
<title>法治测试页--人民网</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="jsnew_line">
|
||||
<a href="/n1/2026/0803/c1008-40772809.html">法治测试新闻</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
),
|
||||
http.get(legalArticleUrl, () =>
|
||||
HttpResponse.html(`
|
||||
<b id="newstime">2026年08月03日10:30</b>
|
||||
<div id="rm_txt_zw"><p>法治测试正文</p></div>
|
||||
`)
|
||||
)
|
||||
);
|
||||
|
||||
const feed = (await route.handler(createCtx('legal'))) as Data;
|
||||
|
||||
expect(feed.title).toBe('法治测试页--人民网');
|
||||
expect(feed.link).toBe(legalRootUrl);
|
||||
expect(feed.item?.[0]).toMatchObject({
|
||||
title: '法治测试新闻',
|
||||
link: legalArticleUrl,
|
||||
description: '<p>法治测试正文</p>',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,75 +0,0 @@
|
||||
import type { Context } from 'hono';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { route } from '@/routes/people';
|
||||
import type { Data } from '@/types';
|
||||
|
||||
const rootUrl = 'http://cpc.people.com.cn';
|
||||
const currentUrl = `${rootUrl}/GB/64093/64387`;
|
||||
const firstArticleUrl = `${rootUrl}/n1/2026/0803/c64387-40772759.html`;
|
||||
const secondArticleUrl = `${rootUrl}/n1/2026/0803/c64387-40772755.html`;
|
||||
|
||||
function createCtx(limit?: string) {
|
||||
return {
|
||||
req: {
|
||||
param: () => ({ site: 'cpc', category: '24h' }),
|
||||
query: (name: string) => (name === 'limit' ? limit : undefined),
|
||||
},
|
||||
} as unknown as Context;
|
||||
}
|
||||
|
||||
describe('GET /people/cpc/24h', () => {
|
||||
it('uses the maintained CPC news list and modern article content selector', async () => {
|
||||
const { default: server } = await import('@/setup.test');
|
||||
const secondDetailRequest = vi.fn();
|
||||
|
||||
server.use(
|
||||
http.get(currentUrl, () =>
|
||||
HttpResponse.html(`
|
||||
<html>
|
||||
<head><meta charset="utf-8"><title>综合报道</title></head>
|
||||
<body>
|
||||
<div class="p2j_con02">
|
||||
<div class="fl">
|
||||
<ul>
|
||||
<li><a href="/n1/2026/0803/c64387-40772759.html">第一条新闻</a></li>
|
||||
<li><a href="/n1/2026/0803/c64387-40772755.html">第二条新闻</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
),
|
||||
http.get(`${rootUrl}/GB/87228`, () => HttpResponse.text('Archived page must not be used', { status: 500 })),
|
||||
http.get(firstArticleUrl, () =>
|
||||
HttpResponse.html(`
|
||||
<html>
|
||||
<body>
|
||||
<b id="newstime">2026年08月03日08:15</b>
|
||||
<div id="rm_txt_zw"><p>正文一</p></div>
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
),
|
||||
http.get(secondArticleUrl, () => {
|
||||
secondDetailRequest();
|
||||
return HttpResponse.html('<div id="rm_txt_zw"><p>正文二</p></div>');
|
||||
})
|
||||
);
|
||||
|
||||
const feed = (await route.handler(createCtx('1'))) as Data;
|
||||
|
||||
expect(feed.title).toBe('综合报道');
|
||||
expect(feed.link).toBe(currentUrl);
|
||||
expect(feed.item).toHaveLength(1);
|
||||
expect(feed.item?.[0]).toMatchObject({
|
||||
title: '第一条新闻',
|
||||
link: firstArticleUrl,
|
||||
description: '<p>正文一</p>',
|
||||
});
|
||||
expect(new Date(feed.item?.[0].pubDate ?? '').toISOString()).toBe('2026-08-03T00:15:00.000Z');
|
||||
expect(secondDetailRequest).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,71 +0,0 @@
|
||||
import type { Context } from 'hono';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { route } from '@/routes/people';
|
||||
import type { Data } from '@/types';
|
||||
|
||||
const rootUrl = 'http://edu.people.com.cn';
|
||||
const currentUrl = `${rootUrl}/`;
|
||||
const firstArticleUrl = `${rootUrl}/n1/2026/0803/c1006-40772728.html`;
|
||||
const secondArticleUrl = `${rootUrl}/n1/2026/0803/c1006-40772725.html`;
|
||||
|
||||
function createCtx(limit?: string) {
|
||||
return {
|
||||
req: {
|
||||
param: () => ({ site: 'edu', category: '' }),
|
||||
query: (name: string) => (name === 'limit' ? limit : undefined),
|
||||
},
|
||||
} as unknown as Context;
|
||||
}
|
||||
|
||||
describe('GET /people/edu', () => {
|
||||
it('extracts the current education news list and applies limit before details', async () => {
|
||||
const { default: server } = await import('@/setup.test');
|
||||
const secondDetailRequest = vi.fn();
|
||||
|
||||
server.use(
|
||||
http.get(`${rootUrl}/GB/`, () => HttpResponse.text('Legacy path must not be used', { status: 500 })),
|
||||
http.get(currentUrl, () =>
|
||||
HttpResponse.html(`
|
||||
<html>
|
||||
<head><meta charset="utf-8"><title>教育--人民网</title></head>
|
||||
<body>
|
||||
<div class="jsnew_line">
|
||||
<a href="/n1/2026/0803/c1006-40772728.html">第一条教育新闻</a>
|
||||
<a href="/n1/2026/0803/c1006-40772725.html">第二条教育新闻</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
),
|
||||
http.get(firstArticleUrl, () =>
|
||||
HttpResponse.html(`
|
||||
<html>
|
||||
<body>
|
||||
<b id="newstime">2026年08月03日08:15</b>
|
||||
<div id="rm_txt_zw"><p>教育正文</p></div>
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
),
|
||||
http.get(secondArticleUrl, () => {
|
||||
secondDetailRequest();
|
||||
return HttpResponse.html('<div id="rm_txt_zw"><p>第二篇正文</p></div>');
|
||||
})
|
||||
);
|
||||
|
||||
const feed = (await route.handler(createCtx('1'))) as Data;
|
||||
|
||||
expect(feed.title).toBe('教育--人民网');
|
||||
expect(feed.link).toBe(currentUrl);
|
||||
expect(feed.item).toHaveLength(1);
|
||||
expect(feed.item?.[0]).toMatchObject({
|
||||
title: '第一条教育新闻',
|
||||
link: firstArticleUrl,
|
||||
description: '<p>教育正文</p>',
|
||||
});
|
||||
expect(new Date(feed.item?.[0].pubDate ?? '').toISOString()).toBe('2026-08-03T00:15:00.000Z');
|
||||
expect(secondDetailRequest).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,141 +0,0 @@
|
||||
import type { Context } from 'hono';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import InvalidParameterError from '@/errors/types/invalid-parameter';
|
||||
import { route } from '@/routes/people/liuyan';
|
||||
import type { Data } from '@/types';
|
||||
|
||||
const rootUrl = 'https://liuyan.people.com.cn';
|
||||
const apiUrl = `${rootUrl}/threads/queryThreadsList`;
|
||||
const forumUrl = `${rootUrl}/threads/list?fid=539`;
|
||||
|
||||
const baseItem = {
|
||||
tid: 1001,
|
||||
subject: '留言标题',
|
||||
content: '留言正文 <script>alert(1)</script>\n第二行',
|
||||
nickName: '网友甲',
|
||||
dateline: 1_785_000_000,
|
||||
threadsCheckTime: 1_785_000_100,
|
||||
forumName: '北京市委书记',
|
||||
typeName: '建言',
|
||||
domainName: '交通',
|
||||
stateInfo: '办理中',
|
||||
answerContent: null,
|
||||
answerDateline: null,
|
||||
answerOrganization: null,
|
||||
};
|
||||
|
||||
function createCtx({ id, state, limit }: { id?: string; state?: string; limit?: string } = {}) {
|
||||
return {
|
||||
req: {
|
||||
param: (name: string) => ({ id, state })[name],
|
||||
query: (name: string) => (name === 'limit' ? limit : undefined),
|
||||
},
|
||||
} as unknown as Context;
|
||||
}
|
||||
|
||||
async function registerApiMock(responseData: Array<Record<string, unknown>>, success = true, expectedState = '1') {
|
||||
const { default: server } = await import('@/setup.test');
|
||||
const response = { result: success ? 'success' : 'error', responseData, success };
|
||||
const detailRequest = vi.fn();
|
||||
|
||||
server.use(
|
||||
http.post(apiUrl, async ({ request }) => {
|
||||
expect(request.headers.get('referer')).toBe(forumUrl);
|
||||
const form = await request.formData();
|
||||
expect(form.get('fid')).toBe('539');
|
||||
expect(form.get('state')).toBe(expectedState);
|
||||
expect(form.get('lastItem')).toBe('0');
|
||||
return new HttpResponse(JSON.stringify(response), {
|
||||
headers: { 'Content-Type': 'text/html;charset=UTF-8' },
|
||||
});
|
||||
}),
|
||||
http.post('http://liuyan.people.com.cn/threads/queryThreadsList', () => HttpResponse.json({}, { status: 500 })),
|
||||
http.get(`${rootUrl}/threads/content`, ({ request }) => {
|
||||
detailRequest(request.url);
|
||||
return HttpResponse.html('<div id="app"></div>');
|
||||
}),
|
||||
http.get('http://liuyan.people.com.cn/threads/content', ({ request }) => {
|
||||
detailRequest(request.url);
|
||||
return HttpResponse.html('<div id="app"></div>');
|
||||
})
|
||||
);
|
||||
|
||||
return detailRequest;
|
||||
}
|
||||
|
||||
describe('GET /people/liuyan/:id/:state?', () => {
|
||||
it('builds escaped feed items directly from the list API and applies limit', async () => {
|
||||
const detailRequest = await registerApiMock([
|
||||
baseItem,
|
||||
{
|
||||
...baseItem,
|
||||
tid: 1002,
|
||||
subject: '第二条留言',
|
||||
},
|
||||
]);
|
||||
|
||||
const feed = (await route.handler(createCtx({ id: '539', limit: '1' }))) as Data;
|
||||
|
||||
expect(feed.title).toBe('北京市委书记 - 领导留言板 - 人民网');
|
||||
expect(feed.link).toBe(`${forumUrl}#state=1`);
|
||||
expect(feed.item).toHaveLength(1);
|
||||
expect(feed.item?.[0]).toMatchObject({
|
||||
title: '留言标题',
|
||||
author: '网友甲',
|
||||
link: `${rootUrl}/threads/content?tid=1001`,
|
||||
category: ['北京市委书记', '建言', '交通', '办理中'],
|
||||
});
|
||||
expect(feed.item?.[0].description).toContain('留言正文 <script>alert(1)</script><br>第二行');
|
||||
expect(feed.item?.[0].description).not.toContain('<script>');
|
||||
expect(new Date(feed.item?.[0].pubDate ?? '').getTime()).toBe(baseItem.dateline * 1000);
|
||||
expect(detailRequest).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('passes a supported state to the API and feed link', async () => {
|
||||
await registerApiMock([baseItem], true, '3');
|
||||
|
||||
const feed = (await route.handler(createCtx({ id: '539', state: '3' }))) as Data;
|
||||
|
||||
expect(feed.link).toBe(`${forumUrl}#state=3`);
|
||||
});
|
||||
|
||||
it('includes an official answer when the API provides one', async () => {
|
||||
await registerApiMock([
|
||||
{
|
||||
...baseItem,
|
||||
answerContent: '回复内容\n下一行',
|
||||
answerDateline: 1_785_000_200,
|
||||
answerOrganization: '北京市交通委',
|
||||
},
|
||||
]);
|
||||
|
||||
const feed = (await route.handler(createCtx({ id: '539' }))) as Data;
|
||||
|
||||
expect(feed.item?.[0].description).toContain('<strong>北京市交通委</strong>');
|
||||
expect(feed.item?.[0].description).toContain('回复内容<br>下一行');
|
||||
expect(new Date(feed.item?.[0].updated ?? '').getTime()).toBe(1_785_000_200_000);
|
||||
});
|
||||
|
||||
it('requires a forum id instead of falling through to the generic People route', async () => {
|
||||
await expect(route.handler(createCtx())).rejects.toBeInstanceOf(InvalidParameterError);
|
||||
await expect(route.handler(createCtx())).rejects.toThrow('Forum id is required');
|
||||
});
|
||||
|
||||
it('rejects a non-numeric forum id', async () => {
|
||||
await expect(route.handler(createCtx({ id: 'invalid' }))).rejects.toBeInstanceOf(InvalidParameterError);
|
||||
await expect(route.handler(createCtx({ id: 'invalid' }))).rejects.toThrow('Invalid forum id');
|
||||
});
|
||||
|
||||
it('rejects an unsupported state', async () => {
|
||||
await expect(route.handler(createCtx({ id: '539', state: '9' }))).rejects.toBeInstanceOf(InvalidParameterError);
|
||||
await expect(route.handler(createCtx({ id: '539', state: '9' }))).rejects.toThrow('Invalid state');
|
||||
});
|
||||
|
||||
it('reports an unsuccessful upstream response clearly', async () => {
|
||||
await registerApiMock([], false);
|
||||
|
||||
await expect(route.handler(createCtx({ id: '539' }))).rejects.toThrow('Failed to fetch messages from the People’s Daily message board');
|
||||
});
|
||||
});
|
||||
@@ -1,171 +0,0 @@
|
||||
import type { Context } from 'hono';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import InvalidParameterError from '@/errors/types/invalid-parameter';
|
||||
import { route } from '@/routes/people/paper';
|
||||
import type { Data } from '@/types';
|
||||
import cache from '@/utils/cache';
|
||||
|
||||
const rootUrl = 'https://paper.people.com.cn/rmrb/pc/';
|
||||
const indexUrl = `${rootUrl}layout/index.html`;
|
||||
const pageOneUrl = `${rootUrl}layout/202608/03/node_01.html`;
|
||||
const pageTwoUrl = `${rootUrl}layout/202608/03/node_02.html`;
|
||||
const articleOneUrl = `${rootUrl}content/202608/03/content_1.html`;
|
||||
const articleTwoUrl = `${rootUrl}content/202608/03/content_2.html`;
|
||||
const articleThreeUrl = `${rootUrl}content/202608/03/content_3.html`;
|
||||
|
||||
const indexHtml = `
|
||||
<ul id="list">
|
||||
<li><a href="202608/03/node_01.html">第01版 要闻</a></li>
|
||||
<li><a href="202608/03/node_02.html">第02版 评论</a></li>
|
||||
</ul>
|
||||
`;
|
||||
|
||||
const pageOneHtml = `
|
||||
<ul class="news-list">
|
||||
<li><a href="../../../content/202608/03/content_1.html">列表标题一</a></li>
|
||||
<li><a href="../../../content/202608/03/content_2.html">列表标题二</a></li>
|
||||
</ul>
|
||||
`;
|
||||
|
||||
const pageTwoHtml = `
|
||||
<ul class="news-list">
|
||||
<li><a href="../../../content/202608/03/content_3.html">列表标题三</a></li>
|
||||
</ul>
|
||||
`;
|
||||
|
||||
function createArticleHtml(title: string, author: string, page: string) {
|
||||
return `
|
||||
<div class="article">
|
||||
<h1>${title}</h1>
|
||||
<p class="sec">
|
||||
${author}
|
||||
<span class="date">
|
||||
《人民日报》(<span class="newstime">2026年08月03日</span> 第 ${page} 版)
|
||||
</span>
|
||||
</p>
|
||||
<div id="ozoom">
|
||||
<p>
|
||||
正文 ${title}
|
||||
<img src="../../../pic/202608/03/image.jpg">
|
||||
<a href="../../../content/202608/03/source.html">相关链接</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function createCtx(page?: string, limit?: string) {
|
||||
return {
|
||||
req: {
|
||||
param: (name: string) => (name === 'page' ? page : undefined),
|
||||
query: (name: string) => (name === 'limit' ? limit : undefined),
|
||||
},
|
||||
} as unknown as Context;
|
||||
}
|
||||
|
||||
describe('GET /people/paper', () => {
|
||||
beforeEach(() => cache.clients.memoryCache?.clear());
|
||||
|
||||
it.each([
|
||||
['the default route', undefined],
|
||||
['the explicit all route', 'all'],
|
||||
])('aggregates all pages and applies limit before fetching article details for %s', async (_, page) => {
|
||||
const { default: server } = await import('@/setup.test');
|
||||
const articleThreeHandler = vi.fn(() => HttpResponse.html(createArticleHtml('正文标题三', '记者三', '02')));
|
||||
|
||||
server.use(
|
||||
http.get(indexUrl, () => HttpResponse.html(indexHtml)),
|
||||
http.get(pageOneUrl, () => HttpResponse.html(pageOneHtml)),
|
||||
http.get(pageTwoUrl, () => HttpResponse.html(pageTwoHtml)),
|
||||
http.get(articleOneUrl, () => HttpResponse.html(createArticleHtml('正文标题一', '记者一', '01'))),
|
||||
http.get(articleTwoUrl, () => HttpResponse.html(createArticleHtml('正文标题二', '记者二', '01'))),
|
||||
http.get(articleThreeUrl, articleThreeHandler)
|
||||
);
|
||||
|
||||
const feed = (await route.handler(createCtx(page, '2'))) as Data;
|
||||
|
||||
expect(feed.title).toBe('人民日报电子版 - 2026年08月03日');
|
||||
expect(feed.link).toBe(indexUrl);
|
||||
expect(feed.item).toHaveLength(2);
|
||||
expect(feed.item?.[0]).toMatchObject({
|
||||
title: '正文标题一',
|
||||
link: articleOneUrl,
|
||||
author: '记者一',
|
||||
category: ['第01版 要闻'],
|
||||
});
|
||||
expect(new Date(feed.item?.[0].pubDate ?? '').getFullYear()).toBe(2026);
|
||||
expect(feed.item?.[0].description).toContain('正文 正文标题一');
|
||||
expect(feed.item?.[0].description).toContain(`${rootUrl}pic/202608/03/image.jpg`);
|
||||
expect(feed.item?.[0].description).toContain(`${rootUrl}content/202608/03/source.html`);
|
||||
expect(articleThreeHandler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('fetches only the requested page', async () => {
|
||||
const { default: server } = await import('@/setup.test');
|
||||
const pageOneHandler = vi.fn(() => HttpResponse.html(pageOneHtml));
|
||||
|
||||
server.use(
|
||||
http.get(indexUrl, () => HttpResponse.html(indexHtml)),
|
||||
http.get(pageOneUrl, pageOneHandler),
|
||||
http.get(pageTwoUrl, () => HttpResponse.html(pageTwoHtml)),
|
||||
http.get(articleThreeUrl, () => HttpResponse.html(createArticleHtml('正文标题三', '记者三', '02')))
|
||||
);
|
||||
|
||||
const feed = (await route.handler(createCtx('02', '3'))) as Data;
|
||||
|
||||
expect(feed.title).toBe('人民日报电子版 - 第02版 评论 - 2026年08月03日');
|
||||
expect(feed.item).toHaveLength(1);
|
||||
expect(feed.item?.[0]).toMatchObject({
|
||||
title: '正文标题三',
|
||||
link: articleThreeUrl,
|
||||
category: ['第02版 评论'],
|
||||
});
|
||||
expect(pageOneHandler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('limits the default route to 30 articles before fetching details', async () => {
|
||||
const { default: server } = await import('@/setup.test');
|
||||
const articleLinks = Array.from({ length: 31 }, (_, index) => `<li><a href="../../../content/202608/03/default_${index + 1}.html">文章 ${index + 1}</a></li>`).join('');
|
||||
const detailHandler = vi.fn(({ request }: { request: Request }) => HttpResponse.html(createArticleHtml(new URL(request.url).pathname, '记者', '01')));
|
||||
|
||||
server.use(
|
||||
http.get(indexUrl, () => HttpResponse.html('<ul id="list"><li><a href="202608/03/node_01.html">第01版 要闻</a></li></ul>')),
|
||||
http.get(pageOneUrl, () => HttpResponse.html(`<ul class="news-list">${articleLinks}</ul>`)),
|
||||
http.get(new RegExp(`${rootUrl}content/202608/03/default_\\d+\\.html`), detailHandler)
|
||||
);
|
||||
|
||||
const feed = (await route.handler(createCtx())) as Data;
|
||||
|
||||
expect(feed.item).toHaveLength(30);
|
||||
expect(detailHandler).toHaveBeenCalledTimes(30);
|
||||
});
|
||||
|
||||
it('keeps list metadata when one article detail request fails', async () => {
|
||||
const { default: server } = await import('@/setup.test');
|
||||
server.use(
|
||||
http.get(indexUrl, () => HttpResponse.html('<ul id="list"><li><a href="202608/03/node_01.html">第01版 要闻</a></li></ul>')),
|
||||
http.get(pageOneUrl, () => HttpResponse.html('<ul class="news-list"><li><a href="../../../content/202608/03/content_1.html">列表标题一</a></li></ul>')),
|
||||
http.get(articleOneUrl, () => HttpResponse.text('upstream failure', { status: 500 }))
|
||||
);
|
||||
|
||||
const feed = (await route.handler(createCtx())) as Data;
|
||||
|
||||
expect(feed.item).toHaveLength(1);
|
||||
expect(feed.item?.[0]).toMatchObject({
|
||||
title: '列表标题一',
|
||||
link: articleOneUrl,
|
||||
category: ['第01版 要闻'],
|
||||
});
|
||||
expect(feed.item?.[0].description).toBeUndefined();
|
||||
});
|
||||
|
||||
it('rejects a page that is not present in the current edition', async () => {
|
||||
const { default: server } = await import('@/setup.test');
|
||||
server.use(http.get(indexUrl, () => HttpResponse.html(indexHtml)));
|
||||
|
||||
await expect(route.handler(createCtx('99'))).rejects.toBeInstanceOf(InvalidParameterError);
|
||||
await expect(route.handler(createCtx('99'))).rejects.toThrow('Invalid page');
|
||||
});
|
||||
});
|
||||
@@ -1,138 +0,0 @@
|
||||
import type { Context } from 'hono';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import InvalidParameterError from '@/errors/types/invalid-parameter';
|
||||
import { route } from '@/routes/people/xjpjh';
|
||||
import type { Data } from '@/types';
|
||||
import cache from '@/utils/cache';
|
||||
|
||||
const rootUrl = 'http://jhsjk.people.cn';
|
||||
const defaultResultUrl = `${rootUrl}/result?keywords=&year=0`;
|
||||
const firstArticleUrl = `${rootUrl}/article/40772030`;
|
||||
const secondArticleUrl = `${rootUrl}/article/40772028`;
|
||||
const thirdArticleUrl = `${rootUrl}/article/40772029`;
|
||||
|
||||
function createCtx({ keyword, year, limit }: { keyword?: string; year?: string; limit?: string } = {}) {
|
||||
return {
|
||||
req: {
|
||||
param: (name: string) => ({ keyword, year })[name],
|
||||
query: (name: string) => (name === 'limit' ? limit : undefined),
|
||||
},
|
||||
} as unknown as Context;
|
||||
}
|
||||
|
||||
function createResultHtml() {
|
||||
return `
|
||||
<ul class="list_14 p1_2 clearfix" id="news_list">
|
||||
<li><a href="article/40772030">第一篇讲话</a><span>[2026-08-01]</span></li>
|
||||
<li><p>第一篇摘要</p></li>
|
||||
<li><a href="article/40772028">第二篇讲话</a><span>[2026-07-31]</span></li>
|
||||
<li><p>第二篇摘要</p></li>
|
||||
<li><a href="article/40772029">第三篇讲话</a><span>[2026-07-30]</span></li>
|
||||
</ul>
|
||||
`;
|
||||
}
|
||||
|
||||
function createArticleHtml(content: string, date: string) {
|
||||
return `
|
||||
<div class="d2txt_1 clearfix">来源:人民网 发布时间:${date}</div>
|
||||
<div class="d2txt_con clearfix"><p>${content}</p></div>
|
||||
`;
|
||||
}
|
||||
|
||||
describe('GET /people/xjpjh/:keyword?/:year?', () => {
|
||||
beforeEach(() => cache.clients.memoryCache?.clear());
|
||||
|
||||
it('selects only linked results and applies limit before fetching details', async () => {
|
||||
const { default: server } = await import('@/setup.test');
|
||||
const thirdDetailRequest = vi.fn();
|
||||
|
||||
server.use(
|
||||
http.get(`${rootUrl}/result`, ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
expect(url.searchParams.get('keywords')).toBe('');
|
||||
expect(url.searchParams.get('year')).toBe('0');
|
||||
return HttpResponse.html(createResultHtml());
|
||||
}),
|
||||
http.get(`${rootUrl}/undefined`, () => HttpResponse.text('Summary rows must not be fetched', { status: 500 })),
|
||||
http.get(firstArticleUrl, () => HttpResponse.html(createArticleHtml('第一篇正文', '2026-08-01'))),
|
||||
http.get(secondArticleUrl, () => HttpResponse.html(createArticleHtml('第二篇正文', '2026-07-31'))),
|
||||
http.get(thirdArticleUrl, () => {
|
||||
thirdDetailRequest();
|
||||
return HttpResponse.html(createArticleHtml('第三篇正文', '2026-07-30'));
|
||||
})
|
||||
);
|
||||
|
||||
const feed = (await route.handler(createCtx({ limit: '2' }))) as Data;
|
||||
|
||||
expect(feed.title).toBe('习近平系列重要讲话-all-all');
|
||||
expect(feed.link).toBe(defaultResultUrl);
|
||||
expect(feed.item).toHaveLength(2);
|
||||
expect(feed.item?.[0]).toMatchObject({
|
||||
title: '第一篇讲话',
|
||||
link: firstArticleUrl,
|
||||
description: '<p>第一篇正文</p>',
|
||||
});
|
||||
expect(new Date(feed.item?.[0].pubDate ?? '').toISOString()).toBe('2026-07-31T16:00:00.000Z');
|
||||
expect(feed.item?.[1]).toMatchObject({
|
||||
title: '第二篇讲话',
|
||||
link: secondArticleUrl,
|
||||
description: '<p>第二篇正文</p>',
|
||||
});
|
||||
expect(thirdDetailRequest).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('passes keyword and calendar year directly to the current search page', async () => {
|
||||
const { default: server } = await import('@/setup.test');
|
||||
const resultUrl = `${rootUrl}/result?keywords=%E7%BB%8F%E6%B5%8E&year=2026`;
|
||||
|
||||
server.use(
|
||||
http.get(`${rootUrl}/result`, ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
expect(url.searchParams.get('keywords')).toBe('经济');
|
||||
expect(url.searchParams.get('year')).toBe('2026');
|
||||
return HttpResponse.html('<ul id="news_list"><li><a href="article/40772030">经济讲话</a></li></ul>');
|
||||
}),
|
||||
http.get(firstArticleUrl, () => HttpResponse.html(createArticleHtml('经济正文', '2026-08-01')))
|
||||
);
|
||||
|
||||
const feed = (await route.handler(createCtx({ keyword: '经济', year: '2026', limit: '1' }))) as Data;
|
||||
|
||||
expect(feed.title).toBe('习近平系列重要讲话-经济-2026');
|
||||
expect(feed.link).toBe(resultUrl);
|
||||
expect(feed.item?.[0]).toMatchObject({
|
||||
title: '经济讲话',
|
||||
link: firstArticleUrl,
|
||||
description: '<p>经济正文</p>',
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
['a negative', '-1'],
|
||||
['a non-numeric', 'invalid'],
|
||||
['an excessive', '999'],
|
||||
])('keeps detail requests within the previous maximum for %s limit', async (_, limit) => {
|
||||
const { default: server } = await import('@/setup.test');
|
||||
const detailRequest = vi.fn();
|
||||
const links = Array.from({ length: 11 }, (__, index) => `<li><a href="article/${index + 1}">讲话 ${index + 1}</a></li>`).join('');
|
||||
|
||||
server.use(
|
||||
http.get(`${rootUrl}/result`, () => HttpResponse.html(`<ul id="news_list">${links}</ul>`)),
|
||||
http.get(new RegExp(`${rootUrl}/article/\\d+`), () => {
|
||||
detailRequest();
|
||||
return HttpResponse.html(createArticleHtml('讲话正文', '2026-08-01'));
|
||||
})
|
||||
);
|
||||
|
||||
const feed = (await route.handler(createCtx({ limit }))) as Data;
|
||||
|
||||
expect(feed.item).toHaveLength(10);
|
||||
expect(detailRequest).toHaveBeenCalledTimes(10);
|
||||
});
|
||||
|
||||
it('rejects an invalid year', async () => {
|
||||
await expect(route.handler(createCtx({ keyword: 'all', year: 'invalid' }))).rejects.toBeInstanceOf(InvalidParameterError);
|
||||
await expect(route.handler(createCtx({ keyword: 'all', year: 'invalid' }))).rejects.toThrow('Invalid year');
|
||||
});
|
||||
});
|
||||
@@ -21,7 +21,7 @@ export const findOrphanFiles = async (): Promise<string[]> => {
|
||||
const entries = await fs.readdir(path.join(repoRoot, 'lib'), { recursive: true, withFileTypes: true });
|
||||
const candidates = entries
|
||||
.filter((entry) => excludedDirs.every((dir) => !`${entry.parentPath}${path.sep}`.startsWith(dir)))
|
||||
.filter((entry) => entry.isFile() && /\.test\.tsx?$/.test(entry.name))
|
||||
.filter((entry) => entry.isFile() && /\.(?:spec|test)\.tsx?$/.test(entry.name))
|
||||
.map((entry) => {
|
||||
const absolute = path.join(entry.parentPath, entry.name);
|
||||
return { absolute, relative: path.relative(repoRoot, absolute).replaceAll('\\', '/') };
|
||||
@@ -30,7 +30,7 @@ export const findOrphanFiles = async (): Promise<string[]> => {
|
||||
|
||||
const orphans = await Promise.all(
|
||||
candidates.map(async ({ absolute, relative }) => {
|
||||
const base = absolute.replace(/\.test\.tsx?$/, '');
|
||||
const base = absolute.replace(/\.(?:spec|test)\.tsx?$/, '');
|
||||
const [tsExists, tsxExists] = await Promise.all([fileExists(`${base}.ts`), fileExists(`${base}.tsx`)]);
|
||||
return tsExists || tsxExists ? null : relative;
|
||||
})
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import remarkParse from 'remark-parse';
|
||||
import { unified } from 'unified';
|
||||
|
||||
@@ -6,7 +7,8 @@ const criticalFailure = 'auto: DO NOT merge';
|
||||
const routeTestFailed = 'auto: not ready to review';
|
||||
const allowedUser = new Set(['dependabot[bot]', 'pull[bot]']); // dependabot and downstream PR requested by pull[bot]
|
||||
const requiredHeadings = ['Involved Issue / 该 PR 相关 Issue', 'Example for the Proposed Route(s) / 路由地址示例', 'New RSS Route Checklist / 新 RSS 路由检查表', 'Note / 说明'];
|
||||
const routeHeading = 'Example for the Proposed Route(s) / 路由地址示例';
|
||||
const routeHeading = requiredHeadings[1];
|
||||
const checklistHeading = requiredHeadings[2];
|
||||
const requiredHeadingDepth = 2;
|
||||
|
||||
/** @type {boolean} */
|
||||
@@ -141,7 +143,7 @@ export default async function identify({ github, context, core }, body, number,
|
||||
let routes;
|
||||
|
||||
if (body) {
|
||||
const ast = unified().use(remarkParse).parse(body);
|
||||
const ast = unified().use(remarkParse).use(remarkGfm).parse(body);
|
||||
|
||||
let searchStart = 0;
|
||||
let searchEnd = ast.children.length;
|
||||
@@ -150,8 +152,15 @@ export default async function identify({ github, context, core }, body, number,
|
||||
/** @param {string} text */
|
||||
const findHeading = (text) => ast.children.findIndex((node) => node.type === 'heading' && node.depth === requiredHeadingDepth && node.children?.some((child) => child.type === 'text' && child.value.trim() === text));
|
||||
|
||||
/** @param {string} text */
|
||||
const missingChecklist = (text) => {
|
||||
const headingIndex = findHeading(text);
|
||||
const nextHeading = ast.children.findIndex((node, i) => i > headingIndex && node.type === 'heading');
|
||||
return ast.children.slice(headingIndex + 1, nextHeading === -1 ? undefined : nextHeading).every((node) => node.type !== 'list' || !node.children?.some((item) => typeof item.checked === 'boolean'));
|
||||
};
|
||||
|
||||
const missingHeadings = requiredHeadings.filter((text) => findHeading(text) === -1);
|
||||
if (missingHeadings.length > 0) {
|
||||
if (missingHeadings.length > 0 || missingChecklist(checklistHeading)) {
|
||||
searchStart = -1; // skip search
|
||||
} else {
|
||||
const headingIndex = findHeading(routeHeading);
|
||||
|
||||
Reference in New Issue
Block a user