Files
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

115 lines
5.1 KiB
JavaScript

import { JSDOM } from 'jsdom';
import { describe, expect, it, vi } from 'vitest';
import { ArgumentError, CommandExecutionError } from '@jackwener/opencli/errors';
import { smzdmSearchCommand, __test__ } from './search.js';
function runBrowserScript(html, script, url = 'https://search.smzdm.com/?c=home&s=test&v=b') {
const dom = new JSDOM(html, { url, runScripts: 'outside-only' });
return dom.window.eval(script);
}
describe('smzdm/search', () => {
it('declares read access and the enriched column set', () => {
expect(smzdmSearchCommand.access).toBe('read');
expect(smzdmSearchCommand.columns).toEqual([
'rank', 'title', 'price', 'mall', 'updated_at',
'zhi_count', 'buzhi_count', 'favorite_count', 'comments', 'url',
]);
});
it('extracts interaction metrics and update time from a search result item', () => {
const html = `<ul><li class="feed-row-wide">
<h5 class="feed-block-title"><a href="https://www.smzdm.com/p/174854494/" title="ThinkBook14+ 轻薄笔记本">ThinkBook14+ 轻薄笔记本</a></h5>
<span class="z-highlight">4015.44元(需用券)</span>
<div class="z-feed-foot-l">
<span class="feed-btn-group price-btn-hover">
<span class="J_zhi_like_fav price-btn-up" data-type="zhi" data-zhi-type="1"><span class="unvoted-wrap"><span>1.2万</span></span></span>
<span class="J_zhi_like_fav price-btn-down" data-type="zhi" data-zhi-type="-1"><span class="unvoted-wrap"><span>3</span></span></span>
</span>
<span class="J_zhi_like_fav z-group-data feed-btn-fav"><span>40</span></span>
<a class="z-group-data feed-btn-comment" title="评论数 24">24</a>
</div>
<div class="z-feed-foot-r"><span class="feed-block-extras">
05-23 00:28
<span>天猫精选</span>
</span></div>
</li></ul>`;
const rows = runBrowserScript(html, __test__.buildSmzdmSearchJs(20));
expect(rows).toEqual([
{
rank: 1,
title: 'ThinkBook14+ 轻薄笔记本',
price: '4015.44元(需用券)',
mall: '天猫精选',
updated_at: '05-23 00:28',
zhi_count: 12000,
buzhi_count: 3,
favorite_count: 40,
comments: 24,
url: 'https://www.smzdm.com/p/174854494/',
},
]);
});
it('defaults missing interaction metrics to 0 without dropping columns', () => {
const html = `<ul><li class="feed-row-wide">
<h5 class="feed-block-title"><a href="/p/1/" title="No-metrics deal">No-metrics deal</a></h5>
</li></ul>`;
const rows = runBrowserScript(html, __test__.buildSmzdmSearchJs(20));
expect(rows).toEqual([
{
rank: 1,
title: 'No-metrics deal',
price: '',
mall: '',
updated_at: '',
zhi_count: 0,
buzhi_count: 0,
favorite_count: 0,
comments: 0,
url: 'https://www.smzdm.com/p/1/',
},
]);
});
it('drops untrusted result URLs before output', () => {
const html = `<ul><li class="feed-row-wide">
<h5 class="feed-block-title"><a href="https://evil.example/p/1/" title="Bad deal">Bad deal</a></h5>
</li></ul>`;
const rows = runBrowserScript(html, __test__.buildSmzdmSearchJs(20));
expect(rows).toEqual([]);
});
it('respects the limit argument', () => {
const li = `<li class="feed-row-wide"><h5 class="feed-block-title"><a href="/p/9/" title="Deal">Deal</a></h5></li>`;
const rows = runBrowserScript(`<ul>${li.repeat(5)}</ul>`, __test__.buildSmzdmSearchJs(2));
expect(rows).toHaveLength(2);
expect(rows.map((r) => r.rank)).toEqual([1, 2]);
});
it('validates --limit before browser navigation', async () => {
const page = {
goto: vi.fn(),
evaluate: vi.fn(),
};
await expect(smzdmSearchCommand.func(page, { query: 'test', limit: 0 })).rejects.toBeInstanceOf(ArgumentError);
await expect(smzdmSearchCommand.func(page, { query: 'test', limit: 101 })).rejects.toBeInstanceOf(ArgumentError);
await expect(smzdmSearchCommand.func(page, { query: 'test', limit: '1e2' })).rejects.toBeInstanceOf(ArgumentError);
expect(page.goto).not.toHaveBeenCalled();
});
it('unwraps Browser Bridge evaluate envelopes', () => {
const rows = [{ rank: 1, title: 'Deal' }];
expect(__test__.requireSearchRows({ session: 'site:smzdm', data: rows })).toBe(rows);
});
it('fails closed on malformed extraction payloads', async () => {
expect(() => __test__.requireSearchRows({ ok: true })).toThrow(CommandExecutionError);
const page = {
goto: vi.fn(),
evaluate: vi.fn().mockResolvedValue({ ok: true }),
};
await expect(smzdmSearchCommand.func(page, { query: 'test', limit: 5 })).rejects.toBeInstanceOf(CommandExecutionError);
});
});