@milaboratories/uikit 2.10.45 → 2.11.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 (41) hide show
  1. package/.turbo/turbo-build.log +19 -19
  2. package/.turbo/turbo-formatter$colon$check.log +2 -2
  3. package/.turbo/turbo-linter$colon$check.log +2 -2
  4. package/.turbo/turbo-types$colon$check.log +1 -1
  5. package/CHANGELOG.md +18 -0
  6. package/dist/components/PlNumberField/PlNumberField.js.map +1 -1
  7. package/dist/components/PlNumberField/PlNumberField.vue.d.ts +45 -75
  8. package/dist/components/PlNumberField/PlNumberField.vue.d.ts.map +1 -1
  9. package/dist/components/PlNumberField/PlNumberField.vue2.js +129 -121
  10. package/dist/components/PlNumberField/PlNumberField.vue2.js.map +1 -1
  11. package/dist/components/PlNumberField/__test__/PlNumberField.spec.d.ts.map +1 -0
  12. package/dist/components/PlNumberField/__test__/parseNumber.spec.d.ts +2 -0
  13. package/dist/components/PlNumberField/__test__/parseNumber.spec.d.ts.map +1 -0
  14. package/dist/components/PlNumberField/parseNumber.d.ts +56 -7
  15. package/dist/components/PlNumberField/parseNumber.d.ts.map +1 -1
  16. package/dist/components/PlNumberField/parseNumber.js +40 -56
  17. package/dist/components/PlNumberField/parseNumber.js.map +1 -1
  18. package/dist/components/PlNumberField/pl-number-field.css +1 -1
  19. package/dist/components/PlSearchField/PlSearchField.js.map +1 -1
  20. package/dist/components/PlSearchField/PlSearchField.style.js.map +1 -1
  21. package/dist/components/PlSearchField/PlSearchField.vue.d.ts +20 -32
  22. package/dist/components/PlSearchField/PlSearchField.vue.d.ts.map +1 -1
  23. package/dist/components/PlSearchField/PlSearchField.vue2.js +4 -2
  24. package/dist/components/PlSearchField/PlSearchField.vue2.js.map +1 -1
  25. package/dist/components/PlTextField/PlTextField.js.map +1 -1
  26. package/dist/components/PlTextField/PlTextField.vue.d.ts +46 -118
  27. package/dist/components/PlTextField/PlTextField.vue.d.ts.map +1 -1
  28. package/dist/components/PlTextField/PlTextField.vue2.js +61 -58
  29. package/dist/components/PlTextField/PlTextField.vue2.js.map +1 -1
  30. package/package.json +5 -5
  31. package/src/components/PlNumberField/PlNumberField.vue +151 -143
  32. package/src/components/PlNumberField/__test__/PlNumberField.spec.ts +296 -0
  33. package/src/components/PlNumberField/__test__/parseNumber.spec.ts +204 -0
  34. package/src/components/PlNumberField/parseNumber.ts +125 -98
  35. package/src/components/PlNumberField/pl-number-field.scss +17 -4
  36. package/src/components/PlSearchField/PlSearchField.vue +8 -4
  37. package/src/components/PlTextField/PlTextField.vue +37 -49
  38. package/src/components/PlTextField/__tests__/TextField.spec.ts +2 -2
  39. package/dist/components/PlNumberField/__tests__/PlNumberField.spec.d.ts.map +0 -1
  40. package/src/components/PlNumberField/__tests__/PlNumberField.spec.ts +0 -182
  41. /package/dist/components/PlNumberField/{__tests__ → __test__}/PlNumberField.spec.d.ts +0 -0
