Skip to content

Commit dc0c47b

Browse files
functionstackxgithub-actions[bot]adibarra
authored
fix(tab-nav): preserve unofficialrun(s) URL param across tab navigation (#320)
* fix(tab-nav): preserve unofficialrun(s) URL param across tab navigation Tab links used bare hrefs (e.g. /evaluation), so clicking a tab while viewing /inference?unofficialruns=xyz dropped the param and unloaded the overlay. Read the param from window.location and append it to every tab href, kept in sync with pathname changes, dismiss/clear (which write via history.pushState), and popstate. Fixes #319 Co-authored-by: functionstackx <functionstackx@users.noreply.github.com> * style: apply oxfmt to tab-nav.cy.tsx Co-authored-by: Alec Ibarra <adibarra@users.noreply.github.com> --------- Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: functionstackx <functionstackx@users.noreply.github.com> Co-authored-by: Alec Ibarra <adibarra@users.noreply.github.com>
1 parent 635d580 commit dc0c47b

2 files changed

Lines changed: 124 additions & 3 deletions

File tree

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { AppRouterContext } from 'next/dist/shared/lib/app-router-context.shared-runtime';
2+
import { PathnameContext } from 'next/dist/shared/lib/hooks-client-context.shared-runtime';
3+
4+
import { TabNav } from '@/components/tab-nav';
5+
import { UnofficialRunContext } from '@/components/unofficial-run-provider';
6+
import { createMockUnofficialRunContext } from '../support/mock-data';
7+
8+
function createMockRouter() {
9+
return {
10+
push: cy.stub(),
11+
replace: cy.stub(),
12+
refresh: cy.stub(),
13+
back: cy.stub(),
14+
forward: cy.stub(),
15+
prefetch: cy.stub().resolves(),
16+
};
17+
}
18+
19+
/**
20+
* Mount TabNav with the provided URL search string written into the window
21+
* via history.replaceState. The component reads `window.location.search` in a
22+
* useEffect, so the URL must be set before mount.
23+
*/
24+
function mountTabNav(opts: { pathname?: string; search?: string }) {
25+
const { pathname = '/inference', search = '' } = opts;
26+
cy.window().then((win) => {
27+
win.history.replaceState(null, '', `${pathname}${search}`);
28+
});
29+
const router = createMockRouter();
30+
const ctxValue = createMockUnofficialRunContext();
31+
32+
cy.mount(
33+
<AppRouterContext.Provider value={router}>
34+
<PathnameContext.Provider value={pathname}>
35+
<UnofficialRunContext.Provider value={ctxValue}>
36+
<TabNav />
37+
</UnofficialRunContext.Provider>
38+
</PathnameContext.Provider>
39+
</AppRouterContext.Provider>,
40+
);
41+
}
42+
43+
describe('TabNav — unofficialrun URL preservation (issue #319)', () => {
44+
afterEach(() => {
45+
// Reset URL between specs so leftover query strings don't leak.
46+
cy.window().then((win) => win.history.replaceState(null, '', '/'));
47+
});
48+
49+
it('renders bare hrefs when the URL has no unofficialrun param', () => {
50+
mountTabNav({});
51+
cy.get('[data-testid="tab-trigger-evaluation"]').should('have.attr', 'href', '/evaluation');
52+
cy.get('[data-testid="tab-trigger-historical"]').should('have.attr', 'href', '/historical');
53+
cy.get('[data-testid="tab-trigger-calculator"]').should('have.attr', 'href', '/calculator');
54+
});
55+
56+
it('appends unofficialruns to every tab href when the URL has the param', () => {
57+
mountTabNav({ search: '?unofficialruns=12345' });
58+
cy.get('[data-testid="tab-trigger-evaluation"]').should(
59+
'have.attr',
60+
'href',
61+
'/evaluation?unofficialruns=12345',
62+
);
63+
cy.get('[data-testid="tab-trigger-inference"]').should(
64+
'have.attr',
65+
'href',
66+
'/inference?unofficialruns=12345',
67+
);
68+
cy.get('[data-testid="tab-trigger-historical"]').should(
69+
'have.attr',
70+
'href',
71+
'/historical?unofficialruns=12345',
72+
);
73+
});
74+
75+
it('preserves a comma-separated list of run ids verbatim', () => {
76+
mountTabNav({ search: '?unofficialruns=111,222,333' });
77+
cy.get('[data-testid="tab-trigger-evaluation"]').should(
78+
'have.attr',
79+
'href',
80+
'/evaluation?unofficialruns=111,222,333',
81+
);
82+
});
83+
84+
it('accepts the singular alias `unofficialrun` and forwards it under `unofficialruns`', () => {
85+
mountTabNav({ search: '?unofficialrun=999' });
86+
cy.get('[data-testid="tab-trigger-evaluation"]').should(
87+
'have.attr',
88+
'href',
89+
'/evaluation?unofficialruns=999',
90+
);
91+
});
92+
});

packages/app/src/components/tab-nav.tsx

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import Link from 'next/link';
44
import { usePathname, useRouter } from 'next/navigation';
5-
import { useEffect, useRef, useState } from 'react';
5+
import { useContext, useEffect, useRef, useState } from 'react';
66

77
import { track } from '@/lib/analytics';
88
import { Card } from '@/components/ui/card';
@@ -14,6 +14,7 @@ import {
1414
SelectTrigger,
1515
SelectValue,
1616
} from '@/components/ui/select';
17+
import { UnofficialRunContext } from '@/components/unofficial-run-provider';
1718
import { cn } from '@/lib/utils';
1819

1920
const FEATURE_GATE_KEY = 'inferencex-feature-gate';
@@ -89,10 +90,38 @@ export function TabNav() {
8990
const current = activeTab(pathname);
9091
const selectedTab = TAB_VALUES.has(current) ? current : '';
9192

93+
// Preserve the `unofficialrun(s)` URL param across tab navigation so an
94+
// overlay loaded on /inference doesn't get dropped when switching to
95+
// /evaluation, etc. The URL is the source of truth (it's still set during
96+
// the in-flight fetch and even when the fetch fails), so we read it from
97+
// window.location and re-sync on pathname change, context update
98+
// (dismiss/clear writes via history.pushState), and popstate.
99+
const unofficialCtx = useContext(UnofficialRunContext);
100+
const ctxRunInfos = unofficialCtx?.unofficialRunInfos;
101+
const [unofficialIds, setUnofficialIds] = useState('');
102+
useEffect(() => {
103+
if (typeof window === 'undefined') return undefined;
104+
const sync = () => {
105+
const sp = new URLSearchParams(window.location.search);
106+
for (const [k, v] of sp) {
107+
if (/^unofficialruns?$/i.test(k) && v) {
108+
setUnofficialIds(v);
109+
return;
110+
}
111+
}
112+
setUnofficialIds('');
113+
};
114+
sync();
115+
window.addEventListener('popstate', sync);
116+
return () => window.removeEventListener('popstate', sync);
117+
}, [pathname, ctxRunInfos]);
118+
const tabHref = (path: string) =>
119+
unofficialIds ? `${path}?unofficialruns=${unofficialIds}` : path;
120+
92121
const handleMobileChange = (value: string) => {
93122
window.dispatchEvent(new CustomEvent('inferencex:tab-change'));
94123
track('tab_changed', { tab: value });
95-
router.push(`/${value}`);
124+
router.push(tabHref(`/${value}`));
96125
};
97126

98127
const handleDesktopClick = (tab: string) => {
@@ -140,7 +169,7 @@ export function TabNav() {
140169
return (
141170
<Link
142171
key={tab.href}
143-
href={tab.href}
172+
href={tabHref(tab.href)}
144173
data-testid={tab.testId}
145174
data-ph-capture-attribute-tab={tab.href.slice(1)}
146175
onClick={() => handleDesktopClick(tab.href.slice(1))}

0 commit comments

Comments
 (0)