srcdev-nuxt-components 9.6.8 → 9.6.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.
Files changed (108) hide show
  1. package/.claude/skills/component-compliance-checklist.md +12 -1
  2. package/.claude/skills/component-inline-action-button.md +1 -1
  3. package/.claude/skills/component-local-style-override.md +1 -1
  4. package/.claude/skills/component-naming.md +11 -11
  5. package/.claude/skills/components/container-glow.md +2 -2
  6. package/.claude/skills/components/form-field.md +2 -2
  7. package/.claude/skills/components/form-wrapper.md +1 -1
  8. package/.claude/skills/components/glowing-border.md +1 -1
  9. package/.claude/skills/components/input-description.md +7 -7
  10. package/.claude/skills/components/input-error.md +3 -3
  11. package/.claude/skills/components/input-label.md +7 -8
  12. package/.claude/skills/components/input-number.md +17 -17
  13. package/.claude/skills/components/input-select.md +153 -0
  14. package/.claude/skills/components/input-text.md +220 -0
  15. package/.claude/skills/components/input-textarea.md +147 -0
  16. package/.claude/skills/components/select-menu.md +4 -4
  17. package/.claude/skills/components/toggle-switch.md +143 -0
  18. package/.claude/skills/composable-zod-validation.md +1 -1
  19. package/.claude/skills/icon-sets.md +1 -1
  20. package/.claude/skills/icon-sizing.md +1 -1
  21. package/.claude/skills/index.md +5 -5
  22. package/.claude/skills/storybook-add-story.md +1 -1
  23. package/.claude/skills/theming-component-token-pattern.md +8 -8
  24. package/.claude/skills/theming-form-geometry-tokens.md +1 -1
  25. package/.vscode/srcdev-component-input-select.code-snippets +20 -14
  26. package/.vscode/srcdev-component-input-text.code-snippets +46 -40
  27. package/.vscode/srcdev-component-input-textarea.code-snippets +19 -14
  28. package/.vscode/srcdev-component-toggle-switch.code-snippets +19 -29
  29. package/app/assets/styles/setup/04.elements/forms/00.element-defaults.css +2 -2
  30. package/app/assets/styles/setup/04.elements/forms/06.button-geometry.css +1 -1
  31. package/app/components/01.atoms/animations/glowing-border/CONSUMER-STYLING.md +2 -2
  32. package/app/components/01.atoms/animations/glowing-border/stories/GlowingBorder.stories.ts +4 -4
  33. package/app/components/02.molecules/progress-tracker/stories/ProgressTracker.stories.ts +4 -4
  34. package/app/components/05.forms/form-errors/CONSUMER-STYLING.md +1 -1
  35. package/app/components/05.forms/form-field/CONSUMER-STYLING.md +2 -2
  36. package/app/components/05.forms/input-button/stories/InputButton.stories.ts +3 -3
  37. package/app/components/05.forms/input-description/CONSUMER-STYLING.md +6 -6
  38. package/app/components/05.forms/input-label/CONSUMER-STYLING.md +5 -5
  39. package/app/components/05.forms/input-number/CONSUMER-STYLING.md +9 -5
  40. package/app/components/05.forms/input-number/InputNumber.vue +2 -2
  41. package/app/components/05.forms/input-select/CONSUMER-STYLING.md +80 -10
  42. package/app/components/05.forms/input-select/InputSelect.vue +198 -0
  43. package/app/components/05.forms/input-select/InputSelectField.vue +130 -0
  44. package/app/components/05.forms/input-select/stories/InputSelect.stories.ts +184 -0
  45. package/app/components/05.forms/input-select/stories/InputSelectField.stories.ts +187 -0
  46. package/app/components/05.forms/input-select/tests/{InputSelectCore.spec.ts → InputSelect.spec.ts} +75 -12
  47. package/app/components/05.forms/input-select/tests/InputSelectField.spec.ts +121 -0
  48. package/app/components/05.forms/input-text/CONSUMER-STYLING.md +85 -15
  49. package/app/components/05.forms/input-text/{InputPasswordWithLabel.vue → InputPasswordField.vue} +16 -19
  50. package/app/components/05.forms/input-text/{InputTextCore.vue → InputText.vue} +42 -78
  51. package/app/components/05.forms/input-text/{InputTextAsNumberWithLabel.vue → InputTextAsNumberField.vue} +66 -62
  52. package/app/components/05.forms/input-text/InputTextField.vue +168 -0
  53. package/app/components/05.forms/input-text/stories/InputPasswordField.stories.ts +163 -0
  54. package/app/components/05.forms/input-text/stories/InputText.stories.ts +194 -0
  55. package/app/components/05.forms/input-text/stories/InputTextAsNumberField.stories.ts +182 -0
  56. package/app/components/05.forms/input-text/stories/InputTextField.stories.ts +206 -0
  57. package/app/components/05.forms/input-text/tests/{InputPasswordWithLabel.spec.ts → InputPasswordField.spec.ts} +42 -3
  58. package/app/components/05.forms/input-text/tests/InputText.spec.ts +166 -0
  59. package/app/components/05.forms/input-text/tests/InputTextAsNumberField.spec.ts +135 -0
  60. package/app/components/05.forms/input-text/tests/InputTextField.spec.ts +130 -0
  61. package/app/components/05.forms/input-textarea/CONSUMER-STYLING.md +94 -12
  62. package/app/components/05.forms/input-textarea/InputTextarea.vue +165 -0
  63. package/app/components/05.forms/input-textarea/InputTextareaField.vue +159 -0
  64. package/app/components/05.forms/input-textarea/stories/InputTextarea.stories.ts +198 -0
  65. package/app/components/05.forms/input-textarea/stories/InputTextareaField.stories.ts +161 -0
  66. package/app/components/05.forms/input-textarea/tests/InputTextarea.spec.ts +149 -0
  67. package/app/components/05.forms/input-textarea/tests/InputTextareaField.spec.ts +122 -0
  68. package/app/components/05.forms/patterns/stories/ContactForm.stories.ts +13 -13
  69. package/app/components/05.forms/patterns/stories/LoginForm.stories.ts +6 -6
  70. package/app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts +30 -32
  71. package/app/components/05.forms/toggle-switch/CONSUMER-STYLING.md +71 -50
  72. package/app/components/05.forms/toggle-switch/{ToggleSwitchCore.vue → ToggleSwitch.vue} +58 -73
  73. package/app/components/05.forms/toggle-switch/{ToggleSwitchWithLabel.vue → ToggleSwitchField.vue} +65 -19
  74. package/app/components/05.forms/toggle-switch/stories/ToggleSwitch.stories.ts +148 -0
  75. package/app/components/05.forms/toggle-switch/stories/ToggleSwitchField.stories.ts +176 -0
  76. package/app/components/05.forms/toggle-switch/tests/ToggleSwitch.spec.ts +145 -0
  77. package/app/components/05.forms/toggle-switch/tests/ToggleSwitchField.spec.ts +155 -0
  78. package/package.json +1 -1
  79. package/.claude/skills/components/input-select-core.md +0 -151
  80. package/.claude/skills/components/input-text-core.md +0 -204
  81. package/.claude/skills/components/input-textarea-core.md +0 -154
  82. package/.claude/skills/components/toggle-switch-core.md +0 -169
  83. package/app/components/05.forms/input-select/InputSelectCore.vue +0 -188
  84. package/app/components/05.forms/input-select/InputSelectWithLabel.vue +0 -118
  85. package/app/components/05.forms/input-select/stories/InputSelectCore.stories.ts +0 -69
  86. package/app/components/05.forms/input-select/stories/InputSelectWithLabel.stories.ts +0 -115
  87. package/app/components/05.forms/input-select/tests/InputSelectWithLabel.spec.ts +0 -87
  88. package/app/components/05.forms/input-text/InputTextWithLabel.vue +0 -154
  89. package/app/components/05.forms/input-text/stories/InputPasswordWithLabel.stories.ts +0 -368
  90. package/app/components/05.forms/input-text/stories/InputTextAsNumberWithLabel.stories.ts +0 -480
  91. package/app/components/05.forms/input-text/stories/InputTextCore.stories.ts +0 -267
  92. package/app/components/05.forms/input-text/stories/InputTextWithLabel.stories.ts +0 -319
  93. package/app/components/05.forms/input-text/tests/InputTextAsNumberWithLabel.spec.ts +0 -75
  94. package/app/components/05.forms/input-text/tests/InputTextCore.spec.ts +0 -60
  95. package/app/components/05.forms/input-text/tests/InputTextWithLabel.spec.ts +0 -71
  96. package/app/components/05.forms/input-textarea/InputTextareaCore.vue +0 -159
  97. package/app/components/05.forms/input-textarea/InputTextareaWithLabel.vue +0 -136
  98. package/app/components/05.forms/input-textarea/stories/InputTextareaCore.stories.ts +0 -247
  99. package/app/components/05.forms/input-textarea/stories/InputTextareaWithLabel.stories.ts +0 -458
  100. package/app/components/05.forms/input-textarea/tests/InputTextareaCore.spec.ts +0 -119
  101. package/app/components/05.forms/input-textarea/tests/InputTextareaWithLabel.spec.ts +0 -93
  102. package/app/components/05.forms/toggle-switch/ToggleSwitchWithLabelInline.vue +0 -81
  103. package/app/components/05.forms/toggle-switch/stories/ToggleSwitchCore.stories.ts +0 -251
  104. package/app/components/05.forms/toggle-switch/stories/ToggleSwitchWithLabel.stories.ts +0 -251
  105. package/app/components/05.forms/toggle-switch/stories/ToggleSwitchWithLabelInline.stories.ts +0 -283
  106. package/app/components/05.forms/toggle-switch/tests/ToggleSwitchCore.spec.ts +0 -101
  107. package/app/components/05.forms/toggle-switch/tests/ToggleSwitchWithLabel.spec.ts +0 -133
  108. package/app/components/05.forms/toggle-switch/tests/ToggleSwitchWithLabelInline.spec.ts +0 -66
