@velkymx/vibeui 0.9.0 → 1.1.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 (184) hide show
  1. package/AGENTS.md +44 -0
  2. package/CLAUDE.md +48 -0
  3. package/README.md +222 -197
  4. package/dist/App.vue.d.ts +2 -1
  5. package/dist/components/HelloWorld.vue.d.ts +2 -1
  6. package/dist/components/VibeAccordion.vue.d.ts +39 -40
  7. package/dist/components/VibeAlert.vue.d.ts +21 -37
  8. package/dist/components/VibeAutocomplete.vue.d.ts +59 -31
  9. package/dist/components/VibeBadge.vue.d.ts +10 -18
  10. package/dist/components/VibeBreadcrumb.vue.d.ts +25 -21
  11. package/dist/components/VibeButton.vue.d.ts +13 -17
  12. package/dist/components/VibeButtonGroup.vue.d.ts +11 -19
  13. package/dist/components/VibeCard.vue.d.ts +51 -24
  14. package/dist/components/VibeCarousel.vue.d.ts +27 -49
  15. package/dist/components/VibeChartBar.vue.d.ts +61 -0
  16. package/dist/components/VibeChartLine.vue.d.ts +70 -0
  17. package/dist/components/VibeChartPie.vue.d.ts +34 -0
  18. package/dist/components/VibeCloseButton.vue.d.ts +9 -7
  19. package/dist/components/VibeCol.vue.d.ts +9 -17
  20. package/dist/components/VibeCollapse.vue.d.ts +24 -32
  21. package/dist/components/VibeContainer.vue.d.ts +9 -17
  22. package/dist/components/VibeDataTable.vue.d.ts +122 -63
  23. package/dist/components/VibeDatePicker.vue.d.ts +4 -6
  24. package/dist/components/VibeDraggable.vue.d.ts +9 -13
  25. package/dist/components/VibeDropdown.vue.d.ts +46 -43
  26. package/dist/components/VibeDroppable.vue.d.ts +9 -13
  27. package/dist/components/VibeFileInput.vue.d.ts +14 -20
  28. package/dist/components/VibeFormCheckbox.vue.d.ts +30 -22
  29. package/dist/components/VibeFormDatepicker.vue.d.ts +27 -30
  30. package/dist/components/VibeFormErrorSummary.vue.d.ts +17 -0
  31. package/dist/components/VibeFormGroup.vue.d.ts +10 -13
  32. package/dist/components/VibeFormInput.vue.d.ts +60 -25
  33. package/dist/components/VibeFormRadio.vue.d.ts +23 -24
  34. package/dist/components/VibeFormSelect.vue.d.ts +28 -34
  35. package/dist/components/VibeFormSpinbutton.vue.d.ts +31 -34
  36. package/dist/components/VibeFormSwitch.vue.d.ts +22 -23
  37. package/dist/components/VibeFormTextarea.vue.d.ts +24 -25
  38. package/dist/components/VibeFormWysiwyg.vue.d.ts +23 -22
  39. package/dist/components/VibeHero.vue.d.ts +117 -0
  40. package/dist/components/VibeIcon.vue.d.ts +27 -7
  41. package/dist/components/VibeInputGroup.vue.d.ts +12 -14
  42. package/dist/components/VibeLink.vue.d.ts +14 -18
  43. package/dist/components/VibeListGroup.vue.d.ts +26 -22
  44. package/dist/components/VibeModal.vue.d.ts +47 -45
  45. package/dist/components/VibeNav.vue.d.ts +40 -20
  46. package/dist/components/VibeNavbar.vue.d.ts +12 -19
  47. package/dist/components/VibeNavbarBrand.vue.d.ts +9 -17
  48. package/dist/components/VibeNavbarNav.vue.d.ts +52 -33
  49. package/dist/components/VibeNavbarToggle.vue.d.ts +7 -5
  50. package/dist/components/VibeOffcanvas.vue.d.ts +28 -43
  51. package/dist/components/VibePagination.vue.d.ts +38 -29
  52. package/dist/components/VibePlaceholder.vue.d.ts +9 -17
  53. package/dist/components/VibePopover.vue.d.ts +17 -40
  54. package/dist/components/VibeProgress.vue.d.ts +13 -21
  55. package/dist/components/VibeResizable.vue.d.ts +10 -14
  56. package/dist/components/VibeRow.vue.d.ts +9 -17
  57. package/dist/components/VibeScrollspy.vue.d.ts +37 -25
  58. package/dist/components/VibeSkeleton.vue.d.ts +2 -1
  59. package/dist/components/VibeSlider.vue.d.ts +4 -5
  60. package/dist/components/VibeSortable.vue.d.ts +37 -18
  61. package/dist/components/VibeSpinner.vue.d.ts +4 -7
  62. package/dist/components/VibeStepper.vue.d.ts +32 -31
  63. package/dist/components/VibeTab.vue.d.ts +7 -11
  64. package/dist/components/VibeTabContent.vue.d.ts +13 -31
  65. package/dist/components/VibeTabs.vue.d.ts +9 -13
  66. package/dist/components/VibeToast.vue.d.ts +31 -44
  67. package/dist/components/VibeToastHost.vue.d.ts +2 -1
  68. package/dist/components/VibeTooltip.vue.d.ts +16 -39
  69. package/dist/components/chart/chartColors.d.ts +3 -0
  70. package/dist/components/chart/chartResize.d.ts +2 -0
  71. package/dist/components/chart/chartTooltip.d.ts +2 -0
  72. package/dist/components/chart/chartTypes.d.ts +6 -0
  73. package/dist/components/chart/drawBar.d.ts +5 -0
  74. package/dist/components/chart/drawLine.d.ts +18 -0
  75. package/dist/components/chart/drawPie.d.ts +4 -0
  76. package/dist/components/dndStore.d.ts +1 -0
  77. package/dist/components/index.d.ts +6 -1
  78. package/dist/composables/useBreakpoints.d.ts +1 -0
  79. package/dist/composables/useColorMode.d.ts +1 -1
  80. package/dist/composables/useId.d.ts +1 -4
  81. package/dist/directives/vTooltip.d.ts +2 -3
  82. package/dist/injectionKeys.d.ts +34 -0
  83. package/dist/purify.es-eYghtX9j.mjs +735 -0
  84. package/dist/types.d.ts +23 -2
  85. package/dist/utils/routeKey.d.ts +11 -0
  86. package/dist/utils/safeCss.d.ts +16 -0
  87. package/dist/utils/safeHref.d.ts +7 -0
  88. package/dist/utils/sanitizeHtml.d.ts +13 -0
  89. package/dist/vibeui.css +2 -1
  90. package/dist/vibeui.es.js +8527 -5855
  91. package/dist/vibeui.umd.js +3 -1
  92. package/docs/README.md +225 -0
  93. package/docs/components/advanced/popover.md +125 -0
  94. package/docs/components/advanced/scrollspy.md +106 -0
  95. package/docs/components/advanced/tooltip.md +90 -0
  96. package/docs/components/card/card.md +223 -0
  97. package/docs/components/charts/chart-bar.md +129 -0
  98. package/docs/components/charts/chart-line.md +136 -0
  99. package/docs/components/charts/chart-pie.md +102 -0
  100. package/docs/components/core/alert.md +94 -0
  101. package/docs/components/core/badge.md +75 -0
  102. package/docs/components/core/button-group.md +99 -0
  103. package/docs/components/core/button.md +145 -0
  104. package/docs/components/core/close-button.md +82 -0
  105. package/docs/components/core/link.md +79 -0
  106. package/docs/components/core/placeholder.md +129 -0
  107. package/docs/components/core/skeleton.md +40 -0
  108. package/docs/components/core/spinner.md +103 -0
  109. package/docs/components/data/datatable.md +437 -0
  110. package/docs/components/hero/hero.md +65 -0
  111. package/docs/components/interactive/accordion.md +121 -0
  112. package/docs/components/interactive/carousel.md +112 -0
  113. package/docs/components/interactive/collapse.md +106 -0
  114. package/docs/components/interactive/date-picker.md +78 -0
  115. package/docs/components/interactive/draggable.md +91 -0
  116. package/docs/components/interactive/dropdown.md +129 -0
  117. package/docs/components/interactive/modal.md +151 -0
  118. package/docs/components/interactive/offcanvas.md +92 -0
  119. package/docs/components/interactive/resizable.md +73 -0
  120. package/docs/components/interactive/slider.md +57 -0
  121. package/docs/components/interactive/sortable.md +52 -0
  122. package/docs/components/interactive/stepper.md +83 -0
  123. package/docs/components/interactive/tabs.md +66 -0
  124. package/docs/components/interactive/toast.md +177 -0
  125. package/docs/components/layout/col.md +117 -0
  126. package/docs/components/layout/container.md +53 -0
  127. package/docs/components/layout/row.md +107 -0
  128. package/docs/components/list/list-group.md +225 -0
  129. package/docs/components/navigation/breadcrumb.md +120 -0
  130. package/docs/components/navigation/nav.md +151 -0
  131. package/docs/components/navigation/navbar.md +114 -0
  132. package/docs/components/navigation/pagination.md +163 -0
  133. package/docs/components/progress/progress.md +188 -0
  134. package/docs/composables/back-button.md +28 -0
  135. package/docs/composables/breakpoints.md +54 -0
  136. package/docs/composables/color-mode.md +179 -0
  137. package/docs/composables/use-form.md +78 -0
  138. package/docs/composables/use-position.md +68 -0
  139. package/docs/composables/use-toast.md +91 -0
  140. package/docs/directives/v-tooltip.md +58 -0
  141. package/docs/forms/README.md +87 -0
  142. package/docs/forms/autocomplete.md +96 -0
  143. package/docs/forms/file-input.md +97 -0
  144. package/docs/forms/form-checkbox.md +100 -0
  145. package/docs/forms/form-datepicker.md +80 -0
  146. package/docs/forms/form-error-summary.md +96 -0
  147. package/docs/forms/form-group.md +118 -0
  148. package/docs/forms/form-input.md +140 -0
  149. package/docs/forms/form-radio.md +82 -0
  150. package/docs/forms/form-select.md +98 -0
  151. package/docs/forms/form-spinbutton.md +94 -0
  152. package/docs/forms/form-switch.md +75 -0
  153. package/docs/forms/form-textarea.md +82 -0
  154. package/docs/forms/form-wysiwyg.md +99 -0
  155. package/docs/forms/input-group.md +70 -0
  156. package/docs/forms/validation.md +213 -0
  157. package/docs/getting-started/starter-template.md +136 -0
  158. package/docs/superpowers/plans/2026-05-12-vibe-charts.md +1999 -0
  159. package/docs/superpowers/plans/2026-05-29-e2e-browser-testing.md +615 -0
  160. package/docs/superpowers/specs/2026-05-12-charting-design.md +185 -0
  161. package/docs/superpowers/specs/2026-05-29-e2e-browser-testing-design.md +168 -0
  162. package/docs/utilities/position.md +82 -0
  163. package/docs/versioning.md +57 -0
  164. package/examples/README.md +201 -0
  165. package/examples/album.html +262 -0
  166. package/examples/blog.html +166 -0
  167. package/examples/carousel.html +114 -0
  168. package/examples/checkout.html +275 -0
  169. package/examples/cover.html +89 -0
  170. package/examples/dashboard.html +156 -0
  171. package/examples/index.html +129 -0
  172. package/examples/jumbotron.html +56 -0
  173. package/examples/mobile-dashboard.html +138 -0
  174. package/examples/pricing.html +172 -0
  175. package/examples/product.html +145 -0
  176. package/examples/sidebars.html +166 -0
  177. package/examples/sign-in.html +119 -0
  178. package/examples/starter.html +314 -0
  179. package/examples/sticky-footer-navbar.html +70 -0
  180. package/examples/sticky-footer.html +60 -0
  181. package/examples/test-simple.html +91 -0
  182. package/llms.txt +801 -0
  183. package/package.json +44 -14
  184. package/dist/types/index.d.ts +0 -6
