import { afterEach, describe, expect, it, vi } from 'vitest' import { createRoot } from '../vdom.ts' import { invariant } from '../invariant.ts' import type { RemixNode } from '../jsx.ts' import { link } from './link-mixin.ts' function render(node: RemixNode) { let container = document.createElement('div') document.body.append(container) let root = createRoot(container) root.render(node) root.flush() return { container, root } } describe('link mixin', () => { afterEach(() => { document.body.innerHTML = '' vi.unstubAllGlobals() }) it('adds href and runtime attributes to anchors', () => { let { container } = render( Login , ) let anchor = container.querySelector('a') invariant(anchor) expect(anchor.getAttribute('href')).toBe('/login') expect(anchor.getAttribute('rmx-target')).toBe('auth') expect(anchor.getAttribute('rmx-src')).toBe('/partials/login') expect(anchor.getAttribute('rmx-reset-scroll')).toBe('false') expect(anchor.getAttribute('role')).toBeNull() }) it('adds link semantics to buttons', () => { let { container } = render() let button = container.querySelector('button') invariant(button) expect(button.getAttribute('role')).toBe('link') expect(button.getAttribute('type')).toBe('button') expect(button.getAttribute('tabindex')).toBeNull() }) it('adds link semantics and tabIndex to generic elements', () => { let { container } = render(
  • Login
  • ) let item = container.querySelector('li') invariant(item) expect(item.getAttribute('role')).toBe('link') expect(item.getAttribute('tabindex')).toBe('0') }) it('omits runtime attributes on anchors when options are not provided', () => { let { container } = render(Docs) let anchor = container.querySelector('a') invariant(anchor) expect(anchor.getAttribute('href')).toBe('/docs') expect(anchor.getAttribute('rmx-target')).toBeNull() expect(anchor.getAttribute('rmx-src')).toBeNull() }) it('navigates on plain click for non-anchor hosts', () => { let navigateMock = vi.fn(() => ({ finished: Promise.resolve() })) vi.stubGlobal('navigation', { navigate: navigateMock }) let { container } = render() let button = container.querySelector('button') invariant(button) button.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })) expect(navigateMock).toHaveBeenCalledWith('/login', { state: { target: 'auth', src: '/login', resetScroll: true, $rmx: true }, history: undefined, }) }) it('passes history options through for non-anchor navigation', () => { let navigateMock = vi.fn(() => ({ finished: Promise.resolve() })) vi.stubGlobal('navigation', { navigate: navigateMock }) let { container } = render() let button = container.querySelector('button') invariant(button) button.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })) expect(navigateMock).toHaveBeenCalledWith('/login', { state: { target: undefined, src: '/login', resetScroll: true, $rmx: true }, history: 'replace', }) }) it('passes resetScroll=false through for non-anchor navigation', () => { let navigateMock = vi.fn(() => ({ finished: Promise.resolve() })) vi.stubGlobal('navigation', { navigate: navigateMock }) let { container } = render() let button = container.querySelector('button') invariant(button) button.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })) expect(navigateMock).toHaveBeenCalledWith('/login', { state: { target: undefined, src: '/login', resetScroll: false, $rmx: true }, history: undefined, }) }) it('activates link buttons on Enter and suppresses keyboard clicks from Enter and Space', () => { let navigateMock = vi.fn(() => ({ finished: Promise.resolve() })) vi.stubGlobal('navigation', { navigate: navigateMock }) let { container } = render() let button = container.querySelector('button') invariant(button) button.dispatchEvent( new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }), ) expect(navigateMock).toHaveBeenCalledTimes(1) button.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, detail: 0 })) expect(navigateMock).toHaveBeenCalledTimes(1) button.dispatchEvent( new KeyboardEvent('keydown', { key: ' ', bubbles: true, cancelable: true }), ) button.dispatchEvent(new KeyboardEvent('keyup', { key: ' ', bubbles: true, cancelable: true })) button.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, detail: 0 })) expect(navigateMock).toHaveBeenCalledTimes(1) }) it('activates generic link elements on Enter', () => { let navigateMock = vi.fn(() => ({ finished: Promise.resolve() })) vi.stubGlobal('navigation', { navigate: navigateMock }) let { container } = render(
  • Login
  • ) let item = container.querySelector('li') invariant(item) item.dispatchEvent( new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }), ) expect(navigateMock).toHaveBeenCalledTimes(1) expect(navigateMock).toHaveBeenCalledWith('/login', { state: { target: undefined, src: '/login', resetScroll: true, $rmx: true }, history: undefined, }) }) it('opens a new tab for meta-click, ctrl-click, and middle-click on non-anchor hosts', () => { let navigateMock = vi.fn(() => ({ finished: Promise.resolve() })) let openMock = vi.fn() vi.stubGlobal('navigation', { navigate: navigateMock }) vi.stubGlobal('open', openMock) let { container } = render() let button = container.querySelector('button') invariant(button) button.dispatchEvent( new MouseEvent('click', { bubbles: true, cancelable: true, metaKey: true }), ) button.dispatchEvent( new MouseEvent('click', { bubbles: true, cancelable: true, ctrlKey: true }), ) button.dispatchEvent(new MouseEvent('auxclick', { bubbles: true, cancelable: true, button: 1 })) expect(openMock).toHaveBeenCalledTimes(3) expect(openMock).toHaveBeenNthCalledWith(1, '/login', '_blank') expect(openMock).toHaveBeenNthCalledWith(2, '/login', '_blank') expect(openMock).toHaveBeenNthCalledWith(3, '/login', '_blank') expect(navigateMock).not.toHaveBeenCalled() }) it('does not navigate disabled or aria-disabled link hosts', () => { let navigateMock = vi.fn(() => ({ finished: Promise.resolve() })) vi.stubGlobal('navigation', { navigate: navigateMock }) let { container } = render(
  • Help
  • , ) let button = container.querySelector('button') let item = container.querySelector('li') invariant(button) invariant(item) button.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })) item.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })) item.dispatchEvent( new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }), ) expect(item.getAttribute('aria-disabled')).toBe('true') expect(navigateMock).not.toHaveBeenCalled() }) })