项目文件夹

文件
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

192 行
8.9 KiB
JavaScript

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
/**
* Unit tests for the 懂车帝 (Dongchedi) adapter.
*
* Every command parses `__NEXT_DATA__` JSON from an SSR page, so the pure
* parsers are exercised against frozen real-data fixtures captured from
* dongchedi.com (series 5273 = 宝马X5). No network, no browser.
*/
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import { getRegistry, Strategy } from '@jackwener/opencli/registry';
import {
SEARCH_COLUMNS,
SERIES_COLUMNS,
MODELS_COLUMNS,
SPECS_COLUMNS,
SCORE_COLUMNS,
KOUBEI_COLUMNS,
extractPageProps,
isFallbackShell,
parseScore,
normalizeSeriesId,
requireLimit,
clean,
snippet,
} from './utils.js';
import { parseSearchRows } from './search.js';
import { parseSeries } from './series.js';
import { parseModels } from './models.js';
import { parseSpecs } from './specs.js';
import { parseScoreBreakdown } from './score.js';
import { parseKoubei } from './koubei.js';
const __dirname = dirname(fileURLToPath(import.meta.url));
const fx = (name) => JSON.parse(readFileSync(join(__dirname, '__fixtures__', name), 'utf8'));
const SEARCH = fx('search.json');
const DETAIL = fx('series-detail.json');
const SCORE = fx('series-score.json');
describe('dongchedi adapter — registration', () => {
const names = ['search', 'series', 'models', 'specs', 'score', 'koubei'];
it('registers all commands as PUBLIC (no browser)', () => {
for (const n of names) {
const cmd = getRegistry().get(`dongchedi/${n}`);
expect(cmd, n).toBeTruthy();
expect(cmd.strategy, n).toBe(Strategy.PUBLIC);
expect(cmd.browser, n).toBe(false);
expect(cmd.access, n).toBe('read');
}
});
it('declares the expected columns', () => {
expect(getRegistry().get('dongchedi/search').columns).toEqual(SEARCH_COLUMNS);
expect(getRegistry().get('dongchedi/series').columns).toEqual(SERIES_COLUMNS);
expect(getRegistry().get('dongchedi/models').columns).toEqual(MODELS_COLUMNS);
expect(getRegistry().get('dongchedi/specs').columns).toEqual(SPECS_COLUMNS);
expect(getRegistry().get('dongchedi/score').columns).toEqual(SCORE_COLUMNS);
expect(getRegistry().get('dongchedi/koubei').columns).toEqual(KOUBEI_COLUMNS);
});
});
describe('dongchedi adapter — utils', () => {
it('extractPageProps pulls pageProps from a __NEXT_DATA__ blob', () => {
const html = `<html><body><script id="__NEXT_DATA__" type="application/json">${JSON.stringify({ props: { pageProps: { hello: 'world' } } })}</script></body></html>`;
expect(extractPageProps(html)).toEqual({ hello: 'world' });
});
it('extractPageProps returns null on missing/invalid blob', () => {
expect(extractPageProps('<html>no data</html>')).toBeNull();
expect(extractPageProps('<script id="__NEXT_DATA__">{not json}</script>')).toBeNull();
});
it('isFallbackShell flags the empty city-gateway shell', () => {
expect(isFallbackShell({ __hasUrlCity: true, is_gray: false, clientIp: '1.2.3.4' })).toBe(true);
expect(isFallbackShell({ seriesHomeHead: {} })).toBe(false);
expect(isFallbackShell(null)).toBe(true);
});
it('parseScore rescales x100 ints to /5 floats', () => {
expect(parseScore(422)).toBe(4.22);
expect(parseScore(500)).toBe(5);
expect(parseScore(0)).toBeNull();
expect(parseScore(undefined)).toBeNull();
});
it('normalizeSeriesId accepts numbers and URLs, rejects junk', () => {
expect(normalizeSeriesId('5273')).toBe('5273');
expect(normalizeSeriesId('https://www.dongchedi.com/auto/series/5273')).toBe('5273');
expect(() => normalizeSeriesId('宝马X5')).toThrow();
expect(() => normalizeSeriesId('')).toThrow();
});
it('requireLimit enforces [1,max]', () => {
expect(requireLimit(undefined, 10, 30)).toBe(10);
expect(requireLimit(5, 10, 30)).toBe(5);
expect(() => requireLimit(0, 10, 30)).toThrow();
expect(() => requireLimit(31, 10, 30)).toThrow();
});
it('clean/snippet normalize whitespace and truncate', () => {
expect(clean(' a\n b ')).toBe('a b');
expect(snippet('x'.repeat(200), 10)).toBe(`${'x'.repeat(10)}…`);
expect(snippet('short', 10)).toBe('short');
});
});
describe('dongchedi adapter — parsers against frozen fixtures', () => {
it('parseSearchRows keeps only car series and respects limit', () => {
const rows = parseSearchRows(SEARCH, 3);
expect(rows.length).toBeGreaterThan(0);
expect(rows.length).toBeLessThanOrEqual(3);
const r = rows[0];
expect(Object.keys(r).sort()).toEqual([...SEARCH_COLUMNS].sort());
expect(r.series_id).toBeTruthy();
expect(r.name).toBeTruthy();
expect(r.url).toContain('/auto/series/');
// every row must be a real series (no zero ids leaked in)
expect(rows.every((x) => x.series_id && x.series_id !== '0')).toBe(true);
});
it('parseSeries builds a complete field/value sheet', () => {
const rows = parseSeries(DETAIL, '5273');
const map = Object.fromEntries(rows.map((r) => [r.field, r.value]));
expect(rows.every((r) => Object.keys(r).sort().join() === 'field,value')).toBe(true);
expect(map.name).toBe('宝马X5');
expect(map.brand).toBe('宝马');
expect(map.official_price).toMatch(/万/);
expect(typeof map.score).toBe('number');
expect(map.score).toBeGreaterThan(0);
expect(map.score).toBeLessThanOrEqual(5);
expect(map.url).toContain('/auto/series/5273');
});
it('parseModels returns on-sale trims with real car_ids', () => {
const rows = parseModels(DETAIL.carModelsData, 'online');
expect(rows.length).toBeGreaterThan(0);
for (const r of rows) {
expect(Object.keys(r).sort()).toEqual([...MODELS_COLUMNS].sort());
expect(r.car_id).toMatch(/^\d+$/);
expect(r.name).toBeTruthy();
}
});
it('parseSpecs surfaces dimensions and powertrain', () => {
const rows = parseSpecs(DETAIL.overviewData, '5273');
const map = Object.fromEntries(rows.map((r) => [r.field, r.value]));
expect(rows.every((r) => Object.keys(r).sort().join() === 'field,value')).toBe(true);
expect(map.dimensions).toMatch(/\d+ × \d+ × \d+ mm/);
expect(map.wheelbase).toMatch(/mm/);
expect(map.power).toBeTruthy();
});
it('parseScoreBreakdown maps the 8 axes with same-level averages', () => {
const sameLevel = (DETAIL.reviewData.same_level_review || []).find((r) => String(r.series_id) === '0')
|| DETAIL.reviewData.average_series_review;
const rows = parseScoreBreakdown(DETAIL.scoreSimpleInfo, sameLevel);
expect(rows.length).toBe(8);
const overall = rows.find((r) => r.dimension === '综合');
expect(overall.score).toBeGreaterThan(0);
expect(overall.same_level_avg).toBeGreaterThan(0);
for (const r of rows) expect(Object.keys(r).sort()).toEqual([...SCORE_COLUMNS].sort());
});
it('parseKoubei returns owner reviews with body + url', () => {
const rows = parseKoubei(SCORE.reviewListData, 3);
expect(rows.length).toBeGreaterThan(0);
expect(rows.length).toBeLessThanOrEqual(3);
const r = rows[0];
expect(Object.keys(r).sort()).toEqual([...KOUBEI_COLUMNS].sort());
expect(r.user).toBeTruthy();
expect(r.content).toBeTruthy();
expect(typeof r.likes).toBe('number');
expect(r.url).toContain('/ugc/article/');
});
it('parsers fail closed on malformed source payloads', () => {
expect(() => parseSearchRows({}, 5)).toThrow(/unexpected payload shape/);
expect(() => parseSearchRows(null, 5)).toThrow(/unexpected payload shape/);
expect(() => parseModels({}, 'online')).toThrow(/unexpected payload shape/);
expect(() => parseKoubei({}, 5)).toThrow(/unexpected payload shape/);
expect(() => parseSpecs(null, '1')).toThrow(/unexpected payload shape/);
expect(() => parseSeries({}, '1')).toThrow(/unexpected payload shape/);
});
it('row parsers reject malformed identities and required text', () => {
expect(() => parseSearchRows({ data: [{ cell_type: 26, series_id: 0, display: { series_name: '坏行' } }] }, 5))
.toThrow(/stable numeric id/);
expect(() => parseSearchRows({ data: [{ cell_type: 26, series_id: 1, display: {} }] }, 5))
.toThrow(/stable text value/);
expect(() => parseModels({ tab_list: [{ tab_key: 'online_all', data: [{ info: { car_id: 1 } }] }] }, 'online'))
.toThrow(/stable text value/);
expect(() => parseKoubei({ review_list: [{ user_info: { name: 'u' }, content: 'body' }] }, 5))
.toThrow(/stable numeric id/);
});
});