@furystack/shades-common-components
Version:
Common UI components for FuryStack Shades
252 lines • 12.6 kB
JavaScript
import { createInjector } from '@furystack/inject';
import { createComponent, flushUpdates, initializeShadeRoot, LocationService } from '@furystack/shades';
import { usingAsync } from '@furystack/utils';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { AppBarLink } from './app-bar-link.js';
describe('AppBarLink component', () => {
beforeEach(() => {
document.body.innerHTML = '<div id="root"></div>';
});
afterEach(() => {
document.body.innerHTML = '';
});
describe('rendering', () => {
it('should render the shade-app-bar-link custom element', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(AppBarLink, { path: "/test" }, "Link"),
});
await flushUpdates();
const appBarLink = document.querySelector('shade-app-bar-link');
expect(appBarLink).not.toBeNull();
expect(appBarLink?.tagName.toLowerCase()).toBe('shade-app-bar-link');
});
});
it('should render children through NestedRouteLink', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(AppBarLink, { path: "/test" }, "Test Link Text"),
});
await flushUpdates();
expect(document.body.innerHTML).toContain('Test Link Text');
});
});
it('should render NestedRouteLink with correct href', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(AppBarLink, { path: "/my-route" }, "Link"),
});
await flushUpdates();
expect(document.body.innerHTML).toContain('href="/my-route"');
});
});
});
describe('route matching', () => {
it('should have active class when current URL matches href', async () => {
await usingAsync(createInjector(), async (injector) => {
history.pushState(null, '', '/dashboard');
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(AppBarLink, { path: "/dashboard" }, "Dashboard"),
});
await flushUpdates();
const appBarLink = document.querySelector('shade-app-bar-link');
expect(appBarLink?.hasAttribute('data-active')).toBe(true);
});
});
it('should not have active class when current URL does not match href', async () => {
await usingAsync(createInjector(), async (injector) => {
history.pushState(null, '', '/other-page');
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(AppBarLink, { path: "/dashboard" }, "Dashboard"),
});
await flushUpdates();
const appBarLink = document.querySelector('shade-app-bar-link');
expect(appBarLink?.hasAttribute('data-active')).toBe(false);
});
});
it('should update active class when location changes', async () => {
await usingAsync(createInjector(), async (injector) => {
history.pushState(null, '', '/home');
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(AppBarLink, { path: "/dashboard" }, "Dashboard"),
});
await flushUpdates();
const appBarLink = document.querySelector('shade-app-bar-link');
expect(appBarLink?.hasAttribute('data-active')).toBe(false);
// Navigate to matching route
history.pushState(null, '', '/dashboard');
injector.get(LocationService).updateState();
await flushUpdates();
expect(appBarLink?.hasAttribute('data-active')).toBe(true);
});
});
it('should remove active class when navigating away', async () => {
await usingAsync(createInjector(), async (injector) => {
history.pushState(null, '', '/dashboard');
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(AppBarLink, { path: "/dashboard" }, "Dashboard"),
});
await flushUpdates();
const appBarLink = document.querySelector('shade-app-bar-link');
expect(appBarLink?.hasAttribute('data-active')).toBe(true);
// Navigate away
history.pushState(null, '', '/other');
injector.get(LocationService).updateState();
await flushUpdates();
expect(appBarLink?.hasAttribute('data-active')).toBe(false);
});
});
it('should match routes with parameters', async () => {
await usingAsync(createInjector(), async (injector) => {
history.pushState(null, '', '/users/123');
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(AppBarLink, { path: "/users/:id" }, "Users"),
});
await flushUpdates();
const appBarLink = document.querySelector('shade-app-bar-link');
expect(appBarLink?.hasAttribute('data-active')).toBe(true);
});
});
it('should support routingOptions with end: false for prefix matching', async () => {
await usingAsync(createInjector(), async (injector) => {
history.pushState(null, '', '/admin/settings/security');
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: (createComponent(AppBarLink, { path: "/admin", routingOptions: { end: false } }, "Admin")),
});
await flushUpdates();
const appBarLink = document.querySelector('shade-app-bar-link');
expect(appBarLink?.hasAttribute('data-active')).toBe(true);
});
});
it('should not match prefix by default (end: true)', async () => {
await usingAsync(createInjector(), async (injector) => {
history.pushState(null, '', '/admin/settings');
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(AppBarLink, { path: "/admin" }, "Admin"),
});
await flushUpdates();
const appBarLink = document.querySelector('shade-app-bar-link');
expect(appBarLink?.hasAttribute('data-active')).toBe(false);
});
});
});
describe('multiple links', () => {
it('should only activate the matching link', async () => {
await usingAsync(createInjector(), async (injector) => {
history.pushState(null, '', '/settings');
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: (createComponent("div", null,
createComponent(AppBarLink, { path: "/home" }, "Home"),
createComponent(AppBarLink, { path: "/settings" }, "Settings"),
createComponent(AppBarLink, { path: "/about" }, "About"))),
});
await flushUpdates();
const links = document.querySelectorAll('shade-app-bar-link');
expect(links[0]?.hasAttribute('data-active')).toBe(false);
expect(links[1]?.hasAttribute('data-active')).toBe(true);
expect(links[2]?.hasAttribute('data-active')).toBe(false);
});
});
it('should update all links when location changes', async () => {
await usingAsync(createInjector(), async (injector) => {
history.pushState(null, '', '/home');
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: (createComponent("div", null,
createComponent(AppBarLink, { path: "/home" }, "Home"),
createComponent(AppBarLink, { path: "/settings" }, "Settings"))),
});
await flushUpdates();
const links = document.querySelectorAll('shade-app-bar-link');
expect(links[0]?.hasAttribute('data-active')).toBe(true);
expect(links[1]?.hasAttribute('data-active')).toBe(false);
// Navigate to settings
history.pushState(null, '', '/settings');
injector.get(LocationService).updateState();
await flushUpdates();
expect(links[0]?.hasAttribute('data-active')).toBe(false);
expect(links[1]?.hasAttribute('data-active')).toBe(true);
});
});
});
describe('styling', () => {
it('should have flex display', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(AppBarLink, { path: "/test" }, "Link"),
});
await flushUpdates();
const appBarLink = document.querySelector('shade-app-bar-link');
const computedStyle = window.getComputedStyle(appBarLink);
expect(computedStyle.display).toBe('flex');
});
});
it('should have pointer cursor', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(AppBarLink, { path: "/test" }, "Link"),
});
await flushUpdates();
const appBarLink = document.querySelector('shade-app-bar-link');
const computedStyle = window.getComputedStyle(appBarLink);
expect(computedStyle.cursor).toBe('pointer');
});
});
it('should have transition for animations', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(AppBarLink, { path: "/test" }, "Link"),
});
await flushUpdates();
const appBarLink = document.querySelector('shade-app-bar-link');
const computedStyle = window.getComputedStyle(appBarLink);
expect(computedStyle.transition).toContain('color');
});
});
});
});
//# sourceMappingURL=app-bar-link.spec.js.map