@fiscozen/card-list 1.0.0

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 (37) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE +21 -0
  3. package/README.md +59 -0
  4. package/dist/card-list.js +427 -0
  5. package/dist/card-list.umd.cjs +1 -0
  6. package/dist/index.d.ts +1 -0
  7. package/dist/src/FzCardList.vue.d.ts +18 -0
  8. package/dist/src/FzCardListItem.vue.d.ts +18 -0
  9. package/dist/src/components/FzCardActionLink.vue.d.ts +17 -0
  10. package/dist/src/components/FzCardFooter.vue.d.ts +13 -0
  11. package/dist/src/components/FzCardHeader.vue.d.ts +13 -0
  12. package/dist/src/components/FzCardMultiActions.vue.d.ts +18 -0
  13. package/dist/src/components/FzCardNoAction.vue.d.ts +13 -0
  14. package/dist/src/components/FzCardTitle.vue.d.ts +13 -0
  15. package/dist/src/components/types.d.ts +34 -0
  16. package/dist/src/index.d.ts +3 -0
  17. package/dist/src/types.d.ts +64 -0
  18. package/package.json +49 -0
  19. package/src/FzCardList.vue +54 -0
  20. package/src/FzCardListItem.vue +71 -0
  21. package/src/__tests__/FzCardList.spec.ts +322 -0
  22. package/src/__tests__/FzCardListItem.spec.ts +458 -0
  23. package/src/__tests__/__snapshots__/FzCardList.spec.ts.snap +52 -0
  24. package/src/__tests__/__snapshots__/FzCardListItem.spec.ts.snap +90 -0
  25. package/src/components/FzCardActionLink.vue +87 -0
  26. package/src/components/FzCardFooter.vue +13 -0
  27. package/src/components/FzCardHeader.vue +33 -0
  28. package/src/components/FzCardMultiActions.vue +75 -0
  29. package/src/components/FzCardNoAction.vue +29 -0
  30. package/src/components/FzCardTitle.vue +29 -0
  31. package/src/components/types.ts +45 -0
  32. package/src/index.ts +3 -0
  33. package/src/types.ts +74 -0
  34. package/tsconfig.json +4 -0
  35. package/tsconfig.tsbuildinfo +1 -0
  36. package/vite.config.ts +42 -0
  37. package/vitest.config.ts +26 -0
