| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157 |
- import { test, beforeEach } from 'node:test';
- import assert from 'node:assert/strict';
- import { match, resolveUrl, navigate, subscribe, currentUrl, opportunityUrl, reportUrl, detailUrl, safeReturnTo, returnToRoute, setQuery, onLinkClick } from '../src/utils/router.js';
- let base, entries, cursor;
- test('详情的来源和证据链接保留筛选,替换上一种详情,并兼容旧链接', () => {
- const source = resolveUrl(detailUrl('source', '原文 1', '/reports/voc?ev=高&stage=S3&artifact=a1&page=2'));
- assert.equal(source.searchParams.get('source'), '原文 1');
- assert.equal(source.searchParams.get('ev'), '高');
- assert.equal(source.searchParams.get('artifact'), null);
- assert.equal(source.searchParams.get('page'), '2');
- const evidence = resolveUrl(detailUrl('evidence', 'ev:1', '/reports/voc?ev=高&source=s1', 'report1'));
- assert.equal(evidence.searchParams.get('ev'), '高');
- assert.equal(evidence.searchParams.get('evidence'), 'ev:1');
- assert.equal(evidence.searchParams.get('evidenceReport'), 'report1');
- assert.equal(evidence.searchParams.get('source'), null);
- const legacy = resolveUrl(detailUrl('', '', '/pool/o1?ev=old&report=r1&returnTo=%2Fmembers'));
- assert.equal(legacy.searchParams.get('ev'), null);
- assert.equal(legacy.searchParams.get('report'), null);
- assert.equal(legacy.searchParams.get('returnTo'), '/members');
- });
- beforeEach(() => {
- base = '/preview/';
- entries = [{ url: new URL('https://board.test/preview/pool?stage=S3&sort=latest'), state: null }];
- cursor = 0;
- globalThis.location = entries[0].url;
- globalThis.document = { querySelector: () => ({ getAttribute: () => base }) };
- globalThis.window = new EventTarget();
- window.scrollY = 350;
- globalThis.PopStateEvent = class extends Event {
- constructor(type, options) { super(type); this.state = options.state; }
- };
- globalThis.history = {
- get state() { return entries[cursor].state; },
- replaceState(state, _, url) {
- entries[cursor] = { state, url: url ? new URL(url, location.href) : location };
- globalThis.location = entries[cursor].url;
- },
- pushState(state, _, url) {
- entries.splice(++cursor, entries.length, { state, url: new URL(url, location.href) });
- globalThis.location = entries[cursor].url;
- },
- back() {
- if (!cursor) return;
- globalThis.location = entries[--cursor].url;
- window.dispatchEvent(new PopStateEvent('popstate', { state: history.state }));
- },
- };
- });
- test('机会卡深链接保留筛选、排序和关闭返回地址;报告携带回卡片地址', () => {
- const opp = opportunityUrl('机会 1');
- const url = resolveUrl(opp);
- assert.equal(url.pathname, '/preview/pool/%E6%9C%BA%E4%BC%9A%201');
- assert.equal(url.searchParams.get('stage'), 'S3');
- assert.equal(url.searchParams.get('sort'), 'latest');
- assert.equal(url.searchParams.get('returnTo'), '/pool?stage=S3&sort=latest');
- const report = resolveUrl(reportUrl('r 1', opp));
- assert.equal(report.searchParams.get('returnTo'), opp);
- assert.equal(match(url.pathname.slice('/preview'.length)).params.id, '机会 1');
- });
- test('来自其他页面的机会卡返回原页,不将来源页参数误用为卡池筛选', () => {
- const url = resolveUrl(opportunityUrl('o1', '/members?member=小智'));
- assert.equal(url.searchParams.get('member'), null);
- assert.equal(url.searchParams.get('returnTo'), '/members?member=小智');
- });
- test('推入历史保存离开页的位置,换卡 replace 保留返回凭据且不多压一层', () => {
- const back = currentUrl();
- navigate(opportunityUrl('o1'), { state: { kfOverlay: { returnTo: back } } });
- assert.equal(entries[0].state.kfScrollY, 350);
- assert.equal(history.state.kfFrom, back);
- navigate(opportunityUrl('o2', back), { replace: true });
- assert.equal(entries.length, 2);
- assert.equal(history.state.kfOverlay.returnTo, back);
- assert.equal(match().params.id, 'o2');
- history.back();
- assert.equal(currentUrl(), back);
- });
- test('订阅分别识别站内导航与浏览器返回,返回读取当前实际路径和位置', () => {
- const changes = [];
- const off = subscribe(value => changes.push(value));
- navigate('/report/r1?ch=limits');
- history.back();
- off();
- assert.deepEqual(changes.map(c => c.navigation), ['initial', 'push', 'traverse']);
- assert.equal(changes.at(-1).name, 'pool');
- assert.equal(changes.at(-1).historyState.kfScrollY, 350);
- });
- test('章节和浮层查询串更新不丢返回地址和历史状态', () => {
- navigate(reportUrl('r1', '/pool/o1?stage=S3'));
- setQuery({ ch: 'limits', ev: 'e1', report: 'r1' });
- setQuery({ ev: null, report: null });
- const q = new URLSearchParams(location.search);
- assert.equal(q.get('returnTo'), '/pool/o1?stage=S3');
- assert.equal(q.get('ch'), 'limits');
- assert.equal(q.get('ev'), null);
- assert.equal(history.state.kfFrom, '/pool?stage=S3&sort=latest');
- });
- test('非法或站外返回地址落到应用内兜底,损坏编码匹配为不存在', () => {
- for (const path of ['https://evil.test/', '//evil.test/', '/\\evil.test/', '/missing', '/pool\n']) {
- assert.equal(safeReturnTo(path, '/reports'), '/reports');
- }
- assert.equal(match('/pool/%E0%A4%A').name, 'notfound');
- assert.equal(match('/reports/competitor/r1').name, 'report');
- });
- test('根部署与子路径导航均保留查询和章节,不重复压入相同 URL', () => {
- base = '/';
- assert.equal(resolveUrl('/report/r1?ch=2#top').href, 'https://board.test/report/r1?ch=2#top');
- base = '/preview/';
- navigate('/report/r1?ch=2#top');
- navigate('/report/r1?ch=2#top');
- assert.equal(entries.length, 2);
- assert.equal(location.pathname, '/preview/report/r1');
- assert.equal(currentUrl(), '/report/r1?ch=2#top');
- });
- test('报告返回链接使用浏览器历史;新标签与下载链接保留浏览器行为', () => {
- globalThis.HTMLAnchorElement = class {
- constructor(path, attrs = {}) { this.href = resolveUrl(path).href; this.attrs = attrs; this.target = attrs.target || ''; }
- matches() { return true; }
- hasAttribute(name) { return Object.hasOwn(this.attrs, name); }
- };
- globalThis.Element = HTMLAnchorElement;
- const click = anchor => {
- const e = { button: 0, composedPath: () => [anchor], preventDefault() { this.prevented = true; }, stopPropagation() {} };
- onLinkClick(e);
- return e;
- };
- const back = currentUrl();
- navigate('/report/r1');
- assert.ok(click(new HTMLAnchorElement(back, { 'data-back': '' })).prevented);
- assert.equal(currentUrl(), back);
- assert.equal(cursor, 0);
- assert.equal(click(new HTMLAnchorElement('/pool/o1', { target: '_blank' })).prevented, undefined);
- assert.equal(click(new HTMLAnchorElement('/report/r1', { download: '' })).prevented, undefined);
- });
- test('历史达到浏览器上限、来源条目被裁剪时,关闭仍回指定页', () => {
- const origin = currentUrl();
- navigate(opportunityUrl('o1'));
- window.navigation = {
- currentEntry: { index: 4 },
- entries: () => [{ index: 3, url: hrefForTest('/members') }, { index: 4, url: location.href }],
- };
- returnToRoute(origin, { canGoBack: true });
- assert.equal(cursor, 1);
- assert.equal(currentUrl(), origin);
- });
- function hrefForTest(path) { return resolveUrl(path).href; }
|