@furystack/shades
Version:
A lightweight UI framework for FuryStack with JSX support
152 lines • 8.18 kB
JavaScript
import { createInjector } from '@furystack/inject';
import { deserializeQueryString, serializeToQueryString, serializeValue } from '@furystack/rest';
import { usingAsync } from '@furystack/utils';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { LocationService, useCustomSearchStateSerializer } from './location-service.js';
describe('LocationService', () => {
beforeEach(() => {
document.body.innerHTML = '<div id="root"></div>';
});
afterEach(() => {
document.body.innerHTML = '';
});
it('Shuld be constructed', async () => {
await usingAsync(createInjector(), async (i) => {
const s = i.get(LocationService);
expect(s).toBeDefined();
expect(typeof s.navigate).toBe('function');
});
});
it('Shuld update state on events', async () => {
await usingAsync(createInjector(), async (i) => {
const onLocaionChanged = vi.fn();
const s = i.get(LocationService);
s.onLocationPathChanged.subscribe(onLocaionChanged);
expect(onLocaionChanged).toBeCalledTimes(0);
history.pushState(null, '', '/loc1');
expect(onLocaionChanged).toBeCalledTimes(1);
history.replaceState(null, '', '/loc2');
expect(onLocaionChanged).toBeCalledTimes(2);
// TODO: Figure out testing hashchange and popstate subscriptions
// window.dispatchEvent(new HashChangeEvent('hashchange', { newURL: '/loc3' }))
// expect(onLocaionChanged).toBeCalledTimes(3)
// window.dispatchEvent(new PopStateEvent('popstate', {}))
// expect(onLocaionChanged).toBeCalledTimes(4)
});
});
it('Should update location path when navigate is called', async () => {
await usingAsync(createInjector(), async (i) => {
const onLocationChanged = vi.fn();
const s = i.get(LocationService);
s.onLocationPathChanged.subscribe(onLocationChanged);
s.navigate('/dashboard');
expect(s.onLocationPathChanged.getValue()).toBe('/dashboard');
expect(onLocationChanged).toHaveBeenCalledWith('/dashboard');
});
});
describe('replace', () => {
it('Should update the observable path without pushing a new history entry', async () => {
await usingAsync(createInjector(), async (i) => {
const s = i.get(LocationService);
const lengthBefore = history.length;
s.replace('/replaced');
expect(s.onLocationPathChanged.getValue()).toBe('/replaced');
expect(history.length).toBe(lengthBefore);
});
});
it('Should call history.replaceState rather than pushState', async () => {
await usingAsync(createInjector(), async (i) => {
const s = i.get(LocationService);
const pushSpy = vi.spyOn(history, 'pushState');
const replaceSpy = vi.spyOn(history, 'replaceState');
s.replace('/replaced-2');
expect(replaceSpy).toHaveBeenCalledTimes(1);
expect(replaceSpy).toHaveBeenCalledWith(null, '', '/replaced-2');
expect(pushSpy).not.toHaveBeenCalled();
pushSpy.mockRestore();
replaceSpy.mockRestore();
});
});
it('Should notify path subscribers after replace', async () => {
await usingAsync(createInjector(), async (i) => {
const s = i.get(LocationService);
const onLocationChanged = vi.fn();
s.onLocationPathChanged.subscribe(onLocationChanged);
s.replace('/notify');
expect(onLocationChanged).toHaveBeenCalledWith('/notify');
});
});
});
describe('useSearchParam', () => {
it('Should create observables lazily', async () => {
await usingAsync(createInjector(), async (i) => {
const service = i.get(LocationService);
const observables = service.searchParamObservables;
const testSearchParam = service.useSearchParam('test', null);
expect(observables.size).toBe(1);
const testSearchParam2 = service.useSearchParam('test', null);
expect(observables.size).toBe(1);
expect(testSearchParam).toBe(testSearchParam2);
const testSearchParam3 = service.useSearchParam('test2', undefined);
expect(observables.size).toBe(2);
expect(testSearchParam3).not.toBe(testSearchParam2);
});
});
it('Should return the default value, if not present in the query string', async () => {
await usingAsync(createInjector(), async (i) => {
const service = i.get(LocationService);
const testSearchParam = service.useSearchParam('test', { value: 'foo' });
expect(testSearchParam.getValue()).toEqual({ value: 'foo' });
});
});
it('Should return the value from the query string', async () => {
await usingAsync(createInjector(), async (i) => {
const service = i.get(LocationService);
history.pushState(null, '', `/loc1?test=${serializeValue(1)}`);
const testSearchParam = service.useSearchParam('test', 123);
expect(testSearchParam.getValue()).toBe(1);
});
});
it('should update the observable value on push / replace states', async () => {
await usingAsync(createInjector(), async (i) => {
const service = i.get(LocationService);
history.pushState(null, '', `/loc1?test=${serializeValue(1)}`);
const testSearchParam = service.useSearchParam('test', 234);
expect(testSearchParam.getValue()).toBe(1);
history.replaceState(null, '', `/loc1?test=${serializeValue('2')}`);
expect(testSearchParam.getValue()).toBe('2');
});
});
it('Should update the URL based on search value change', async () => {
await usingAsync(createInjector(), async (i) => {
const service = i.get(LocationService);
history.pushState(null, '', `/loc1?test=${serializeValue('2')}`);
const testSearchParam = service.useSearchParam('test', '');
testSearchParam.setValue('2');
expect(location.search).toBe('?test=IjIi');
});
});
it('Should throw when called after LocationService has been resolved', async () => {
await usingAsync(createInjector(), async (i) => {
const customSerializer = vi.fn((value) => serializeToQueryString(value));
const customDeserializer = vi.fn((value) => deserializeQueryString(value));
// Eagerly resolve once so the service patches history / adds listeners.
i.get(LocationService);
expect(() => useCustomSearchStateSerializer(i, customSerializer, customDeserializer)).toThrow(/before LocationService is resolved/);
});
});
it('Should use custom serializer and deserializer', async () => {
await usingAsync(createInjector(), async (i) => {
const customSerializer = vi.fn((value) => serializeToQueryString(value));
const customDeserializer = vi.fn((value) => deserializeQueryString(value));
useCustomSearchStateSerializer(i, customSerializer, customDeserializer);
const locationService = i.get(LocationService);
const testSearchParam = locationService.useSearchParam('test', { value: 'foo' });
testSearchParam.setValue({ value: 'bar' });
expect(customSerializer).toBeCalledWith({ test: { value: 'bar' } });
expect(customDeserializer).toBeCalledWith('?test=eyJ2YWx1ZSI6ImJhciJ9');
});
});
});
});
//# sourceMappingURL=location-service.spec.js.map