@@ -0,0 +1,13 @@
1
+ import { FzCardTitleProps } from './types';
2
+
3
+ declare const _default: import('vue').DefineComponent<import('vue').ExtractPropTypes<__VLS_TypePropsToRuntimeProps<FzCardTitleProps>>, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<import('vue').ExtractPropTypes<__VLS_TypePropsToRuntimeProps<FzCardTitleProps>>> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
4
+ export default _default;
5
+ type __VLS_NonUndefinedable<T> = T extends undefined ? never : T;
6
+ type __VLS_TypePropsToRuntimeProps<T> = {
7
+ [K in keyof T]-?: {} extends Pick<T, K> ? {
8
+ type: import('vue').PropType<__VLS_NonUndefinedable<T[K]>>;
9
+ } : {
10
+ type: import('vue').PropType<T[K]>;
11
+ required: true;
12
+ };
13
+ };
@@ -0,0 +1,34 @@
1
+ import { FzActionLinkProps, FzActionProps } from '@fiscozen/action';
2
+ import { FzBadgeTone } from '@fiscozen/badge';
3
+
4
+ export type FzCardBadge = {
5
+ text: string;
6
+ tone: FzBadgeTone;
7
+ };
8
+ export interface FzCardTitleProps {
9
+ showIndicator?: boolean;
10
+ title: string;
11
+ titleId?: string;
12
+ }
13
+ export interface FzCardHeaderProps extends FzCardTitleProps {
14
+ hasTitleOnly?: boolean;
15
+ value?: string;
16
+ }
17
+ export interface FzCardListFooterProps {
18
+ descriptions?: string[];
19
+ }
20
+ export interface FzCardNoActionProps extends FzCardHeaderProps, FzCardListFooterProps {
21
+ badge?: FzCardBadge;
22
+ }
23
+ export interface FzCardSingleActionProps extends FzCardHeaderProps, FzCardListFooterProps, FzCardNoActionProps {
24
+ action?: FzActionLinkProps;
25
+ }
26
+ export interface FzCardSingleActionEmits {
27
+ (event: "fzaction:click", action: FzActionLinkProps): void;
28
+ }
29
+ export interface FzCardMultiActionsProps extends FzCardHeaderProps, FzCardListFooterProps, FzCardNoActionProps {
30
+ actions: FzActionProps[];
31
+ }
32
+ export interface FzCardMultiActionsEmits {
33
+ (event: "fzaction:click", actionIndex: number, action: FzActionProps): void;
34
+ }
@@ -0,0 +1,3 @@
1
+ export { default as FzCardList } from './FzCardList.vue';
2
+ export { default as FzCardListItem } from './FzCardListItem.vue';
3
+ export type * from './types';
@@ -0,0 +1,64 @@
1
+ import { FzActionProps } from '@fiscozen/action';
2
+ import { FzBadgeTone } from '@fiscozen/badge';
3
+
4
+ export type ActionsMode = "none" | "link" | "actions";
5
+ export interface FzCardListItemProps {
6
+ /**
7
+ * Badge displayed inside the card at the top-left.
8
+ * When omitted, no badge is displayed.
9
+ */
10
+ badge?: {
11
+ /**
12
+ * Text of the badge.
13
+ */
14
+ text: string;
15
+ /**
16
+ * Tone of the badge.
17
+ */
18
+ tone: FzBadgeTone;
19
+ };
20
+ /**
21
+ * Main title of the item, displayed in bold.
22
+ */
23
+ title: string;
24
+ /**
25
+ * Value displayed on the right side of the title row (e.g. "0,00 €").
26
+ */
27
+ value?: string;
28
+ /**
29
+ * Description lines rendered below the title row.
30
+ */
31
+ descriptions?: string[];
32
+ /**
33
+ * Whether to show the indicator icon before the title.
34
+ */
35
+ showIndicator?: boolean;
36
+ /**
37
+ * Row actions. When omitted or `[]`, no trailing control is shown. With one item, an arrow button is shown; with more than one, an ellipsis opens a dropdown.
38
+ */
39
+ actions?: FzActionProps[];
40
+ }
41
+ export interface FzCardListItemEmits {
42
+ /**
43
+ * Emitted when a row action is chosen: single-arrow click, or an item from the overflow dropdown.
44
+ */
45
+ (event: "fzaction:click", actionIndex: number, action: FzActionProps): void;
46
+ }
47
+ /**
48
+ * A single item in the card list when using data-driven rendering.
49
+ * Maps directly to FzCardListItemProps.
50
+ */
51
+ export type FzCardListItem = FzCardListItemProps;
52
+ export type FzCardListProps = {
53
+ /**
54
+ * Array of card item data for data-driven rendering.
55
+ * Each item is rendered as an FzCardListItem.
56
+ */
57
+ items: FzCardListItem[];
58
+ };
59
+ export interface FzCardListEmits {
60
+ /**
61
+ * Emitted when a row action is triggered for an item built from `items`.
62
+ */
63
+ (event: "fzaction:click", itemIndex: number, actionIndex: number, action: FzActionProps): void;
64
+ }
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@fiscozen/card-list",
3
+ "version": "1.0.0",
4
+ "description": "Design System CardList component",
5
+ "main": "src/index.ts",
6
+ "type": "module",
7
+ "keywords": [],
8
+ "author": "Francesca Vago",
9
+ "dependencies": {
10
+ "@fiscozen/action": "3.0.0",
11
+ "@fiscozen/button": "3.0.0",
12
+ "@fiscozen/divider": "1.0.1",
13
+ "@fiscozen/container": "0.4.2",
14
+ "@fiscozen/badge": "3.0.0",
15
+ "@fiscozen/dropdown": "1.0.6"
16
+ },
17
+ "peerDependencies": {
18
+ "tailwindcss": "^3.4.1",
19
+ "vue": "^3.4.13",
20
+ "@fiscozen/icons": "^1.0.1"
21
+ },
22
+ "devDependencies": {
23
+ "@rushstack/eslint-patch": "^1.3.3",
24
+ "@types/jsdom": "^21.1.6",
25
+ "@types/node": "^18.19.3",
26
+ "@vitejs/plugin-vue": "^4.5.2",
27
+ "@vitest/coverage-v8": "^1.2.1",
28
+ "@vue/test-utils": "^2.4.3",
29
+ "@vue/tsconfig": "^0.5.0",
30
+ "eslint": "^8.49.0",
31
+ "jsdom": "^23.0.1",
32
+ "prettier": "^3.0.3",
33
+ "typescript": "~5.3.0",
34
+ "vite": "^5.0.10",
35
+ "vite-plugin-dts": "^3.8.3",
36
+ "vitest": "^1.2.0",
37
+ "vue-tsc": "^1.8.25",
38
+ "@fiscozen/eslint-config": "^0.1.0",
39
+ "@fiscozen/prettier-config": "^0.1.0",
40
+ "@fiscozen/tsconfig": "^0.1.0"
41
+ },
42
+ "license": "MIT",
43
+ "scripts": {
44
+ "coverage": "vitest run --coverage",
45
+ "format": "prettier --write src/",
46
+ "test:unit": "vitest run",
47
+ "build": "vue-tsc && vite build"
48
+ }
49
+ }
@@ -0,0 +1,54 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * FzCardList Component
4
+ *
5
+ * A layout container that renders a list of FzCardListItem components either from
6
+ * a data array (via the `items` prop).
7
+ *
8
+ *
9
+ * @component
10
+ * @example
11
+ * <FzCardList title="My List" :items="listItems" />
12
+ *
13
+ * @example
14
+ * <FzCardList>
15
+ * <FzCardListItem title="Item 1" amount="0,00 €" />
16
+ * <FzCardListItem title="Item 2" amount="1,00 €" />
17
+ * </FzCardList>
18
+ */
19
+ import FzCardListItem from './FzCardListItem.vue';
20
+ import type { FzActionProps } from '@fiscozen/action';
21
+ import { FzCardListProps, FzCardListEmits } from './types';
22
+ import { FzContainer } from '@fiscozen/container';
23
+
24
+ defineProps<FzCardListProps>();
25
+
26
+ const emit = defineEmits<FzCardListEmits>();
27
+
28
+ function emitItemActionClick(
29
+ itemIndex: number,
30
+ actionIndex: number,
31
+ action: FzActionProps,
32
+ ) {
33
+ emit('fzaction:click', itemIndex, actionIndex, action);
34
+ }
35
+ </script>
36
+
37
+ <template>
38
+ <FzContainer gap="xs">
39
+ <FzCardListItem
40
+ v-for="(item, index) in items"
41
+ :key="index"
42
+ :badge="item.badge"
43
+ :title="item.title"
44
+ :value="item.value"
45
+ :descriptions="item.descriptions"
46
+ :showIndicator="item.showIndicator"
47
+ :actions="item.actions"
48
+ @fzaction:click="
49
+ (actionIndex, action) =>
50
+ emitItemActionClick(index, actionIndex, action)
51
+ "
52
+ />
53
+ </FzContainer>
54
+ </template>
@@ -0,0 +1,71 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * FzCardListItem Component
4
+ *
5
+ * A list item row used inside FzCardList.
6
+ * Displays an optional badge, row actions (single arrow, or action dropdown),
7
+ * a status indicator dot, a bold title with an optional amount on the right, and optional description lines.
8
+ *
9
+ * @component
10
+ * @example
11
+ * <FzCardListItem
12
+ * badge="Text badge"
13
+ * title="Title"
14
+ * amount="0,00 €"
15
+ * :descriptions="['Description 1', 'Description 2']"
16
+ * @fzaction:click="onActionClick"
17
+ * />
18
+ */
19
+ import { computed } from "vue";
20
+ import type {
21
+ ActionsMode,
22
+ FzCardListItemProps,
23
+ FzCardListItemEmits,
24
+ } from "./types";
25
+ import type { FzActionLinkProps, FzActionProps } from "@fiscozen/action";
26
+ import FzCardActionLink from "./components/FzCardActionLink.vue";
27
+ import FzCardMultiActions from "./components/FzCardMultiActions.vue";
28
+ import FzCardNoAction from "./components/FzCardNoAction.vue";
29
+
30
+ const props = defineProps<FzCardListItemProps>();
31
+
32
+ const emit = defineEmits<FzCardListItemEmits>();
33
+
34
+ const actionsMode = computed<ActionsMode>(() => {
35
+ if (!props.actions?.length) return "none";
36
+ if (props.actions.length === 1 && props.actions[0].type === "link") return "link";
37
+ return "actions";
38
+ });
39
+
40
+ </script>
41
+
42
+ <template>
43
+ <FzCardNoAction
44
+ v-if="actionsMode === 'none'"
45
+ :badge="badge"
46
+ :title="title"
47
+ :show-indicator="showIndicator"
48
+ :value="value"
49
+ :descriptions="descriptions"
50
+ />
51
+ <FzCardActionLink
52
+ v-else-if="actionsMode === 'link'"
53
+ :badge="badge"
54
+ :title="title"
55
+ :show-indicator="showIndicator"
56
+ :value="value"
57
+ :descriptions="descriptions"
58
+ :action="actions?.[0] as FzActionLinkProps"
59
+ @fzaction:click="(action) => emit('fzaction:click', 0, action as FzActionProps)"
60
+ />
61
+ <FzCardMultiActions
62
+ v-else
63
+ :badge="badge"
64
+ :title="title"
65
+ :show-indicator="showIndicator"
66
+ :value="value"
67
+ :descriptions="descriptions"
68
+ :actions="actions as FzActionProps[]"
69
+ @fzaction:click="(index, action) => emit('fzaction:click', index, action)"
70
+ />
71
+ </template>
@@ -0,0 +1,322 @@
1
+ import { mount, config } from "@vue/test-utils";
2
+ import { describe, it, expect, vi, beforeEach } from "vitest";
3
+ import type { FzActionProps } from "@fiscozen/action";
4
+ import FzCardList from "../FzCardList.vue";
5
+
6
+ const sampleAction: FzActionProps = {
7
+ type: "action",
8
+ variant: "textLeft",
9
+ label: "Go",
10
+ };
11
+
12
+ const linkAction = { type: "link" as const, to: "/" };
13
+
14
+ beforeEach(() => {
15
+ // Mock IntersectionObserver for FzFloating / dropdown (not in jsdom)
16
+ global.IntersectionObserver = class IntersectionObserver {
17
+ constructor() {}
18
+ observe() {}
19
+ unobserve() {}
20
+ disconnect() {}
21
+ } as any;
22
+
23
+ config.global.directives = {
24
+ bold: () => {},
25
+ color: () => {},
26
+ small: () => {},
27
+ };
28
+
29
+ Object.defineProperty(window, "matchMedia", {
30
+ writable: true,
31
+ configurable: true,
32
+ value: vi.fn().mockImplementation((query) => ({
33
+ matches: false,
34
+ media: query,
35
+ onchange: null,
36
+ addListener: vi.fn(),
37
+ removeListener: vi.fn(),
38
+ addEventListener: vi.fn(),
39
+ removeEventListener: vi.fn(),
40
+ dispatchEvent: vi.fn(),
41
+ })),
42
+ });
43
+ });
44
+
45
+ describe("FzCardList", () => {
46
+ describe("Rendering", () => {
47
+ it("should render with empty items", async () => {
48
+ const wrapper = mount(FzCardList, {
49
+ props: { items: [] },
50
+ });
51
+ await wrapper.vm.$nextTick();
52
+ expect(wrapper.exists()).toBe(true);
53
+ expect(
54
+ wrapper.findAllComponents({ name: "FzCardListItem" }),
55
+ ).toHaveLength(0);
56
+ expect(wrapper.element.children.length).toBe(0);
57
+ });
58
+
59
+ it("should not render a title heading (title prop is not used by the template)", async () => {
60
+ const wrapper = mount(FzCardList, {
61
+ props: { title: "My Card List", items: [] },
62
+ });
63
+ await wrapper.vm.$nextTick();
64
+ expect(wrapper.find("h2").exists()).toBe(false);
65
+ });
66
+
67
+ it("does not render default slot content (list is driven by items only)", async () => {
68
+ const wrapper = mount(FzCardList, {
69
+ props: { items: [] },
70
+ slots: {
71
+ default: '<div class="custom-item">Custom Item</div>',
72
+ },
73
+ });
74
+ await wrapper.vm.$nextTick();
75
+ expect(wrapper.html()).not.toContain("Custom Item");
76
+ });
77
+
78
+ it("should render FzCardListItem for each item in items array", async () => {
79
+ const wrapper = mount(FzCardList, {
80
+ props: {
81
+ items: [
82
+ { title: "Item 1" },
83
+ { title: "Item 2" },
84
+ { title: "Item 3" },
85
+ ],
86
+ },
87
+ });
88
+ await wrapper.vm.$nextTick();
89
+ const items = wrapper.findAllComponents({ name: "FzCardListItem" });
90
+ expect(items).toHaveLength(3);
91
+ });
92
+
93
+ it("should render item title text", async () => {
94
+ const wrapper = mount(FzCardList, {
95
+ props: {
96
+ items: [{ title: "Hello World" }],
97
+ },
98
+ });
99
+ await wrapper.vm.$nextTick();
100
+ expect(wrapper.html()).toContain("Hello World");
101
+ });
102
+
103
+ it("should not render default slot when items is an empty array", async () => {
104
+ const wrapper = mount(FzCardList, {
105
+ props: { items: [] },
106
+ slots: {
107
+ default: '<div class="slot-content">Slot Content</div>',
108
+ },
109
+ });
110
+ await wrapper.vm.$nextTick();
111
+ expect(wrapper.html()).not.toContain("Slot Content");
112
+ });
113
+ });
114
+
115
+ describe("Props", () => {
116
+ describe("items prop", () => {
117
+ it("should pass title to FzCardListItem", async () => {
118
+ const wrapper = mount(FzCardList, {
119
+ props: { items: [{ title: "Item Title" }] },
120
+ });
121
+ await wrapper.vm.$nextTick();
122
+ const item = wrapper.findComponent({ name: "FzCardListItem" });
123
+ expect(item.props("title")).toBe("Item Title");
124
+ });
125
+
126
+ it("should pass value to FzCardListItem", async () => {
127
+ const wrapper = mount(FzCardList, {
128
+ props: { items: [{ title: "Item", value: "1.234,56 €" }] },
129
+ });
130
+ await wrapper.vm.$nextTick();
131
+ const item = wrapper.findComponent({ name: "FzCardListItem" });
132
+ expect(item.props("value")).toBe("1.234,56 €");
133
+ });
134
+
135
+ it("should pass badge to FzCardListItem", async () => {
136
+ const badge = { text: "New", tone: "dark" as const };
137
+ const wrapper = mount(FzCardList, {
138
+ props: { items: [{ title: "Item", badge }] },
139
+ });
140
+ await wrapper.vm.$nextTick();
141
+ const item = wrapper.findComponent({ name: "FzCardListItem" });
142
+ expect(item.props("badge")).toEqual(badge);
143
+ });
144
+
145
+ it("should pass descriptions to FzCardListItem", async () => {
146
+ const wrapper = mount(FzCardList, {
147
+ props: {
148
+ items: [{ title: "Item", descriptions: ["Line 1", "Line 2"] }],
149
+ },
150
+ });
151
+ await wrapper.vm.$nextTick();
152
+ const item = wrapper.findComponent({ name: "FzCardListItem" });
153
+ expect(item.props("descriptions")).toEqual(["Line 1", "Line 2"]);
154
+ });
155
+
156
+ it("should pass showIndicator to FzCardListItem", async () => {
157
+ const wrapper = mount(FzCardList, {
158
+ props: { items: [{ title: "Item", showIndicator: true }] },
159
+ });
160
+ await wrapper.vm.$nextTick();
161
+ const item = wrapper.findComponent({ name: "FzCardListItem" });
162
+ expect(item.props("showIndicator")).toBe(true);
163
+ });
164
+
165
+ it("should pass actions to FzCardListItem", async () => {
166
+ const actions = [
167
+ { type: "action" as const, variant: "textLeft" as const, label: "A" },
168
+ ];
169
+ const wrapper = mount(FzCardList, {
170
+ props: { items: [{ title: "Item", actions }] },
171
+ });
172
+ await wrapper.vm.$nextTick();
173
+ const item = wrapper.findComponent({ name: "FzCardListItem" });
174
+ expect(item.props("actions")).toEqual(actions);
175
+ });
176
+ });
177
+ });
178
+
179
+ describe("Events", () => {
180
+ it("should emit fzaction:click with item index when link-action arrow is clicked", async () => {
181
+ const wrapper = mount(FzCardList, {
182
+ props: {
183
+ items: [
184
+ { title: "Item 0", actions: [linkAction] },
185
+ { title: "Item 1", actions: [linkAction] },
186
+ ],
187
+ },
188
+ });
189
+ await wrapper.vm.$nextTick();
190
+
191
+ const items = wrapper.findAllComponents({ name: "FzCardListItem" });
192
+ await items[1].get('[role="button"]').trigger("click");
193
+
194
+ expect(wrapper.emitted("fzaction:click")).toBeTruthy();
195
+ expect(wrapper.emitted("fzaction:click")![0]).toEqual([
196
+ 1,
197
+ 0,
198
+ linkAction,
199
+ ]);
200
+ });
201
+
202
+ it("should emit fzaction:click with index 0 when first item link-action arrow is clicked", async () => {
203
+ const wrapper = mount(FzCardList, {
204
+ props: {
205
+ items: [{ title: "Item 0", actions: [linkAction] }],
206
+ },
207
+ });
208
+ await wrapper.vm.$nextTick();
209
+
210
+ const item = wrapper.findComponent({ name: "FzCardListItem" });
211
+ await item.get('[role="button"]').trigger("click");
212
+
213
+ expect(wrapper.emitted("fzaction:click")).toBeTruthy();
214
+ expect(wrapper.emitted("fzaction:click")![0]).toEqual([
215
+ 0,
216
+ 0,
217
+ linkAction,
218
+ ]);
219
+ });
220
+
221
+ it("should emit fzaction:click with item index and action payload", async () => {
222
+ const wrapper = mount(FzCardList, {
223
+ props: {
224
+ items: [
225
+ { title: "A" },
226
+ {
227
+ title: "B",
228
+ actions: [sampleAction, { ...sampleAction, label: "B" }],
229
+ },
230
+ ],
231
+ },
232
+ });
233
+ await wrapper.vm.$nextTick();
234
+
235
+ const items = wrapper.findAllComponents({ name: "FzCardListItem" });
236
+ await items[1].vm.$emit("fzaction:click", 0, sampleAction);
237
+
238
+ expect(wrapper.emitted("fzaction:click")).toBeTruthy();
239
+ expect(wrapper.emitted("fzaction:click")![0]).toEqual([
240
+ 1,
241
+ 0,
242
+ sampleAction,
243
+ ]);
244
+ });
245
+ });
246
+
247
+ describe("Accessibility", () => {
248
+ it("should wrap items in a container element", async () => {
249
+ const wrapper = mount(FzCardList, {
250
+ props: { items: [{ title: "A" }] },
251
+ });
252
+ await wrapper.vm.$nextTick();
253
+ expect(wrapper.findComponent({ name: "FzCardListItem" }).exists()).toBe(
254
+ true,
255
+ );
256
+ });
257
+ });
258
+
259
+ describe("Edge Cases", () => {
260
+ it("should render a FzCardListItem even with only a title", async () => {
261
+ const wrapper = mount(FzCardList, {
262
+ props: { items: [{ title: "Minimal" }] },
263
+ });
264
+ await wrapper.vm.$nextTick();
265
+ const item = wrapper.findComponent({ name: "FzCardListItem" });
266
+ expect(item.exists()).toBe(true);
267
+ });
268
+
269
+ it("should handle a large number of items", async () => {
270
+ const items = Array.from({ length: 20 }, (_, i) => ({
271
+ title: `Item ${i}`,
272
+ }));
273
+ const wrapper = mount(FzCardList, { props: { items } });
274
+ await wrapper.vm.$nextTick();
275
+ const rendered = wrapper.findAllComponents({ name: "FzCardListItem" });
276
+ expect(rendered).toHaveLength(20);
277
+ });
278
+
279
+ it("should render list items when title prop is also provided (title is unused in template)", async () => {
280
+ const wrapper = mount(FzCardList, {
281
+ props: {
282
+ title: "My List",
283
+ items: [{ title: "Item 1" }],
284
+ },
285
+ });
286
+ await wrapper.vm.$nextTick();
287
+ expect(wrapper.find("h2").exists()).toBe(false);
288
+ expect(
289
+ wrapper.findAllComponents({ name: "FzCardListItem" }),
290
+ ).toHaveLength(1);
291
+ });
292
+ });
293
+
294
+ describe("Snapshots", () => {
295
+ it("should match snapshot - default state", () => {
296
+ const wrapper = mount(FzCardList, {
297
+ props: { items: [] },
298
+ });
299
+ expect(wrapper.html()).toMatchSnapshot();
300
+ });
301
+
302
+ it("should match snapshot - with items", () => {
303
+ const wrapper = mount(FzCardList, {
304
+ props: {
305
+ items: [
306
+ {
307
+ title: "Item 1",
308
+ value: "1.200,00 €",
309
+ showIndicator: true,
310
+ },
311
+ {
312
+ title: "Item 2",
313
+ badge: { text: "New", tone: "dark" },
314
+ descriptions: ["Due date: 2024-03-01"],
315
+ },
316
+ ],
317
+ },
318
+ });
319
+ expect(wrapper.html()).toMatchSnapshot();
320
+ });
321
+ });
322
+ });