qwen-code/packages/web-shell/client/utils/splitUrl.test.ts
Shaojin Wen adf2caea39
feat(web-shell): persist the split view across refresh, per tab (#7136)
The split view (2+ sessions side by side) was lost on every refresh: its
pane set lived only in React state, and the one-shot ?split= deep link is
consumed on load. Persist the live session set to sessionStorage while the
split is the active view, and restore it on load when no ?split= deep link
is present, so a refresh brings the split back.

sessionStorage (not localStorage) is deliberate: it is scoped per browser
tab, so a split opened in its own tab and the in-window split never clobber
each other, and a fresh unrelated tab restores nothing — while still
surviving a refresh of the same tab. The ?split= URL stays the shareable,
cross-tab channel.

Only an explicit close (the split's back button) clears the persisted set;
detours to a single session keep it, so the split is treated as the user's
lasting context until they close it. Controlled hosts (which own their split
lifecycle) never auto-persist or auto-restore.

Co-authored-by: wenshao <wenshao@example.com>
2026-07-18 08:29:23 +00:00

112 lines
3.5 KiB
TypeScript

// @vitest-environment jsdom
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { beforeEach, describe, expect, it } from 'vitest';
import {
buildSplitUrl,
clearSplitSessions,
loadSplitSessions,
parseSplitSessionIds,
saveSplitSessions,
} from './splitUrl';
describe('buildSplitUrl', () => {
it('opens the split for the given sessions on the same origin', () => {
expect(
buildSplitUrl(['a', 'b'], 'https://host:7777/session/other?x=1'),
).toBe('https://host:7777/?x=1&split=a%2Cb');
});
it('preserves the daemon/token query a dev deployment relies on', () => {
const url = buildSplitUrl(
['s1', 's2'],
'http://localhost:5173/?daemon=http://localhost:9000&token=secret',
);
expect(url).toContain('daemon=http');
expect(url).toContain('token=secret');
expect(url).toContain('split=s1%2Cs2');
});
it('resets the path so no single-session deep-link competes', () => {
expect(
new URL(buildSplitUrl(['a'], 'https://host/session/x')).pathname,
).toBe('/');
});
it('carries the daemon token in the fragment when provided', () => {
const url = new URL(
buildSplitUrl(['a', 'b'], 'https://host/', 'secret-tok'),
);
expect(url.searchParams.get('split')).toBe('a,b');
// In the hash, not the query — never sent to the server / logs.
expect(url.search).not.toContain('secret-tok');
expect(new URLSearchParams(url.hash.slice(1)).get('token')).toBe(
'secret-tok',
);
});
it('adds no fragment when no token is given', () => {
expect(buildSplitUrl(['a'], 'https://host/')).not.toContain('#');
});
});
describe('parseSplitSessionIds', () => {
it('reads the comma-separated ids', () => {
expect(parseSplitSessionIds('?split=a,b,c')).toEqual(['a', 'b', 'c']);
});
it('round-trips with buildSplitUrl', () => {
const url = new URL(buildSplitUrl(['s1', 's2'], 'https://host/'));
expect(parseSplitSessionIds(url.search)).toEqual(['s1', 's2']);
});
it('returns an empty array when the param is absent or empty', () => {
expect(parseSplitSessionIds('')).toEqual([]);
expect(parseSplitSessionIds('?split=')).toEqual([]);
expect(parseSplitSessionIds('?other=1')).toEqual([]);
});
it('trims and drops blank ids', () => {
expect(parseSplitSessionIds('?split=a,,%20b%20,')).toEqual(['a', 'b']);
});
});
describe('split session persistence (sessionStorage)', () => {
beforeEach(() => {
sessionStorage.clear();
});
it('round-trips the saved session set', () => {
saveSplitSessions(['s1', 's2', 's3']);
expect(loadSplitSessions()).toEqual(['s1', 's2', 's3']);
});
it('returns an empty array when nothing is saved', () => {
expect(loadSplitSessions()).toEqual([]);
});
it('dedupes, drops blanks, and caps at MAX_SPLIT_PANES (6)', () => {
saveSplitSessions(['a', 'a', '', 'b', 'c', 'd', 'e', 'f', 'g']);
expect(loadSplitSessions()).toEqual(['a', 'b', 'c', 'd', 'e', 'f']);
});
it('clears the saved set', () => {
saveSplitSessions(['s1', 's2']);
clearSplitSessions();
expect(loadSplitSessions()).toEqual([]);
});
it('falls back to [] on malformed stored JSON', () => {
sessionStorage.setItem('qwen-webshell-split-sessions', '{not json');
expect(loadSplitSessions()).toEqual([]);
});
it('falls back to [] when the stored value is not an array', () => {
sessionStorage.setItem('qwen-webshell-split-sessions', '"s1"');
expect(loadSplitSessions()).toEqual([]);
});
});