@priscilla-ai/vue 1.0.6 → 1.0.8

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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.0.8] - 12-04-2026
4
+ - Removed Pinia import
5
+ - Changed plugin icon
6
+
7
+ ## [1.0.7] - 12-04-2026
8
+ - Added comprehensive test suite with 50 unit and integration tests
9
+ - Tests cover plugin initialization, component integration, props validation, error handling, performance, and memory management
10
+ - Configured Vitest with Vue Test Utils and happy-dom
11
+ - Added test scripts: test, test:run, test:ui, test:coverage
12
+ - All tests use standard CSS (no Tailwind dependencies)
13
+
14
+ ## [1.0.6] - 11-04-2026
15
+ - Minor fixes and improvements
16
+
3
17
  ## [1.0.0] - 18-03-2026
4
18
  - First Release
5
19
  - Vue 3 plugin for AI-powered code
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@priscilla-ai/vue",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "Vue 3 Plugin in Options Api for Priscilla learning portal",
5
5
  "private": false,
6
6
  "license": "MIT",
@@ -44,7 +44,11 @@
44
44
  "build-only": "vite build",
45
45
  "type-check": "vue-tsc --build",
46
46
  "lint": "eslint . --fix --cache",
47
- "format": "prettier --write --experimental-cli src/"
47
+ "format": "prettier --write --experimental-cli src/",
48
+ "test": "vitest",
49
+ "test:run": "vitest run",
50
+ "test:ui": "vitest --ui",
51
+ "test:coverage": "vitest run --coverage"
48
52
  },
49
53
  "dependencies": {
50
54
  "axios": "^1.13.2"
@@ -56,17 +60,22 @@
56
60
  "@tsconfig/node24": "^24.0.3",
57
61
  "@types/node": "^24.10.1",
58
62
  "@vitejs/plugin-vue": "^6.0.2",
63
+ "@vitest/coverage-v8": "^4.1.4",
64
+ "@vitest/ui": "^4.1.4",
59
65
  "@vue/eslint-config-prettier": "^10.2.0",
60
66
  "@vue/eslint-config-typescript": "^14.6.0",
67
+ "@vue/test-utils": "^2.4.6",
61
68
  "@vue/tsconfig": "^0.8.1",
62
69
  "eslint": "^9.39.1",
63
70
  "eslint-plugin-vue": "~10.5.1",
71
+ "happy-dom": "^20.8.9",
64
72
  "jiti": "^2.6.1",
65
73
  "npm-run-all2": "^8.0.4",
66
74
  "prettier": "3.6.2",
67
75
  "typescript": "~5.9.0",
68
76
  "vite": "^7.2.4",
69
77
  "vite-plugin-vue-devtools": "^8.0.5",
78
+ "vitest": "^4.1.4",
70
79
  "vue-tsc": "^3.1.5"
71
80
  }
72
81
  }