@@ -0,0 +1,296 @@
1
+ import PlNumberField from "../PlNumberField.vue";
2
+ import { mount } from "@vue/test-utils";
3
+ import { describe, expect, it } from "vitest";
4
+
5
+ describe("PlNumberField.vue", () => {
6
+ it("renders correctly with default props", () => {
7
+ const wrapper = mount(PlNumberField, {
8
+ props: {
9
+ modelValue: 10,
10
+ },
11
+ });
12
+ expect(wrapper.find("input").element.value).toBe("10");
13
+ });
14
+
15
+ it("displays the label when provided", () => {
16
+ const wrapper = mount(PlNumberField, {
17
+ props: {
18
+ modelValue: 10,
19
+ label: "Test Label",
20
+ },
21
+ });
22
+ expect(wrapper.find("label").text()).toBe("Test Label");
23
+ });
24
+
25
+ it("increments the value when increment button is clicked", async () => {
26
+ const wrapper = mount(PlNumberField, {
27
+ props: {
28
+ modelValue: 10,
29
+ step: 2,
30
+ },
31
+ });
32
+ const incrementButton = wrapper.find(".pl-number-field__icons div:first-child");
33
+ await incrementButton.trigger("click");
34
+ expect(wrapper.vm.modelValue).toEqual(12);
35
+ });
36
+
37
+ it("decrements the value when decrement button is clicked", async () => {
38
+ const wrapper = mount(PlNumberField, {
39
+ props: {
40
+ modelValue: 10,
41
+ step: 1,
42
+ },
43
+ });
44
+ const decrementButton = wrapper.find(".pl-number-field__icons div:last-child");
45
+ await decrementButton.trigger("click");
46
+ expect(wrapper.vm.modelValue).toEqual(9);
47
+ });
48
+
49
+ it("disables increment button when value exceeds maxValue", () => {
50
+ const wrapper = mount(PlNumberField, {
51
+ props: {
52
+ modelValue: 10,
53
+ maxValue: 10,
54
+ },
55
+ });
56
+ const incrementButton = wrapper.find(".pl-number-field__icons div:first-child");
57
+ expect(incrementButton.classes()).toContain("disabled");
58
+ });
59
+
60
+ it("disables decrement button when value is below minValue", () => {
61
+ const wrapper = mount(PlNumberField, {
62
+ props: {
63
+ modelValue: 1,
64
+ minValue: 1,
65
+ },
66
+ });
67
+ const decrementButton = wrapper.find(".pl-number-field__icons div:last-child");
68
+ expect(decrementButton.classes()).toContain("disabled");
69
+ });
70
+
71
+ it("renders external errorMessage with priority", () => {
72
+ const wrapper = mount(PlNumberField, {
73
+ props: {
74
+ modelValue: 5,
75
+ minValue: 10,
76
+ errorMessage: "Custom error message",
77
+ },
78
+ });
79
+ const errorText = wrapper.find(".pl-number-field__error").text();
80
+ expect(errorText).toBe("Custom error message");
81
+ });
82
+
83
+ it("renders validation error when no external errorMessage", () => {
84
+ const wrapper = mount(PlNumberField, {
85
+ props: {
86
+ modelValue: 5,
87
+ minValue: 10,
88
+ },
89
+ });
90
+ const errorText = wrapper.find(".pl-number-field__error").text();
91
+ expect(errorText).toBe("Value must be higher than 10");
92
+ });
93
+
94
+ it("disables step buttons when input has an error", async () => {
95
+ const wrapper = mount(PlNumberField, {
96
+ props: {
97
+ modelValue: 10,
98
+ },
99
+ });
100
+ const input = wrapper.find("input");
101
+ await input.setValue("abc");
102
+
103
+ const incrementButton = wrapper.find(".pl-number-field__icons div:first-child");
104
+ const decrementButton = wrapper.find(".pl-number-field__icons div:last-child");
105
+ expect(incrementButton.classes()).toContain("disabled");
106
+ expect(decrementButton.classes()).toContain("disabled");
107
+ });
108
+
109
+ describe("typing and value updates", () => {
110
+ it("updates model for valid input", async () => {
111
+ const wrapper = mount(PlNumberField, {
112
+ props: { modelValue: 5 },
113
+ });
114
+ const input = wrapper.find("input");
115
+
116
+ await input.setValue("15");
117
+ expect(wrapper.vm.modelValue).toEqual(15);
118
+ });
119
+
120
+ it("does not change model for invalid input", async () => {
121
+ const wrapper = mount(PlNumberField, {
122
+ props: { modelValue: 15 },
123
+ });
124
+ const input = wrapper.find("input");
125
+
126
+ await input.setValue("abc");
127
+ expect(wrapper.vm.modelValue).toEqual(15);
128
+ });
129
+
130
+ it("clears to undefined for partial input (-) on blur", async () => {
131
+ const wrapper = mount(PlNumberField, {
132
+ props: { modelValue: 15 },
133
+ });
134
+ const input = wrapper.find("input");
135
+
136
+ await input.setValue("-");
137
+ await input.trigger("focusout");
138
+ expect(wrapper.vm.modelValue).toEqual(undefined);
139
+ expect(input.element.value).toBe("");
140
+ });
141
+
142
+ it("clears to undefined for partial input (.) on blur", async () => {
143
+ const wrapper = mount(PlNumberField, {
144
+ props: { modelValue: 15 },
145
+ });
146
+ const input = wrapper.find("input");
147
+
148
+ await input.setValue(".");
149
+ await input.trigger("focusout");
150
+ expect(wrapper.vm.modelValue).toEqual(undefined);
151
+ expect(input.element.value).toBe("");
152
+ });
153
+
154
+ it("accepts negative numbers", async () => {
155
+ const wrapper = mount(PlNumberField, {
156
+ props: { modelValue: 5 },
157
+ });
158
+ const input = wrapper.find("input");
159
+
160
+ await input.setValue("-1");
161
+ await input.trigger("focusout");
162
+ expect(wrapper.vm.modelValue).toEqual(-1);
163
+ expect(input.element.value).toBe("-1");
164
+ });
165
+
166
+ it("shows separator error for comma-formatted input", async () => {
167
+ const wrapper = mount(PlNumberField, {
168
+ props: { modelValue: 5 },
169
+ });
170
+ const input = wrapper.find("input");
171
+
172
+ await input.setValue("1,5");
173
+ expect(wrapper.vm.modelValue).toEqual(5); // unchanged
174
+ expect(wrapper.find(".pl-number-field__error").text()).toContain("separator");
175
+ });
176
+ });
177
+
178
+ describe("blur/enter formatting", () => {
179
+ it("formats trailing dot on blur: '123.' → '123'", async () => {
180
+ const wrapper = mount(PlNumberField, {
181
+ props: { modelValue: 0 },
182
+ });
183
+ const input = wrapper.find("input");
184
+
185
+ await input.setValue("123.");
186
+ expect(wrapper.vm.modelValue).toEqual(123);
187
+
188
+ await input.trigger("focusout");
189
+ expect(input.element.value).toBe("123");
190
+ });
191
+
192
+ it("formats leading dot on blur: '.5' → '0.5'", async () => {
193
+ const wrapper = mount(PlNumberField, {
194
+ props: { modelValue: 0 },
195
+ });
196
+ const input = wrapper.find("input");
197
+
198
+ await input.setValue(".5");
199
+ expect(wrapper.vm.modelValue).toEqual(0.5);
200
+
201
+ await input.trigger("focusout");
202
+ expect(input.element.value).toBe("0.5");
203
+ });
204
+
205
+ it("formats leading zero on blur: '01' → '1'", async () => {
206
+ const wrapper = mount(PlNumberField, {
207
+ props: { modelValue: 0 },
208
+ });
209
+ const input = wrapper.find("input");
210
+
211
+ await input.setValue("01");
212
+ expect(wrapper.vm.modelValue).toEqual(1);
213
+
214
+ await input.trigger("focusout");
215
+ expect(input.element.value).toBe("1");
216
+ });
217
+
218
+ it("formats trailing zeros on blur: '1.10' → '1.1'", async () => {
219
+ const wrapper = mount(PlNumberField, {
220
+ props: { modelValue: 0 },
221
+ });
222
+ const input = wrapper.find("input");
223
+
224
+ await input.setValue("1.10");
225
+ expect(wrapper.vm.modelValue).toEqual(1.1);
226
+
227
+ await input.trigger("focusout");
228
+ expect(input.element.value).toBe("1.1");
229
+ });
230
+
231
+ it("formats exponential on blur: '1e-5' → '0.00001'", async () => {
232
+ const wrapper = mount(PlNumberField, {
233
+ props: { modelValue: 0 },
234
+ });
235
+ const input = wrapper.find("input");
236
+
237
+ await input.setValue("1e-5");
238
+ expect(wrapper.vm.modelValue).toEqual(0.00001);
239
+
240
+ await input.trigger("focusout");
241
+ expect(input.element.value).toBe("0.00001");
242
+ });
243
+
244
+ it("formats partial exponential on blur: '1e' → '1'", async () => {
245
+ const wrapper = mount(PlNumberField, {
246
+ props: { modelValue: 0 },
247
+ });
248
+ const input = wrapper.find("input");
249
+
250
+ await input.setValue("1e");
251
+ expect(wrapper.vm.modelValue).toEqual(1);
252
+
253
+ await input.trigger("focusout");
254
+ expect(input.element.value).toBe("1");
255
+ });
256
+ });
257
+
258
+ it("update model with undefined when input is cleared", async () => {
259
+ const wrapper = mount(PlNumberField, {
260
+ props: {
261
+ modelValue: 10,
262
+ },
263
+ });
264
+ const input = wrapper.find("input");
265
+ await input.setValue("");
266
+ await input.trigger("focusout");
267
+ expect(wrapper.vm.modelValue).toEqual(undefined);
268
+ });
269
+
270
+ it("external modelValue change", async () => {
271
+ const wrapper = mount(PlNumberField, {
272
+ props: {
273
+ modelValue: 10,
274
+ },
275
+ });
276
+
277
+ const input = wrapper.find("input");
278
+ await input.trigger("focusout");
279
+ expect(wrapper.vm.modelValue).toEqual(10);
280
+ expect(input.element.value).toEqual("10");
281
+
282
+ await input.setValue("");
283
+ await input.trigger("focusout");
284
+ expect(wrapper.vm.modelValue).toEqual(undefined);
285
+
286
+ wrapper.setProps({ modelValue: 1 });
287
+
288
+ await input.trigger("focusout");
289
+ expect(wrapper.vm.modelValue).toEqual(1);
290
+ expect(input.element.value).toEqual("1");
291
+
292
+ await input.setValue("10");
293
+ expect(wrapper.vm.modelValue).toEqual(10);
294
+ expect(input.element.value).toEqual("10");
295
+ });
296
+ });
@@ -0,0 +1,204 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { tryParseNumber, numberToDecimalString, validateNumber } from "../parseNumber";
3
+
4
+ describe("tryParseNumber", () => {
5
+ describe("empty and partial inputs → {} (no value, no error)", () => {
6
+ it.each(["", "-", ".", "-."])("'%s' → {}", (input) => {
7
+ const result = tryParseNumber(input);
8
+ expect(result.value).toBeUndefined();
9
+ expect(result.error).toBeUndefined();
10
+ });
11
+ });
12
+
13
+ describe("trailing dot → value, no error", () => {
14
+ it.each([
15
+ ["123.", 123],
16
+ ["-123.", -123],
17
+ ["0.", 0],
18
+ ])("'%s' → { value: %d }", (input, expected) => {
19
+ const result = tryParseNumber(input);
20
+ expect(result.value).toBe(expected);
21
+ expect(result.error).toBeUndefined();
22
+ });
23
+ });
24
+
25
+ describe("partial exponential → completed and parsed", () => {
26
+ it.each([
27
+ ["1e", 1],
28
+ ["1e-", 1],
29
+ ["1e+", 1],
30
+ ["5e", 5],
31
+ ["5E", 5],
32
+ ["5e-", 5],
33
+ ["5e+", 5],
34
+ ])("'%s' → { value: %d }", (input, expected) => {
35
+ const result = tryParseNumber(input);
36
+ expect(result.value).toBe(expected);
37
+ expect(result.error).toBeUndefined();
38
+ });
39
+ });
40
+
41
+ describe("exact match → value, no error", () => {
42
+ it.each([
43
+ ["123", 123],
44
+ ["-5", -5],
45
+ ["0", 0],
46
+ ["0.5", 0.5],
47
+ ["-0.5", -0.5],
48
+ ["1000", 1000],
49
+ ["999999", 999999],
50
+ ])("'%s' → { value: %d }", (input, expected) => {
51
+ const result = tryParseNumber(input);
52
+ expect(result.value).toBe(expected);
53
+ expect(result.error).toBeUndefined();
54
+ });
55
+ });
56
+
57
+ describe("decimal form of very small numbers → value, no error", () => {
58
+ it("'0.0000000001' → { value: 1e-10 }", () => {
59
+ const result = tryParseNumber("0.0000000001");
60
+ expect(result.value).toBe(1e-10);
61
+ expect(result.error).toBeUndefined();
62
+ });
63
+ });
64
+
65
+ describe("exponential notation → value, no error", () => {
66
+ it.each([
67
+ ["1e-5", 0.00001],
68
+ ["2e+10", 2e10],
69
+ ["1E-5", 0.00001],
70
+ ["1e5", 1e5],
71
+ ["1.5e2", 150],
72
+ ["-3e4", -30000],
73
+ ])("'%s' → { value: %d }", (input, expected) => {
74
+ const result = tryParseNumber(input);
75
+ expect(result.value).toBe(expected);
76
+ expect(result.error).toBeUndefined();
77
+ });
78
+ });
79
+
80
+ describe("non-canonical but parseable → value, no error (formatted on blur)", () => {
81
+ it.each([
82
+ [".5", 0.5],
83
+ ["01", 1],
84
+ ["007", 7],
85
+ ["1.0", 1],
86
+ ["1.10", 1.1],
87
+ ["+5", 5],
88
+ ["00.5", 0.5],
89
+ ])("'%s' → { value: %d }", (input, expected) => {
90
+ const result = tryParseNumber(input);
91
+ expect(result.value).toBe(expected);
92
+ expect(result.error).toBeUndefined();
93
+ });
94
+ });
95
+
96
+ describe("locale-formatted numbers → separator error", () => {
97
+ it.each([
98
+ "1,5",
99
+ "1.232,111",
100
+ "1.237.62",
101
+ "555.555.555,100",
102
+ "1,222,333.05",
103
+ "1 234",
104
+ "1.000.000",
105
+ ])("'%s' → separator error", (input) => {
106
+ const result = tryParseNumber(input);
107
+ expect(result.error).toBe("Use dot as decimal separator, e.g. 3.14");
108
+ expect(result.value).toBeUndefined();
109
+ });
110
+ });
111
+
112
+ describe("non-numeric input → 'not a number' error", () => {
113
+ it.each(["abc", "12abc", "1.237.asdf62", "hello", "#$%", "1.2.3a"])(
114
+ "'%s' → not a number error",
115
+ (input) => {
116
+ const result = tryParseNumber(input);
117
+ expect(result.error).toBe("Value is not a number");
118
+ expect(result.value).toBeUndefined();
119
+ },
120
+ );
121
+ });
122
+
123
+ describe("precision loss → error with actual value", () => {
124
+ it("too many decimal digits", () => {
125
+ const result = tryParseNumber("0.1234567890123456789");
126
+ expect(result.error).toMatch(/^Precision exceeded/);
127
+ expect(result.value).toBeUndefined();
128
+ });
129
+
130
+ it("integer exceeds safe range", () => {
131
+ const result = tryParseNumber("9007199254740993");
132
+ expect(result.error).toMatch(/^Precision exceeded/);
133
+ expect(result.value).toBeUndefined();
134
+ });
135
+
136
+ it("trailing zeros beyond precision", () => {
137
+ const result = tryParseNumber("1.00000000000000001");
138
+ expect(result.error).toMatch(/^Precision exceeded/);
139
+ expect(result.value).toBeUndefined();
140
+ });
141
+
142
+ it("safe integer does not trigger precision error", () => {
143
+ const result = tryParseNumber("9007199254740991");
144
+ expect(result.value).toBe(9007199254740991);
145
+ expect(result.error).toBeUndefined();
146
+ });
147
+ });
148
+
149
+ describe("whitespace handling", () => {
150
+ it("trims leading/trailing spaces", () => {
151
+ const result = tryParseNumber(" 123 ");
152
+ expect(result.value).toBe(123);
153
+ });
154
+
155
+ it("space inside number → separator error", () => {
156
+ const result = tryParseNumber("1 234");
157
+ expect(result.error).toBe("Use dot as decimal separator, e.g. 3.14");
158
+ });
159
+ });
160
+ });
161
+
162
+ describe("numberToDecimalString", () => {
163
+ it("undefined → empty string", () => {
164
+ expect(numberToDecimalString(undefined)).toBe("");
165
+ });
166
+
167
+ it("plain numbers pass through", () => {
168
+ expect(numberToDecimalString(123)).toBe("123");
169
+ expect(numberToDecimalString(0.5)).toBe("0.5");
170
+ expect(numberToDecimalString(-42)).toBe("-42");
171
+ expect(numberToDecimalString(0)).toBe("0");
172
+ });
173
+
174
+ it("converts exponential to decimal form", () => {
175
+ expect(numberToDecimalString(1e-7)).toBe("0.0000001");
176
+ expect(numberToDecimalString(1e-10)).toBe("0.0000000001");
177
+ expect(numberToDecimalString(2e10)).toBe("20000000000");
178
+ });
179
+ });
180
+
181
+ describe("validateNumber", () => {
182
+ it("returns undefined when within bounds", () => {
183
+ expect(validateNumber(5, { minValue: 0, maxValue: 10 })).toBeUndefined();
184
+ });
185
+
186
+ it("returns error when below minValue", () => {
187
+ expect(validateNumber(-1, { minValue: 0 })).toBe("Value must be higher than 0");
188
+ });
189
+
190
+ it("returns error when above maxValue", () => {
191
+ expect(validateNumber(11, { maxValue: 10 })).toBe("Value must be less than 10");
192
+ });
193
+
194
+ it("runs custom validate function", () => {
195
+ const validate = (v: number) => (v % 2 !== 0 ? "Must be even" : undefined);
196
+ expect(validateNumber(3, { validate })).toBe("Must be even");
197
+ expect(validateNumber(4, { validate })).toBeUndefined();
198
+ });
199
+
200
+ it("minValue/maxValue checked before custom validate", () => {
201
+ const validate = (v: number) => (v % 2 !== 0 ? "Must be even" : undefined);
202
+ expect(validateNumber(-1, { minValue: 0, validate })).toBe("Value must be higher than 0");
203
+ });
204
+ });