**Add Vitest support and migrate frontend tests**

- Added Vitest configuration (`vitest.config.mjs`) for frontend unit testing with a `jsdom` environment.
- Migrated and rewrote key frontend tests (`api.test.js`, `comiclist.test.js`, `comicreader.test.js`, `historytable.test.js`, `router.test.js`, `store.test.js`) to Vitest, improving maintainability.
- Removed `tsconfig.json` and related TypeScript loaders from `webpack` configurations (`webpack.dev.js`, `webpack.prod.js`).
- Adjusted `eslint.config.mjs` to support Vitest tests.
- Updated `package-lock.json` to include Vitest, test utilities (`jsdom`, `@vue/test-utils`), and other associated packages.
- Updated Nginx configuration for better compression on assets (`gzip` adjustments).
- Enhanced `entrypoint.sh` to include first-run library indexing (`--if-empty` argument).
- Simplified the `scan_comics` command with a dedicated test for empty database scans.

This transition improves testing workflows and backend/frontend synergy while removing unused TypeScript dependencies.
This commit is contained in:
ajurna committed 2026-10-14 10:07:14 +01:00
1 parent ce809ed1a9
commit cc557543c5
19 files changed
+2388 -586

No files matched your search

+167
View File
@@ -0,0 +1,167 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { flushPromises, mount } from '@vue/test-utils'
const api = vi.hoisted(() => ({ get: vi.fn(), put: vi.fn() }))
const router = vi.hoisted(() => ({ push: vi.fn() }))
const deck = vi.hoisted(() => ({
initialize: vi.fn(() => Promise.resolve()),
slide: vi.fn(),
on: vi.fn(),
off: vi.fn(),
destroy: vi.fn(),
isFirstSlide: vi.fn(() => false),
isLastSlide: vi.fn(() => false),
}))
const hammertime = vi.hoisted(() => ({ on: vi.fn(), off: vi.fn() }))
const reveal = vi.hoisted(() => vi.fn(() => deck))
vi.mock('@/api', () => ({ default: api }))
vi.mock('reveal.js', () => ({ default: reveal }))
vi.mock('hammerjs', () => ({ default: vi.fn(function () { return hammertime }) }))
import TheComicReader from '@/components/TheComicReader.vue'
const COMIC = {
title: 'Alpha',
last_read_page: 2,
pages: 5,
prev_comic: { route: 'browse', selector: 'p1' },
next_comic: { route: 'browse', selector: 'n1' },
}
const mounted = []
function mountReader() {
const wrapper = mount(TheComicReader, {
props: { selector: 'a1' },
global: {
mocks: { $router: router },
stubs: { 'comic-paginate': true },
},
})
mounted.push(wrapper)
return wrapper
}
beforeEach(() => {
vi.useFakeTimers()
HTMLElement.prototype.scrollIntoView = vi.fn()
api.get.mockReset()
api.put.mockReset()
api.get.mockResolvedValue({ data: { ...COMIC } })
api.put.mockResolvedValue({ data: {} })
router.push.mockReset()
reveal.mockClear()
for (const fn of Object.values(deck)) {
if (typeof fn === 'function') fn.mockClear?.()
}
for (const fn of Object.values(hammertime)) fn.mockClear()
})
afterEach(() => {
mounted.forEach((wrapper) => {
if (!wrapper.vm?.isUnmounted) wrapper.unmount()
})
mounted.length = 0
})
describe('TheComicReader', () => {
it('loads the book, starts the deck and records the page it resumed on', async () => {
const wrapper = mountReader()
await flushPromises()
expect(api.get).toHaveBeenCalledWith('/api/read/a1/')
expect(deck.initialize).toHaveBeenCalled()
expect(wrapper.vm.current_page).toBe(2)
expect(deck.slide).toHaveBeenCalledWith(2)
expect(api.put).toHaveBeenCalledWith('/api/read/a1/set_page/', { page: 2 })
})
it('keeps the page indicator in step with the deck', async () => {
const wrapper = mountReader()
await flushPromises()
wrapper.vm.setPage(4)
expect(deck.slide).toHaveBeenCalledWith(3)
await flushPromises()
expect(wrapper.vm.paginate_page).toBe(4)
})
it('debounces arrow keys so held keys do not spam the deck', async () => {
const wrapper = mountReader()
await flushPromises()
window.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowRight' }))
window.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowRight' }))
vi.advanceTimersByTime(50)
expect(deck.slide).toHaveBeenCalledTimes(2)
expect(wrapper.vm.current_page).toBe(3)
})
it('moves to the next book when the last slide is reached', async () => {
const wrapper = mountReader()
await flushPromises()
deck.isLastSlide.mockReturnValueOnce(true)
wrapper.vm.keyPress({ key: 'ArrowRight' })
expect(router.push).toHaveBeenCalledWith({ name: 'browse', params: { selector: 'n1' } })
})
it('moves to the previous book when the first slide is reached', async () => {
const wrapper = mountReader()
await flushPromises()
deck.isFirstSlide.mockReturnValueOnce(true)
wrapper.vm.keyPress({ key: 'ArrowLeft' })
expect(router.push).toHaveBeenCalledWith({ name: 'browse', params: { selector: 'p1' } })
})
it('records the page again whenever the visible slide changes', async () => {
mountReader()
await flushPromises()
const handler = deck.on.mock.calls[0][1]
api.put.mockClear()
handler({ indexh: 4 })
expect(api.put).toHaveBeenCalledWith('/api/read/a1/set_page/', { page: 4 })
})
it('removes every listener and the deck when it is unmounted', async () => {
const addSpy = vi.spyOn(window, 'addEventListener')
const removeSpy = vi.spyOn(window, 'removeEventListener')
const wrapper = mountReader()
await flushPromises()
expect(addSpy).toHaveBeenCalledWith('keyup', wrapper.vm.keyPressDebounce)
wrapper.unmount()
expect(removeSpy).toHaveBeenCalledWith('keyup', wrapper.vm.keyPressDebounce)
expect(deck.off).toHaveBeenCalledWith('slidechanged', wrapper.vm.slideChanged)
expect(deck.destroy).toHaveBeenCalled()
expect(hammertime.off).toHaveBeenCalledWith('swipeleft')
expect(hammertime.off).toHaveBeenCalledWith('swiperight')
expect(hammertime.off).toHaveBeenCalledWith('tap')
addSpy.mockRestore()
removeSpy.mockRestore()
})
it('does not act on a key press that was debounced just before unmounting', async () => {
const wrapper = mountReader()
await flushPromises()
deck.slide.mockClear()
window.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowRight' }))
wrapper.unmount()
vi.advanceTimersByTime(100)
expect(deck.slide).not.toHaveBeenCalled()
})
})