feat: guard CommandBar busy actions (AEG-V16-019)

Keep action order while preventing disabled or busy actions from emitting a command. Preserve test evidence; status remains IN_PROGRESS pending predecessor acceptance.
This commit is contained in:
2026-08-08 13:07:54 +09:00
parent f2b7b40668
commit 30c941cc5b
4 changed files with 62 additions and 2 deletions
@@ -1,8 +1,13 @@
<script setup lang="ts">
import KsButton from './KsButton.vue'
export interface CommandBarAction { id: string; label: string; severity?: 'primary'|'secondary'|'success'|'info'|'warning'|'danger'; disabled?: boolean; busy?: boolean }
defineProps<{ actions: readonly CommandBarAction[]; ariaLabel?: string }>()
const props = defineProps<{ actions: readonly CommandBarAction[]; ariaLabel?: string }>()
const emit = defineEmits<{ execute: [actionId: string] }>()
function execute(action: CommandBarAction): void {
if (action.disabled || action.busy) return
emit('execute', action.id)
}
</script>
<template><nav class="ks-command-bar" :aria-label="ariaLabel ?? 'Page actions'"><KsButton v-for="action in actions" :key="action.id" :label="action.label" :severity="action.severity" :disabled="action.disabled" :loading="action.busy" @click="emit('execute', action.id)" /></nav></template>
<template><nav class="ks-command-bar" :aria-label="ariaLabel ?? 'Page actions'" :aria-busy="actions.some(action => action.busy) || undefined"><KsButton v-for="action in actions" :key="action.id" :label="action.label" :severity="action.severity" :disabled="action.disabled" :loading="action.busy" @click="execute(action)" /></nav></template>
<style scoped>.ks-command-bar{display:flex;gap:var(--ks-space-2);flex-wrap:wrap;justify-content:flex-end}</style>
@@ -0,0 +1,28 @@
import { mount } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
import KsCommandBar from '../KsCommandBar.vue'
const actions = [
{ id: 'save', label: 'Save' },
{ id: 'review', label: 'Review', disabled: true },
{ id: 'publish', label: 'Publish', busy: true }
] as const
describe('KsCommandBar', () => {
it('preserves supplied action order and executes only enabled, idle actions', async () => {
const wrapper = mount(KsCommandBar, {
props: { actions, ariaLabel: 'Decision actions' },
global: { stubs: { KsButton: { name: 'KsButton', props: ['label'], template: '<button>{{ label }}</button>' } } }
})
expect(wrapper.findAll('button').map(button => button.text())).toEqual(['Save', 'Review', 'Publish'])
const buttons = wrapper.findAllComponents({ name: 'KsButton' })
buttons[0].vm.$emit('click')
buttons[1].vm.$emit('click')
buttons[2].vm.$emit('click')
await wrapper.vm.$nextTick()
expect(wrapper.emitted('execute')).toEqual([['save']])
expect(wrapper.get('nav').attributes()).toMatchObject({ 'aria-label': 'Decision actions', 'aria-busy': 'true' })
})
})