chore: merge executor worktree (10-02 EmptyState) — kept 10-01 AppIcon.vue (resolvedPaths vs paths[name]; 10-01 is canonical)

This commit is contained in:
curo1305
2026-06-15 20:17:49 +02:00
3 changed files with 207 additions and 0 deletions
+38
View File
@@ -0,0 +1,38 @@
<template>
<div :class="containerClass">
<AppIcon v-if="icon" :name="icon" :class="iconClass" />
<p :class="headlineClass">{{ headline }}</p>
<p v-if="subtext" :class="subtextClass">{{ subtext }}</p>
<slot name="cta" />
</div>
</template>
<script>
import AppIcon from './AppIcon.vue'
export default {
name: 'EmptyState',
components: { AppIcon },
props: {
icon: { type: String, default: null },
headline: { type: String, required: true },
subtext: { type: String, default: '' },
size: { type: String, default: 'md' },
},
computed: {
containerClass() {
return this.size === 'sm'
? 'flex items-center gap-2 py-1 text-xs text-gray-400'
: 'text-center py-10 px-4'
},
iconClass() {
return this.size === 'sm' ? 'w-3.5 h-3.5 shrink-0' : 'w-8 h-8 mx-auto mb-3 text-gray-300'
},
headlineClass() {
return this.size === 'sm' ? '' : 'text-sm font-medium text-gray-500'
},
subtextClass() {
return this.size === 'sm' ? 'hidden' : 'text-xs text-gray-400 mt-1'
},
},
}
</script>
@@ -0,0 +1,80 @@
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import EmptyState from '../EmptyState.vue'
describe('EmptyState', () => {
it('renders headline text from the headline prop', () => {
const w = mount(EmptyState, {
props: { headline: 'Nothing here' },
global: { stubs: { AppIcon: true } },
})
expect(w.text()).toContain('Nothing here')
})
it('renders subtext when provided; omits when empty', () => {
const w = mount(EmptyState, {
props: { headline: 'Title', subtext: 'hello' },
global: { stubs: { AppIcon: true } },
})
expect(w.text()).toContain('hello')
const w2 = mount(EmptyState, {
props: { headline: 'Title' },
global: { stubs: { AppIcon: true } },
})
const paragraphs = w2.findAll('p')
expect(paragraphs.length).toBe(1)
})
it('renders AppIcon when icon prop is set; renders no icon when null', () => {
const w = mount(EmptyState, {
props: { headline: 'Title', icon: 'folder' },
global: { stubs: { AppIcon: true } },
})
expect(w.findComponent({ name: 'AppIcon' }).exists()).toBe(true)
const w2 = mount(EmptyState, {
props: { headline: 'Title', icon: null },
global: { stubs: { AppIcon: true } },
})
expect(w2.findComponent({ name: 'AppIcon' }).exists()).toBe(false)
})
it('renders nothing in the CTA area when #cta slot is empty', () => {
const w = mount(EmptyState, {
props: { headline: 'Title' },
global: { stubs: { AppIcon: true } },
})
expect(w.find('button').exists()).toBe(false)
expect(w.find('a').exists()).toBe(false)
})
it('renders the #cta slot content when provided', () => {
const w = mount(EmptyState, {
props: { headline: 'Title' },
slots: { cta: '<button data-test="cta-btn">Upload</button>' },
global: { stubs: { AppIcon: true } },
})
expect(w.find('[data-test="cta-btn"]').exists()).toBe(true)
})
it("size='sm' applies the sidebar micro layout (flex container with gap-2)", () => {
const w = mount(EmptyState, {
props: { headline: 'No folders yet', size: 'sm' },
global: { stubs: { AppIcon: true } },
})
const root = w.element
expect(root.classList.contains('flex')).toBe(true)
expect(root.classList.contains('gap-2')).toBe(true)
})
it('default size (md) applies the centered layout (text-center py-10)', () => {
const w = mount(EmptyState, {
props: { headline: 'Nothing here' },
global: { stubs: { AppIcon: true } },
})
const root = w.element
expect(root.classList.contains('text-center')).toBe(true)
expect(root.classList.contains('py-10')).toBe(true)
})
})