Newer
Older
navi-1 / webclient / tests / unit / components / ui / contentLightbox.test.js
// @vitest-environment jsdom
// happy-dom reports `window.location.origin` as "null", and the viewer page
// URL is built against it — jsdom has a real origin.
import { afterEach, describe, expect, it, vi } from 'vitest'
import { nextTick } from 'vue'
import { mount } from '@vue/test-utils'

import ContentLightbox from '@/components/ui/ContentLightbox.vue'
import { useLightbox } from '@/composables/useLightbox.js'

const { openContent, close } = useLightbox()

let wrapper = null

function mountLightbox() {
  wrapper = mount(ContentLightbox, { attachTo: document.body })
  return wrapper
}

async function open(payload) {
  openContent(payload)
  await nextTick()
  await nextTick()
}

afterEach(() => {
  close()
  wrapper?.unmount()
  wrapper = null
  vi.unstubAllGlobals()
})

describe('ContentLightbox', () => {
  it('renders nothing until something is opened', () => {
    mountLightbox()
    expect(document.body.querySelector('.lightbox-dialog')).toBeNull()
  })

  it('shows a text file as highlighted source', async () => {
    vi.stubGlobal('fetch', vi.fn(async () => ({
      ok: true,
      text: async () => 'const answer = 42\n',
    })))
    mountLightbox()

    await open({ url: '/api/files/notes.js', type: 'text', title: 'notes.js' })
    // Let the fetch resolve.
    await vi.waitFor(() => {
      expect(document.body.querySelector('.lightbox-code code')).not.toBeNull()
    })

    const code = document.body.querySelector('.lightbox-code code')
    expect(code.className).toContain('language-javascript')
    expect(code.textContent).toContain('const answer = 42')
    expect(fetch).toHaveBeenCalledWith('/api/files/notes.js')
  })

  it('reports a failed text fetch instead of an empty viewer', async () => {
    vi.stubGlobal('fetch', vi.fn(async () => ({ ok: false, status: 404 })))
    mountLightbox()

    await open({ url: '/api/files/missing.txt', type: 'text', title: 'missing.txt' })
    await vi.waitFor(() => {
      expect(document.body.querySelector('.lightbox-state-error')).not.toBeNull()
    })
    expect(document.body.querySelector('.lightbox-state-error').textContent)
      .toContain('404')
  })

  it('gives audio its own player around a hidden audio element', async () => {
    mountLightbox()

    await open({ url: '/api/files/track.mp3', type: 'audio', title: 'track.mp3' })

    const audio = document.body.querySelector('audio.ap-audio-el')
    expect(audio).not.toBeNull()
    expect(audio.getAttribute('src')).toBe('/api/files/track.mp3')
    expect(audio.hasAttribute('controls')).toBe(false)
    expect(document.body.querySelector('.ap-controls')).not.toBeNull()
    expect(document.body.querySelector('.ap-title').textContent).toBe('track.mp3')
    // play/pause, seek and the time read-outs are kit-rendered
    expect(document.body.querySelector('.ap-controls .btn-icon')).not.toBeNull()
    expect(document.body.querySelectorAll('.ap-controls .range').length).toBe(2)
  })

  it('keeps images and video on their native branches', async () => {
    mountLightbox()

    await open({ url: '/api/files/photo.png', type: 'image', title: 'photo.png' })
    expect(document.body.querySelector('img.lightbox-img')?.getAttribute('src'))
      .toBe('/api/files/photo.png')

    await open({ url: '/api/files/clip.mp4', type: 'video', title: 'clip.mp4' })
    expect(document.body.querySelector('video.lightbox-video')?.getAttribute('src'))
      .toBe('/api/files/clip.mp4')
  })

  it('loads the iframe viewer page for documents', async () => {
    mountLightbox()

    await open({ url: '/api/files/report.pdf', type: 'pdf', title: 'report.pdf' })

    const frame = document.body.querySelector('iframe.lightbox-frame')
    expect(frame).not.toBeNull()
    expect(frame.getAttribute('src')).toContain('/content-viewers/pdf.html?url=')
  })

  it('closes on Escape', async () => {
    mountLightbox()
    await open({ url: '/api/files/photo.png', type: 'image' })
    expect(document.body.querySelector('.lightbox-dialog')).not.toBeNull()

    window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }))
    await nextTick()

    expect(document.body.querySelector('.lightbox-dialog')).toBeNull()
  })

  it('offers the raw file for download in its header', async () => {
    mountLightbox()
    await open({ url: '/api/files/photo.png', type: 'image', title: 'photo.png' })

    const link = document.body.querySelector('.lightbox-tools a')
    expect(link.getAttribute('href')).toBe('/api/files/photo.png')
    expect(link.hasAttribute('download')).toBe(true)
  })
})