diff --git a/frontend/src/views/user/settings/ApiTokens.test.ts b/frontend/src/views/user/settings/ApiTokens.test.ts index ac4db11d0..ef348f2ef 100644 --- a/frontend/src/views/user/settings/ApiTokens.test.ts +++ b/frontend/src/views/user/settings/ApiTokens.test.ts @@ -30,7 +30,13 @@ const tokens = [ let stored = tokens.slice() const {getAll, del} = vi.hoisted(() => ({ - getAll: vi.fn(async () => ({data: {items: stored.slice(), total_pages: 1}})), + getAll: vi.fn(async ({query}: {query: {page: number, per_page: number}}) => ({data: { + items: stored.slice((query.page - 1) * query.per_page, query.page * query.per_page), + page: query.page, + per_page: query.per_page, + total: stored.length, + total_pages: Math.ceil(stored.length / query.per_page), + }})), del: vi.fn(async ({path}: {path: {id: number}}) => { stored = stored.filter(token => token.id !== path.id) return {} @@ -41,13 +47,13 @@ vi.mock('@/message', () => ({success: vi.fn(), error: vi.fn()})) const i18n = createI18n({legacy: false, locale: 'en', messages: {en}}) -async function mountPage() { +async function mountPage(path = '/user/settings/api-tokens') { const errors: unknown[] = [] const router = createRouter({ history: createMemoryHistory(), routes: [{path: '/user/settings/api-tokens', name: 'user.settings.apiTokens', component: ApiTokens}], }) - await router.push('/user/settings/api-tokens') + await router.push(path) await router.isReady() const wrapper = mount(ApiTokens, { @@ -78,7 +84,7 @@ async function mountPage() { attachTo: document.body, }) await flushPromises() - return {wrapper, errors} + return {wrapper, errors, router} } async function openDeleteModalForFirstToken(wrapper: VueWrapper) { @@ -112,6 +118,7 @@ describe('ApiTokens settings page', () => { setActivePinia(createPinia()) document.body.innerHTML = '' del.mockClear() + getAll.mockClear() stored = tokens.slice() }) @@ -174,4 +181,39 @@ describe('ApiTokens settings page', () => { expect(del).toHaveBeenCalledWith({path: {id: 1}}) expect(runtimeErrorMessages(errors)).toEqual([]) }) + + it('loads the page from the route and steps back once its last token is deleted', async () => { + stored = [ + ...tokens, + ...Array.from({length: 23}, (_, index) => ({ + ...tokens[1], + id: index + 3, + title: `token-${index + 3}`, + })), + ] + stored.push({ + ...tokens[0], + id: 99, + title: 'last-page-token', + }) + const mounted = await mountPage('/user/settings/api-tokens?page=2') + wrapper = mounted.wrapper + + expect(getAll).toHaveBeenLastCalledWith(expect.objectContaining({query: { + page: 2, + per_page: 25, + }})) + expect(wrapper.findAll('tbody tr')).toHaveLength(1) + expect(wrapper.find('nav.pagination').exists()).toBe(true) + + const confirmBtn = await openDeleteModalForFirstToken(wrapper) + confirmBtn.click() + await settleCloseTransition() + + expect(del).toHaveBeenCalledWith({path: {id: 99}}) + expect(mounted.router.currentRoute.value.query.page).toBe('1') + expect(wrapper.findAll('tbody tr')).toHaveLength(25) + expect(wrapper.find('nav.pagination').exists()).toBe(false) + expect(runtimeErrorMessages(mounted.errors)).toEqual([]) + }) }) diff --git a/frontend/tests/e2e/user/api-tokens.spec.ts b/frontend/tests/e2e/user/api-tokens.spec.ts index dda3fd5b5..cb6fc6372 100644 --- a/frontend/tests/e2e/user/api-tokens.spec.ts +++ b/frontend/tests/e2e/user/api-tokens.spec.ts @@ -96,3 +96,28 @@ test('creates and revokes a scoped token with stored state surviving reload', as const revoked = await apiContext.get('/api/v2/tasks', {headers: {Authorization: `Bearer ${token.token}`}}) expect(revoked.status()).toBe(401) }) + +test('pages through more tokens than fit on one page', async ({authenticatedPage: page, apiContext, userToken}) => { + for (let i = 1; i <= 26; i++) { + const response = await apiContext.post('/api/v2/tokens', { + headers: {Authorization: `Bearer ${userToken}`}, + data: { + title: `Paged token ${i}`, + permissions: {tasks: ['read_all']}, + expires_at: '2099-01-01T00:00:00Z', + }, + }) + expect(response.ok()).toBe(true) + } + await page.goto('/user/settings/api-tokens') + await expect(page.locator('tbody tr')).toHaveCount(25) + await page.locator('nav.pagination').getByText('2', {exact: true}).click() + await expect(page).toHaveURL(/[?&]page=2/) + await expect(page.locator('tbody tr')).toHaveCount(1) + await page.reload() + await expect(page.locator('tbody tr')).toHaveCount(1) + await page.getByRole('button', {name: 'Delete', exact: true}).click() + await page.locator('[data-cy="modalPrimary"]').click() + await expect(page.locator('tbody tr')).toHaveCount(25) + await expect(page.locator('nav.pagination')).toHaveCount(0) +})