135 lines
4.9 KiB
TypeScript
135 lines
4.9 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
|
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import { useBrowserPathname } from './use-browser-pathname.js';
|
|
|
|
function Harness({
|
|
link = <a href="/automation">Automation</a>,
|
|
onMainFocus,
|
|
resolvePathname,
|
|
}: {
|
|
link?: React.ReactNode;
|
|
onMainFocus?: (pathname: string) => void;
|
|
resolvePathname?: (pathname: string) => string;
|
|
}) {
|
|
const pathname = useBrowserPathname(resolvePathname);
|
|
return (
|
|
<>
|
|
{link}
|
|
<main id="main-content" tabIndex={-1} onFocus={() => onMainFocus?.(pathname)}>
|
|
{pathname}
|
|
</main>
|
|
</>
|
|
);
|
|
}
|
|
|
|
afterEach(() => {
|
|
cleanup();
|
|
window.history.replaceState(null, '', '/');
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe('useBrowserPathname', () => {
|
|
it('pushes eligible same-origin links and updates pathname without reloading', () => {
|
|
window.history.replaceState(null, '', '/fleet');
|
|
const pushState = vi.spyOn(window.history, 'pushState');
|
|
const scrollTo = vi.spyOn(window, 'scrollTo').mockImplementation(() => undefined);
|
|
const focusedPathnames: string[] = [];
|
|
render(<Harness onMainFocus={(pathname) => focusedPathnames.push(pathname)} />);
|
|
|
|
const link = screen.getByRole('link', { name: 'Automation' });
|
|
const accepted = fireEvent.click(link);
|
|
|
|
expect(accepted).toBe(false);
|
|
expect(pushState).toHaveBeenCalledWith(null, '', '/automation');
|
|
expect(screen.getByRole('main').textContent).toBe('/automation');
|
|
expect(document.activeElement).toBe(screen.getByRole('main'));
|
|
expect(focusedPathnames).toEqual(['/automation']);
|
|
expect(scrollTo).toHaveBeenCalledWith({ top: 0, left: 0, behavior: 'auto' });
|
|
});
|
|
|
|
it('replaces a legacy pathname with its canonical destination', () => {
|
|
window.history.replaceState(null, '', '/settings/instances');
|
|
const replaceState = vi.spyOn(window.history, 'replaceState');
|
|
|
|
render(
|
|
<Harness
|
|
resolvePathname={(pathname) =>
|
|
pathname === '/settings/instances' ? '/settings/system' : pathname
|
|
}
|
|
/>,
|
|
);
|
|
|
|
expect(replaceState).toHaveBeenCalledWith(null, '', '/settings/system');
|
|
expect(window.location.pathname).toBe('/settings/system');
|
|
expect(screen.getByRole('main').textContent).toBe('/settings/system');
|
|
});
|
|
|
|
it('makes an exact-current link a no-op without adding history', () => {
|
|
window.history.replaceState(null, '', '/fleet');
|
|
const pushState = vi.spyOn(window.history, 'pushState');
|
|
render(<Harness link={<a href="/fleet">Current</a>} />);
|
|
|
|
const accepted = fireEvent.click(screen.getByRole('link', { name: 'Current' }));
|
|
|
|
expect(accepted).toBe(false);
|
|
expect(pushState).not.toHaveBeenCalled();
|
|
expect(window.location.pathname).toBe('/fleet');
|
|
});
|
|
|
|
it('tracks browser history traversal and restores the page start', () => {
|
|
window.history.replaceState(null, '', '/automation');
|
|
const scrollTo = vi.spyOn(window, 'scrollTo').mockImplementation(() => undefined);
|
|
render(<Harness />);
|
|
|
|
window.history.replaceState(null, '', '/fleet');
|
|
fireEvent(window, new PopStateEvent('popstate'));
|
|
|
|
expect(screen.getByRole('main').textContent).toBe('/fleet');
|
|
expect(document.activeElement).toBe(screen.getByRole('main'));
|
|
expect(scrollTo).toHaveBeenCalledWith({ top: 0, left: 0, behavior: 'auto' });
|
|
});
|
|
|
|
it.each([
|
|
[
|
|
'target window',
|
|
<a href="/automation" target="_blank">
|
|
Excluded
|
|
</a>,
|
|
{},
|
|
],
|
|
[
|
|
'download',
|
|
<a href="/automation" download>
|
|
Excluded
|
|
</a>,
|
|
{},
|
|
],
|
|
['external origin', <a href="https://example.com/automation">Excluded</a>, {}],
|
|
['hash-only navigation', <a href="/fleet#cards">Excluded</a>, {}],
|
|
['middle click', <a href="/automation">Excluded</a>, { button: 1 }],
|
|
['meta click', <a href="/automation">Excluded</a>, { metaKey: true }],
|
|
['control click', <a href="/automation">Excluded</a>, { ctrlKey: true }],
|
|
['shift click', <a href="/automation">Excluded</a>, { shiftKey: true }],
|
|
['alt click', <a href="/automation">Excluded</a>, { altKey: true }],
|
|
])('leaves %s to the browser', (_label, link, eventInit) => {
|
|
window.history.replaceState(null, '', '/fleet');
|
|
const pushState = vi.spyOn(window.history, 'pushState');
|
|
render(<Harness link={link} />);
|
|
|
|
let hookPrevented = true;
|
|
const suppressBrowserNavigation = (event: MouseEvent) => {
|
|
hookPrevented = event.defaultPrevented;
|
|
event.preventDefault();
|
|
};
|
|
document.addEventListener('click', suppressBrowserNavigation, { once: true });
|
|
const event = new MouseEvent('click', { bubbles: true, cancelable: true, ...eventInit });
|
|
screen.getByRole('link', { name: 'Excluded' }).dispatchEvent(event);
|
|
|
|
expect(hookPrevented).toBe(false);
|
|
expect(pushState).not.toHaveBeenCalled();
|
|
expect(screen.getByRole('main').textContent).toBe('/fleet');
|
|
});
|
|
});
|