mirror of
https://github.com/lukaszraczylo/ghtracker.git
synced 2026-10-14 00:58:36 +00:00
* feat(refresh): refresh a single repository with POST /refresh?repo= The endpoint refreshes one configured repository and returns the full state plus the repo name. Each repository has a 60-second cooldown that is separate from the full-refresh limit. A running full refresh or a refresh of the same repository returns 409. An unknown repository returns 404. Cross-site and disabled-config behaviour stay the same. The collector reuses its fetch path and replaces only that repository. * feat(ui): add a refresh button beside each repository The button appears in the project list and in each attention group header. It stays hidden when the server disables manual refresh. The button shows a spinner while its repository refreshes and reports refusals in the notice line. The store swaps in the returned state, so no extra fetch follows.
141 lines
5.7 KiB
TypeScript
141 lines
5.7 KiB
TypeScript
import { flushPromises, mount } from '@vue/test-utils'
|
|
import { createPinia } from 'pinia'
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import App from './App.vue'
|
|
import { mixedState, repo, state } from '@/test/fixtures'
|
|
import type { TrackerState } from '@/lib/types'
|
|
|
|
function mountWith(body: TrackerState | Error) {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn(() =>
|
|
body instanceof Error
|
|
? Promise.reject(body)
|
|
: Promise.resolve(new Response(JSON.stringify(body), { status: 200 })),
|
|
),
|
|
)
|
|
return mount(App, { global: { plugins: [createPinia()] } })
|
|
}
|
|
|
|
describe('App', () => {
|
|
afterEach(() => vi.unstubAllGlobals())
|
|
|
|
it('shows the overview, only the problems and every project', async () => {
|
|
const w = mountWith(mixedState())
|
|
await flushPromises()
|
|
expect(w.get('[data-test=headline]').text()).toContain('2')
|
|
expect(w.findAll('[data-test=segment]')).toHaveLength(2)
|
|
expect(w.findAll('[data-test=repo-block]')).toHaveLength(1)
|
|
expect(w.findAll('[data-test=row]')).toHaveLength(2)
|
|
expect(w.findAll('[data-test=project]')).toHaveLength(2)
|
|
})
|
|
|
|
it('shows an all-clear state without repo blocks', async () => {
|
|
const w = mountWith(state({ repos: [repo()] }))
|
|
await flushPromises()
|
|
expect(w.get('[data-test=headline]').text()).toBe('All clear')
|
|
expect(w.find('[data-test=repo-block]').exists()).toBe(false)
|
|
expect(w.findAll('[data-test=project]')).toHaveLength(1)
|
|
})
|
|
|
|
it('focuses one project from the strip and clears the focus', async () => {
|
|
const w = mountWith(mixedState())
|
|
await flushPromises()
|
|
await w.findAll('[data-test=segment]')[1].trigger('click')
|
|
expect(w.find('[data-test=repo-block]').exists()).toBe(false)
|
|
expect(w.find('[data-test=empty]').exists()).toBe(true)
|
|
await w.get('[data-test=clear-all]').trigger('click')
|
|
expect(w.findAll('[data-test=repo-block]')).toHaveLength(1)
|
|
})
|
|
|
|
it('narrows the list as you type and restores it when cleared', async () => {
|
|
const w = mountWith(mixedState())
|
|
await flushPromises()
|
|
await w.get('[data-test=search]').setValue('zzz-no-match')
|
|
expect(w.find('[data-test=repo-block]').exists()).toBe(false)
|
|
expect(w.find('[data-test=no-projects]').exists()).toBe(true)
|
|
await w.get('[data-test=clear-all]').trigger('click')
|
|
expect(w.findAll('[data-test=repo-block]')).toHaveLength(1)
|
|
expect(w.findAll('[data-test=project]')).toHaveLength(2)
|
|
})
|
|
|
|
it('applies a kind filter from the toolbar', async () => {
|
|
const w = mountWith(mixedState())
|
|
await flushPromises()
|
|
await w.get('[data-test=kind-workflow]').trigger('click')
|
|
expect(w.findAll('[data-test=row]')).toHaveLength(1)
|
|
expect(w.get('[data-test=sev-all]').text()).toBe('All 1')
|
|
})
|
|
|
|
it('waits while the server has not finished its first refresh', async () => {
|
|
const w = mountWith(state({ loaded: false, scan: { done: 4, total: 10 } }))
|
|
await flushPromises()
|
|
expect(w.find('[data-test=waiting]').exists()).toBe(true)
|
|
expect(w.get('[data-test=loading-detail]').text()).toContain('4 of 10 scanned')
|
|
expect(w.find('[data-test=headline]').exists()).toBe(false)
|
|
})
|
|
|
|
it('surfaces a connection error', async () => {
|
|
const w = mountWith(new Error('offline'))
|
|
await flushPromises()
|
|
expect(w.get('[data-test=error]').text()).toContain('offline')
|
|
})
|
|
|
|
it('offers a rescan only when the server allows it', async () => {
|
|
const on = mountWith(state({ manualRefresh: true, repos: [repo()] }))
|
|
await flushPromises()
|
|
expect(on.find('[data-test=rescan]').exists()).toBe(true)
|
|
const off = mountWith(state({ manualRefresh: false, repos: [repo()] }))
|
|
await flushPromises()
|
|
expect(off.find('[data-test=rescan]').exists()).toBe(false)
|
|
})
|
|
|
|
it('refreshes one repo from its button, shows the new state and announces it', async () => {
|
|
const first = state({
|
|
manualRefresh: true,
|
|
repos: [repo({ refreshedAt: '2026-10-02T10:00:00Z' })],
|
|
})
|
|
const after = { ...first, repo: 'o/r', repos: [repo({ refreshedAt: '2026-10-02T12:00:00Z' })] }
|
|
const fetchMock = vi.fn((_url: string, init?: RequestInit) =>
|
|
Promise.resolve(
|
|
new Response(JSON.stringify(init?.method === 'POST' ? after : first), { status: 200 }),
|
|
),
|
|
)
|
|
vi.stubGlobal('fetch', fetchMock)
|
|
const w = mount(App, { global: { plugins: [createPinia()] } })
|
|
await flushPromises()
|
|
await w.get('[data-test=project] [data-test=repo-refresh]').trigger('click')
|
|
await flushPromises()
|
|
expect(fetchMock).toHaveBeenCalledWith('/refresh?repo=o%2Fr', { method: 'POST' })
|
|
expect(w.get('[data-test=announcement]').text()).toBe('Refreshed o/r')
|
|
})
|
|
|
|
it('shows the refusal message when a repo is refreshed too soon', async () => {
|
|
const body = state({ manualRefresh: true, repos: [repo()] })
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn((_url: string, init?: RequestInit) =>
|
|
Promise.resolve(
|
|
init?.method === 'POST'
|
|
? new Response('{}', { status: 429, headers: { 'Retry-After': '30' } })
|
|
: new Response(JSON.stringify(body), { status: 200 }),
|
|
),
|
|
),
|
|
)
|
|
const w = mount(App, { global: { plugins: [createPinia()] } })
|
|
await flushPromises()
|
|
await w.get('[data-test=project] [data-test=repo-refresh]').trigger('click')
|
|
await flushPromises()
|
|
expect(w.get('[data-test=notice]').text()).toBe('o/r was refreshed recently. Try again in 30s')
|
|
expect(
|
|
w.get('[data-test=project] [data-test=repo-refresh]').attributes('disabled'),
|
|
).toBeUndefined()
|
|
})
|
|
|
|
it('hides the repo buttons when the server disables manual refresh', async () => {
|
|
const w = mountWith(state({ manualRefresh: false, repos: [repo()] }))
|
|
await flushPromises()
|
|
expect(w.find('[data-test=repo-refresh]').exists()).toBe(false)
|
|
})
|
|
})
|