@@ -1,458 +0,0 @@
1
- import type { Meta, StoryFn } from "@nuxtjs/storybook";
2
- import { computed, reactive } from "vue";
3
- import StorybookComponent from "../InputTextareaWithLabel.vue";
4
- import type { FormUiTheme, InputUiVariant } from "~/types/forms/types.forms.d";
5
-
6
- interface InputTextareaWithLabelStoryArgs {
7
- modelValue: string;
8
- maxlength: number;
9
- name: string;
10
- placeholder: string;
11
- label: string;
12
- errorMessage: string;
13
- fieldHasError: boolean;
14
- required: boolean;
15
- theme: FormUiTheme;
16
- inputVariant: InputUiVariant;
17
- styleClassPassthrough: string[];
18
- useDescriptionTextSlot: boolean;
19
- useDescriptionHtmlSlot: boolean;
20
- descriptionTextContent: string;
21
- descriptionHtmlContent: string;
22
- useLeftSlot: boolean;
23
- useRightSlot: boolean;
24
- leftSlotContent: string;
25
- rightSlotContent: string;
26
- }
27
-
28
- export default {
29
- title: "Components/Forms/Input Textarea/InputTextareaWithLabel",
30
- component: StorybookComponent,
31
- argTypes: {
32
- // Model
33
- modelValue: {
34
- control: "text",
35
- description: "The textarea value",
36
- table: {
37
- category: "Model",
38
- },
39
- },
40
-
41
- // Basic Configuration
42
- maxlength: {
43
- control: { type: "number", min: 1, max: 10000 },
44
- description: "Maximum length of textarea",
45
- table: {
46
- category: "Basic",
47
- },
48
- },
49
- name: {
50
- control: "text",
51
- description: "Textarea name attribute",
52
- table: {
53
- category: "Basic",
54
- },
55
- },
56
- placeholder: {
57
- control: "text",
58
- description: "Placeholder text",
59
- table: {
60
- category: "Basic",
61
- },
62
- },
63
- label: {
64
- control: "text",
65
- description: "Textarea label",
66
- table: {
67
- category: "Basic",
68
- },
69
- },
70
-
71
- // Validation
72
- errorMessage: {
73
- control: "text",
74
- description: "Error message to display",
75
- table: {
76
- category: "Validation",
77
- },
78
- },
79
- fieldHasError: {
80
- control: "boolean",
81
- description: "Whether field has validation error",
82
- table: {
83
- category: "Validation",
84
- },
85
- },
86
- required: {
87
- control: "boolean",
88
- description: "Whether field is required",
89
- table: {
90
- category: "Validation",
91
- },
92
- },
93
-
94
- // Styling
95
- theme: {
96
- control: { type: "select" },
97
- options: ["primary", "secondary", "tertiary", "ghost", "error", "success", "warning"],
98
- description: "Theme for styling",
99
- table: {
100
- category: "Styling",
101
- },
102
- },
103
- inputVariant: {
104
- control: { type: "select" },
105
- options: ["normal", "outlined", "underlined"],
106
- description: "Textarea variant style",
107
- table: {
108
- category: "Styling",
109
- },
110
- },
111
- styleClassPassthrough: {
112
- control: "object",
113
- description: "Additional CSS classes",
114
- table: {
115
- category: "Styling",
116
- },
117
- },
118
-
119
- // Slots
120
- useDescriptionTextSlot: {
121
- control: "boolean",
122
- description: "Use description text slot",
123
- table: {
124
- category: "Slots",
125
- },
126
- },
127
- useDescriptionHtmlSlot: {
128
- control: "boolean",
129
- description: "Use description HTML slot",
130
- table: {
131
- category: "Slots",
132
- },
133
- },
134
- descriptionTextContent: {
135
- control: "text",
136
- description: "Content for description text slot",
137
- table: {
138
- category: "Slots",
139
- },
140
- },
141
- descriptionHtmlContent: {
142
- control: "text",
143
- description: "HTML content for description slot",
144
- table: {
145
- category: "Slots",
146
- },
147
- },
148
- useLeftSlot: {
149
- control: "boolean",
150
- description: "Use left slot",
151
- table: {
152
- category: "Slots",
153
- },
154
- },
155
- useRightSlot: {
156
- control: "boolean",
157
- description: "Use right slot",
158
- table: {
159
- category: "Slots",
160
- },
161
- },
162
- leftSlotContent: {
163
- control: "text",
164
- description: "Content for left slot",
165
- table: {
166
- category: "Slots",
167
- },
168
- },
169
- rightSlotContent: {
170
- control: "text",
171
- description: "Content for right slot",
172
- table: {
173
- category: "Slots",
174
- },
175
- },
176
- },
177
- args: {
178
- modelValue: "",
179
- maxlength: 1000,
180
- name: "example-textarea",
181
- placeholder: "Enter your message...",
182
- label: "Message",
183
- errorMessage: "",
184
- fieldHasError: false,
185
- required: false,
186
- theme: "default",
187
- inputVariant: "normal",
188
- styleClassPassthrough: [],
189
- useDescriptionTextSlot: false,
190
- useDescriptionHtmlSlot: false,
191
- descriptionTextContent: "Please provide a detailed message",
192
- descriptionHtmlContent: "Please provide a <strong>detailed</strong> message with <em>important</em> information",
193
- useLeftSlot: false,
194
- useRightSlot: false,
195
- leftSlotContent: "📝",
196
- rightSlotContent: "✨",
197
- },
198
- } as Meta<typeof StorybookComponent>;
199
-
200
- // `args` is Storybook's own reactive object — bind to it directly (`args.x`) rather than
201
- // destructuring it into local variables/refs, which would snapshot the values once at setup()
202
- // and stop reflecting later Controls-panel changes. `componentArgs` strips the non-prop
203
- // slot-toggle args so they don't leak onto the component as unknown attributes.
204
- const Template: StoryFn<InputTextareaWithLabelStoryArgs> = (args) => ({
205
- components: { StorybookComponent },
206
- setup() {
207
- const componentArgs = computed(() => {
208
- const {
209
- useDescriptionTextSlot,
210
- useDescriptionHtmlSlot,
211
- descriptionTextContent,
212
- descriptionHtmlContent,
213
- useLeftSlot,
214
- useRightSlot,
215
- leftSlotContent,
216
- rightSlotContent,
217
- ...rest
218
- } = args;
219
- return rest;
220
- });
221
-
222
- return { args, componentArgs };
223
- },
224
- template: `
225
- <StorybookComponent
226
- v-model="args.modelValue"
227
- v-bind="componentArgs"
228
- >
229
- <template v-if="args.useDescriptionTextSlot" #descriptionText>{{ args.descriptionTextContent }}</template>
230
- <template v-if="args.useDescriptionHtmlSlot" #descriptionHtml v-html="args.descriptionHtmlContent"></template>
231
- <template v-if="args.useLeftSlot" #left>{{ args.leftSlotContent }}</template>
232
- <template v-if="args.useRightSlot" #right>{{ args.rightSlotContent }}</template>
233
- </StorybookComponent>
234
- <div class="mt-4 text-sm text-gray-600">
235
- Character count: {{ args.modelValue.length }} / {{ args.maxlength }}
236
- </div>
237
- `,
238
- });
239
-
240
- export const Default = Template.bind({});
241
- Default.args = {};
242
-
243
- export const WithDescription = Template.bind({});
244
- WithDescription.args = {
245
- useDescriptionTextSlot: true,
246
- label: "Feedback",
247
- placeholder: "Share your thoughts...",
248
- };
249
-
250
- export const WithHtmlDescription = Template.bind({});
251
- WithHtmlDescription.args = {
252
- useDescriptionHtmlSlot: true,
253
- label: "Product Review",
254
- placeholder: "Write your detailed review...",
255
- descriptionHtmlContent:
256
- "Please include <strong>specific details</strong> about the product, including <em>pros</em> and <em>cons</em>",
257
- };
258
-
259
- export const ContactForm = Template.bind({});
260
- ContactForm.args = {
261
- label: "Message",
262
- placeholder: "Tell us how we can help you...",
263
- useDescriptionTextSlot: true,
264
- descriptionTextContent: "Please provide as much detail as possible so we can assist you effectively",
265
- maxlength: 500,
266
- required: true,
267
- };
268
-
269
- export const CommentBox = Template.bind({});
270
- CommentBox.args = {
271
- label: "Comment",
272
- placeholder: "Add a comment...",
273
- maxlength: 280,
274
- modelValue: "This is a great article! Thanks for sharing.",
275
- };
276
-
277
- export const CodeSnippet = Template.bind({});
278
- CodeSnippet.args = {
279
- label: "Code",
280
- placeholder: "Paste your code here...",
281
- useDescriptionTextSlot: true,
282
- descriptionTextContent: "Please include any relevant context or error messages",
283
- useLeftSlot: true,
284
- leftSlotContent: "💻",
285
- maxlength: 2000,
286
- modelValue: `function greet(name) {
287
- return "Hello, " + name + "!";
288
- }
289
-
290
- console.log(greet("World"));`,
291
- };
292
-
293
- export const WithError = Template.bind({});
294
- WithError.args = {
295
- fieldHasError: true,
296
- errorMessage: "Message is too short. Please provide more details.",
297
- theme: "error",
298
- label: "Error Example",
299
- placeholder: "This field has an error",
300
- modelValue: "Too short",
301
- };
302
-
303
- export const Required = Template.bind({});
304
- Required.args = {
305
- required: true,
306
- label: "Required Message",
307
- placeholder: "This field is required",
308
- useDescriptionTextSlot: true,
309
- descriptionTextContent: "This field is required and cannot be left empty",
310
- };
311
-
312
- export const Outlined = Template.bind({});
313
- Outlined.args = {
314
- inputVariant: "outlined",
315
- label: "Outlined Textarea",
316
- placeholder: "This is an outlined textarea",
317
- useDescriptionTextSlot: true,
318
- descriptionTextContent: "Notice how the description appears below in outlined variant",
319
- };
320
-
321
- export const Underlined = Template.bind({});
322
- Underlined.args = {
323
- inputVariant: "underlined",
324
- label: "Underlined Textarea",
325
- placeholder: "This is an underlined textarea",
326
- };
327
-
328
- export const WithSlots = Template.bind({});
329
- WithSlots.args = {
330
- label: "Message with Icons",
331
- placeholder: "Type your message...",
332
- useLeftSlot: true,
333
- useRightSlot: true,
334
- leftSlotContent: "📝",
335
- rightSlotContent: "💌",
336
- useDescriptionTextSlot: true,
337
- descriptionTextContent: "Icons are displayed on the left and right of the textarea",
338
- };
339
-
340
- export const AllVariants = Template.bind({});
341
- AllVariants.storyName = "All Input Variants";
342
- AllVariants.render = (args) => ({
343
- components: { StorybookComponent },
344
- setup() {
345
- const variants: InputUiVariant[] = ["normal", "outlined", "underlined"];
346
- const textareaValues = reactive(
347
- variants.reduce(
348
- (acc, variant) => ({
349
- ...acc,
350
- [variant]: `Sample text for ${variant} variant`,
351
- }),
352
- {} as Record<string, string>
353
- )
354
- );
355
-
356
- return {
357
- variants,
358
- textareaValues,
359
- args,
360
- };
361
- },
362
- template: `
363
- <div class="space-y-6">
364
- <div v-for="variant in variants" :key="variant" class="space-y-2">
365
- <h3 class="text-lg font-semibold capitalize">{{ variant }} Variant</h3>
366
- <StorybookComponent
367
- v-model="textareaValues[variant]"
368
- v-bind="{ ...args, inputVariant: variant, label: variant + ' Textarea', name: variant + '-textarea' }"
369
- />
370
- </div>
371
- </div>
372
- `,
373
- });
374
-
375
- export const FormExamples = Template.bind({});
376
- FormExamples.storyName = "Real-World Form Examples";
377
- FormExamples.render = (args) => ({
378
- components: { StorybookComponent },
379
- setup() {
380
- const formData = reactive({
381
- feedback: "",
382
- bio: "",
383
- message: "",
384
- code: "",
385
- });
386
-
387
- return {
388
- formData,
389
- args,
390
- };
391
- },
392
- template: `
393
- <div class="space-y-6">
394
- <div class="space-y-4">
395
- <h2 class="text-xl font-bold">Contact Form</h2>
396
- <StorybookComponent
397
- v-model="formData.message"
398
- label="Message"
399
- name="contact-message"
400
- placeholder="How can we help you?"
401
- :required="true"
402
- :maxlength="500"
403
- >
404
- <template #descriptionText>Please describe your inquiry in detail (max 500 characters)</template>
405
- </StorybookComponent>
406
- </div>
407
-
408
- <div class="space-y-4">
409
- <h2 class="text-xl font-bold">User Profile</h2>
410
- <StorybookComponent
411
- v-model="formData.bio"
412
- label="Bio"
413
- name="user-bio"
414
- placeholder="Tell us about yourself..."
415
- :maxlength="200"
416
- input-variant="outlined"
417
- >
418
- <template #descriptionText>Share a brief description about yourself (max 200 characters)</template>
419
- </StorybookComponent>
420
- </div>
421
-
422
- <div class="space-y-4">
423
- <h2 class="text-xl font-bold">Feedback Form</h2>
424
- <StorybookComponent
425
- v-model="formData.feedback"
426
- label="Feedback"
427
- name="user-feedback"
428
- placeholder="Share your experience..."
429
- :maxlength="1000"
430
- >
431
- <template #descriptionHtml>Rate your experience and provide <strong>detailed feedback</strong></template>
432
- <template #left>⭐</template>
433
- </StorybookComponent>
434
- </div>
435
-
436
- <div class="space-y-4">
437
- <h2 class="text-xl font-bold">Code Submission</h2>
438
- <StorybookComponent
439
- v-model="formData.code"
440
- label="Code Snippet"
441
- name="code-snippet"
442
- placeholder="Paste your code here..."
443
- :maxlength="5000"
444
- input-variant="underlined"
445
- >
446
- <template #descriptionText>Paste your code snippet with proper formatting</template>
447
- <template #left>💻</template>
448
- <template #right>📋</template>
449
- </StorybookComponent>
450
- </div>
451
-
452
- <div class="p-4 bg-gray-100 rounded">
453
- <h3 class="font-semibold mb-2">Form Data Preview:</h3>
454
- <pre class="text-sm">{{ JSON.stringify(formData, null, 2) }}</pre>
455
- </div>
456
- </div>
457
- `,
458
- });
@@ -1,119 +0,0 @@
1
- // https://nuxt.com/docs/getting-started/testing#unit-testing
2
- import type { VueWrapper } from "@vue/test-utils";
3
- import { mountSuspended } from "@nuxt/test-utils/runtime";
4
- import ComponentUnderTest from "../InputTextareaCore.vue";
5
-
6
- const initialPropsData = {
7
- id: "test-textarea",
8
- name: "testName",
9
- modelValue: "",
10
- };
11
-
12
- let wrapper: VueWrapper<InstanceType<typeof ComponentUnderTest>>;
13
- const wrapperFactory = (propsData = {}, slots = {}) => {
14
- const mockPropsData = { ...initialPropsData, ...propsData };
15
-
16
- return mountSuspended(ComponentUnderTest, {
17
- attachTo: document.body,
18
- props: mockPropsData,
19
- slots,
20
- });
21
- };
22
-
23
- describe("InputTextareaCore", () => {
24
- it("mounts without error", async () => {
25
- wrapper = await wrapperFactory();
26
- expect(wrapper.vm).toBeTruthy();
27
- });
28
-
29
- it("renders a native textarea", async () => {
30
- wrapper = await wrapperFactory();
31
- expect(wrapper.find("textarea").exists()).toBe(true);
32
- });
33
-
34
- it("binds v-model to the textarea value", async () => {
35
- wrapper = await wrapperFactory({ modelValue: "hello world" });
36
- expect(wrapper.find("textarea").element.value).toBe("hello world");
37
- });
38
-
39
- it("forwards placeholder", async () => {
40
- wrapper = await wrapperFactory({ placeholder: "Enter your message" });
41
- expect(wrapper.find("textarea").attributes("placeholder")).toBe("Enter your message");
42
- });
43
-
44
- it("forwards maxlength, defaulting to 255", async () => {
45
- wrapper = await wrapperFactory();
46
- expect(wrapper.find("textarea").attributes("maxlength")).toBe("255");
47
-
48
- wrapper = await wrapperFactory({ maxlength: 100 });
49
- expect(wrapper.find("textarea").attributes("maxlength")).toBe("100");
50
- });
51
-
52
- it("forwards aria-describedby", async () => {
53
- wrapper = await wrapperFactory({ ariaDescribedby: "some-description-id" });
54
- expect(wrapper.find("textarea").attributes("aria-describedby")).toBe("some-description-id");
55
- });
56
-
57
- it("sets data-theme from the theme prop", async () => {
58
- wrapper = await wrapperFactory({ theme: "error" });
59
- expect(wrapper.find(".input-textarea-wrapper").attributes("data-theme")).toBe("error");
60
- });
61
-
62
- it("sets data-invalid and aria-invalid when fieldHasError is true", async () => {
63
- wrapper = await wrapperFactory({ fieldHasError: true });
64
- expect(wrapper.find(".input-textarea-wrapper").attributes("data-invalid")).toBe("");
65
- expect(wrapper.find("textarea").attributes("aria-invalid")).toBe("true");
66
- });
67
-
68
- it("defaults inputVariant to normal", async () => {
69
- wrapper = await wrapperFactory();
70
- expect(wrapper.find(".input-textarea-wrapper").classes()).toContain("normal");
71
- });
72
-
73
- it("applies inputVariant as a class on the wrapper", async () => {
74
- wrapper = await wrapperFactory({ inputVariant: "underlined" });
75
- expect(wrapper.find(".input-textarea-wrapper").classes()).toContain("underlined");
76
- });
77
-
78
- it("does not render the left/right slot wrappers when the slots are not used", async () => {
79
- wrapper = await wrapperFactory();
80
- expect(wrapper.find(".slot.left-slot").exists()).toBe(false);
81
- expect(wrapper.find(".slot.right-slot").exists()).toBe(false);
82
- });
83
-
84
- it("renders the left/right slots when provided", async () => {
85
- wrapper = await wrapperFactory(
86
- {},
87
- {
88
- left: '<span data-testid="left-slot">📝</span>',
89
- right: '<span data-testid="right-slot">✨</span>',
90
- }
91
- );
92
-
93
- expect(wrapper.find(".slot.left-slot [data-testid='left-slot']").exists()).toBe(true);
94
- expect(wrapper.find(".slot.right-slot [data-testid='right-slot']").exists()).toBe(true);
95
- expect(wrapper.find(".input-textarea-wrapper").classes()).toContain("has-left-slot");
96
- expect(wrapper.find(".input-textarea-wrapper").classes()).toContain("has-right-slot");
97
- });
98
-
99
- it("applies styleClassPassthrough to the textarea", async () => {
100
- wrapper = await wrapperFactory({ styleClassPassthrough: ["custom-textarea-class"] });
101
- expect(wrapper.find("textarea").classes()).toContain("custom-textarea-class");
102
- });
103
-
104
- it("sets required attribute when required is true", async () => {
105
- wrapper = await wrapperFactory({ required: true });
106
- expect(wrapper.find("textarea").attributes("required")).toBeDefined();
107
- });
108
-
109
- it("sets isActive true on focus and false on blur", async () => {
110
- wrapper = await wrapperFactory();
111
- const textarea = wrapper.find("textarea");
112
-
113
- await textarea.trigger("focusin");
114
- expect(wrapper.emitted("update:isActive")?.at(-1)).toEqual([true]);
115
-
116
- await textarea.trigger("focusout");
117
- expect(wrapper.emitted("update:isActive")?.at(-1)).toEqual([false]);
118
- });
119
- });
@@ -1,93 +0,0 @@
1
- // https://nuxt.com/docs/getting-started/testing#unit-testing
2
- import type { VueWrapper } from "@vue/test-utils";
3
- import { mountSuspended } from "@nuxt/test-utils/runtime";
4
- import ComponentUnderTest from "../InputTextareaWithLabel.vue";
5
-
6
- const initialPropsData = {
7
- name: "message",
8
- label: "Message",
9
- errorMessage: "",
10
- modelValue: "",
11
- };
12
-
13
- let wrapper: VueWrapper<InstanceType<typeof ComponentUnderTest>>;
14
- const wrapperFactory = (propsData = {}, slots = {}) => {
15
- const mockPropsData = { ...initialPropsData, ...propsData };
16
-
17
- return mountSuspended(ComponentUnderTest, {
18
- attachTo: document.body,
19
- props: mockPropsData,
20
- slots,
21
- });
22
- };
23
-
24
- describe("InputTextareaWithLabel", () => {
25
- it("mounts without error", async () => {
26
- wrapper = await wrapperFactory();
27
- expect(wrapper.vm).toBeTruthy();
28
- });
29
-
30
- it("renders the label text", async () => {
31
- wrapper = await wrapperFactory({ label: "Feedback" });
32
- expect(wrapper.text()).toContain("Feedback");
33
- });
34
-
35
- it("renders a native textarea", async () => {
36
- wrapper = await wrapperFactory();
37
- expect(wrapper.find("textarea").exists()).toBe(true);
38
- });
39
-
40
- it("does not leak a label attribute onto InputTextareaCore's wrapper", async () => {
41
- // Regression test: InputTextareaCore has no `label` prop (matches InputTextCore), so passing
42
- // it down would fall through as a meaningless raw `label` HTML attribute on the wrapper div.
43
- wrapper = await wrapperFactory({ label: "Feedback" });
44
- expect(wrapper.find(".input-textarea-wrapper").attributes("label")).toBeUndefined();
45
- });
46
-
47
- it("sets data-invalid and the error class when fieldHasError is true", async () => {
48
- wrapper = await wrapperFactory({ fieldHasError: true });
49
- expect(wrapper.find(".input-textarea-with-label").attributes("data-invalid")).toBe("");
50
- });
51
-
52
- it("renders the error message when fieldHasError is true", async () => {
53
- wrapper = await wrapperFactory({ fieldHasError: true, errorMessage: "This field is required" });
54
- expect(wrapper.text()).toContain("This field is required");
55
- });
56
-
57
- it("renders the descriptionText slot", async () => {
58
- wrapper = await wrapperFactory(
59
- {},
60
- { descriptionText: () => "Please provide more detail" }
61
- );
62
- expect(wrapper.text()).toContain("Please provide more detail");
63
- });
64
-
65
- it("renders the left/right slots when provided", async () => {
66
- wrapper = await wrapperFactory(
67
- {},
68
- {
69
- left: '<span data-testid="left-slot">📝</span>',
70
- right: '<span data-testid="right-slot">✨</span>',
71
- }
72
- );
73
-
74
- expect(wrapper.find('[data-testid="left-slot"]').exists()).toBe(true);
75
- expect(wrapper.find('[data-testid="right-slot"]').exists()).toBe(true);
76
- });
77
-
78
- it("sets isDirty true once the value becomes non-empty", async () => {
79
- wrapper = await wrapperFactory({ modelValue: "hello" });
80
- const vm = wrapper.vm as unknown as { isDirty: boolean };
81
- expect(vm.isDirty).toBe(true);
82
- });
83
-
84
- it("applies inputVariant as a class on the wrapper", async () => {
85
- wrapper = await wrapperFactory({ inputVariant: "underlined" });
86
- expect(wrapper.find(".input-textarea-with-label").classes()).toContain("underlined");
87
- });
88
-
89
- it("applies styleClassPassthrough", async () => {
90
- wrapper = await wrapperFactory({ styleClassPassthrough: "custom-textarea" });
91
- expect(wrapper.find(".input-textarea-with-label").classes()).toContain("custom-textarea");
92
- });
93
- });