feat(16.3-02): dialog rules smoke tests and My account API tokens docs
Cover scope *, 422 focus, clipboard copy/fallback, close-without-copy guard, and document the SPA tokens card on My account. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { enableAutoUnmount, flushPromises } from '@vue/test-utils'
|
||||
import { toasts } from '../../src/state/useToasts'
|
||||
import { API, mountApp, queryOf, requestsTo, resetState, type Reply, type Route } from '../helpers'
|
||||
import { API, keydown, mountApp, queryOf, requestsTo, resetState, type Reply, type Route } from '../helpers'
|
||||
|
||||
const TOKENS = `${API}/account/tokens`
|
||||
const SCOPES = `${API}/account/tokens/scopes`
|
||||
@@ -224,4 +224,104 @@ describe('ApiTokensPanel', () => {
|
||||
expect(panel()!.querySelector('[data-token-row="11"] [data-action="revoke-token"]')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('dialog rules', () => {
|
||||
async function openCreateDialog(routes: Record<string, Route> = {}) {
|
||||
const mounted = await openAccount(routes)
|
||||
await click('[data-action="create-token"]', panel()!)
|
||||
await flushPromises()
|
||||
return mounted
|
||||
}
|
||||
|
||||
async function fillName(value: string): Promise<void> {
|
||||
const nameInput = dialog()!.querySelector<HTMLInputElement>('#api-token-name')!
|
||||
nameInput.value = value
|
||||
nameInput.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
await flushPromises()
|
||||
}
|
||||
|
||||
it('disables other scopes when * is checked and posts only ["*"]', async () => {
|
||||
const { calls } = await openCreateDialog()
|
||||
await fillName('Full')
|
||||
const star = dialog()!.querySelector<HTMLInputElement>('[data-scope="*"] input')!
|
||||
star.checked = true
|
||||
star.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
await flushPromises()
|
||||
expect(dialog()!.querySelector<HTMLInputElement>('[data-scope="admin:read"] input')!.disabled).toBe(true)
|
||||
expect(dialog()!.textContent).toContain('Pełny dostęp obejmuje wszystkie pozostałe zakresy.')
|
||||
await click('[data-action="create-token-submit"]', dialog()!)
|
||||
const [post] = requestsTo(calls, 'POST', TOKENS)
|
||||
expect(await post!.clone().json()).toEqual({ name: 'Full', expires: '90d', scopes: ['*'] })
|
||||
})
|
||||
|
||||
it('renders a 422 on scopes and focuses the first scope checkbox', async () => {
|
||||
await openCreateDialog({
|
||||
[`POST ${TOKENS}`]: {
|
||||
status: 422,
|
||||
body: {
|
||||
error: {
|
||||
code: 'validation_failed',
|
||||
message: 'Validation failed',
|
||||
details: { scopes: ['Wybierz co najmniej jeden zakres.'] },
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
await fillName('No scope')
|
||||
await click('[data-action="create-token-submit"]', dialog()!)
|
||||
expect(dialog()!.textContent).toContain('Wybierz co najmniej jeden zakres.')
|
||||
expect(document.activeElement).toBe(dialog()!.querySelector('[data-scope="*"] input'))
|
||||
})
|
||||
|
||||
it('shows Copied on clipboard success and copy_failed on rejection', async () => {
|
||||
const writeText = vi.fn().mockResolvedValue(undefined)
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
configurable: true,
|
||||
value: { writeText },
|
||||
})
|
||||
await openCreateDialog()
|
||||
await fillName('Copy me')
|
||||
const scope = dialog()!.querySelector<HTMLInputElement>('[data-scope="admin:read"] input')!
|
||||
scope.checked = true
|
||||
scope.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
await flushPromises()
|
||||
await click('[data-action="create-token-submit"]', dialog()!)
|
||||
await click('[data-action="copy-token"]', dialog()!)
|
||||
expect(writeText).toHaveBeenCalledWith(createdToken.token)
|
||||
expect(dialog()!.querySelector('[data-action="copy-token"]')!.textContent).toContain('Skopiowano')
|
||||
expect(dialog()!.querySelector('[aria-live="polite"]')!.textContent).toContain(
|
||||
'Skopiowano token do schowka.',
|
||||
)
|
||||
|
||||
writeText.mockRejectedValueOnce(new Error('denied'))
|
||||
await click('[data-action="copy-token"]', dialog()!)
|
||||
expect(dialog()!.querySelector('[role="alert"]')!.textContent).toContain(
|
||||
'Nie udało się skopiować automatycznie',
|
||||
)
|
||||
expect(document.activeElement).toBe(dialog()!.querySelector('[data-token-plaintext]'))
|
||||
})
|
||||
|
||||
it('asks before closing step 2 without a copy and keeps the step on cancel', async () => {
|
||||
await openCreateDialog()
|
||||
await fillName('Guard')
|
||||
const scope = dialog()!.querySelector<HTMLInputElement>('[data-scope="admin:read"] input')!
|
||||
scope.checked = true
|
||||
scope.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
await flushPromises()
|
||||
await click('[data-action="create-token-submit"]', dialog()!)
|
||||
expect(dialog()!.querySelector('[data-token-plaintext]')).toBeTruthy()
|
||||
keydown(dialog()!, 'Escape')
|
||||
await flushPromises()
|
||||
// Reka Escape may be handled as escape-key-down; fall back to the close button.
|
||||
if (!confirmDialog()) {
|
||||
await click('[data-action="close"]', dialog()!)
|
||||
}
|
||||
expect(confirmDialog()).toBeTruthy()
|
||||
expect(confirmDialog()!.textContent).toContain('Zamknąć bez kopiowania')
|
||||
await click('[data-action="cancel"]', confirmDialog()!)
|
||||
expect(dialog()!.querySelector<HTMLInputElement>('[data-token-plaintext]')!.value).toBe(
|
||||
createdToken.token,
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user