vueless 1.0.2-beta.8 → 1.0.2-beta.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vueless",
3
- "version": "1.0.2-beta.8",
3
+ "version": "1.0.2-beta.9",
4
4
  "license": "MIT",
5
5
  "description": "Vue Styleless UI Component Library, powered by Tailwind CSS.",
6
6
  "keywords": [
@@ -30,13 +30,13 @@
30
30
  "scripts": {
31
31
  "pre:start": "npx node .scripts/icons",
32
32
  "dev:docs": "storybook dev -p 6006 --docs --no-open",
33
- "dev": "STORYBOOK_FULL=1 storybook dev -p 6006 --no-open",
33
+ "dev": "storybook dev -p 6006 --no-open",
34
34
  "build": "storybook build --docs",
35
35
  "preview": "vite preview --host --outDir=storybook-static",
36
36
  "ts:check": "vue-tsc --build --force",
37
37
  "release:prepare": "npm run pre:start && rm -rf dist && mkdir -p dist && cp -r src/. package.json LICENSE README.md dist/ && npx node .scripts/writeLocales",
38
38
  "release:beta": "release-it --ci --npm.publish --preRelease=beta --increment=prerelease",
39
- "release:patch": "release-it patch --ci --npm.publish",
39
+ "release:patch": "release-it patch --ci --npm.publish --git.tag --github.release",
40
40
  "release:minor": "release-it minor --ci --npm.publish --git.tag --github.release",
41
41
  "release:major": "release-it major --ci --npm.publish --git.tag --github.release",
42
42
  "lint": "eslint --no-fix src/ .storybook/",
@@ -75,7 +75,7 @@
75
75
  "@vue/eslint-config-typescript": "^14.5.0",
76
76
  "@vue/test-utils": "^2.4.6",
77
77
  "@vue/tsconfig": "^0.7.0",
78
- "@vueless/storybook": "^1.0.0",
78
+ "@vueless/storybook": "^1.0.1-beta.1",
79
79
  "eslint": "^9.27.0",
80
80
  "eslint-plugin-storybook": "^0.12.0",
81
81
  "eslint-plugin-vue": "^10.1.0",
@@ -795,7 +795,7 @@ const {
795
795
  @focus="activate"
796
796
  @blur="onListboxBlur"
797
797
  @search-blur="onListboxSearchBlur"
798
- @update:model-value="onSearchChange"
798
+ @search-change="onSearchChange"
799
799
  >
800
800
  <template #before-option="{ option, index }">
801
801
  <!--
@@ -21,7 +21,7 @@ const props = withDefaults(defineProps<Props>(), {
21
21
 
22
22
  const emit = defineEmits([
23
23
  /**
24
- * Triggers when remove button is clicked.
24
+ * Triggers when the remove button is clicked.
25
25
  * @property {string} fileId
26
26
  */
27
27
  "remove",
@@ -0,0 +1,257 @@
1
+ import { mount } from "@vue/test-utils";
2
+ import { describe, it, expect } from "vitest";
3
+
4
+ import UFile from "../UFile.vue";
5
+ import ULink from "../../ui.button-link/ULink.vue";
6
+ import UIcon from "../../ui.image-icon/UIcon.vue";
7
+
8
+ import type { ComponentPublicInstance } from "vue";
9
+ import type { Props } from "../types.ts";
10
+
11
+ describe("UFile.vue", () => {
12
+ // Props tests
13
+ describe("Props", () => {
14
+ // URL prop
15
+ it("passes the correct url to ULink", () => {
16
+ const url = "https://example.com/file.pdf";
17
+
18
+ const component = mount(UFile, {
19
+ props: {
20
+ url,
21
+ },
22
+ });
23
+
24
+ const linkComponent = component.findComponent(ULink);
25
+
26
+ expect(linkComponent.props("href")).toBe(url);
27
+ });
28
+
29
+ // ImageUrl prop
30
+ it("renders image when imageUrl prop is provided", () => {
31
+ const imageUrl = "https://example.com/image.jpg";
32
+
33
+ const component = mount(UFile, {
34
+ props: {
35
+ imageUrl,
36
+ },
37
+ });
38
+
39
+ const image = component.find("img");
40
+
41
+ expect(image.exists()).toBe(true);
42
+ expect(image.attributes("src")).toBe(imageUrl);
43
+ });
44
+
45
+ // Label prop
46
+ it("renders the correct label text", () => {
47
+ const label = "Example File";
48
+
49
+ const component = mount(UFile, {
50
+ props: {
51
+ label,
52
+ },
53
+ });
54
+
55
+ const linkComponent = component.findComponent(ULink);
56
+
57
+ expect(linkComponent.text()).toContain(label);
58
+ });
59
+
60
+ // Size prop
61
+ it("applies the correct size class", async () => {
62
+ const sizeClasses = {
63
+ sm: "gap-0.5",
64
+ md: "gap-1",
65
+ lg: "gap-1.5",
66
+ };
67
+
68
+ Object.entries(sizeClasses).forEach(([size, classes]) => {
69
+ const component = mount(UFile, {
70
+ props: {
71
+ size: size as Props["size"],
72
+ },
73
+ });
74
+
75
+ expect(component.attributes("class")).toContain(classes);
76
+ });
77
+ });
78
+
79
+ // ID prop
80
+ it("uses provided id", () => {
81
+ const id = "test-file-id";
82
+ const removable = true;
83
+
84
+ const component = mount(UFile, {
85
+ props: {
86
+ id,
87
+ removable, // Need to set removable to true to show the remove button
88
+ },
89
+ });
90
+
91
+ // Directly call the onRemove method
92
+ (component.vm as ComponentPublicInstance & { onRemove: () => void }).onRemove();
93
+
94
+ expect(component.emitted("remove")?.[0][0]).toBe(id);
95
+ });
96
+
97
+ // Removable prop
98
+ it("shows remove button when removable prop is true", () => {
99
+ const removable = true;
100
+ const dataTest = "test-file";
101
+ const removeIconDataTest = "test-file-remove-item";
102
+
103
+ const component = mount(UFile, {
104
+ props: {
105
+ removable,
106
+ dataTest,
107
+ },
108
+ });
109
+
110
+ // Get the remove button directly using the same approach as in the event test
111
+ const removeIcon = component.findAllComponents(UIcon).find((component) => {
112
+ return component.props("dataTest") === removeIconDataTest;
113
+ });
114
+
115
+ expect(removeIcon).toBeDefined();
116
+ });
117
+
118
+ // DataTest prop
119
+ it("applies the correct data-test attribute", () => {
120
+ const dataTest = "test-file";
121
+
122
+ const component = mount(UFile, {
123
+ props: {
124
+ dataTest,
125
+ },
126
+ });
127
+
128
+ expect(component.attributes("data-test")).toBe(dataTest);
129
+ });
130
+ });
131
+
132
+ // Slots tests
133
+ describe("Slots", () => {
134
+ // Default slot
135
+ it("renders content from default slot", () => {
136
+ const slotContent = "Custom Content";
137
+
138
+ const component = mount(UFile, {
139
+ slots: {
140
+ default: slotContent,
141
+ },
142
+ });
143
+
144
+ expect(component.text()).toContain(slotContent);
145
+ });
146
+
147
+ // Default slot with bindings
148
+ it("provides correct bindings to default slot", () => {
149
+ const id = "test-id";
150
+ const label = "Test File";
151
+ const url = "https://example.com/file.pdf";
152
+ const imageUrl = "https://example.com/image.jpg";
153
+
154
+ // Define class names as constants instead of hardcoding them
155
+ const idClass = "file-id";
156
+ const labelClass = "file-label";
157
+ const urlClass = "file-url";
158
+ const imageUrlClass = "file-image-url";
159
+
160
+ const component = mount(UFile, {
161
+ props: {
162
+ id,
163
+ label,
164
+ url,
165
+ imageUrl,
166
+ },
167
+ slots: {
168
+ default: `
169
+ <template #default="{ id, label, url, imageUrl }">
170
+ <div class="${idClass}">{{ id }}</div>
171
+ <div class="${labelClass}">{{ label }}</div>
172
+ <div class="${urlClass}">{{ url }}</div>
173
+ <div class="${imageUrlClass}">{{ imageUrl }}</div>
174
+ </template>
175
+ `,
176
+ },
177
+ });
178
+
179
+ expect(component.find(`.${idClass}`).text()).toBe(id);
180
+ expect(component.find(`.${labelClass}`).text()).toBe(label);
181
+ expect(component.find(`.${urlClass}`).text()).toBe(url);
182
+ expect(component.find(`.${imageUrlClass}`).text()).toBe(imageUrl);
183
+ });
184
+
185
+ // Left slot
186
+ it("renders content from left slot", () => {
187
+ const slotText = "Left";
188
+ const slotClass = "left-content";
189
+
190
+ const component = mount(UFile, {
191
+ slots: {
192
+ left: `<span class='${slotClass}'>${slotText}</span>`,
193
+ },
194
+ });
195
+
196
+ expect(component.find(`.${slotClass}`).exists()).toBe(true);
197
+ expect(component.find(`.${slotClass}`).text()).toBe(slotText);
198
+ });
199
+
200
+ // Right slot
201
+ it("renders content from right slot", () => {
202
+ const slotText = "Right";
203
+ const slotClass = "right-content";
204
+
205
+ const component = mount(UFile, {
206
+ slots: {
207
+ right: `<span class='${slotClass}'>${slotText}</span>`,
208
+ },
209
+ });
210
+
211
+ expect(component.find(`.${slotClass}`).exists()).toBe(true);
212
+ expect(component.find(`.${slotClass}`).text()).toBe(slotText);
213
+ });
214
+ });
215
+
216
+ // Events tests
217
+ describe("Events", () => {
218
+ // Remove event
219
+ it("emits remove event when remove button is clicked", async () => {
220
+ const id = "test-file-id";
221
+ const removable = true;
222
+ const dataTest = "test-file";
223
+ const removeIconDataTest = "test-file-remove-item";
224
+
225
+ const component = mount(UFile, {
226
+ props: {
227
+ id,
228
+ removable,
229
+ dataTest,
230
+ },
231
+ });
232
+
233
+ const removeIcon = component.findAllComponents(UIcon).find((component) => {
234
+ return component.props("dataTest") === removeIconDataTest;
235
+ });
236
+
237
+ expect(removeIcon).toBeDefined();
238
+
239
+ // Directly call the onRemove method
240
+ (component.vm as ComponentPublicInstance & { onRemove: () => void }).onRemove();
241
+
242
+ // Check if the remove event was emitted with the correct value
243
+ expect(component.emitted("remove")).toBeTruthy();
244
+ expect(component.emitted("remove")?.[0][0]).toBe(id);
245
+ });
246
+ });
247
+
248
+ // Exposed refs tests
249
+ describe("Exposed refs", () => {
250
+ // link ref
251
+ it("exposes link ref", () => {
252
+ const component = mount(UFile, {});
253
+
254
+ expect(component.vm.link).toBeDefined();
255
+ });
256
+ });
257
+ });
@@ -98,36 +98,35 @@ const { getDataTest, filesLabelAttrs, itemsAttrs, itemAttrs } = useUI<Config>(de
98
98
  >
99
99
  <template #left>
100
100
  <!--
101
- @slot Use it to add something left.
101
+ @slot Use it to add something before the file content.
102
102
  @binding {number} index
103
103
  -->
104
- <slot name="left" :index="index" />
104
+ <slot name="before-file" :index="index" />
105
105
  </template>
106
106
 
107
- <template #default="{ id, label, url, imageUrl }">
108
- <!-- @slot Use it to add a file directly.
109
- @binding {string | number} id
110
- @binding {string} label
111
- @binding {string} url
112
- @binding {string} image-url
113
- @binding {number} index
114
- -->
115
- <slot
116
- :id="id"
117
- name="default"
118
- :label="label"
119
- :url="url"
120
- :image-url="imageUrl"
121
- :index="index"
122
- />
123
- </template>
107
+ <!--
108
+ @slot Use it to add a file directly.
109
+ @binding {string | number} id
110
+ @binding {string} label
111
+ @binding {string} url
112
+ @binding {string} image-url
113
+ @binding {number} index
114
+ -->
115
+ <slot
116
+ :id="file?.id"
117
+ name="file"
118
+ :label="file?.label"
119
+ :url="file?.url"
120
+ :image-url="file?.imageUrl"
121
+ :index="index"
122
+ />
124
123
 
125
124
  <template #right>
126
125
  <!--
127
- @slot Use it to add something right.
126
+ @slot Use it to add something after the file content.
128
127
  @binding {number} index
129
128
  -->
130
- <slot name="right" :index="index" />
129
+ <slot name="after-file" :index="index" />
131
130
  </template>
132
131
  </UFile>
133
132
  </slot>
@@ -92,10 +92,10 @@ export const Slots: StoryFn<UFilesArgs> = (args) => ({
92
92
  },
93
93
  template: `
94
94
  <UFiles v-bind="args">
95
- <template #left="{ index }">
95
+ <template #before-file="{ index }">
96
96
  <UIcon v-if="index === 0" name="info" color="warning" size="xs" />
97
97
  </template>
98
- <template #right="{ index }">
98
+ <template #after-file="{ index }">
99
99
  <UIcon v-if="index === 1" name="check_circle" color="success" size="xs" />
100
100
  </template>
101
101
  </UFiles>
@@ -0,0 +1,325 @@
1
+ import { mount } from "@vue/test-utils";
2
+ import { describe, it, expect, beforeAll, afterAll, vi } from "vitest";
3
+
4
+ import UFiles from "../UFiles.vue";
5
+ import UFile from "../../ui.text-file/UFile.vue";
6
+ import ULabel from "../../ui.form-label/ULabel.vue";
7
+
8
+ import type { Props } from "../types.ts";
9
+
10
+ // Mock URL.createObjectURL
11
+ const originalCreateObjectURL = URL.createObjectURL;
12
+ const mockCreateObjectURL = vi.fn().mockImplementation((file) => `mock-url-for-${file.name}`);
13
+
14
+ describe("UFiles.vue", () => {
15
+ const fileList = [createMockFile("file1.pdf"), createMockFile("file2.pdf")];
16
+
17
+ // Create a mock File object
18
+ function createMockFile(name: string, type: string = "application/pdf") {
19
+ return new File(["dummy content"], name, { type });
20
+ }
21
+
22
+ // Setup and teardown for URL.createObjectURL mock
23
+ beforeAll(() => {
24
+ // Mock URL.createObjectURL before tests
25
+ URL.createObjectURL = mockCreateObjectURL;
26
+ });
27
+
28
+ afterAll(() => {
29
+ // Restore original URL.createObjectURL after tests
30
+ URL.createObjectURL = originalCreateObjectURL;
31
+ });
32
+
33
+ // Props tests
34
+ describe("Props", () => {
35
+ // FileList prop
36
+ it("renders the correct number of files", () => {
37
+ const component = mount(UFiles, {
38
+ props: {
39
+ fileList,
40
+ },
41
+ });
42
+
43
+ const fileComponents = component.findAllComponents(UFile);
44
+
45
+ expect(fileComponents.length).toBe(fileList.length);
46
+ });
47
+
48
+ // Label prop
49
+ it("renders the correct label text", () => {
50
+ const label = "File List";
51
+
52
+ const component = mount(UFiles, {
53
+ props: {
54
+ fileList: [],
55
+ label,
56
+ },
57
+ });
58
+
59
+ const labelComponent = component.findComponent(ULabel);
60
+
61
+ expect(labelComponent.props("label")).toBe(label);
62
+ });
63
+
64
+ // LabelAlign prop
65
+ it("passes the correct labelAlign to ULabel", () => {
66
+ const labelAligns = ["top", "topWithDesc"];
67
+
68
+ labelAligns.forEach((labelAlign) => {
69
+ const component = mount(UFiles, {
70
+ props: {
71
+ fileList,
72
+ labelAlign: labelAlign as Props["labelAlign"],
73
+ },
74
+ });
75
+
76
+ const labelComponent = component.findComponent(ULabel);
77
+
78
+ expect(labelComponent.props("align")).toBe(labelAlign);
79
+ });
80
+ });
81
+
82
+ // Description prop
83
+ it("passes the correct description to ULabel", () => {
84
+ const description = "List of files";
85
+
86
+ const component = mount(UFiles, {
87
+ props: {
88
+ fileList,
89
+ description,
90
+ },
91
+ });
92
+
93
+ const labelComponent = component.findComponent(ULabel);
94
+
95
+ expect(labelComponent.props("description")).toBe(description);
96
+ });
97
+
98
+ // Size prop
99
+ it("passes the correct size prop to ULabel", () => {
100
+ const sizes = ["sm", "md", "lg"];
101
+
102
+ sizes.forEach((size) => {
103
+ const component = mount(UFiles, {
104
+ props: {
105
+ fileList,
106
+ size: size as Props["size"],
107
+ },
108
+ });
109
+
110
+ const labelComponent = component.findComponent(ULabel);
111
+
112
+ expect(labelComponent.props("size")).toBe(size);
113
+ });
114
+ });
115
+
116
+ // Removable prop
117
+ it("passes removable prop to UFile components", () => {
118
+ const removable = true;
119
+ const fileList = [createMockFile("file1.pdf")];
120
+
121
+ const component = mount(UFiles, {
122
+ props: {
123
+ fileList,
124
+ removable,
125
+ },
126
+ });
127
+
128
+ const fileComponent = component.findComponent(UFile);
129
+
130
+ expect(fileComponent.props("removable")).toBe(removable);
131
+ });
132
+
133
+ // DataTest prop
134
+ it("applies the correct data-test attribute to file items", () => {
135
+ const dataTest = "test-files";
136
+ const fileList = [createMockFile("file1.pdf")];
137
+
138
+ const component = mount(UFiles, {
139
+ props: {
140
+ fileList,
141
+ dataTest,
142
+ },
143
+ });
144
+
145
+ const fileComponent = component.findComponent(UFile);
146
+
147
+ expect(fileComponent.attributes("data-test")).toBe(`${dataTest}-item-0`);
148
+ });
149
+
150
+ // Image file detection
151
+ it("detects image files and sets imageUrl prop", () => {
152
+ const imageFile = createMockFile("image.jpg", "image/jpeg");
153
+ const fileList = [imageFile];
154
+
155
+ const component = mount(UFiles, {
156
+ props: {
157
+ fileList,
158
+ },
159
+ });
160
+
161
+ const fileComponent = component.findComponent(UFile);
162
+
163
+ expect(fileComponent.props("imageUrl")).toBeDefined();
164
+ });
165
+ });
166
+
167
+ // Slots tests
168
+ describe("Slots", () => {
169
+ // Default slot
170
+ it("renders content from default slot", () => {
171
+ const slotContent = "Custom Content";
172
+
173
+ const component = mount(UFiles, {
174
+ props: {
175
+ fileList: [],
176
+ },
177
+ slots: {
178
+ default: slotContent,
179
+ },
180
+ });
181
+
182
+ expect(component.text()).toContain(slotContent);
183
+ });
184
+
185
+ // Label slot
186
+ it("renders content from label slot", () => {
187
+ const label = "File List";
188
+ const slotText = "Custom Label";
189
+ const slotClass = "label-content";
190
+
191
+ const component = mount(UFiles, {
192
+ props: {
193
+ fileList: [],
194
+ label,
195
+ },
196
+ slots: {
197
+ label: `<span class='${slotClass}'>${slotText}</span>`,
198
+ },
199
+ });
200
+
201
+ expect(component.find(`.${slotClass}`).exists()).toBe(true);
202
+ expect(component.find(`.${slotClass}`).text()).toBe(slotText);
203
+ });
204
+
205
+ // Before-file slot
206
+ it("renders content from before-file slot", () => {
207
+ const slotText = "Before";
208
+ const slotClass = "before-content";
209
+
210
+ const component = mount(UFiles, {
211
+ props: {
212
+ fileList,
213
+ },
214
+ slots: {
215
+ "before-file": `
216
+ <template #before-file="{ index }">
217
+ <span class='${slotClass}'>${slotText}{{ index }}</span>
218
+ </template>
219
+ `,
220
+ },
221
+ });
222
+
223
+ expect(component.find(`.${slotClass}`).exists()).toBe(true);
224
+ expect(component.find(`.${slotClass}`).text()).toBe(`${slotText}0`);
225
+ });
226
+
227
+ // After-file slot
228
+ it("renders content from after-file slot", () => {
229
+ const slotText = "After";
230
+ const slotClass = "after-content";
231
+
232
+ const component = mount(UFiles, {
233
+ props: {
234
+ fileList,
235
+ },
236
+ slots: {
237
+ "after-file": `
238
+ <template #after-file="{ index }">
239
+ <span class='${slotClass}'>${slotText}{{ index }}</span>
240
+ </template>
241
+ `,
242
+ },
243
+ });
244
+
245
+ expect(component.find(`.${slotClass}`).exists()).toBe(true);
246
+ expect(component.find(`.${slotClass}`).text()).toBe(`${slotText}0`);
247
+ });
248
+
249
+ // File slot with bindings
250
+ it("provides correct bindings to file slot", () => {
251
+ const fileName = "file1.pdf";
252
+ const fileIndex = "0";
253
+ const fileList = [createMockFile(fileName)];
254
+
255
+ // Define class names as constants instead of hardcoding them
256
+ const idClass = "file-id";
257
+ const labelClass = "file-label";
258
+ const urlClass = "file-url";
259
+ const imageUrlClass = "file-image-url";
260
+ const indexClass = "file-index";
261
+
262
+ const component = mount(UFiles, {
263
+ props: {
264
+ fileList,
265
+ },
266
+ slots: {
267
+ file: `
268
+ <template #file="{ id, label, url, imageUrl, index }">
269
+ <div v-if="id" class="${idClass}">{{ id }}</div>
270
+ <div v-if="label" class="${labelClass}">{{ label }}</div>
271
+ <div v-if="url" class="${urlClass}">{{ url }}</div>
272
+ <div v-if="imageUrl" class="${imageUrlClass}">{{ imageUrl }}</div>
273
+ <div v-if="index >= 0" class="${indexClass}">{{ index }}</div>
274
+ </template>
275
+ `,
276
+ },
277
+ });
278
+
279
+ expect(component.find(`.${idClass}`).exists()).toBe(true);
280
+ expect(component.find(`.${labelClass}`).text()).toBe(fileName);
281
+ expect(component.find(`.${urlClass}`).exists()).toBe(true);
282
+ expect(component.find(`.${imageUrlClass}`).exists()).toBe(false);
283
+ expect(component.find(`.${indexClass}`).text()).toBe(fileIndex);
284
+ });
285
+ });
286
+
287
+ // Events tests
288
+ describe("Events", () => {
289
+ // Remove event
290
+ it("emits remove event when file is removed", async () => {
291
+ const fileList = [createMockFile("file1.pdf")];
292
+ const fileId = "test-id";
293
+ const removable = true;
294
+
295
+ const component = mount(UFiles, {
296
+ props: {
297
+ fileList,
298
+ removable,
299
+ },
300
+ });
301
+
302
+ // Find the UFile component and trigger its remove event
303
+ const fileComponent = component.findComponent(UFile);
304
+
305
+ await fileComponent.vm.$emit("remove", fileId);
306
+
307
+ expect(component.emitted("remove")).toBeTruthy();
308
+ expect(component.emitted("remove")?.[0][0]).toBe(fileId);
309
+ });
310
+ });
311
+
312
+ // Exposed refs tests
313
+ describe("Exposed refs", () => {
314
+ // itemsRef
315
+ it("exposes itemsRef", () => {
316
+ const component = mount(UFiles, {
317
+ props: {
318
+ fileList: [],
319
+ },
320
+ });
321
+
322
+ expect(component.vm.itemsRef).toBeDefined();
323
+ });
324
+ });
325
+ });
@@ -0,0 +1,351 @@
1
+ import { mount } from "@vue/test-utils";
2
+ import { describe, it, expect, beforeAll } from "vitest";
3
+
4
+ import UNumber from "../UNumber.vue";
5
+ import { MATH_SIGN, MATH_SIGN_TYPE } from "../utilNumber.ts";
6
+
7
+ import type { Props } from "../types.ts";
8
+
9
+ describe("UNumber.vue", () => {
10
+ let value: number;
11
+
12
+ beforeAll(() => {
13
+ value = 1234.56;
14
+ });
15
+
16
+ // Props tests
17
+ describe("Props", () => {
18
+ // Size prop
19
+ it("applies the correct size class", async () => {
20
+ const sizeClasses = {
21
+ xs: "text-tiny",
22
+ sm: "text-small",
23
+ md: "text-medium",
24
+ lg: "text-large",
25
+ };
26
+
27
+ Object.entries(sizeClasses).forEach(([size, classes]) => {
28
+ const component = mount(UNumber, {
29
+ props: {
30
+ size: size as Props["size"],
31
+ },
32
+ });
33
+
34
+ expect(component.attributes("class")).toContain(classes);
35
+ });
36
+ });
37
+
38
+ // Color prop
39
+ it("applies the correct color class", async () => {
40
+ const colors = [
41
+ "primary",
42
+ "secondary",
43
+ "error",
44
+ "warning",
45
+ "success",
46
+ "info",
47
+ "notice",
48
+ "neutral",
49
+ "grayscale",
50
+ ];
51
+
52
+ colors.forEach((color) => {
53
+ const component = mount(UNumber, {
54
+ props: {
55
+ color: color as Props["color"],
56
+ },
57
+ });
58
+
59
+ expect(component.attributes("class")).toContain(color);
60
+ });
61
+ });
62
+
63
+ // Value prop
64
+ it("renders the correct number value", () => {
65
+ const expectedFormattedNumber = "1 234,56";
66
+
67
+ const component = mount(UNumber, {
68
+ props: {
69
+ value,
70
+ },
71
+ });
72
+
73
+ expect(component.text()).toContain(expectedFormattedNumber);
74
+ });
75
+
76
+ // Sign prop
77
+ it("renders the correct sign based on sign prop", () => {
78
+ const testNegativeValue = -123;
79
+
80
+ // Auto sign (negative value)
81
+ // Should show minus sign for negative values
82
+ const autoComponent = mount(UNumber, {
83
+ props: {
84
+ value: testNegativeValue,
85
+ sign: MATH_SIGN_TYPE.auto as Props["sign"],
86
+ },
87
+ });
88
+
89
+ expect(autoComponent.text()).toContain(MATH_SIGN.MINUS);
90
+
91
+ // Auto sign (positive value)
92
+ // Should not show any sign for positive values
93
+ const autoPositiveComponent = mount(UNumber, {
94
+ props: {
95
+ value,
96
+ sign: MATH_SIGN_TYPE.auto as Props["sign"],
97
+ },
98
+ });
99
+
100
+ expect(autoPositiveComponent.text()).not.toContain(MATH_SIGN.MINUS);
101
+ expect(autoPositiveComponent.text()).not.toContain(MATH_SIGN.PLUS);
102
+
103
+ // Positive sign
104
+ // Should show plus sign regardless of value
105
+ const positiveComponent = mount(UNumber, {
106
+ props: {
107
+ value,
108
+ sign: MATH_SIGN_TYPE.positive as Props["sign"],
109
+ },
110
+ });
111
+
112
+ expect(positiveComponent.text()).toContain(MATH_SIGN.PLUS);
113
+
114
+ // Negative sign
115
+ // Should show minus sign regardless of value
116
+ const negativeComponent = mount(UNumber, {
117
+ props: {
118
+ value,
119
+ sign: MATH_SIGN_TYPE.negative as Props["sign"],
120
+ },
121
+ });
122
+
123
+ expect(negativeComponent.text()).toContain(MATH_SIGN.MINUS);
124
+
125
+ // Unsigned
126
+ // Should not show any sign regardless of value
127
+ const unsignedComponent = mount(UNumber, {
128
+ props: {
129
+ value: testNegativeValue,
130
+ sign: MATH_SIGN_TYPE.unsigned as Props["sign"],
131
+ },
132
+ });
133
+
134
+ expect(unsignedComponent.text()).not.toContain(MATH_SIGN.MINUS);
135
+ expect(unsignedComponent.text()).not.toContain(MATH_SIGN.PLUS);
136
+ });
137
+
138
+ // Currency prop
139
+ it("renders the currency symbol", () => {
140
+ const currency = "$";
141
+
142
+ const component = mount(UNumber, {
143
+ props: {
144
+ value,
145
+ currency,
146
+ },
147
+ });
148
+
149
+ expect(component.text()).toContain(currency);
150
+ });
151
+
152
+ // CurrencyAlign prop
153
+ it("aligns currency correctly based on currencyAlign prop", () => {
154
+ const currency = "$";
155
+
156
+ const alignTests = [
157
+ { align: "left", expectation: (text: string) => text.startsWith(currency) },
158
+ { align: "right", expectation: (text: string) => text.endsWith(currency) },
159
+ ];
160
+
161
+ alignTests.forEach(({ align, expectation }) => {
162
+ const component = mount(UNumber, {
163
+ props: {
164
+ value,
165
+ currency,
166
+ currencyAlign: align as Props["currencyAlign"],
167
+ },
168
+ });
169
+
170
+ expect(expectation(component.text())).toBe(true);
171
+ });
172
+ });
173
+
174
+ // CurrencySpace prop
175
+ it("adds space between currency and number when currencySpace is true", () => {
176
+ const currency = "$";
177
+
178
+ const spaceTests = [
179
+ { space: true, align: "left" },
180
+ { space: false, align: "left" },
181
+ { space: true, align: "right" },
182
+ { space: false, align: "right" },
183
+ ];
184
+
185
+ spaceTests.forEach(({ space, align }) => {
186
+ const component = mount(UNumber, {
187
+ props: {
188
+ value,
189
+ currency,
190
+ currencySpace: space,
191
+ currencyAlign: align as Props["currencyAlign"],
192
+ },
193
+ });
194
+
195
+ expect(component.html()).toContain(currency);
196
+ // Visual inspection of spacing would be done in a real browser
197
+ // Here we just verify the component renders with the given props
198
+ expect(component.props().currencySpace).toBe(space);
199
+ expect(component.props().currencyAlign).toBe(align);
200
+ });
201
+ });
202
+
203
+ // MinFractionDigits prop
204
+ it("adds zeros to meet the minimum fraction digits requirement", () => {
205
+ const value = 123;
206
+ const minFractionDigits = 2;
207
+ const expectedMinFractionResult = "123,00";
208
+
209
+ const component = mount(UNumber, {
210
+ props: {
211
+ value,
212
+ minFractionDigits,
213
+ },
214
+ });
215
+
216
+ expect(component.text()).toContain(expectedMinFractionResult);
217
+ });
218
+
219
+ // MaxFractionDigits prop
220
+ it("rounds the fraction to the maximum number of digits", () => {
221
+ const value = 123.456789;
222
+ const maxFractionDigits = 2;
223
+ const expectedMaxFractionResult = "123,46"; // Rounded from .456789 to 2 digits
224
+
225
+ const component = mount(UNumber, {
226
+ props: {
227
+ value,
228
+ maxFractionDigits,
229
+ },
230
+ });
231
+
232
+ expect(component.text()).toContain(expectedMaxFractionResult);
233
+ // Original decimal part should not be present
234
+ const originalDecimalPart = value.toString().split(".")[1];
235
+
236
+ expect(component.text()).not.toContain(originalDecimalPart);
237
+ });
238
+
239
+ // DecimalSeparator prop
240
+ it("uses the correct decimal separator", () => {
241
+ const value = 123.45;
242
+ const decimalSeparator = ",";
243
+ const expectedFormattedNumber = "123,45";
244
+
245
+ const component = mount(UNumber, {
246
+ props: {
247
+ value,
248
+ decimalSeparator,
249
+ },
250
+ });
251
+
252
+ expect(component.text()).toContain(expectedFormattedNumber);
253
+ });
254
+
255
+ // ThousandsSeparator prop
256
+ it("uses the correct thousands separator", () => {
257
+ const value = 1234567.89;
258
+ const thousandsSeparator = " ";
259
+ const expectedFormattedInteger = "1 234 567";
260
+
261
+ const component = mount(UNumber, {
262
+ props: {
263
+ value,
264
+ thousandsSeparator,
265
+ },
266
+ });
267
+
268
+ expect(component.text()).toContain(expectedFormattedInteger);
269
+ });
270
+
271
+ // Align prop
272
+ it("applies the correct align class", () => {
273
+ const alignClasses = {
274
+ left: "justify-start",
275
+ right: "justify-end",
276
+ };
277
+
278
+ Object.entries(alignClasses).forEach(([align, classes]) => {
279
+ const component = mount(UNumber, {
280
+ props: {
281
+ align: align as Props["align"],
282
+ },
283
+ });
284
+
285
+ expect(component.attributes("class")).toContain(classes);
286
+ });
287
+ });
288
+
289
+ // DataTest prop
290
+ it("applies the correct data-test attribute", () => {
291
+ const testDataTest = "test-number";
292
+
293
+ const component = mount(UNumber, {
294
+ props: {
295
+ dataTest: testDataTest,
296
+ },
297
+ });
298
+
299
+ expect(component.find("[data-test]").attributes("data-test")).toBe(testDataTest);
300
+ });
301
+ });
302
+
303
+ // Slots tests
304
+ describe("Slots", () => {
305
+ // Left slot
306
+ it("renders content from left slot", () => {
307
+ const slotText = "Left";
308
+ const slotClass = "left-content";
309
+
310
+ const component = mount(UNumber, {
311
+ props: {
312
+ value,
313
+ },
314
+ slots: {
315
+ left: `<span class='${slotClass}'>${slotText}</span>`,
316
+ },
317
+ });
318
+
319
+ expect(component.find(`.${slotClass}`).exists()).toBe(true);
320
+ expect(component.find(`.${slotClass}`).text()).toBe(slotText);
321
+ });
322
+
323
+ // Right slot
324
+ it("renders content from right slot", () => {
325
+ const slotText = "Right";
326
+ const slotClass = "right-content";
327
+
328
+ const component = mount(UNumber, {
329
+ props: {
330
+ value,
331
+ },
332
+ slots: {
333
+ right: `<span class='${slotClass}'>${slotText}</span>`,
334
+ },
335
+ });
336
+
337
+ expect(component.find(`.${slotClass}`).exists()).toBe(true);
338
+ expect(component.find(`.${slotClass}`).text()).toBe(slotText);
339
+ });
340
+ });
341
+
342
+ // Exposed refs tests
343
+ describe("Exposed refs", () => {
344
+ // wrapperRef
345
+ it("exposes wrapperRef", () => {
346
+ const component = mount(UNumber, {});
347
+
348
+ expect(component.vm.wrapperRef).toBeDefined();
349
+ });
350
+ });
351
+ });
@@ -1,6 +1,7 @@
1
1
  import esbuild from "esbuild";
2
2
  import path from "node:path";
3
3
  import { cwd } from "node:process";
4
+ import { pathToFileURL } from "node:url";
4
5
  import { existsSync, statSync } from "node:fs";
5
6
  import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
6
7
 
@@ -100,7 +101,9 @@ export async function getDefaultComponentConfig(name, configDir) {
100
101
  }
101
102
 
102
103
  if (existsSync(configOutPath)) {
103
- config = (await import(configOutPath)).default;
104
+ const module = await import(pathToFileURL(configOutPath));
105
+
106
+ config = module.default;
104
107
  }
105
108
 
106
109
  return config;
@@ -125,7 +125,7 @@ export function generateIconExports() {
125
125
  const entries = files
126
126
  .map((relativePath) => {
127
127
  const fullImportPath = path.resolve(cachePath, relativePath).replace(/\\/g, "/");
128
- const virtualPath = path.join(cwd(), ICONS_CACHED_DIR, relativePath);
128
+ const virtualPath = path.join(cwd(), ICONS_CACHED_DIR, relativePath).replace(/\\/g, "/");
129
129
 
130
130
  return ` ["${virtualPath}", import("${fullImportPath}?component")]`;
131
131
  })
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { cwd } from "node:process";
4
+ import { pathToFileURL } from "node:url";
4
5
  import { defineConfig } from "cva";
5
6
  import { createGetMergedConfig } from "./mergeConfigs.js";
6
7
  import { merge } from "lodash-es";
@@ -35,7 +36,9 @@ export let vuelessConfig = {};
35
36
  fs.existsSync(configPathTs) && (await buildTSFile(configPathTs, configOutPath));
36
37
 
37
38
  if (fs.existsSync(configOutPath)) {
38
- vuelessConfig = (await import(configOutPath)).default;
39
+ const module = await import(pathToFileURL(configOutPath));
40
+
41
+ vuelessConfig = module.default;
39
42
  }
40
43
  })();
41
44
 
@@ -304,7 +304,7 @@ export function getArgTypes(componentName: string | undefined) {
304
304
  description: event.description,
305
305
  };
306
306
 
307
- if (import.meta.env.STORYBOOK_FULL) {
307
+ if (import.meta.env.DEV) {
308
308
  const eventName = "on" + event.name.charAt(0).toUpperCase() + event.name.slice(1);
309
309
 
310
310
  types[eventName] = {