@htlkg/components 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/composables/index.js +388 -0
- package/dist/composables/index.js.map +1 -0
- package/package.json +41 -0
- package/src/composables/index.ts +6 -0
- package/src/composables/useForm.test.ts +229 -0
- package/src/composables/useForm.ts +130 -0
- package/src/composables/useFormValidation.test.ts +189 -0
- package/src/composables/useFormValidation.ts +83 -0
- package/src/composables/useModal.property.test.ts +164 -0
- package/src/composables/useModal.ts +43 -0
- package/src/composables/useNotifications.test.ts +166 -0
- package/src/composables/useNotifications.ts +81 -0
- package/src/composables/useTable.property.test.ts +198 -0
- package/src/composables/useTable.ts +134 -0
- package/src/composables/useTabs.property.test.ts +247 -0
- package/src/composables/useTabs.ts +101 -0
- package/src/data/Chart.demo.vue +340 -0
- package/src/data/Chart.md +525 -0
- package/src/data/Chart.vue +133 -0
- package/src/data/DataList.md +80 -0
- package/src/data/DataList.test.ts +69 -0
- package/src/data/DataList.vue +46 -0
- package/src/data/SearchableSelect.md +107 -0
- package/src/data/SearchableSelect.vue +124 -0
- package/src/data/Table.demo.vue +296 -0
- package/src/data/Table.md +588 -0
- package/src/data/Table.property.test.ts +548 -0
- package/src/data/Table.test.ts +562 -0
- package/src/data/Table.unit.test.ts +544 -0
- package/src/data/Table.vue +321 -0
- package/src/data/index.ts +5 -0
- package/src/domain/BrandCard.md +81 -0
- package/src/domain/BrandCard.vue +63 -0
- package/src/domain/BrandSelector.md +84 -0
- package/src/domain/BrandSelector.vue +65 -0
- package/src/domain/ProductBadge.md +60 -0
- package/src/domain/ProductBadge.vue +47 -0
- package/src/domain/UserAvatar.md +84 -0
- package/src/domain/UserAvatar.vue +60 -0
- package/src/domain/domain-components.property.test.ts +449 -0
- package/src/domain/index.ts +4 -0
- package/src/forms/DateRange.demo.vue +273 -0
- package/src/forms/DateRange.md +337 -0
- package/src/forms/DateRange.vue +110 -0
- package/src/forms/JsonSchemaForm.demo.vue +549 -0
- package/src/forms/JsonSchemaForm.md +112 -0
- package/src/forms/JsonSchemaForm.property.test.ts +817 -0
- package/src/forms/JsonSchemaForm.test.ts +601 -0
- package/src/forms/JsonSchemaForm.unit.test.ts +801 -0
- package/src/forms/JsonSchemaForm.vue +615 -0
- package/src/forms/index.ts +3 -0
- package/src/index.ts +17 -0
- package/src/navigation/Breadcrumbs.demo.vue +142 -0
- package/src/navigation/Breadcrumbs.md +102 -0
- package/src/navigation/Breadcrumbs.test.ts +69 -0
- package/src/navigation/Breadcrumbs.vue +58 -0
- package/src/navigation/Stepper.demo.vue +337 -0
- package/src/navigation/Stepper.md +174 -0
- package/src/navigation/Stepper.vue +146 -0
- package/src/navigation/Tabs.demo.vue +293 -0
- package/src/navigation/Tabs.md +163 -0
- package/src/navigation/Tabs.test.ts +176 -0
- package/src/navigation/Tabs.vue +104 -0
- package/src/navigation/index.ts +5 -0
- package/src/overlays/Alert.demo.vue +377 -0
- package/src/overlays/Alert.md +248 -0
- package/src/overlays/Alert.test.ts +166 -0
- package/src/overlays/Alert.vue +70 -0
- package/src/overlays/Drawer.md +140 -0
- package/src/overlays/Drawer.test.ts +92 -0
- package/src/overlays/Drawer.vue +76 -0
- package/src/overlays/Modal.demo.vue +149 -0
- package/src/overlays/Modal.md +385 -0
- package/src/overlays/Modal.test.ts +128 -0
- package/src/overlays/Modal.vue +86 -0
- package/src/overlays/Notification.md +150 -0
- package/src/overlays/Notification.test.ts +96 -0
- package/src/overlays/Notification.vue +58 -0
- package/src/overlays/index.ts +4 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue';
|
|
3
|
+
import { uiTag } from '@hotelinking/ui';
|
|
4
|
+
|
|
5
|
+
interface Product {
|
|
6
|
+
name: string;
|
|
7
|
+
status?: 'enabled' | 'disabled' | 'pending';
|
|
8
|
+
version?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
product: Product;
|
|
13
|
+
showVersion?: boolean;
|
|
14
|
+
loading?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
18
|
+
showVersion: false,
|
|
19
|
+
loading: false
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const statusColor = computed(() => {
|
|
23
|
+
switch (props.product.status) {
|
|
24
|
+
case 'enabled': return 'green' as const;
|
|
25
|
+
case 'disabled': return 'gray' as const;
|
|
26
|
+
case 'pending': return 'yellow' as const;
|
|
27
|
+
default: return 'gray' as const;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const badgeText = computed(() => {
|
|
32
|
+
let text = props.product.name;
|
|
33
|
+
if (props.showVersion && props.product.version) {
|
|
34
|
+
text += ` v${props.product.version}`;
|
|
35
|
+
}
|
|
36
|
+
return text;
|
|
37
|
+
});
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<template>
|
|
41
|
+
<uiTag
|
|
42
|
+
:loading="loading"
|
|
43
|
+
:color="statusColor"
|
|
44
|
+
:text="badgeText"
|
|
45
|
+
size="medium"
|
|
46
|
+
/>
|
|
47
|
+
</template>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# UserAvatar Component
|
|
2
|
+
|
|
3
|
+
A user avatar component displaying profile pictures or initials.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Image support**: Display user profile pictures
|
|
8
|
+
- **Initials fallback**: Auto-generates initials from name
|
|
9
|
+
- **Three sizes**: Small, medium, and large
|
|
10
|
+
- **Exposed methods**: Access to user data and initials
|
|
11
|
+
|
|
12
|
+
## Import
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
import { UserAvatar } from '@htlkg/components';
|
|
16
|
+
// or
|
|
17
|
+
import { UserAvatar } from '@htlkg/components/domain';
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Props
|
|
21
|
+
|
|
22
|
+
| Prop | Type | Default | Description |
|
|
23
|
+
|------|------|---------|-------------|
|
|
24
|
+
| `user` | `User` | required | User information |
|
|
25
|
+
| `size` | `'small' \| 'medium' \| 'large'` | `'medium'` | Avatar size |
|
|
26
|
+
|
|
27
|
+
### User Interface
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
interface User {
|
|
31
|
+
username?: string;
|
|
32
|
+
email?: string;
|
|
33
|
+
avatar?: string;
|
|
34
|
+
name?: string;
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Exposed Methods
|
|
39
|
+
|
|
40
|
+
| Method | Description |
|
|
41
|
+
|--------|-------------|
|
|
42
|
+
| `getInitials()` | Get computed initials |
|
|
43
|
+
| `getUser()` | Get user object |
|
|
44
|
+
|
|
45
|
+
## Usage Examples
|
|
46
|
+
|
|
47
|
+
### With Avatar Image
|
|
48
|
+
|
|
49
|
+
```vue
|
|
50
|
+
<script setup>
|
|
51
|
+
import { UserAvatar } from '@htlkg/components';
|
|
52
|
+
|
|
53
|
+
const user = {
|
|
54
|
+
name: 'John Doe',
|
|
55
|
+
email: 'john@example.com',
|
|
56
|
+
avatar: 'https://example.com/avatar.jpg'
|
|
57
|
+
};
|
|
58
|
+
</script>
|
|
59
|
+
|
|
60
|
+
<template>
|
|
61
|
+
<UserAvatar :user="user" size="large" />
|
|
62
|
+
</template>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### With Initials
|
|
66
|
+
|
|
67
|
+
```vue
|
|
68
|
+
<script setup>
|
|
69
|
+
import { UserAvatar } from '@htlkg/components';
|
|
70
|
+
|
|
71
|
+
const user = {
|
|
72
|
+
name: 'Jane Smith',
|
|
73
|
+
email: 'jane@example.com'
|
|
74
|
+
};
|
|
75
|
+
</script>
|
|
76
|
+
|
|
77
|
+
<template>
|
|
78
|
+
<UserAvatar :user="user" />
|
|
79
|
+
</template>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Demo
|
|
83
|
+
|
|
84
|
+
See the [UserAvatar demo page](/components/user-avatar) for interactive examples.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue';
|
|
3
|
+
|
|
4
|
+
interface User {
|
|
5
|
+
username?: string;
|
|
6
|
+
email?: string;
|
|
7
|
+
avatar?: string;
|
|
8
|
+
name?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
user: User;
|
|
13
|
+
size?: 'small' | 'medium' | 'large';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
17
|
+
size: 'medium'
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const initials = computed(() => {
|
|
21
|
+
const name = props.user.name || props.user.username || props.user.email || '';
|
|
22
|
+
const parts = name.split(' ');
|
|
23
|
+
if (parts.length >= 2) {
|
|
24
|
+
return `${parts[0][0]}${parts[1][0]}`.toUpperCase();
|
|
25
|
+
}
|
|
26
|
+
return name.substring(0, 2).toUpperCase();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const sizeClasses = computed(() => {
|
|
30
|
+
const sizes = {
|
|
31
|
+
small: 'w-8 h-8 text-xs',
|
|
32
|
+
medium: 'w-10 h-10 text-sm',
|
|
33
|
+
large: 'w-14 h-14 text-lg'
|
|
34
|
+
};
|
|
35
|
+
return sizes[props.size];
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Expose methods for parent components
|
|
39
|
+
defineExpose({
|
|
40
|
+
getInitials: () => initials.value,
|
|
41
|
+
getUser: () => props.user
|
|
42
|
+
});
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<template>
|
|
46
|
+
<div
|
|
47
|
+
class="flex items-center justify-center rounded-full overflow-hidden bg-blue-500 text-white font-medium"
|
|
48
|
+
:class="sizeClasses"
|
|
49
|
+
>
|
|
50
|
+
<img
|
|
51
|
+
v-if="user.avatar"
|
|
52
|
+
:src="user.avatar"
|
|
53
|
+
:alt="user.username || user.email"
|
|
54
|
+
class="w-full h-full object-cover"
|
|
55
|
+
/>
|
|
56
|
+
<div v-else class="flex items-center justify-center w-full h-full">
|
|
57
|
+
{{ initials }}
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</template>
|
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import * as fc from 'fast-check';
|
|
3
|
+
import { mount } from '@vue/test-utils';
|
|
4
|
+
import BrandCard from './BrandCard.vue';
|
|
5
|
+
import BrandSelector from './BrandSelector.vue';
|
|
6
|
+
import UserAvatar from './UserAvatar.vue';
|
|
7
|
+
import ProductBadge from './ProductBadge.vue';
|
|
8
|
+
|
|
9
|
+
// Mock @hotelinking/ui components
|
|
10
|
+
vi.mock('@hotelinking/ui', () => ({
|
|
11
|
+
uiCard: {
|
|
12
|
+
name: 'uiCard',
|
|
13
|
+
props: ['id', 'name', 'type', 'logo', 'tags', 'loading'],
|
|
14
|
+
emits: ['card-selected'],
|
|
15
|
+
template: `
|
|
16
|
+
<div
|
|
17
|
+
class="ui-card"
|
|
18
|
+
:data-id="id"
|
|
19
|
+
:data-name="name"
|
|
20
|
+
:data-type="type"
|
|
21
|
+
:data-logo="logo"
|
|
22
|
+
:data-loading="loading"
|
|
23
|
+
@click="$emit('card-selected', { id, name })"
|
|
24
|
+
>
|
|
25
|
+
<slot />
|
|
26
|
+
<div v-for="tag in tags" :key="tag.name" :data-tag-color="tag.color">{{ tag.name }}</div>
|
|
27
|
+
</div>
|
|
28
|
+
`
|
|
29
|
+
},
|
|
30
|
+
uiTag: {
|
|
31
|
+
name: 'uiTag',
|
|
32
|
+
props: ['text', 'color', 'size', 'loading'],
|
|
33
|
+
template: `
|
|
34
|
+
<span
|
|
35
|
+
class="ui-tag"
|
|
36
|
+
:data-color="color"
|
|
37
|
+
:data-size="size"
|
|
38
|
+
:data-loading="loading"
|
|
39
|
+
>{{ text }}</span>
|
|
40
|
+
`
|
|
41
|
+
}
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Feature: htlkg-modular-architecture, Property 11: Domain components integrate with data hooks
|
|
46
|
+
* Validates: Requirements 13.1, 13.5
|
|
47
|
+
*/
|
|
48
|
+
describe('Domain components data hook integration', () => {
|
|
49
|
+
it('BrandSelector should accept brands from data hooks', () => {
|
|
50
|
+
fc.assert(
|
|
51
|
+
fc.property(
|
|
52
|
+
fc.array(
|
|
53
|
+
fc.record({
|
|
54
|
+
id: fc.string({ minLength: 1 }),
|
|
55
|
+
name: fc.string({ minLength: 1 }),
|
|
56
|
+
logo: fc.option(fc.webUrl()),
|
|
57
|
+
type: fc.option(fc.string())
|
|
58
|
+
}),
|
|
59
|
+
{ minLength: 0, maxLength: 20 }
|
|
60
|
+
),
|
|
61
|
+
fc.boolean(),
|
|
62
|
+
(brands, loading) => {
|
|
63
|
+
// Property: BrandSelector should accept brands array from data hooks
|
|
64
|
+
const wrapper = mount(BrandSelector, {
|
|
65
|
+
props: {
|
|
66
|
+
brands,
|
|
67
|
+
loading
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Property: Loading state should be reflected in UI
|
|
72
|
+
if (loading) {
|
|
73
|
+
expect(wrapper.text()).toContain('Loading brands');
|
|
74
|
+
} else {
|
|
75
|
+
// Property: All brands should be rendered
|
|
76
|
+
const cards = wrapper.findAll('.ui-card');
|
|
77
|
+
expect(cards.length).toBe(brands.length);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
),
|
|
81
|
+
{ numRuns: 100 }
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('BrandSelector should support v-model for selection', () => {
|
|
86
|
+
fc.assert(
|
|
87
|
+
fc.property(
|
|
88
|
+
fc.array(
|
|
89
|
+
fc.record({
|
|
90
|
+
id: fc.string({ minLength: 1 }),
|
|
91
|
+
name: fc.string({ minLength: 1 }),
|
|
92
|
+
logo: fc.option(fc.webUrl())
|
|
93
|
+
}),
|
|
94
|
+
{ minLength: 1, maxLength: 10 }
|
|
95
|
+
),
|
|
96
|
+
(brands) => {
|
|
97
|
+
const wrapper = mount(BrandSelector, {
|
|
98
|
+
props: {
|
|
99
|
+
brands,
|
|
100
|
+
modelValue: null
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Property: Selecting a brand should emit update:modelValue
|
|
105
|
+
const firstCard = wrapper.find('.ui-card');
|
|
106
|
+
firstCard.trigger('click');
|
|
107
|
+
|
|
108
|
+
const updateEvents = wrapper.emitted('update:modelValue');
|
|
109
|
+
expect(updateEvents).toBeDefined();
|
|
110
|
+
|
|
111
|
+
if (updateEvents && updateEvents.length > 0) {
|
|
112
|
+
const selectedBrand = updateEvents[0][0];
|
|
113
|
+
expect(selectedBrand).toBeDefined();
|
|
114
|
+
expect(brands).toContainEqual(selectedBrand);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
),
|
|
118
|
+
{ numRuns: 100 }
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('BrandSelector should expose methods for parent components', () => {
|
|
123
|
+
fc.assert(
|
|
124
|
+
fc.property(
|
|
125
|
+
fc.array(
|
|
126
|
+
fc.record({
|
|
127
|
+
id: fc.string({ minLength: 1 }),
|
|
128
|
+
name: fc.string({ minLength: 1 })
|
|
129
|
+
}),
|
|
130
|
+
{ minLength: 1, maxLength: 5 }
|
|
131
|
+
),
|
|
132
|
+
(brands) => {
|
|
133
|
+
const selectedBrand = brands[0];
|
|
134
|
+
const wrapper = mount(BrandSelector, {
|
|
135
|
+
props: {
|
|
136
|
+
brands,
|
|
137
|
+
modelValue: selectedBrand
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Property: Component should expose getSelected method
|
|
142
|
+
expect(wrapper.vm.getSelected).toBeDefined();
|
|
143
|
+
expect(wrapper.vm.getSelected()).toEqual(selectedBrand);
|
|
144
|
+
|
|
145
|
+
// Property: Component should expose clearSelection method
|
|
146
|
+
expect(wrapper.vm.clearSelection).toBeDefined();
|
|
147
|
+
wrapper.vm.clearSelection();
|
|
148
|
+
|
|
149
|
+
const clearEvents = wrapper.emitted('update:modelValue');
|
|
150
|
+
expect(clearEvents).toBeDefined();
|
|
151
|
+
if (clearEvents && clearEvents.length > 0) {
|
|
152
|
+
expect(clearEvents[clearEvents.length - 1][0]).toBeNull();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
),
|
|
156
|
+
{ numRuns: 100 }
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Feature: htlkg-modular-architecture, Property 12: Domain components display data correctly
|
|
163
|
+
* Validates: Requirements 13.2, 13.3, 13.4
|
|
164
|
+
*/
|
|
165
|
+
describe('Domain components data display', () => {
|
|
166
|
+
it('BrandCard should display brand data with appropriate styling', () => {
|
|
167
|
+
fc.assert(
|
|
168
|
+
fc.property(
|
|
169
|
+
fc.record({
|
|
170
|
+
id: fc.string({ minLength: 1 }),
|
|
171
|
+
name: fc.string({ minLength: 1 }),
|
|
172
|
+
logo: fc.option(fc.webUrl()),
|
|
173
|
+
status: fc.constantFrom('active', 'inactive', 'maintenance', 'suspended'),
|
|
174
|
+
timezone: fc.option(fc.string())
|
|
175
|
+
}),
|
|
176
|
+
fc.boolean(),
|
|
177
|
+
(brand, showStatus) => {
|
|
178
|
+
const wrapper = mount(BrandCard, {
|
|
179
|
+
props: {
|
|
180
|
+
brand,
|
|
181
|
+
showStatus
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Property: Brand name should be displayed
|
|
186
|
+
const card = wrapper.find('.ui-card');
|
|
187
|
+
expect(card.attributes('data-name')).toBe(brand.name);
|
|
188
|
+
|
|
189
|
+
// Property: Brand ID should be set
|
|
190
|
+
expect(card.attributes('data-id')).toBe(brand.id);
|
|
191
|
+
|
|
192
|
+
// Property: Status should be displayed when showStatus is true
|
|
193
|
+
if (showStatus && brand.status) {
|
|
194
|
+
const tags = wrapper.findAll('[data-tag-color]');
|
|
195
|
+
expect(tags.length).toBeGreaterThan(0);
|
|
196
|
+
|
|
197
|
+
// Property: Status color should match status type
|
|
198
|
+
const statusColorMap: Record<string, string> = {
|
|
199
|
+
active: 'green',
|
|
200
|
+
inactive: 'gray',
|
|
201
|
+
maintenance: 'yellow',
|
|
202
|
+
suspended: 'red'
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const expectedColor = statusColorMap[brand.status];
|
|
206
|
+
const statusTag = tags.find(tag => tag.attributes('data-tag-color') === expectedColor);
|
|
207
|
+
expect(statusTag).toBeDefined();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
),
|
|
211
|
+
{ numRuns: 100 }
|
|
212
|
+
);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('BrandCard should handle missing optional fields gracefully', () => {
|
|
216
|
+
fc.assert(
|
|
217
|
+
fc.property(
|
|
218
|
+
fc.record({
|
|
219
|
+
id: fc.string({ minLength: 1 }),
|
|
220
|
+
name: fc.string({ minLength: 1 })
|
|
221
|
+
}),
|
|
222
|
+
(brand) => {
|
|
223
|
+
// Property: Component should render without optional fields
|
|
224
|
+
const wrapper = mount(BrandCard, {
|
|
225
|
+
props: {
|
|
226
|
+
brand
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const card = wrapper.find('.ui-card');
|
|
231
|
+
expect(card.exists()).toBe(true);
|
|
232
|
+
expect(card.attributes('data-name')).toBe(brand.name);
|
|
233
|
+
}
|
|
234
|
+
),
|
|
235
|
+
{ numRuns: 100 }
|
|
236
|
+
);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('UserAvatar should display user data with fallback to initials', () => {
|
|
240
|
+
fc.assert(
|
|
241
|
+
fc.property(
|
|
242
|
+
fc.record({
|
|
243
|
+
username: fc.option(fc.string({ minLength: 1 })),
|
|
244
|
+
email: fc.option(fc.emailAddress()),
|
|
245
|
+
avatar: fc.option(fc.webUrl()),
|
|
246
|
+
name: fc.option(fc.string({ minLength: 2 }))
|
|
247
|
+
}),
|
|
248
|
+
fc.constantFrom('small', 'medium', 'large'),
|
|
249
|
+
(user, size) => {
|
|
250
|
+
const wrapper = mount(UserAvatar, {
|
|
251
|
+
props: {
|
|
252
|
+
user,
|
|
253
|
+
size
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// Property: Component should always render
|
|
258
|
+
expect(wrapper.find('div').exists()).toBe(true);
|
|
259
|
+
|
|
260
|
+
// Property: If avatar exists, image should be rendered
|
|
261
|
+
if (user.avatar) {
|
|
262
|
+
const img = wrapper.find('img');
|
|
263
|
+
expect(img.exists()).toBe(true);
|
|
264
|
+
expect(img.attributes('src')).toBe(user.avatar);
|
|
265
|
+
} else {
|
|
266
|
+
// Property: If no avatar, initials should be displayed
|
|
267
|
+
const initials = wrapper.vm.getInitials();
|
|
268
|
+
expect(initials).toBeDefined();
|
|
269
|
+
expect(initials.length).toBeGreaterThan(0);
|
|
270
|
+
expect(initials.length).toBeLessThanOrEqual(2);
|
|
271
|
+
expect(wrapper.text()).toContain(initials);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Property: Size classes should be applied correctly
|
|
275
|
+
const sizeClasses = {
|
|
276
|
+
small: 'w-8 h-8',
|
|
277
|
+
medium: 'w-10 h-10',
|
|
278
|
+
large: 'w-14 h-14'
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const expectedSizeClass = sizeClasses[size];
|
|
282
|
+
expect(wrapper.find('div').classes()).toContain(expectedSizeClass.split(' ')[0]);
|
|
283
|
+
}
|
|
284
|
+
),
|
|
285
|
+
{ numRuns: 100 }
|
|
286
|
+
);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('UserAvatar should generate correct initials from user data', () => {
|
|
290
|
+
fc.assert(
|
|
291
|
+
fc.property(
|
|
292
|
+
fc.record({
|
|
293
|
+
name: fc.string({ minLength: 2 })
|
|
294
|
+
}),
|
|
295
|
+
(user) => {
|
|
296
|
+
const wrapper = mount(UserAvatar, {
|
|
297
|
+
props: { user }
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
const initials = wrapper.vm.getInitials();
|
|
301
|
+
|
|
302
|
+
// Property: Initials should be uppercase
|
|
303
|
+
expect(initials).toBe(initials.toUpperCase());
|
|
304
|
+
|
|
305
|
+
// Property: Initials should be 1-2 characters
|
|
306
|
+
expect(initials.length).toBeGreaterThan(0);
|
|
307
|
+
expect(initials.length).toBeLessThanOrEqual(2);
|
|
308
|
+
|
|
309
|
+
// Property: Initials should come from the name
|
|
310
|
+
const nameParts = user.name.split(' ');
|
|
311
|
+
if (nameParts.length >= 2) {
|
|
312
|
+
// Two-word names should use first letter of each word
|
|
313
|
+
expect(initials).toBe(`${nameParts[0][0]}${nameParts[1][0]}`.toUpperCase());
|
|
314
|
+
} else {
|
|
315
|
+
// Single-word names should use first two characters
|
|
316
|
+
expect(initials).toBe(user.name.substring(0, 2).toUpperCase());
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
),
|
|
320
|
+
{ numRuns: 100 }
|
|
321
|
+
);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('ProductBadge should display product data with appropriate styling', () => {
|
|
325
|
+
fc.assert(
|
|
326
|
+
fc.property(
|
|
327
|
+
fc.record({
|
|
328
|
+
name: fc.string({ minLength: 1 }),
|
|
329
|
+
status: fc.constantFrom('enabled', 'disabled', 'pending'),
|
|
330
|
+
version: fc.option(fc.string({ minLength: 1 }))
|
|
331
|
+
}),
|
|
332
|
+
fc.boolean(),
|
|
333
|
+
(product, showVersion) => {
|
|
334
|
+
const wrapper = mount(ProductBadge, {
|
|
335
|
+
props: {
|
|
336
|
+
product,
|
|
337
|
+
showVersion
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// Property: Product name should be displayed
|
|
342
|
+
const badge = wrapper.find('.ui-tag');
|
|
343
|
+
expect(badge.exists()).toBe(true);
|
|
344
|
+
expect(badge.text()).toContain(product.name);
|
|
345
|
+
|
|
346
|
+
// Property: Version should be displayed when showVersion is true
|
|
347
|
+
if (showVersion && product.version) {
|
|
348
|
+
expect(badge.text()).toContain(`v${product.version}`);
|
|
349
|
+
} else if (!showVersion || !product.version) {
|
|
350
|
+
// Property: Version should not be displayed when showVersion is false
|
|
351
|
+
expect(badge.text()).not.toContain('v');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Property: Status color should match status type
|
|
355
|
+
const statusColorMap: Record<string, string> = {
|
|
356
|
+
enabled: 'green',
|
|
357
|
+
disabled: 'gray',
|
|
358
|
+
pending: 'yellow'
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const expectedColor = statusColorMap[product.status || 'gray'];
|
|
362
|
+
expect(badge.attributes('data-color')).toBe(expectedColor);
|
|
363
|
+
}
|
|
364
|
+
),
|
|
365
|
+
{ numRuns: 100 }
|
|
366
|
+
);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it('ProductBadge should handle missing status gracefully', () => {
|
|
370
|
+
fc.assert(
|
|
371
|
+
fc.property(
|
|
372
|
+
fc.record({
|
|
373
|
+
name: fc.string({ minLength: 1 })
|
|
374
|
+
}),
|
|
375
|
+
(product) => {
|
|
376
|
+
// Property: Component should render without status
|
|
377
|
+
const wrapper = mount(ProductBadge, {
|
|
378
|
+
props: {
|
|
379
|
+
product
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
const badge = wrapper.find('.ui-tag');
|
|
384
|
+
expect(badge.exists()).toBe(true);
|
|
385
|
+
expect(badge.text()).toContain(product.name);
|
|
386
|
+
|
|
387
|
+
// Property: Default color should be gray when status is missing
|
|
388
|
+
expect(badge.attributes('data-color')).toBe('gray');
|
|
389
|
+
}
|
|
390
|
+
),
|
|
391
|
+
{ numRuns: 100 }
|
|
392
|
+
);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it('Domain components should emit events correctly', () => {
|
|
396
|
+
fc.assert(
|
|
397
|
+
fc.property(
|
|
398
|
+
fc.record({
|
|
399
|
+
id: fc.string({ minLength: 1 }),
|
|
400
|
+
name: fc.string({ minLength: 1 }),
|
|
401
|
+
status: fc.constantFrom('active', 'inactive', 'maintenance', 'suspended')
|
|
402
|
+
}),
|
|
403
|
+
(brand) => {
|
|
404
|
+
const wrapper = mount(BrandCard, {
|
|
405
|
+
props: {
|
|
406
|
+
brand
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// Property: Clicking card should emit click event with brand data
|
|
411
|
+
const card = wrapper.find('.ui-card');
|
|
412
|
+
card.trigger('click');
|
|
413
|
+
|
|
414
|
+
const clickEvents = wrapper.emitted('click');
|
|
415
|
+
expect(clickEvents).toBeDefined();
|
|
416
|
+
|
|
417
|
+
if (clickEvents && clickEvents.length > 0) {
|
|
418
|
+
const emittedBrand = clickEvents[0][0];
|
|
419
|
+
expect(emittedBrand).toEqual(brand);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
),
|
|
423
|
+
{ numRuns: 100 }
|
|
424
|
+
);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it('Domain components should expose data access methods', () => {
|
|
428
|
+
fc.assert(
|
|
429
|
+
fc.property(
|
|
430
|
+
fc.record({
|
|
431
|
+
id: fc.string({ minLength: 1 }),
|
|
432
|
+
name: fc.string({ minLength: 1 })
|
|
433
|
+
}),
|
|
434
|
+
(brand) => {
|
|
435
|
+
const wrapper = mount(BrandCard, {
|
|
436
|
+
props: {
|
|
437
|
+
brand
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
// Property: Component should expose getBrand method
|
|
442
|
+
expect(wrapper.vm.getBrand).toBeDefined();
|
|
443
|
+
expect(wrapper.vm.getBrand()).toEqual(brand);
|
|
444
|
+
}
|
|
445
|
+
),
|
|
446
|
+
{ numRuns: 100 }
|
|
447
|
+
);
|
|
448
|
+
});
|
|
449
|
+
});
|