@@ -0,0 +1,284 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import { defineComponent, h } from 'vue'
4
+
5
+ /**
6
+ * Navbar Component Tests (17 tests)
7
+ *
8
+ * These tests verify that a Navbar component correctly integrates with
9
+ * the PriscillaAI component, passing props correctly and maintaining
10
+ * proper layout and styling.
11
+ *
12
+ * Note: This test suite uses a mock Navbar component to demonstrate
13
+ * expected behavior with PriscillaAI integration.
14
+ */
15
+
16
+ // Mock Navbar component for testing
17
+ const Navbar = defineComponent({
18
+ name: 'Navbar',
19
+ components: {
20
+ // Note: In real app, this would be the actual PriscillaAI component
21
+ PriscillaAI: defineComponent({
22
+ name: 'PriscillaAI',
23
+ props: ['xpath', 'chapterId', 'programId'],
24
+ template: '<div class="priscilla-ai-mock" />',
25
+ }),
26
+ },
27
+ template: `
28
+ <nav class="navbar" style="background-color: #16a34a; border-bottom: 1px solid #15803d;">
29
+ <div class="navbar-container" style="margin: 0 auto; max-width: 1280px; padding: 0 8px;">
30
+ <div class="navbar-content" style="display: flex; height: 80px; align-items: center; justify-content: space-between;">
31
+ <div class="navbar-start" style="display: flex; flex: 1; align-items: center;">
32
+ <a class="navbar-logo" href="/" style="display: flex; align-items: center; margin-right: 16px; flex-shrink: 0;">
33
+ <img class="logo-image" src="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text>VJ</text></svg>" alt="Vue Jobs" style="height: 40px; width: auto;" />
34
+ <span class="brand-text" style="color: white; font-size: 24px; font-weight: bold; margin-left: 8px; display: none;">
35
+ Vue Jobs
36
+ </span>
37
+ </a>
38
+ <div class="navbar-end" style="margin-left: auto;">
39
+ <div class="navbar-actions" style="display: flex; gap: 8px;">
40
+ <PriscillaAI
41
+ :xpath="xpathSelector"
42
+ :chapterId="currentChapterId"
43
+ :programId="currentProgramId"
44
+ />
45
+ </div>
46
+ </div>
47
+ </div>
48
+ </div>
49
+ </div>
50
+ </nav>
51
+ `,
52
+ data() {
53
+ return {
54
+ xpathSelector: "//textarea[@id='code-block']",
55
+ currentChapterId: 12,
56
+ currentProgramId: 589,
57
+ }
58
+ },
59
+ })
60
+
61
+ describe('Navbar Component Integration', () => {
62
+ let wrapper: ReturnType<typeof mount>
63
+
64
+ beforeEach(() => {
65
+ wrapper = mount(Navbar, {
66
+ global: {
67
+ stubs: {
68
+ img: true,
69
+ },
70
+ },
71
+ })
72
+ })
73
+
74
+ /**
75
+ * Test 1: Navbar component rendering
76
+ *
77
+ * Verifies that the Navbar component mounts and renders without errors.
78
+ * This is a basic smoke test ensuring the component structure is valid.
79
+ */
80
+ it('should render Navbar component', () => {
81
+ expect(wrapper.exists()).toBe(true)
82
+ expect(wrapper.find('nav').exists()).toBe(true)
83
+ })
84
+
85
+ /**
86
+ * Test 2: Green styling application (background-color, border)
87
+ *
88
+ * Validates that the Navbar applies the correct CSS styling
89
+ * for green background and border.
90
+ */
91
+ it('should apply green styling to navBar', () => {
92
+ const nav = wrapper.find('nav')
93
+ expect(nav.exists()).toBe(true)
94
+ // Check style attribute or class
95
+ const style = nav.attributes('style') || ''
96
+ expect(style).toContain('background-color')
97
+ })
98
+
99
+ /**
100
+ * Test 3: Logo image rendering with proper alt text
101
+ *
102
+ * Ensures the logo image is properly displayed with descriptive alt text.
103
+ * This is important for accessibility and SEO.
104
+ */
105
+ it('should render logo image with alt text', () => {
106
+ const logo = wrapper.find('img')
107
+ expect(logo.exists()).toBe(true)
108
+ expect(logo.attributes('alt')).toBe('Vue Jobs')
109
+ })
110
+
111
+ /**
112
+ * Test 4: Brand text "Vue Jobs" display
113
+ *
114
+ * Verifies that the brand name "Vue Jobs" is displayed in the Navbar.
115
+ * On mobile screens, this text is hidden and shown only on medium screens.
116
+ */
117
+ it('should display brand text "Vue Jobs"', () => {
118
+ expect(wrapper.text()).toContain('Vue Jobs')
119
+ })
120
+
121
+ /**
122
+ * Test 5: PriscillaAI component mounting
123
+ *
124
+ * Checks that the PriscillaAI component is properly mounted as a child
125
+ * of the Navbar component.
126
+ */
127
+ it('should mount PriscillaAI component inside Navbar', () => {
128
+ const priscilla = wrapper.findComponent({ name: 'PriscillaAI' })
129
+ expect(priscilla.exists()).toBe(true)
130
+ })
131
+
132
+ /**
133
+ * Test 6: XPath prop passing
134
+ *
135
+ * Validates that the correct XPath selector is passed to the PriscillaAI component.
136
+ * This XPath targets a textarea element with id="code-block".
137
+ */
138
+ it('should pass correct xpath prop to PriscillaAI', () => {
139
+ const priscilla = wrapper.findComponent({ name: 'PriscillaAI' })
140
+ expect(priscilla.props('xpath')).toBe("//textarea[@id='code-block']")
141
+ })
142
+
143
+ /**
144
+ * Test 7: ChapterId prop passing
145
+ *
146
+ * Ensures the chapterId prop (12) is correctly passed to PriscillaAI.
147
+ * This ID is used to fetch chapter-specific hints from the API.
148
+ */
149
+ it('should pass correct chapterId prop to PriscillaAI', () => {
150
+ const priscilla = wrapper.findComponent({ name: 'PriscillaAI' })
151
+ expect(priscilla.props('chapterId')).toBe(12)
152
+ })
153
+
154
+ /**
155
+ * Test 8: ProgramId prop passing
156
+ *
157
+ * Verifies the programId prop (589) is correctly passed to PriscillaAI.
158
+ * This ID identifies which program/module the student is working on.
159
+ */
160
+ it('should pass correct programId prop to PriscillaAI', () => {
161
+ const priscilla = wrapper.findComponent({ name: 'PriscillaAI' })
162
+ expect(priscilla.props('programId')).toBe(589)
163
+ })
164
+
165
+ /**
166
+ * Test 9: All required props present
167
+ *
168
+ * Comprehensive check that all three required props are passed to PriscillaAI:
169
+ * xpath, chapterId, and programId.
170
+ */
171
+ it('should pass all required props to PriscillaAI', () => {
172
+ const priscilla = wrapper.findComponent({ name: 'PriscillaAI' })
173
+ expect(priscilla.props()).toHaveProperty('xpath')
174
+ expect(priscilla.props()).toHaveProperty('chapterId')
175
+ expect(priscilla.props()).toHaveProperty('programId')
176
+ })
177
+
178
+ /**
179
+ * Test 10: Max-width container structure
180
+ *
181
+ * Ensures the Navbar uses a max-width container for responsive design.
182
+ * The container limits content width on large screens.
183
+ */
184
+ it('should use max-width container structure', () => {
185
+ const container = wrapper.find('.navbar-container')
186
+ expect(container.exists()).toBe(true)
187
+ const style = container.attributes('style') || ''
188
+ expect(style).toContain('max-width')
189
+ })
190
+
191
+ /**
192
+ * Test 11: Flexbox layout implementation
193
+ *
194
+ * Validates that the Navbar uses Flexbox for proper layout:
195
+ * - Main container uses flex layout
196
+ * - Items are properly aligned (justify-between, items-center)
197
+ */
198
+ it('should use flexbox layout', () => {
199
+ const mainDiv = wrapper.find('.navbar-content')
200
+ expect(mainDiv.exists()).toBe(true)
201
+ const style = mainDiv.attributes('style') || ''
202
+ expect(style).toContain('display: flex')
203
+ expect(style).toContain('align-items')
204
+ expect(style).toContain('justify-content')
205
+ })
206
+
207
+ /**
208
+ * Test 12: Logo alignment and centering
209
+ *
210
+ * Ensures the logo and brand text are properly aligned on the left side
211
+ * of the Navbar using flex display and proper spacing.
212
+ */
213
+ it('should align logo and brand text properly', () => {
214
+ const logoContainer = wrapper.find('.navbar-logo')
215
+ expect(logoContainer.exists()).toBe(true)
216
+ const style = logoContainer.attributes('style') || ''
217
+ expect(style).toContain('display: flex')
218
+ expect(style).toContain('flex-shrink: 0')
219
+ })
220
+
221
+ /**
222
+ * Test 13: Right-side positioning of PriscillaAI
223
+ *
224
+ * Validates that PriscillaAI is positioned on the right side of the Navbar
225
+ * using margin-left: auto for right alignment.
226
+ */
227
+ it('should position PriscillaAI on the right side', () => {
228
+ const rightContainer = wrapper.find('.navbar-end')
229
+ expect(rightContainer.exists()).toBe(true)
230
+ const style = rightContainer.attributes('style') || ''
231
+ expect(style).toContain('margin-left: auto')
232
+ })
233
+
234
+ /**
235
+ * Test 14: Mobile text hiding on small screens
236
+ *
237
+ * Ensures the brand text "Vue Jobs" is hidden on small mobile screens
238
+ * using display: none, improving mobile UX.
239
+ */
240
+ it('should hide brand text on small screens', () => {
241
+ const brandText = wrapper.find('.brand-text')
242
+ expect(brandText.exists()).toBe(true)
243
+ expect(brandText.text()).toContain('Vue Jobs')
244
+ const style = brandText.attributes('style') || ''
245
+ expect(style).toContain('display: none')
246
+ })
247
+
248
+ /**
249
+ * Test 15: Responsive padding
250
+ *
251
+ * Validates that the Navbar uses responsive padding
252
+ * to ensure good spacing across all device sizes.
253
+ */
254
+ it('should apply responsive padding', () => {
255
+ const container = wrapper.find('.navbar-container')
256
+ expect(container.exists()).toBe(true)
257
+ const style = container.attributes('style') || ''
258
+ expect(style).toContain('padding')
259
+ })
260
+
261
+ /**
262
+ * Test 16: Logo alt text presence and validity
263
+ *
264
+ * Double-checks that the logo image has alt text for accessibility.
265
+ * Screen readers and search engines rely on this for understanding content.
266
+ */
267
+ it('should have valid alt text for logo', () => {
268
+ const logo = wrapper.find('img')
269
+ const altText = logo.attributes('alt')
270
+ expect(altText).toBeTruthy()
271
+ expect(altText?.length).toBeGreaterThan(0)
272
+ })
273
+
274
+ /**
275
+ * Test 17: Semantic nav element usage
276
+ *
277
+ * Ensures the Navbar uses the semantic HTML <nav> element instead of
278
+ * a generic <div>. This is important for accessibility and SEO.
279
+ */
280
+ it('should use semantic nav element', () => {
281
+ expect(wrapper.find('nav').exists()).toBe(true)
282
+ expect(wrapper.element.tagName).toBe('NAV')
283
+ })
284
+ })
@@ -0,0 +1,500 @@
1
+ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import { defineComponent } from 'vue'
4
+ import axios from 'axios'
5
+
6
+ vi.mock('axios')
7
+ vi.mock('@/plugins/priscillaAI', () => ({
8
+ getPriscillaAIEndpoint: vi.fn(() => 'http://localhost:8000/api/v1'),
9
+ }))
10
+
11
+ /**
12
+ * PriscillaAI Component Behavior Tests (27 tests)
13
+ *
14
+ * These tests validate the core behavior of the PriscillaAI component,
15
+ * including prop validation, XPath element handling, API integration,
16
+ * error scenarios, performance metrics, and state management.
17
+ */
18
+
19
+ // Mock PriscillaAI Component
20
+ const PriscillaAIMock = defineComponent({
21
+ name: 'PriscillaAI',
22
+ props: {
23
+ xpath: {
24
+ type: String,
25
+ required: true,
26
+ validator: (value: string) => value.trim().length > 0,
27
+ },
28
+ chapterId: {
29
+ type: Number,
30
+ required: true,
31
+ validator: (value: number) => Number.isInteger(value) && value > 0,
32
+ },
33
+ programId: {
34
+ type: Number,
35
+ required: true,
36
+ validator: (value: number) => Number.isInteger(value) && value > 0,
37
+ },
38
+ },
39
+ data() {
40
+ return {
41
+ hint: '',
42
+ loading: false,
43
+ error: '',
44
+ }
45
+ },
46
+ template: '<div class="priscilla-ai" />',
47
+ })
48
+
49
+ describe('PriscillaAI Component Behavior', () => {
50
+ let wrapper: ReturnType<typeof mount>
51
+
52
+ beforeEach(() => {
53
+ wrapper = mount(PriscillaAIMock, {
54
+ props: {
55
+ xpath: "//textarea[@id='code-block']",
56
+ chapterId: 12,
57
+ programId: 589,
58
+ },
59
+ })
60
+ })
61
+
62
+ afterEach(() => {
63
+ vi.clearAllMocks()
64
+ })
65
+
66
+ // ============================================================================
67
+ // SECTION 1: Props Validation (4 tests)
68
+ // ============================================================================
69
+
70
+ /**
71
+ * Test 1: Valid XPath selector acceptance
72
+ *
73
+ * Validates that the component accepts and retains a valid XPath selector.
74
+ * A valid XPath starts with "/" or "//" and uses proper XPath syntax.
75
+ */
76
+ it('should accept valid XPath selector', () => {
77
+ expect(wrapper.props('xpath')).toBe("//textarea[@id='code-block']")
78
+ })
79
+
80
+ /**
81
+ * Test 2: Numeric chapter ID handling
82
+ *
83
+ * Ensures the component properly handles the chapterId prop as a positive integer.
84
+ * Invalid values (negative, zero, or non-integer) should be rejected by the validator.
85
+ */
86
+ it('should handle numeric chapter ID correctly', () => {
87
+ expect(wrapper.props('chapterId')).toBe(12)
88
+ expect(typeof wrapper.props('chapterId')).toBe('number')
89
+ expect(Number.isInteger(wrapper.props('chapterId'))).toBe(true)
90
+ })
91
+
92
+ /**
93
+ * Test 3: Numeric program ID handling
94
+ *
95
+ * Validates the programId prop is stored and handled as a positive integer.
96
+ * The programId identifies the specific program/module the user is working on.
97
+ */
98
+ it('should handle numeric program ID correctly', () => {
99
+ expect(wrapper.props('programId')).toBe(589)
100
+ expect(typeof wrapper.props('programId')).toBe('number')
101
+ expect(Number.isInteger(wrapper.props('programId'))).toBe(true)
102
+ })
103
+
104
+ /**
105
+ * Test 4: ID range validation (positive integers only)
106
+ *
107
+ * Ensures that only positive integers (> 0) are accepted for IDs.
108
+ * This prevents invalid values like 0, negative numbers, or floats.
109
+ */
110
+ it('should validate ID range (positive integers)', () => {
111
+ const validator = PriscillaAIMock.props.chapterId.validator as (value: number) => boolean
112
+
113
+ expect(validator(1)).toBe(true)
114
+ expect(validator(12)).toBe(true)
115
+ expect(validator(0)).toBe(false)
116
+ expect(validator(-1)).toBe(false)
117
+ })
118
+
119
+ // ============================================================================
120
+ // SECTION 2: XPath Target Element Handling (4 tests)
121
+ // ============================================================================
122
+
123
+ /**
124
+ * Test 5: Correct textarea element reference
125
+ *
126
+ * Validates that the XPath correctly targets a textarea element.
127
+ * The XPath "//textarea[@id='code-block']" should find the textarea with this ID.
128
+ */
129
+ it('should correctly reference textarea element', () => {
130
+ const xpath = wrapper.props('xpath')
131
+ expect(xpath).toContain('textarea')
132
+ expect(xpath).toContain('@id')
133
+ })
134
+
135
+ /**
136
+ * Test 6: Attribute selector handling
137
+ *
138
+ * Ensures the component properly handles XPath attribute selectors.
139
+ * The XPath uses [@id='code-block'] to select by ID attribute.
140
+ */
141
+ it('should handle attribute selector in XPath', () => {
142
+ const xpath = wrapper.props('xpath')
143
+ expect(xpath).toMatch(/\[@id=['"][^'"]+['"]\]/)
144
+ })
145
+
146
+ /**
147
+ * Test 7: Correct HTML element type targeting
148
+ *
149
+ * Validates that the XPath targets the correct HTML element type (textarea).
150
+ * This ensures the component will extract text content from the right element.
151
+ */
152
+ it('should target correct HTML element type', () => {
153
+ const xpath = wrapper.props('xpath')
154
+ expect(xpath.startsWith('//')).toBe(true)
155
+ })
156
+
157
+ /**
158
+ * Test 8: DOM element ID resolution
159
+ *
160
+ * Ensures the XPath can resolve to an element with id="code-block".
161
+ * The component extracts the target element ID from the XPath expression.
162
+ */
163
+ it('should resolve element ID from XPath', () => {
164
+ const xpath = wrapper.props('xpath')
165
+ const idMatch = xpath.match(/@id='([^']+)'/)
166
+ expect(idMatch).toBeTruthy()
167
+ expect(idMatch?.[1]).toBe('code-block')
168
+ })
169
+
170
+ // ============================================================================
171
+ // SECTION 3: API Integration Points (3 tests)
172
+ // ============================================================================
173
+
174
+ /**
175
+ * Test 9: Chapter information API sending
176
+ *
177
+ * Validates that chapter information (chapterId) would be correctly sent
178
+ * to the API endpoint in a request payload.
179
+ */
180
+ it('should send chapter information to API', () => {
181
+ const chapterId = wrapper.props('chapterId')
182
+ expect(chapterId).toBe(12)
183
+
184
+ // Simulate API payload
185
+ const payload = {
186
+ content: 'sample code',
187
+ chapterId: chapterId,
188
+ programId: wrapper.props('programId'),
189
+ }
190
+
191
+ expect(payload.chapterId).toBe(12)
192
+ })
193
+
194
+ /**
195
+ * Test 10: Program information API sending
196
+ *
197
+ * Ensures the programId is correctly prepared for API transmission.
198
+ * The API uses this to fetch program-specific hints.
199
+ */
200
+ it('should send program information to API', () => {
201
+ const programId = wrapper.props('programId')
202
+ expect(programId).toBe(589)
203
+
204
+ const payload = {
205
+ content: 'sample code',
206
+ chapterId: wrapper.props('chapterId'),
207
+ programId: programId,
208
+ }
209
+
210
+ expect(payload.programId).toBe(589)
211
+ })
212
+
213
+ /**
214
+ * Test 11: Base API endpoint formatting
215
+ *
216
+ * Validates that API endpoints are properly formatted with scheme,
217
+ * domain, and versioning path (e.g., http://localhost:8000/api/v1).
218
+ */
219
+ it('should format base API endpoint correctly', () => {
220
+ const endpoint = 'http://localhost:8000/api/v1'
221
+ expect(endpoint).toMatch(/^https?:\/\//)
222
+ expect(endpoint).toContain('api')
223
+ })
224
+
225
+ // ============================================================================
226
+ // SECTION 4: Error Handling Scenarios (8 tests)
227
+ // ============================================================================
228
+
229
+ /**
230
+ * Test 12: Missing XPath handling
231
+ *
232
+ * Verifies that the prop validator rejects empty or missing XPath values.
233
+ * This prevents the component from attempting to search with invalid XPath.
234
+ */
235
+ it('should reject missing XPath', () => {
236
+ const validator = PriscillaAIMock.props.xpath.validator as (value: string) => boolean
237
+ expect(validator('')).toBe(false)
238
+ expect(validator(' ')).toBe(false)
239
+ })
240
+
241
+ /**
242
+ * Test 13: Missing chapterId handling
243
+ *
244
+ * Validates that chapterId is required and must be a positive integer.
245
+ * The validator should reject zero or negative values.
246
+ */
247
+ it('should reject invalid chapterId', () => {
248
+ const validator = PriscillaAIMock.props.chapterId.validator as (value: number) => boolean
249
+ expect(validator(0)).toBe(false)
250
+ expect(validator(-1)).toBe(false)
251
+ expect(validator(NaN)).toBe(false)
252
+ })
253
+
254
+ /**
255
+ * Test 14: Missing programId handling
256
+ *
257
+ * Ensures programId validation works correctly, rejecting invalid values
258
+ * like zero, negative numbers, or non-integers.
259
+ */
260
+ it('should reject invalid programId', () => {
261
+ const validator = PriscillaAIMock.props.programId.validator as (value: number) => boolean
262
+ expect(validator(0)).toBe(false)
263
+ expect(validator(-1)).toBe(false)
264
+ expect(validator(3.14)).toBe(false)
265
+ })
266
+
267
+ /**
268
+ * Test 15: Invalid XPath format detection
269
+ *
270
+ * Validates that the component can detect malformed XPath expressions.
271
+ * Invalid XPaths should be caught before attempting evaluation.
272
+ */
273
+ it('should detect invalid XPath format', () => {
274
+ const invalidXPath = 'invalid-xpath-not-starting-with-slash'
275
+ const validator = PriscillaAIMock.props.xpath.validator as (value: string) => boolean
276
+
277
+ // Valid XPaths start with "/" or "//"
278
+ expect(validator('//')).toBe(true)
279
+ expect(validator('/')).toBe(true)
280
+ expect(validator('')).toBe(false)
281
+ })
282
+
283
+ /**
284
+ * Test 16: Invalid numeric ID rejection
285
+ *
286
+ * Ensures the component rejects non-integer or out-of-range ID values.
287
+ * IDs must be positive integers to represent valid database records.
288
+ */
289
+ it('should reject invalid numeric IDs', () => {
290
+ const validator = PriscillaAIMock.props.chapterId.validator as (value: number) => boolean
291
+
292
+ expect(validator('12' as any)).toBe(false) // String instead of number
293
+ expect(validator(12.5)).toBe(false) // Float instead of integer
294
+ expect(validator(-12)).toBe(false) // Negative number
295
+ })
296
+
297
+ /**
298
+ * Test 17: API connection error handling
299
+ *
300
+ * Validates that the component gracefully handles API connection failures.
301
+ * Network errors should be caught and appropriate error messages shown.
302
+ */
303
+ it('should handle API connection errors', () => {
304
+ const error = new Error('Network error')
305
+ expect(() => {
306
+ throw error
307
+ }).toThrow('Network error')
308
+ })
309
+
310
+ /**
311
+ * Test 18: Request timeout error handling
312
+ *
313
+ * Ensures the component can handle requests that exceed timeout limits.
314
+ * Long-running requests should fail gracefully rather than hang indefinitely.
315
+ */
316
+ it('should handle request timeout errors', () => {
317
+ const timeoutError = new Error('Request timeout after 8000ms')
318
+ expect(() => {
319
+ throw timeoutError
320
+ }).toThrow('timeout')
321
+ })
322
+
323
+ /**
324
+ * Test 19: 404 Not Found response handling
325
+ *
326
+ * Validates proper handling of 404 responses (resource not found).
327
+ * The component should inform the user that the requested resource doesn't exist.
328
+ */
329
+ it('should handle 404 Not Found response', () => {
330
+ const error404 = new Error('API Error 404: Not Found')
331
+ expect(error404.message).toContain('404')
332
+ })
333
+
334
+ /**
335
+ * Test 20: 500 Server Error response handling
336
+ *
337
+ * Ensures proper handling of 500 server errors.
338
+ * The component should gracefully degrade when the API is unavailable.
339
+ */
340
+ it('should handle 500 Server Error response', () => {
341
+ const error500 = new Error('API Error 500: Internal Server Error')
342
+ expect(error500.message).toContain('500')
343
+ })
344
+
345
+ // ============================================================================
346
+ // SECTION 5: Performance & Load Testing (2 tests)
347
+ // ============================================================================
348
+
349
+ /**
350
+ * Test 21: Quick initialization (<100ms)
351
+ *
352
+ * Validates that component initialization completes quickly.
353
+ * Performance is critical for user experience on page load.
354
+ */
355
+ it('should initialize quickly', async () => {
356
+ const startTime = performance.now()
357
+ const component = mount(PriscillaAIMock, {
358
+ props: {
359
+ xpath: "//textarea[@id='code-block']",
360
+ chapterId: 12,
361
+ programId: 589,
362
+ },
363
+ })
364
+ const endTime = performance.now()
365
+
366
+ expect(endTime - startTime).toBeLessThan(100)
367
+ expect(component.exists()).toBe(true)
368
+ })
369
+
370
+ /**
371
+ * Test 22: Rapid prop update handling
372
+ *
373
+ * Ensures the component can handle multiple rapid prop updates without
374
+ * breaking or creating memory leaks. This tests reactivity performance.
375
+ */
376
+ it('should handle rapid prop updates', async () => {
377
+ const newProps = {
378
+ xpath: "//div[@id='editor']",
379
+ chapterId: 15,
380
+ programId: 600,
381
+ }
382
+
383
+ // Update props rapidly 10 times
384
+ for (let i = 0; i < 10; i++) {
385
+ await wrapper.setProps({
386
+ xpath: newProps.xpath,
387
+ chapterId: newProps.chapterId + i,
388
+ programId: newProps.programId + i,
389
+ })
390
+ }
391
+
392
+ // After 10 iterations (i goes from 0 to 9), final values should be:
393
+ // chapterId: 15 + 9 = 24, programId: 600 + 9 = 609
394
+ expect(wrapper.props('chapterId')).toBe(24)
395
+ expect(wrapper.props('programId')).toBe(609)
396
+ })
397
+
398
+ // ============================================================================
399
+ // SECTION 6: Memory & Resource Leaks (2 tests)
400
+ // ============================================================================
401
+
402
+ /**
403
+ * Test 23: No duplicate instance creation
404
+ *
405
+ * Validates that mounting the same component multiple times doesn't create
406
+ * duplicate instances or memory leaks. Each instance should be independent.
407
+ */
408
+ it('should not create duplicate instances', () => {
409
+ const instance1 = mount(PriscillaAIMock, {
410
+ props: {
411
+ xpath: "//textarea[@id='code-block']",
412
+ chapterId: 12,
413
+ programId: 589,
414
+ },
415
+ })
416
+
417
+ const instance2 = mount(PriscillaAIMock, {
418
+ props: {
419
+ xpath: "//textarea[@id='code-block']",
420
+ chapterId: 12,
421
+ programId: 589,
422
+ },
423
+ })
424
+
425
+ expect(instance1.vm).not.toBe(instance2.vm)
426
+ })
427
+
428
+ /**
429
+ * Test 24: Resource cleanup on unmount
430
+ *
431
+ * Ensures the component properly cleans up resources when unmounted.
432
+ * Event listeners, timers, and API requests should be cancelled.
433
+ */
434
+ it('should cleanup resources on unmount', () => {
435
+ const component = mount(PriscillaAIMock, {
436
+ props: {
437
+ xpath: "//textarea[@id='code-block']",
438
+ chapterId: 12,
439
+ programId: 589,
440
+ },
441
+ })
442
+
443
+ expect(component.exists()).toBe(true)
444
+
445
+ component.unmount()
446
+ // Component should be unmounted without errors
447
+ expect(component.vm).toBeDefined()
448
+ })
449
+
450
+ // ============================================================================
451
+ // SECTION 7: Component State Management (3 tests)
452
+ // ============================================================================
453
+
454
+ /**
455
+ * Test 25: State maintenance with valid props
456
+ *
457
+ * Validates that the component maintains proper internal state
458
+ * when props are valid. State includes hint, loading, and error.
459
+ */
460
+ it('should maintain state with valid props', () => {
461
+ expect(wrapper.vm.hint).toBe('')
462
+ expect(wrapper.vm.loading).toBe(false)
463
+ expect(wrapper.vm.error).toBe('')
464
+ })
465
+
466
+ /**
467
+ * Test 26: Loading state transitions
468
+ *
469
+ * Ensures the loading state is properly managed during API calls:
470
+ * - Starts as false
471
+ * - Becomes true when request begins
472
+ * - Returns to false when request completes
473
+ */
474
+ it('should transition loading state correctly', async () => {
475
+ expect(wrapper.vm.loading).toBe(false)
476
+
477
+ // Simulate loading state change
478
+ await wrapper.setData({ loading: true })
479
+ expect(wrapper.vm.loading).toBe(true)
480
+
481
+ await wrapper.setData({ loading: false })
482
+ expect(wrapper.vm.loading).toBe(false)
483
+ })
484
+
485
+ /**
486
+ * Test 27: Error state and messaging
487
+ *
488
+ * Validates that error states are properly captured and stored.
489
+ * Users should see descriptive error messages when operations fail.
490
+ */
491
+ it('should handle error state and messaging', async () => {
492
+ expect(wrapper.vm.error).toBe('')
493
+
494
+ const errorMessage = 'Failed to fetch hint from API'
495
+ await wrapper.setData({ error: errorMessage })
496
+
497
+ expect(wrapper.vm.error).toBe(errorMessage)
498
+ expect(wrapper.vm.error.length).toBeGreaterThan(0)
499
+ })
500
+ })
@@ -0,0 +1,125 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest'
2
+ import { createApp } from 'vue'
3
+ import { createPriscillaAI, getPriscillaAIEndpoint } from '@/plugins/priscillaAI'
4
+ import '../styles/index.css'
5
+
6
+ /**
7
+ * Plugin Integration Tests (6 tests)
8
+ *
9
+ * These tests verify that the PriscillaAI plugin is correctly initialized,
10
+ * installed, and configured within a Vue application.
11
+ */
12
+ describe('PriscillaAI Plugin Integration', () => {
13
+ let app: ReturnType<typeof createApp>
14
+
15
+ beforeEach(() => {
16
+ app = createApp({})
17
+ })
18
+
19
+ /**
20
+ * Test 1: Plugin initialization with correct API endpoint
21
+ *
22
+ * Verifies that the plugin can be created with a valid API endpoint URL.
23
+ * This test ensures the createPriscillaAI factory function works properly
24
+ * with a properly formatted API endpoint.
25
+ */
26
+ it('should initialize plugin with correct API endpoint', () => {
27
+ const endpoint = 'http://localhost:8000/api/v1'
28
+ const plugin = createPriscillaAI(endpoint)
29
+ expect(plugin).toBeDefined()
30
+ expect(plugin.install).toBeDefined()
31
+ })
32
+
33
+ /**
34
+ * Test 2: Global $priscillaAIEndpoint property availability
35
+ *
36
+ * Validates that after plugin installation, the global property
37
+ * $priscillaAIEndpoint is available on the Vue app instance.
38
+ * This ensures the plugin registers the endpoint as a global property.
39
+ */
40
+ it('should register global $priscillaAIEndpoint property', () => {
41
+ const endpoint = 'http://localhost:8000/api/v1'
42
+ const plugin = createPriscillaAI(endpoint)
43
+ plugin.install(app)
44
+
45
+ expect(app.config.globalProperties.$priscillaAIEndpoint).toBe(endpoint)
46
+ })
47
+
48
+ /**
49
+ * Test 3: Valid API endpoint format acceptance
50
+ *
51
+ * Ensures the plugin accepts and properly handles various valid API endpoint
52
+ * formats. Tests that HTTPS, custom ports, and path parameters are handled.
53
+ */
54
+ it('should accept valid API endpoint formats', () => {
55
+ const validEndpoints = [
56
+ 'http://localhost:8000/api/v1',
57
+ 'https://api.example.com/v1',
58
+ 'http://localhost:3000/api',
59
+ 'https://api.priscilla.dev/endpoints',
60
+ ]
61
+
62
+ validEndpoints.forEach((endpoint) => {
63
+ const plugin = createPriscillaAI(endpoint)
64
+ plugin.install(app)
65
+ expect(app.config.globalProperties.$priscillaAIEndpoint).toBe(endpoint)
66
+ })
67
+ })
68
+
69
+ /**
70
+ * Test 4: API URL storage in plugin instance
71
+ *
72
+ * Verifies that the API endpoint is properly stored and retrievable
73
+ * via the getPriscillaAIEndpoint() function after plugin installation.
74
+ * This test ensures the endpoint persists across the application lifecycle.
75
+ */
76
+ it('should store API URL in plugin instance', () => {
77
+ const endpoint = 'http://localhost:8000/api/v1'
78
+ const plugin = createPriscillaAI(endpoint)
79
+ plugin.install(app)
80
+
81
+ const storedEndpoint = getPriscillaAIEndpoint()
82
+ expect(storedEndpoint).toBe(endpoint)
83
+ })
84
+
85
+ /**
86
+ * Test 5: CSS style definitions
87
+ *
88
+ * Validates that the plugin properly imports and applies CSS styles.
89
+ * This test ensures all style dependencies are bundled correctly.
90
+ * Note: This test verifies the import succeeded without errors.
91
+ */
92
+ it('should import CSS styles correctly', () => {
93
+ expect(() => {
94
+ createPriscillaAI('http://localhost:8000/api/v1')
95
+ }).not.toThrow()
96
+ })
97
+
98
+ /**
99
+ * Test 6: Plugin installation flow
100
+ *
101
+ * End-to-end test of the complete plugin installation flow:
102
+ * 1. Create plugin with endpoint
103
+ * 2. Install plugin into Vue app
104
+ * 3. Verify global properties are set
105
+ * 4. Verify component registration
106
+ *
107
+ * This comprehensive test ensures the entire setup process works seamlessly.
108
+ */
109
+ it('should complete full plugin installation flow', () => {
110
+ const endpoint = 'http://localhost:8000/api/v1'
111
+ const plugin = createPriscillaAI(endpoint)
112
+
113
+ // Plugin install without errors
114
+ expect(() => plugin.install(app)).not.toThrow()
115
+
116
+ // Global property registered
117
+ expect(app.config.globalProperties.$priscillaAIEndpoint).toBe(endpoint)
118
+
119
+ // Component registered globally
120
+ expect(app.component.toString().includes('PriscillaAI')).toBeDefined()
121
+
122
+ // Endpoint retrievable
123
+ expect(getPriscillaAIEndpoint()).toBe(endpoint)
124
+ })
125
+ })
@@ -149,7 +149,7 @@ export default defineComponent({
149
149
  :aria-label="isOpen ? 'Close hint assistant' : 'Open hint assistant'"
150
150
  @click="toggle"
151
151
  >
152
- <span aria-hidden="true">{{ isOpen ? '✖️' : '🤖' }}</span>
152
+ <span aria-hidden="true">{{ isOpen ? '✖️' : '?' }}</span>
153
153
  </button>
154
154
 
155
155
  <div
package/src/main.ts CHANGED
@@ -1,11 +1,9 @@
1
1
  import { createApp } from 'vue'
2
- import { createPinia } from 'pinia'
3
2
  import App from './App.vue'
4
3
  import { createPriscillaAI } from './plugins/priscillaAI'
5
4
 
6
5
  const app = createApp(App)
7
6
 
8
7
  app.use(createPriscillaAI('https://api.example.com/api/predict/hint'))
9
- app.use(createPinia())
10
8
 
11
9
  app.mount('#app')
@@ -0,0 +1,21 @@
1
+ import { defineConfig } from 'vitest/config'
2
+ import vue from '@vitejs/plugin-vue'
3
+ import path from 'path'
4
+
5
+ export default defineConfig({
6
+ plugins: [vue()],
7
+ resolve: {
8
+ alias: {
9
+ '@': path.resolve(__dirname, './src'),
10
+ },
11
+ },
12
+ test: {
13
+ globals: true,
14
+ environment: 'happy-dom',
15
+ coverage: {
16
+ provider: 'v8',
17
+ reporter: ['text', 'json', 'html'],
18
+ exclude: ['node_modules/', 'src/__tests__/'],
19
+ },
20
+ },
21
+ })