@furystack/shades
Version:
A lightweight UI framework for FuryStack with JSX support
131 lines (109 loc) • 4.6 kB
text/typescript
import { createInjector } from '@furystack/inject'
import { usingAsync } from '@furystack/utils'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { ScreenService, ScreenSizes } from './screen-service.js'
describe('ScreenService', () => {
beforeEach(() => {
document.body.innerHTML = '<div id="root"></div>'
})
afterEach(() => {
document.body.innerHTML = ''
vi.restoreAllMocks()
})
it('Should be constructed', async () => {
await usingAsync(createInjector(), async (i) => {
const s = i.get(ScreenService)
expect(s).toBeDefined()
expect(s.breakpoints).toBeDefined()
})
})
describe('breakpoints', () => {
it('Should have correct breakpoint definitions', async () => {
await usingAsync(createInjector(), async (i) => {
const s = i.get(ScreenService)
expect(s.breakpoints.xs.minSize).toBe(0)
expect(s.breakpoints.sm.minSize).toBe(600)
expect(s.breakpoints.md.minSize).toBe(960)
expect(s.breakpoints.lg.minSize).toBe(1280)
expect(s.breakpoints.xl.minSize).toBe(1920)
})
})
})
describe('screenSize.atLeast', () => {
it('Should have observable for each screen size', async () => {
await usingAsync(createInjector(), async (i) => {
const s = i.get(ScreenService)
for (const size of ScreenSizes) {
expect(s.screenSize.atLeast[size]).toBeDefined()
expect(typeof s.screenSize.atLeast[size].getValue()).toBe('boolean')
}
})
})
it('Should return true for xs on any screen size', async () => {
await usingAsync(createInjector(), async (i) => {
const s = i.get(ScreenService)
// xs has minSize 0, so it should always be true
expect(s.screenSize.atLeast.xs.getValue()).toBe(true)
})
})
it('Should update screenSize observables on window resize', async () => {
await usingAsync(createInjector(), async (i) => {
const s = i.get(ScreenService)
// Mock window.innerWidth to simulate a large screen
vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(1920)
// Trigger resize event
window.dispatchEvent(new Event('resize'))
// All breakpoints should be true for 1920px width
expect(s.screenSize.atLeast.xs.getValue()).toBe(true)
expect(s.screenSize.atLeast.sm.getValue()).toBe(true)
expect(s.screenSize.atLeast.md.getValue()).toBe(true)
expect(s.screenSize.atLeast.lg.getValue()).toBe(true)
expect(s.screenSize.atLeast.xl.getValue()).toBe(true)
// Mock a small screen
vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(500)
window.dispatchEvent(new Event('resize'))
// Only xs should be true for 500px width
expect(s.screenSize.atLeast.xs.getValue()).toBe(true)
expect(s.screenSize.atLeast.sm.getValue()).toBe(false)
expect(s.screenSize.atLeast.md.getValue()).toBe(false)
expect(s.screenSize.atLeast.lg.getValue()).toBe(false)
expect(s.screenSize.atLeast.xl.getValue()).toBe(false)
})
})
})
describe('orientation', () => {
it('Should have an orientation observable', async () => {
await usingAsync(createInjector(), async (i) => {
const s = i.get(ScreenService)
const orientation = s.orientation.getValue()
expect(['landscape', 'portrait']).toContain(orientation)
})
})
it('Should update orientation on resize', async () => {
// Mock matchMedia before creating the service
const matchMediaMock = vi.fn()
window.matchMedia = matchMediaMock
await usingAsync(createInjector(), async (i) => {
// Set initial orientation to landscape
matchMediaMock.mockReturnValue({ matches: true })
const s = i.get(ScreenService)
// Verify initial landscape
window.dispatchEvent(new Event('resize'))
expect(s.orientation.getValue()).toBe('landscape')
// Change to portrait
matchMediaMock.mockReturnValue({ matches: false })
window.dispatchEvent(new Event('resize'))
expect(s.orientation.getValue()).toBe('portrait')
})
})
})
describe('disposal', () => {
it('Should remove resize event listener on dispose', async () => {
const removeEventListenerSpy = vi.spyOn(window, 'removeEventListener')
await usingAsync(createInjector(), async (i) => {
i.get(ScreenService)
})
expect(removeEventListenerSpy).toHaveBeenCalledWith('resize', expect.any(Function))
})
})
})