feat(16.3-02): list states, show-revoked toggle, and revoke via ConfirmDialog
Add skeleton/empty/error coverage, revoked-row handling, ConfirmDialog overflow wrap for long names, and rebuild boardwalk dist. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -60,7 +60,7 @@ function onConfirm(): void {
|
||||
<AlertDialogTitle class="text-[20px] font-bold tracking-[-0.01em]">
|
||||
{{ title ?? t('backend::lang.form.confirm') }}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription class="text-muted">{{ message }}</AlertDialogDescription>
|
||||
<AlertDialogDescription class="text-muted [overflow-wrap:anywhere]">{{ message }}</AlertDialogDescription>
|
||||
<div class="flex gap-2.5">
|
||||
<AlertDialogCancel
|
||||
data-action="cancel"
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { enableAutoUnmount, flushPromises } from '@vue/test-utils'
|
||||
import { API, mountApp, requestsTo, resetState, type Route } from '../helpers'
|
||||
import { toasts } from '../../src/state/useToasts'
|
||||
import { API, mountApp, queryOf, requestsTo, resetState, type Reply, type Route } from '../helpers'
|
||||
|
||||
const TOKENS = `${API}/account/tokens`
|
||||
const SCOPES = `${API}/account/tokens/scopes`
|
||||
const TOKEN_ID = `${API}/account/tokens/11`
|
||||
|
||||
const envelope = (data: unknown, status = 200) => ({ status, body: { data, meta: {} } })
|
||||
|
||||
@@ -27,6 +29,14 @@ const existingToken = {
|
||||
expired: false,
|
||||
}
|
||||
|
||||
const revokedToken = {
|
||||
...existingToken,
|
||||
id: 13,
|
||||
name: 'Old token',
|
||||
hint: 'dead',
|
||||
revoked_at: '2026-02-01T08:30:00Z',
|
||||
}
|
||||
|
||||
const createdToken = {
|
||||
...existingToken,
|
||||
id: 12,
|
||||
@@ -52,6 +62,14 @@ const scopes = [
|
||||
},
|
||||
]
|
||||
|
||||
function deferred(): { route: () => Promise<Reply>; release: (reply: Reply) => void } {
|
||||
let release!: (reply: Reply) => void
|
||||
const pending = new Promise<Reply>((resolve) => {
|
||||
release = resolve
|
||||
})
|
||||
return { route: () => pending, release }
|
||||
}
|
||||
|
||||
function tokenRoutes(overrides: Record<string, Route> = {}, list: unknown[] = [existingToken]): Record<string, Route> {
|
||||
let listed = list
|
||||
return {
|
||||
@@ -71,6 +89,7 @@ async function openAccount(routes: Record<string, Route> = {}, list: unknown[] =
|
||||
|
||||
const panel = () => document.body.querySelector<HTMLElement>('[data-api-tokens]')
|
||||
const dialog = () => document.body.querySelector<HTMLElement>('[data-token-dialog]')
|
||||
const confirmDialog = () => document.body.querySelector<HTMLElement>('[data-confirm-dialog]')
|
||||
|
||||
async function click(selector: string, root: ParentNode = document.body): Promise<void> {
|
||||
const el = root.querySelector<HTMLElement>(selector)
|
||||
@@ -128,4 +147,81 @@ describe('ApiTokensPanel', () => {
|
||||
expect(dialog()).toBeNull()
|
||||
expect(document.body.textContent).not.toContain(createdToken.token)
|
||||
})
|
||||
|
||||
describe('list states', () => {
|
||||
it('shows skeleton then the empty block', async () => {
|
||||
const pending = deferred()
|
||||
const { wrapper } = await mountApp(
|
||||
'/account',
|
||||
tokenRoutes({ [`GET ${TOKENS}`]: pending.route }, []),
|
||||
{ attach: true },
|
||||
)
|
||||
expect(panel()!.querySelector('[data-token-list]')!.getAttribute('aria-busy')).toBe('true')
|
||||
expect(panel()!.querySelectorAll('[aria-hidden="true"]').length).toBeGreaterThanOrEqual(3)
|
||||
expect(panel()!.querySelector<HTMLButtonElement>('[data-action="create-token"]')!.disabled).toBe(true)
|
||||
pending.release(envelope([]))
|
||||
await flushPromises()
|
||||
expect(panel()!.querySelector('[data-tokens-empty]')).toBeTruthy()
|
||||
expect(panel()!.querySelector('[data-action="create-token"]')!.hasAttribute('disabled')).toBe(false)
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('shows the alert and hides the toggle when the load fails', async () => {
|
||||
await openAccount({
|
||||
[`GET ${TOKENS}`]: { status: 500, body: { error: { code: 'x', message: 'fail', details: {} } } },
|
||||
})
|
||||
expect(panel()!.querySelector('[data-tokens-error]')!.getAttribute('role')).toBe('alert')
|
||||
expect(panel()!.querySelector('[data-show-revoked]')).toBeNull()
|
||||
expect(panel()!.querySelector<HTMLButtonElement>('[data-action="create-token"]')!.disabled).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('revoke', () => {
|
||||
it('runs DELETE through the shared confirm and toasts success', async () => {
|
||||
const { calls } = await openAccount({
|
||||
[`DELETE ${TOKEN_ID}`]: envelope({ ...existingToken, revoked_at: '2026-03-01T10:00:00Z' }),
|
||||
})
|
||||
await click('[data-action="revoke-token"]', panel()!)
|
||||
expect(confirmDialog()).toBeTruthy()
|
||||
await click('[data-action="confirm"]', confirmDialog()!)
|
||||
expect(requestsTo(calls, 'DELETE', TOKEN_ID)).toHaveLength(1)
|
||||
expect(toasts.value[0]).toMatchObject({ text: 'Unieważniono token', tone: 'success' })
|
||||
})
|
||||
|
||||
it('toasts gone on 404 and reloads', async () => {
|
||||
let listed: unknown[] = [existingToken]
|
||||
const { calls } = await openAccount({
|
||||
[`GET ${TOKENS}`]: () => envelope(listed),
|
||||
[`DELETE ${TOKEN_ID}`]: () => {
|
||||
listed = []
|
||||
return { status: 404, body: { error: { code: 'not_found', message: 'gone', details: {} } } }
|
||||
},
|
||||
})
|
||||
const getsBefore = requestsTo(calls, 'GET', TOKENS).length
|
||||
await click('[data-action="revoke-token"]', panel()!)
|
||||
await click('[data-action="confirm"]', confirmDialog()!)
|
||||
expect(toasts.value[0]).toMatchObject({
|
||||
text: 'Ten token już nie istnieje. Lista została odświeżona.',
|
||||
tone: 'danger',
|
||||
})
|
||||
expect(requestsTo(calls, 'GET', TOKENS).length).toBeGreaterThan(getsBefore)
|
||||
expect(panel()!.querySelector('[data-tokens-empty]')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('requests revoked=1 and hides revoke on revoked rows', async () => {
|
||||
const { calls } = await openAccount({
|
||||
[`GET ${TOKENS}`]: (request) =>
|
||||
envelope(queryOf(request).get('revoked') === '1' ? [existingToken, revokedToken] : [existingToken]),
|
||||
})
|
||||
const toggle = panel()!.querySelector<HTMLInputElement>('[data-show-revoked]')!
|
||||
toggle.checked = true
|
||||
toggle.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
await flushPromises()
|
||||
const withRevoked = requestsTo(calls, 'GET', TOKENS).filter((request) => queryOf(request).get('revoked') === '1')
|
||||
expect(withRevoked.length).toBeGreaterThan(0)
|
||||
expect(panel()!.querySelector('[data-token-row="13"]')).toBeTruthy()
|
||||
expect(panel()!.querySelector('[data-token-row="13"] [data-action="revoke-token"]')).toBeNull()
|
||||
expect(panel()!.querySelector('[data-token-row="11"] [data-action="revoke-token"]')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
File diff suppressed because one or more lines are too long
2
modules/boardwalk/dist/index.html
vendored
2
modules/boardwalk/dist/index.html
vendored
@@ -7,7 +7,7 @@
|
||||
<meta name="summer-admin-base" content="__SUMMER_ADMIN_BASE__" />
|
||||
<link rel="icon" href="./favicon.ico" sizes="48x48" />
|
||||
<title>SummerCMS</title>
|
||||
<script type="module" crossorigin src="./assets/index-DlYKDHwm.js"></script>
|
||||
<script type="module" crossorigin src="./assets/index-wuKo5sc0.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-d9w3cFkl.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Reference in New Issue
Block a user