diff --git a/web/src/features/access-keys/AccessKeyDeleteDialog.test.ts b/web/src/features/access-keys/AccessKeyDeleteDialog.test.ts index 156feba1..77b33caf 100644 --- a/web/src/features/access-keys/AccessKeyDeleteDialog.test.ts +++ b/web/src/features/access-keys/AccessKeyDeleteDialog.test.ts @@ -46,7 +46,7 @@ describe('AccessKeyDeleteDialog', () => { method: 'DELETE', signal: expect.any(AbortSignal), }) - expect(wrapper.emitted('deleted')).toEqual([[]]) + expect(wrapper.emitted('deleted')).toEqual([['client']]) expect(invalidate).not.toHaveBeenCalled() expect(client.getMutationCache().getAll()).toHaveLength(0) wrapper.unmount() @@ -108,7 +108,7 @@ describe('AccessKeyDeleteDialog', () => { resolveDelete() await flushPromises() - expect(wrapper.emitted('deleted')).toEqual([[]]) + expect(wrapper.emitted('deleted')).toEqual([['client']]) wrapper.unmount() }) }) diff --git a/web/src/features/access-keys/AccessKeyDeleteDialog.vue b/web/src/features/access-keys/AccessKeyDeleteDialog.vue index d407bbac..b32da6d2 100644 --- a/web/src/features/access-keys/AccessKeyDeleteDialog.vue +++ b/web/src/features/access-keys/AccessKeyDeleteDialog.vue @@ -12,7 +12,7 @@ import AppDialog from '@/components/ui/AppDialog.vue' import InlineFeedback from '@/components/ui/InlineFeedback.vue' const props = defineProps<{ accessKey: AccessKeyDto; total: number }>() -const emit = defineEmits<{ deleted: [] }>() +const emit = defineEmits<{ deleted: [name: string] }>() const client = useApiClient() const { t } = useI18n() const open = ref(false) @@ -39,7 +39,7 @@ async function confirmDelete(): Promise { try { await deleteAccessKey(client, props.accessKey.id, activeController.signal) open.value = false - emit('deleted') + emit('deleted', props.accessKey.name) } catch (error: unknown) { if (!(error instanceof RequestCancelledError)) failed.value = true } finally { diff --git a/web/src/features/access-keys/AccessKeyTable.vue b/web/src/features/access-keys/AccessKeyTable.vue index 7372799e..329757f1 100644 --- a/web/src/features/access-keys/AccessKeyTable.vue +++ b/web/src/features/access-keys/AccessKeyTable.vue @@ -15,7 +15,10 @@ import AccessKeyDeleteDialog from './AccessKeyDeleteDialog.vue' import { useEphemeralSecret } from './use-ephemeral-secret' const props = defineProps<{ accessKeys: AccessKeyDto[]; groups: GroupSummary[] }>() -const emit = defineEmits<{ edit: [accessKey: AccessKeyDto, trigger: HTMLElement]; deleted: [] }>() +const emit = defineEmits<{ + edit: [accessKey: AccessKeyDto, trigger: HTMLElement] + deleted: [name: string] +}>() const client = useApiClient() const { locale, t } = useI18n() const secret = useEphemeralSecret() @@ -179,7 +182,7 @@ function rpmSummary(rpm: number): string { diff --git a/web/src/features/access-keys/AccessKeysView.test.ts b/web/src/features/access-keys/AccessKeysView.test.ts index 8e7fa8c3..5b6e07d2 100644 --- a/web/src/features/access-keys/AccessKeysView.test.ts +++ b/web/src/features/access-keys/AccessKeysView.test.ts @@ -183,6 +183,9 @@ describe('AccessKeysView', () => { expect(document.activeElement).toBe( wrapper.get('button[data-test="access-key-create"]').element, ) + const announcement = wrapper.get('[data-test="access-key-delete-announcement"]') + expect(announcement.attributes('aria-live')).toBe('polite') + expect(announcement.text()).toContain('Deleted AccessKey “client”.') wrapper.unmount() }) @@ -292,7 +295,7 @@ describe('AccessKeysView', () => { ).element const focus = vi.spyOn(createButton, 'focus') - wrapper.getComponent(AccessKeyTable).vm.$emit('deleted') + wrapper.getComponent(AccessKeyTable).vm.$emit('deleted', 'client') wrapper.unmount() await nextTick() await flushPromises() diff --git a/web/src/features/access-keys/AccessKeysView.vue b/web/src/features/access-keys/AccessKeysView.vue index c739c702..0be9b728 100644 --- a/web/src/features/access-keys/AccessKeysView.vue +++ b/web/src/features/access-keys/AccessKeysView.vue @@ -27,6 +27,7 @@ const drawerOpen = ref(false) const selected = ref(null) const createOperation = ref(null) const viewRoot = ref(null) +const deletionAnnouncement = ref('') let restoreFocus: HTMLElement | null = null let mounted = true @@ -88,7 +89,9 @@ async function setDrawerOpen(open: boolean): Promise { } } -async function focusCreateAfterDelete(): Promise { +async function focusCreateAfterDelete(name: string): Promise { + deletionAnnouncement.value = '' + await nextTick() await Promise.all( accessKeyMutationInvalidations.delete.map((queryKey) => queryClient.invalidateQueries({ queryKey, exact: true }), @@ -96,6 +99,7 @@ async function focusCreateAfterDelete(): Promise { ) await nextTick() if (!mounted) return + deletionAnnouncement.value = t('accessKeys.delete.deletedAnnouncement', { name }) const target = viewRoot.value?.querySelector('button[data-test="access-key-create"]') if (target instanceof HTMLButtonElement && target.isConnected) target.focus() } @@ -143,6 +147,15 @@ async function focusCreateAfterDelete(): Promise { +

+ {{ deletionAnnouncement }} +

+ { + it('focuses the review heading and then the confirmed result heading', async () => { + const request = vi.fn(async (path: string) => { + if (path === '/api/groups') return groups + if (path === '/api/groups/7/keys/import') { + return { group_id: 7, keys_added: 1, keys_duplicated: 0 } + } + throw new Error(`unexpected request: ${path}`) + }) as ApiClient['request'] + const { wrapper } = await mountExisting( + '/import?mode=existing&group_id=7', + request, + undefined, + document.body, + ) + + await wrapper.get('[data-test="keys"]').setValue('raw-key') + await wrapper.get('[data-test="existing-review"]').trigger('click') + await flushPromises() + expect(document.activeElement).toBe( + wrapper.get('[data-test="existing-review-heading"]').element, + ) + + await wrapper.get('[data-test="existing-submit"]').trigger('click') + await flushPromises() + expect(document.activeElement).toBe( + wrapper.get('[data-test="existing-result-heading"]').element, + ) + wrapper.unmount() + }) + it('preselects a positive route Group and imports exact raw keys without discovery or Group mutation', async () => { const requestMock = vi.fn(async (path: string, _options?: ApiRequestOptions) => { void _options diff --git a/web/src/features/import/ExistingGroupImport.vue b/web/src/features/import/ExistingGroupImport.vue index 17520b54..c9b46f31 100644 --- a/web/src/features/import/ExistingGroupImport.vue +++ b/web/src/features/import/ExistingGroupImport.vue @@ -1,7 +1,7 @@