@furystack/shades
Version:
A lightweight UI framework for FuryStack with JSX support
182 lines (153 loc) • 7.22 kB
text/typescript
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: any) => serializeToQueryString(value))
const customDeserializer = vi.fn((value: string) => 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: any) => serializeToQueryString(value))
const customDeserializer = vi.fn((value: string) => 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')
})
})
})
})