@@ -0,0 +1,615 @@
1
+ # E2E Browser Testing Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Add a real-browser Vitest project that executes the actual Bootstrap JS and Quill code paths the happy-dom unit suite mocks or cannot run, covering every interactive component.
6
+
7
+ **Architecture:** Two Vitest 4 projects share one Vite 8 pipeline. `unit` keeps happy-dom + the `bootstrap → mock` alias (today's 925 tests, unchanged). A new `browser` project runs headless Chromium via the Playwright provider with **no** bootstrap alias, so components' own `import('bootstrap')` resolves the real package. Tests live in `tests/browser/*.browser.test.ts`, mount via `vitest-browser-vue`, and assert against the real DOM (tests run inside the browser, so `document` is real).
8
+
9
+ **Tech Stack:** Vitest 4 browser mode, `@vitest/browser`, Playwright (driver), `vitest-browser-vue`, Vite 8, real Bootstrap 5.3 JS + CSS, real Quill 2.
10
+
11
+ **Spec:** `docs/superpowers/specs/2026-05-29-e2e-browser-testing-design.md`
12
+
13
+ **Scope note (decided during planning):** `VibeTabs` is pure Vue reactivity (no `import('bootstrap')`); its behavior is fully covered by the happy-dom unit suite, so it is **excluded** from the browser suite. Browser motion coverage = Collapse, Accordion, Carousel, Scrollspy.
14
+
15
+ **Conventions used in every spec:**
16
+ - Interactions: `userEvent` from `@vitest/browser/context` (`click`, `hover`, `keyboard`, `type`).
17
+ - Body/teleported queries (Modal/Offcanvas/Toast/Tooltip/Popover append to `document.body`): direct `document.querySelector` inside `vi.waitFor`, via the `waitForSelector` / `waitForGone` helpers from `tests/browser/helpers.ts`.
18
+ - `vi` and test globals are available (`globals: true`).
19
+
20
+ ---
21
+
22
+ ### Task 1: Install dependencies and prove the toolchain (smoke)
23
+
24
+ **Files:**
25
+ - Modify: `package.json` (devDependencies + scripts)
26
+ - Modify: `vite.config.ts` (test block → projects)
27
+ - Create: `tests/browser/setup.ts`
28
+ - Create: `tests/browser/smoke.browser.test.ts`
29
+
30
+ - [ ] **Step 1: Install dev dependencies and the Chromium binary**
31
+
32
+ Run:
33
+ ```bash
34
+ npm i -D @vitest/browser playwright vitest-browser-vue
35
+ npx playwright install chromium
36
+ ```
37
+ Expected: packages added; Chromium downloaded.
38
+
39
+ - [ ] **Step 2: Replace the `test` block in `vite.config.ts` with a two-project config**
40
+
41
+ Replace the entire existing `test: { ... }` object with:
42
+
43
+ ```ts
44
+ test: {
45
+ globals: true,
46
+ coverage: {
47
+ provider: 'v8',
48
+ reporter: ['text', 'json', 'html'],
49
+ exclude: [
50
+ 'node_modules/',
51
+ 'dist/',
52
+ 'examples/',
53
+ '**/*.d.ts',
54
+ '**/*.config.*',
55
+ '**/index.ts'
56
+ ]
57
+ },
58
+ projects: [
59
+ {
60
+ extends: true,
61
+ test: {
62
+ name: 'unit',
63
+ environment: 'happy-dom',
64
+ include: ['tests/**/*.test.ts'],
65
+ exclude: ['tests/browser/**'],
66
+ // Mock alias is scoped to this project ONLY — the browser project must
67
+ // resolve the real bootstrap package.
68
+ alias: {
69
+ bootstrap: path.resolve(__dirname, 'tests/mocks/bootstrap.ts')
70
+ }
71
+ }
72
+ },
73
+ {
74
+ extends: true,
75
+ test: {
76
+ name: 'browser',
77
+ include: ['tests/browser/**/*.browser.test.ts'],
78
+ setupFiles: ['tests/browser/setup.ts'],
79
+ browser: {
80
+ enabled: true,
81
+ provider: 'playwright',
82
+ headless: true,
83
+ instances: [{ browser: 'chromium' }]
84
+ }
85
+ }
86
+ }
87
+ ]
88
+ }
89
+ ```
90
+
91
+ Leave the `plugins`, `build`, and top-of-file imports untouched. `path` is already imported.
92
+
93
+ - [ ] **Step 3: Create the browser setup file**
94
+
95
+ `tests/browser/setup.ts`:
96
+ ```ts
97
+ // Loaded once per browser test file. Pulls in the real Bootstrap + Quill CSS so
98
+ // the real Bootstrap JS and Quill render against authentic styles. vitest-browser-vue
99
+ // auto-registers component cleanup via afterEach when globals are enabled.
100
+ import 'bootstrap/dist/css/bootstrap.min.css'
101
+ import 'quill/dist/quill.snow.css'
102
+ ```
103
+
104
+ - [ ] **Step 4: Write the smoke spec**
105
+
106
+ `tests/browser/smoke.browser.test.ts`:
107
+ ```ts
108
+ import { render } from 'vitest-browser-vue'
109
+ import { expect, test } from 'vitest'
110
+ import VibeButton from '../../src/components/VibeButton.vue'
111
+
112
+ test('mounts a Vibe SFC in a real browser', async () => {
113
+ const screen = render(VibeButton, {
114
+ props: { variant: 'primary' },
115
+ slots: { default: 'Click me' }
116
+ })
117
+ await expect.element(screen.getByRole('button', { name: 'Click me' })).toBeVisible()
118
+ })
119
+ ```
120
+
121
+ - [ ] **Step 5: Run the smoke spec**
122
+
123
+ Run: `npx vitest run --project browser tests/browser/smoke.browser.test.ts`
124
+ Expected: PASS. Proves Chromium launches, our Vite 8 + `@vitejs/plugin-vue` bundles a `<script setup lang="ts">` SFC, and `vitest-browser-vue` mounts it. If this fails, the toolchain issue is isolated here — fix it before writing any real spec.
125
+
126
+ - [ ] **Step 6: Verify the unit suite is untouched**
127
+
128
+ Run: `npx vitest run --project unit`
129
+ Expected: all existing tests PASS (the 925-test suite), confirming the mock alias still applies in the unit project.
130
+
131
+ - [ ] **Step 7: Commit**
132
+
133
+ ```bash
134
+ git add package.json package-lock.json vite.config.ts tests/browser/setup.ts tests/browser/smoke.browser.test.ts
135
+ git commit -m "test(e2e): add Vitest browser project + Playwright; smoke spec"
136
+ ```
137
+
138
+ ---
139
+
140
+ ### Task 2: Browser test helpers + proof that real Bootstrap JS runs
141
+
142
+ **Files:**
143
+ - Create: `tests/browser/helpers.ts`
144
+ - Create: `tests/browser/_realbootstrap.browser.test.ts`
145
+
146
+ - [ ] **Step 1: Write the helpers**
147
+
148
+ `tests/browser/helpers.ts`:
149
+ ```ts
150
+ import { expect, vi } from 'vitest'
151
+
152
+ // Poll the real DOM until a node matching `selector` exists; returns it.
153
+ // Used for Bootstrap/Quill nodes that lack a stable role (e.g. .modal-backdrop)
154
+ // and for teleported content appended to document.body.
155
+ export async function waitForSelector(selector: string, timeout = 4000): Promise<Element> {
156
+ let found: Element | null = null
157
+ await vi.waitFor(() => {
158
+ found = document.querySelector(selector)
159
+ expect(found, `expected to find "${selector}"`).not.toBeNull()
160
+ }, { timeout })
161
+ return found as unknown as Element
162
+ }
163
+
164
+ // Poll until no node matches `selector` (e.g. after a close/hide animation).
165
+ export async function waitForGone(selector: string, timeout = 4000): Promise<void> {
166
+ await vi.waitFor(() => {
167
+ expect(document.querySelector(selector), `expected "${selector}" to be gone`).toBeNull()
168
+ }, { timeout })
169
+ }
170
+ ```
171
+
172
+ - [ ] **Step 2: Write the real-Bootstrap proof spec**
173
+
174
+ `tests/browser/_realbootstrap.browser.test.ts`:
175
+ ```ts
176
+ import { render } from 'vitest-browser-vue'
177
+ import { test } from 'vitest'
178
+ import VibeModal from '../../src/components/VibeModal.vue'
179
+ import { waitForSelector } from './helpers'
180
+
181
+ // The happy-dom mock NEVER creates a .modal-backdrop element. Its presence proves
182
+ // the real Bootstrap Modal JS executed (no mock alias leaked into this project).
183
+ test('real Bootstrap JS runs: opening a modal creates a .modal-backdrop', async () => {
184
+ render(VibeModal, { props: { modelValue: true, title: 'Real BS' }, slots: { default: 'Body' } })
185
+ await waitForSelector('.modal.show')
186
+ await waitForSelector('.modal-backdrop')
187
+ })
188
+ ```
189
+
190
+ - [ ] **Step 3: Run it**
191
+
192
+ Run: `npx vitest run --project browser tests/browser/_realbootstrap.browser.test.ts`
193
+ Expected: PASS. The `.modal-backdrop` assertion is the load-bearing check — it cannot pass under the mock or in happy-dom.
194
+
195
+ - [ ] **Step 4: Commit**
196
+
197
+ ```bash
198
+ git add tests/browser/helpers.ts tests/browser/_realbootstrap.browser.test.ts
199
+ git commit -m "test(e2e): helpers + proof real Bootstrap JS executes"
200
+ ```
201
+
202
+ ---
203
+
204
+ ### Task 3: Overlays spec (Modal, Offcanvas, Toast)
205
+
206
+ **Files:**
207
+ - Create: `tests/browser/overlays.browser.test.ts`
208
+
209
+ - [ ] **Step 1: Write the overlays spec**
210
+
211
+ `tests/browser/overlays.browser.test.ts`:
212
+ ```ts
213
+ import { defineComponent, ref } from 'vue'
214
+ import { render } from 'vitest-browser-vue'
215
+ import { userEvent } from '@vitest/browser/context'
216
+ import { expect, test, describe } from 'vitest'
217
+ import VibeModal from '../../src/components/VibeModal.vue'
218
+ import VibeOffcanvas from '../../src/components/VibeOffcanvas.vue'
219
+ import VibeToast from '../../src/components/VibeToast.vue'
220
+ import { waitForSelector, waitForGone } from './helpers'
221
+
222
+ describe('VibeModal', () => {
223
+ test('ESC closes the modal and focus returns to the trigger (WCAG 2.4.3)', async () => {
224
+ const Host = defineComponent({
225
+ components: { VibeModal },
226
+ setup() {
227
+ const open = ref(false)
228
+ return { open }
229
+ },
230
+ template: `
231
+ <div>
232
+ <button type="button" @click="open = true">Open</button>
233
+ <VibeModal v-model="open" title="Hi">Body</VibeModal>
234
+ </div>`
235
+ })
236
+ const screen = render(Host)
237
+ // Clicking focuses the trigger; Bootstrap captures it as the pre-open activeElement.
238
+ await userEvent.click(screen.getByRole('button', { name: 'Open' }))
239
+ await waitForSelector('.modal.show')
240
+ await userEvent.keyboard('{Escape}')
241
+ await waitForGone('.modal.show')
242
+ await waitForGone('.modal-backdrop')
243
+ expect(document.activeElement?.textContent).toBe('Open')
244
+ })
245
+ })
246
+
247
+ describe('VibeOffcanvas', () => {
248
+ test('opens with a backdrop and ESC closes it', async () => {
249
+ const Host = defineComponent({
250
+ components: { VibeOffcanvas },
251
+ setup() {
252
+ const open = ref(false)
253
+ return { open }
254
+ },
255
+ template: `
256
+ <div>
257
+ <button type="button" @click="open = true">Open panel</button>
258
+ <VibeOffcanvas v-model="open" title="Panel">Panel body</VibeOffcanvas>
259
+ </div>`
260
+ })
261
+ const screen = render(Host)
262
+ await userEvent.click(screen.getByRole('button', { name: 'Open panel' }))
263
+ await waitForSelector('.offcanvas.show')
264
+ await waitForSelector('.offcanvas-backdrop')
265
+ await userEvent.keyboard('{Escape}')
266
+ await waitForGone('.offcanvas.show')
267
+ })
268
+ })
269
+
270
+ describe('VibeToast', () => {
271
+ test('autohide hides the toast after the delay', async () => {
272
+ render(VibeToast, {
273
+ props: { modelValue: true, title: 'Saved', autohide: true, delay: 150 },
274
+ slots: { default: 'Your changes were saved' }
275
+ })
276
+ await waitForSelector('.toast.show')
277
+ // delay (150ms) + Bootstrap fade — generous window via waitForGone's polling.
278
+ await waitForGone('.toast.show')
279
+ })
280
+ })
281
+ ```
282
+
283
+ - [ ] **Step 2: Run it**
284
+
285
+ Run: `npx vitest run --project browser tests/browser/overlays.browser.test.ts`
286
+ Expected: PASS (3 tests). Focus-return and autohide-timer paths are the ones happy-dom cannot verify.
287
+
288
+ - [ ] **Step 3: Commit**
289
+
290
+ ```bash
291
+ git add tests/browser/overlays.browser.test.ts
292
+ git commit -m "test(e2e): overlays — Modal focus-return, Offcanvas, Toast autohide"
293
+ ```
294
+
295
+ ---
296
+
297
+ ### Task 4: Popper-positioned spec (Tooltip, Popover, Dropdown)
298
+
299
+ **Files:**
300
+ - Create: `tests/browser/popper.browser.test.ts`
301
+
302
+ - [ ] **Step 1: Write the popper spec**
303
+
304
+ `tests/browser/popper.browser.test.ts`:
305
+ ```ts
306
+ import { defineComponent } from 'vue'
307
+ import { render } from 'vitest-browser-vue'
308
+ import { userEvent } from '@vitest/browser/context'
309
+ import { expect, test, describe } from 'vitest'
310
+ import VibeTooltip from '../../src/components/VibeTooltip.vue'
311
+ import VibePopover from '../../src/components/VibePopover.vue'
312
+ import VibeDropdown from '../../src/components/VibeDropdown.vue'
313
+ import { waitForSelector, waitForGone } from './helpers'
314
+
315
+ describe('VibeTooltip', () => {
316
+ test('hover shows a Popper-positioned tooltip; leaving removes it', async () => {
317
+ const Host = defineComponent({
318
+ components: { VibeTooltip },
319
+ template: `<div><VibeTooltip text="Helpful tip"><button type="button">Hover me</button></VibeTooltip></div>`
320
+ })
321
+ const screen = render(Host)
322
+ await userEvent.hover(screen.getByRole('button', { name: 'Hover me' }))
323
+ const tip = await waitForSelector('.tooltip')
324
+ // Real Popper applies an inline transform for positioning — absent in happy-dom.
325
+ expect((tip as HTMLElement).style.transform).not.toBe('')
326
+ await userEvent.unhover(screen.getByRole('button', { name: 'Hover me' }))
327
+ await waitForGone('.tooltip')
328
+ })
329
+ })
330
+
331
+ describe('VibePopover', () => {
332
+ test('click shows a popover; clicking outside dismisses it', async () => {
333
+ const Host = defineComponent({
334
+ components: { VibePopover },
335
+ template: `
336
+ <div>
337
+ <VibePopover title="More info" text="Popover body"><button type="button">Toggle</button></VibePopover>
338
+ <button type="button">Outside</button>
339
+ </div>`
340
+ })
341
+ const screen = render(Host)
342
+ await userEvent.click(screen.getByRole('button', { name: 'Toggle' }))
343
+ await waitForSelector('.popover')
344
+ await userEvent.click(screen.getByRole('button', { name: 'Outside' }))
345
+ await waitForGone('.popover')
346
+ })
347
+ })
348
+
349
+ describe('VibeDropdown', () => {
350
+ test('toggle opens a Popper-positioned menu; ESC closes it', async () => {
351
+ const screen = render(VibeDropdown, {
352
+ props: { text: 'Menu', items: [{ text: 'Alpha' }, { text: 'Beta' }] }
353
+ })
354
+ const toggle = screen.getByRole('button', { name: 'Menu' })
355
+ await userEvent.click(toggle)
356
+ await waitForSelector('.dropdown-menu.show')
357
+ expect((await waitForSelector('.dropdown-toggle')).getAttribute('aria-expanded')).toBe('true')
358
+ await userEvent.keyboard('{Escape}')
359
+ await waitForGone('.dropdown-menu.show')
360
+ })
361
+ })
362
+ ```
363
+
364
+ - [ ] **Step 2: Run it**
365
+
366
+ Run: `npx vitest run --project browser tests/browser/popper.browser.test.ts`
367
+ Expected: PASS (3 tests). The tooltip `transform` assertion confirms real Popper positioning.
368
+
369
+ - [ ] **Step 3: Commit**
370
+
371
+ ```bash
372
+ git add tests/browser/popper.browser.test.ts
373
+ git commit -m "test(e2e): popper — Tooltip/Popover/Dropdown real positioning"
374
+ ```
375
+
376
+ ---
377
+
378
+ ### Task 5: Motion spec (Collapse, Accordion, Carousel, Scrollspy)
379
+
380
+ **Files:**
381
+ - Create: `tests/browser/motion.browser.test.ts`
382
+
383
+ - [ ] **Step 1: Write the motion spec**
384
+
385
+ `tests/browser/motion.browser.test.ts`:
386
+ ```ts
387
+ import { defineComponent } from 'vue'
388
+ import { render } from 'vitest-browser-vue'
389
+ import { userEvent } from '@vitest/browser/context'
390
+ import { expect, test, describe, vi } from 'vitest'
391
+ import VibeCollapse from '../../src/components/VibeCollapse.vue'
392
+ import VibeAccordion from '../../src/components/VibeAccordion.vue'
393
+ import VibeCarousel from '../../src/components/VibeCarousel.vue'
394
+ import VibeScrollspy from '../../src/components/VibeScrollspy.vue'
395
+ import { waitForSelector } from './helpers'
396
+
397
+ describe('VibeCollapse', () => {
398
+ test('toggling modelValue runs the real show transition to .collapse.show', async () => {
399
+ const screen = render(VibeCollapse, {
400
+ props: { modelValue: false },
401
+ slots: { default: 'Hidden content' }
402
+ })
403
+ await screen.rerender({ modelValue: true })
404
+ await waitForSelector('.collapse.show')
405
+ })
406
+ })
407
+
408
+ describe('VibeAccordion', () => {
409
+ test('opening one panel closes its sibling (real Bootstrap parent behavior)', async () => {
410
+ const screen = render(VibeAccordion, {
411
+ props: {
412
+ id: 'acc1',
413
+ items: [
414
+ { id: 'panelA', title: 'Panel A', content: 'A body', show: true },
415
+ { id: 'panelB', title: 'Panel B', content: 'B body' }
416
+ ]
417
+ }
418
+ })
419
+ await waitForSelector('#panelA.show')
420
+ await userEvent.click(screen.getByRole('button', { name: 'Panel B' }))
421
+ await waitForSelector('#panelB.show')
422
+ await vi.waitFor(() => {
423
+ expect(document.querySelector('#panelA.show'), 'Panel A should have closed').toBeNull()
424
+ })
425
+ })
426
+ })
427
+
428
+ describe('VibeCarousel', () => {
429
+ test('clicking next advances the active slide', async () => {
430
+ render(VibeCarousel, {
431
+ props: {
432
+ id: 'car1',
433
+ ride: false,
434
+ items: [{ captionText: 'Slide one' }, { captionText: 'Slide two' }]
435
+ }
436
+ })
437
+ await waitForSelector('.carousel-item.active')
438
+ await userEvent.click(await waitForSelector('.carousel-control-next') as HTMLElement)
439
+ await vi.waitFor(() => {
440
+ const items = document.querySelectorAll('.carousel-item')
441
+ expect(items[1].classList.contains('active'), 'second slide should be active').toBe(true)
442
+ })
443
+ })
444
+ })
445
+
446
+ describe('VibeScrollspy', () => {
447
+ test('scrolling the container moves the active nav link', async () => {
448
+ const Host = defineComponent({
449
+ components: { VibeScrollspy },
450
+ template: `
451
+ <div>
452
+ <nav id="ss-nav">
453
+ <ul class="nav nav-pills flex-column">
454
+ <li class="nav-item"><a class="nav-link" href="#sec1">Section 1</a></li>
455
+ <li class="nav-item"><a class="nav-link" href="#sec2">Section 2</a></li>
456
+ </ul>
457
+ </nav>
458
+ <VibeScrollspy target="#ss-nav" height="120px">
459
+ <div id="sec1" style="height: 400px">Section 1 content</div>
460
+ <div id="sec2" style="height: 400px">Section 2 content</div>
461
+ </VibeScrollspy>
462
+ </div>`
463
+ })
464
+ render(Host)
465
+ const root = await waitForSelector('[data-bs-spy="scroll"]') as HTMLElement
466
+ root.scrollTop = root.scrollHeight
467
+ await vi.waitFor(() => {
468
+ const active = document.querySelector('#ss-nav .nav-link.active') as HTMLElement | null
469
+ expect(active?.getAttribute('href'), 'second section should be active after scroll').toBe('#sec2')
470
+ }, { timeout: 5000 })
471
+ })
472
+ })
473
+ ```
474
+
475
+ - [ ] **Step 2: Run it**
476
+
477
+ Run: `npx vitest run --project browser tests/browser/motion.browser.test.ts`
478
+ Expected: PASS (4 tests). These exercise real Collapse transitions, Accordion parent grouping, Carousel slide advance, and ScrollSpy IntersectionObserver — none of which happy-dom drives.
479
+
480
+ - [ ] **Step 3: Commit**
481
+
482
+ ```bash
483
+ git add tests/browser/motion.browser.test.ts
484
+ git commit -m "test(e2e): motion — Collapse/Accordion/Carousel/Scrollspy"
485
+ ```
486
+
487
+ ---
488
+
489
+ ### Task 6: Wysiwyg spec (Quill render + our DOMPurify sanitize)
490
+
491
+ **Files:**
492
+ - Create: `tests/browser/wysiwyg.browser.test.ts`
493
+
494
+ - [ ] **Step 1: Write the wysiwyg spec**
495
+
496
+ `tests/browser/wysiwyg.browser.test.ts`:
497
+ ```ts
498
+ import { render } from 'vitest-browser-vue'
499
+ import { expect, test, describe } from 'vitest'
500
+ import VibeFormWysiwyg from '../../src/components/VibeFormWysiwyg.vue'
501
+ import { waitForSelector } from './helpers'
502
+
503
+ describe('VibeFormWysiwyg (our integration glue only)', () => {
504
+ test('mounts a real Quill editor (init path happy-dom cannot run)', async () => {
505
+ render(VibeFormWysiwyg, { props: { modelValue: '<p>Hello</p>' } })
506
+ // Quill builds .ql-toolbar + .ql-editor only when it initializes against real layout.
507
+ await waitForSelector('.ql-toolbar')
508
+ const editor = await waitForSelector('.ql-editor')
509
+ expect(editor.textContent).toContain('Hello')
510
+ })
511
+
512
+ test('our sanitizeHtml layer strips dangerous markup before it reaches the editor', async () => {
513
+ // Reset any prior pollution.
514
+ delete (window as unknown as Record<string, unknown>).__xssScript
515
+ delete (window as unknown as Record<string, unknown>).__xssImg
516
+ const malicious =
517
+ '<p>safe text</p>' +
518
+ '<script>window.__xssScript = true<\/script>' +
519
+ '<img src="x" onerror="window.__xssImg = true">'
520
+ render(VibeFormWysiwyg, { props: { modelValue: malicious } })
521
+ const editor = await waitForSelector('.ql-editor')
522
+ expect(editor.textContent).toContain('safe text')
523
+ expect(editor.innerHTML).not.toContain('onerror')
524
+ expect(editor.innerHTML.toLowerCase()).not.toContain('<script')
525
+ // Neither payload executed.
526
+ expect((window as unknown as Record<string, unknown>).__xssScript).toBeUndefined()
527
+ expect((window as unknown as Record<string, unknown>).__xssImg).toBeUndefined()
528
+ })
529
+ })
530
+ ```
531
+
532
+ - [ ] **Step 2: Run it**
533
+
534
+ Run: `npx vitest run --project browser tests/browser/wysiwyg.browser.test.ts`
535
+ Expected: PASS (2 tests). First proves real Quill init; second proves our `sanitizeHtml.ts` strips XSS end-to-end in a real browser.
536
+
537
+ - [ ] **Step 3: Commit**
538
+
539
+ ```bash
540
+ git add tests/browser/wysiwyg.browser.test.ts
541
+ git commit -m "test(e2e): wysiwyg — real Quill init + DOMPurify sanitize"
542
+ ```
543
+
544
+ ---
545
+
546
+ ### Task 7: npm scripts + CI job
547
+
548
+ **Files:**
549
+ - Modify: `package.json` (scripts)
550
+ - Modify: `.github/workflows/ci.yml`
551
+
552
+ - [ ] **Step 1: Update scripts in `package.json`**
553
+
554
+ Set the `scripts` entries to:
555
+ ```jsonc
556
+ "test": "vitest --project unit",
557
+ "test:run": "vitest run --project unit",
558
+ "test:browser": "vitest run --project browser",
559
+ "test:all": "vitest run"
560
+ ```
561
+ (Keep any other existing scripts such as `build`, `lint`, `type-check` unchanged. `test` and `test:run` stay pinned to `unit` so the inner loop and the existing CI unit job never pay browser-launch cost.)
562
+
563
+ - [ ] **Step 2: Verify the unit scripts still pass**
564
+
565
+ Run: `npm run test:run`
566
+ Expected: the 925-test unit suite PASSES (now via `--project unit`).
567
+
568
+ - [ ] **Step 3: Add the `e2e` job to `.github/workflows/ci.yml`**
569
+
570
+ Add this job alongside the existing job(s) (same indentation level under `jobs:`):
571
+ ```yaml
572
+ e2e:
573
+ runs-on: ubuntu-latest
574
+ steps:
575
+ - uses: actions/checkout@v5
576
+ - uses: actions/setup-node@v5
577
+ with:
578
+ node-version: 24
579
+ cache: npm
580
+ - run: npm ci
581
+ - run: npx playwright install --with-deps chromium
582
+ - run: npm run test:browser
583
+ ```
584
+ Do not modify the existing unit job (it continues to run `npm run test:run`).
585
+
586
+ - [ ] **Step 4: Run the full browser suite locally as a final gate**
587
+
588
+ Run: `npm run test:browser`
589
+ Expected: every browser spec PASSES (smoke + real-bootstrap proof + overlays + popper + motion + wysiwyg).
590
+
591
+ - [ ] **Step 5: Commit**
592
+
593
+ ```bash
594
+ git add package.json .github/workflows/ci.yml
595
+ git commit -m "test(e2e): npm scripts + required CI e2e job (Chromium)"
596
+ ```
597
+
598
+ ---
599
+
600
+ ## Self-Review
601
+
602
+ **Spec coverage:**
603
+ - Two Vitest projects, one Vite 8 pipeline, mock alias scoped to `unit` → Task 1. ✓
604
+ - New dev deps (`@vitest/browser`, `playwright`, `vitest-browser-vue`) → Task 1. ✓
605
+ - Real Bootstrap/Quill CSS via setup, real BS JS via component dynamic import → Tasks 1–2. ✓
606
+ - File layout (`setup.ts`, `helpers.ts`, four spec files) → Tasks 1, 2, 3, 4, 5, 6. ✓
607
+ - Coverage matrix: overlays (Task 3), popper (Task 4), motion (Task 5), wysiwyg render+sanitize (Task 6). ✓
608
+ - Scripts pinned to `unit`; `test:browser`/`test:all` added → Task 7. ✓
609
+ - Separate required `e2e` CI job, Chromium-only install → Task 7. ✓
610
+ - Risk verifications: smoke proves bundling (Task 1); `.modal-backdrop` proves real BS JS (Task 2); tooltip `transform` proves real Popper (Task 4); event-based polling via `waitForSelector`/`vi.waitFor`, no fixed `setTimeout` (all tasks). ✓
611
+ - Tabs intentionally excluded (pure Vue, no BS JS) — documented in the scope note; deviates from the spec's component list by design, consistent with the spec's "only behavior happy-dom can't reach" principle.
612
+
613
+ **Placeholder scan:** No TBD/TODO; every step has concrete code or an exact command + expected result.
614
+
615
+ **Type/name consistency:** Helper names (`waitForSelector`, `waitForGone`) defined in Task 2 and used identically in Tasks 3–6. Project names (`unit`, `browser`) consistent across `vite.config.ts`, scripts, and CI. Component import paths use `../../src/components/`.