@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.
Files changed (79) hide show
  1. package/dist/composables/index.js +388 -0
  2. package/dist/composables/index.js.map +1 -0
  3. package/package.json +41 -0
  4. package/src/composables/index.ts +6 -0
  5. package/src/composables/useForm.test.ts +229 -0
  6. package/src/composables/useForm.ts +130 -0
  7. package/src/composables/useFormValidation.test.ts +189 -0
  8. package/src/composables/useFormValidation.ts +83 -0
  9. package/src/composables/useModal.property.test.ts +164 -0
  10. package/src/composables/useModal.ts +43 -0
  11. package/src/composables/useNotifications.test.ts +166 -0
  12. package/src/composables/useNotifications.ts +81 -0
  13. package/src/composables/useTable.property.test.ts +198 -0
  14. package/src/composables/useTable.ts +134 -0
  15. package/src/composables/useTabs.property.test.ts +247 -0
  16. package/src/composables/useTabs.ts +101 -0
  17. package/src/data/Chart.demo.vue +340 -0
  18. package/src/data/Chart.md +525 -0
  19. package/src/data/Chart.vue +133 -0
  20. package/src/data/DataList.md +80 -0
  21. package/src/data/DataList.test.ts +69 -0
  22. package/src/data/DataList.vue +46 -0
  23. package/src/data/SearchableSelect.md +107 -0
  24. package/src/data/SearchableSelect.vue +124 -0
  25. package/src/data/Table.demo.vue +296 -0
  26. package/src/data/Table.md +588 -0
  27. package/src/data/Table.property.test.ts +548 -0
  28. package/src/data/Table.test.ts +562 -0
  29. package/src/data/Table.unit.test.ts +544 -0
  30. package/src/data/Table.vue +321 -0
  31. package/src/data/index.ts +5 -0
  32. package/src/domain/BrandCard.md +81 -0
  33. package/src/domain/BrandCard.vue +63 -0
  34. package/src/domain/BrandSelector.md +84 -0
  35. package/src/domain/BrandSelector.vue +65 -0
  36. package/src/domain/ProductBadge.md +60 -0
  37. package/src/domain/ProductBadge.vue +47 -0
  38. package/src/domain/UserAvatar.md +84 -0
  39. package/src/domain/UserAvatar.vue +60 -0
  40. package/src/domain/domain-components.property.test.ts +449 -0
  41. package/src/domain/index.ts +4 -0
  42. package/src/forms/DateRange.demo.vue +273 -0
  43. package/src/forms/DateRange.md +337 -0
  44. package/src/forms/DateRange.vue +110 -0
  45. package/src/forms/JsonSchemaForm.demo.vue +549 -0
  46. package/src/forms/JsonSchemaForm.md +112 -0
  47. package/src/forms/JsonSchemaForm.property.test.ts +817 -0
  48. package/src/forms/JsonSchemaForm.test.ts +601 -0
  49. package/src/forms/JsonSchemaForm.unit.test.ts +801 -0
  50. package/src/forms/JsonSchemaForm.vue +615 -0
  51. package/src/forms/index.ts +3 -0
  52. package/src/index.ts +17 -0
  53. package/src/navigation/Breadcrumbs.demo.vue +142 -0
  54. package/src/navigation/Breadcrumbs.md +102 -0
  55. package/src/navigation/Breadcrumbs.test.ts +69 -0
  56. package/src/navigation/Breadcrumbs.vue +58 -0
  57. package/src/navigation/Stepper.demo.vue +337 -0
  58. package/src/navigation/Stepper.md +174 -0
  59. package/src/navigation/Stepper.vue +146 -0
  60. package/src/navigation/Tabs.demo.vue +293 -0
  61. package/src/navigation/Tabs.md +163 -0
  62. package/src/navigation/Tabs.test.ts +176 -0
  63. package/src/navigation/Tabs.vue +104 -0
  64. package/src/navigation/index.ts +5 -0
  65. package/src/overlays/Alert.demo.vue +377 -0
  66. package/src/overlays/Alert.md +248 -0
  67. package/src/overlays/Alert.test.ts +166 -0
  68. package/src/overlays/Alert.vue +70 -0
  69. package/src/overlays/Drawer.md +140 -0
  70. package/src/overlays/Drawer.test.ts +92 -0
  71. package/src/overlays/Drawer.vue +76 -0
  72. package/src/overlays/Modal.demo.vue +149 -0
  73. package/src/overlays/Modal.md +385 -0
  74. package/src/overlays/Modal.test.ts +128 -0
  75. package/src/overlays/Modal.vue +86 -0
  76. package/src/overlays/Notification.md +150 -0
  77. package/src/overlays/Notification.test.ts +96 -0
  78. package/src/overlays/Notification.vue +58 -0
  79. 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
+ });
@@ -0,0 +1,4 @@
1
+ export { default as BrandSelector } from './BrandSelector.vue';
2
+ export { default as BrandCard } from './BrandCard.vue';
3
+ export { default as UserAvatar } from './UserAvatar.vue';
4
+ export { default as ProductBadge } from './ProductBadge.vue';