ima-claude 2.9.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.
- package/LICENSE +21 -0
- package/README.md +463 -0
- package/dist/cli.js +1064 -0
- package/package.json +49 -0
- package/platforms/claude/adapter.ts +115 -0
- package/platforms/junie/adapter.ts +254 -0
- package/platforms/junie/agents-template.md +113 -0
- package/platforms/junie/hook-translations.md +84 -0
- package/platforms/shared/detector.ts +27 -0
- package/platforms/shared/installer.ts +202 -0
- package/platforms/shared/types.ts +78 -0
- package/plugins/ima-claude/.claude-plugin/plugin.json +25 -0
- package/plugins/ima-claude/agents/explorer.md +30 -0
- package/plugins/ima-claude/agents/implementer.md +30 -0
- package/plugins/ima-claude/agents/memory.md +42 -0
- package/plugins/ima-claude/agents/reviewer.md +53 -0
- package/plugins/ima-claude/agents/tester.md +33 -0
- package/plugins/ima-claude/agents/wp-developer.md +46 -0
- package/plugins/ima-claude/hooks/README.md +145 -0
- package/plugins/ima-claude/hooks/atlassian_prereqs.py +112 -0
- package/plugins/ima-claude/hooks/block_sed_edits.py +59 -0
- package/plugins/ima-claude/hooks/bootstrap.sh +90 -0
- package/plugins/ima-claude/hooks/bootstrap_utility_check.py +94 -0
- package/plugins/ima-claude/hooks/composer_autoload_check.py +70 -0
- package/plugins/ima-claude/hooks/docs_organization.py +104 -0
- package/plugins/ima-claude/hooks/enforce_rg_over_grep.py +56 -0
- package/plugins/ima-claude/hooks/fp_utility_check.py +90 -0
- package/plugins/ima-claude/hooks/hook_logger.py +69 -0
- package/plugins/ima-claude/hooks/hooks.json +239 -0
- package/plugins/ima-claude/hooks/jira_issue_fetch.py +79 -0
- package/plugins/ima-claude/hooks/jquery_in_wordpress.py +92 -0
- package/plugins/ima-claude/hooks/memory_bootstrap.py +79 -0
- package/plugins/ima-claude/hooks/memory_store_reminder.py +75 -0
- package/plugins/ima-claude/hooks/prompt_coach.py +125 -0
- package/plugins/ima-claude/hooks/prompt_coach_digest.md +48 -0
- package/plugins/ima-claude/hooks/prompt_coach_system.md +30 -0
- package/plugins/ima-claude/hooks/sequential_thinking_check.py +81 -0
- package/plugins/ima-claude/hooks/serena_over_grep.py +96 -0
- package/plugins/ima-claude/hooks/serena_over_read.py +66 -0
- package/plugins/ima-claude/hooks/serena_project_check.py +133 -0
- package/plugins/ima-claude/hooks/sql_injection_check.py +73 -0
- package/plugins/ima-claude/hooks/task_master_after_plan.py +31 -0
- package/plugins/ima-claude/hooks/task_master_before_impl.py +93 -0
- package/plugins/ima-claude/hooks/tavily_extract_advanced.py +48 -0
- package/plugins/ima-claude/hooks/vestige_before_external.py +86 -0
- package/plugins/ima-claude/hooks/webfetch_to_tavily.py +42 -0
- package/plugins/ima-claude/hooks/websearch_to_tavily.py +41 -0
- package/plugins/ima-claude/hooks/wp_security_check.py +150 -0
- package/plugins/ima-claude/personalities/README.md +45 -0
- package/plugins/ima-claude/personalities/enable-40k.md +69 -0
- package/plugins/ima-claude/personalities/enable-templars.md +69 -0
- package/plugins/ima-claude/skills/.research-summary.md +340 -0
- package/plugins/ima-claude/skills/architect/SKILL.md +304 -0
- package/plugins/ima-claude/skills/compound-bridge/SKILL.md +200 -0
- package/plugins/ima-claude/skills/discourse/SKILL.md +440 -0
- package/plugins/ima-claude/skills/discourse-admin/SKILL.md +192 -0
- package/plugins/ima-claude/skills/discourse-admin/references/api-endpoints.md +441 -0
- package/plugins/ima-claude/skills/discourse-admin/references/gotchas.md +107 -0
- package/plugins/ima-claude/skills/discourse-admin/references/staging-defaults.md +98 -0
- package/plugins/ima-claude/skills/discourse-admin/scripts/discourse-admin.py +319 -0
- package/plugins/ima-claude/skills/docs-organize/SKILL.md +254 -0
- package/plugins/ima-claude/skills/docs-organize/templates/active-README.md +50 -0
- package/plugins/ima-claude/skills/docs-organize/templates/archive-README.md +57 -0
- package/plugins/ima-claude/skills/docs-organize/templates/docs-README.md +43 -0
- package/plugins/ima-claude/skills/docs-organize/templates/phase-archive-README.md +83 -0
- package/plugins/ima-claude/skills/docs-organize/templates/section-README.md +48 -0
- package/plugins/ima-claude/skills/docs-organize/templates/transient-README.md +79 -0
- package/plugins/ima-claude/skills/docs-organize/templates/transient-gitignore +9 -0
- package/plugins/ima-claude/skills/ember-discourse/SKILL.md +496 -0
- package/plugins/ima-claude/skills/functional-programmer/SKILL.md +258 -0
- package/plugins/ima-claude/skills/ima-bootstrap/SKILL.md +278 -0
- package/plugins/ima-claude/skills/ima-bootstrap/references/bootstrap-patterns.md +356 -0
- package/plugins/ima-claude/skills/ima-bootstrap/references/ima-brand.md +273 -0
- package/plugins/ima-claude/skills/ima-bootstrap/references/theme-integration.md +212 -0
- package/plugins/ima-claude/skills/ima-brand/SKILL.md +108 -0
- package/plugins/ima-claude/skills/ima-brand/references/brand-identity.md +140 -0
- package/plugins/ima-claude/skills/ima-brand/references/digital-standards.md +180 -0
- package/plugins/ima-claude/skills/ima-brand/references/visual-system.md +173 -0
- package/plugins/ima-claude/skills/ima-forms-expert/SKILL.md +175 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/container-components.md +154 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/examples.md +328 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/field-components.md +298 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/form-factory.md +193 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/quick-reference.md +153 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/validation-engine.md +336 -0
- package/plugins/ima-claude/skills/jira-checkpoint/SKILL.md +178 -0
- package/plugins/ima-claude/skills/jquery/SKILL.md +413 -0
- package/plugins/ima-claude/skills/js-fp/SKILL.md +463 -0
- package/plugins/ima-claude/skills/js-fp/core-principles.md +487 -0
- package/plugins/ima-claude/skills/js-fp/examples/pure-functions.js +260 -0
- package/plugins/ima-claude/skills/js-fp/examples/tests/pure-functions.test.js +262 -0
- package/plugins/ima-claude/skills/js-fp/references/anti-patterns.md +120 -0
- package/plugins/ima-claude/skills/js-fp/references/performance-patterns.md +116 -0
- package/plugins/ima-claude/skills/js-fp/references/testing-patterns.md +134 -0
- package/plugins/ima-claude/skills/js-fp-api/SKILL.md +280 -0
- package/plugins/ima-claude/skills/js-fp-api/examples/crud-endpoint.js +258 -0
- package/plugins/ima-claude/skills/js-fp-api/references/middleware-patterns.md +134 -0
- package/plugins/ima-claude/skills/js-fp-api/references/security-sql.md +110 -0
- package/plugins/ima-claude/skills/js-fp-api/references/validation-patterns.md +165 -0
- package/plugins/ima-claude/skills/js-fp-react/SKILL.md +447 -0
- package/plugins/ima-claude/skills/js-fp-react/examples/ProductCard.tsx +65 -0
- package/plugins/ima-claude/skills/js-fp-react/references/hooks-advanced.md +136 -0
- package/plugins/ima-claude/skills/js-fp-react/references/performance-patterns.md +175 -0
- package/plugins/ima-claude/skills/js-fp-vue/SKILL.md +322 -0
- package/plugins/ima-claude/skills/js-fp-vue/references/complete-examples.md +397 -0
- package/plugins/ima-claude/skills/js-fp-vue/references/composables-advanced.md +282 -0
- package/plugins/ima-claude/skills/js-fp-vue/references/reactivity-patterns.md +348 -0
- package/plugins/ima-claude/skills/js-fp-vue/references/testing.md +314 -0
- package/plugins/ima-claude/skills/js-fp-wordpress/SKILL.md +301 -0
- package/plugins/ima-claude/skills/js-fp-wordpress/references/ajax-patterns.md +192 -0
- package/plugins/ima-claude/skills/js-fp-wordpress/references/event-patterns.md +136 -0
- package/plugins/ima-claude/skills/js-fp-wordpress/references/wp-integration.md +248 -0
- package/plugins/ima-claude/skills/livecanvas/SKILL.md +209 -0
- package/plugins/ima-claude/skills/livecanvas/references/livecanvas-features.md +311 -0
- package/plugins/ima-claude/skills/livecanvas/references/loops-and-logic.md +730 -0
- package/plugins/ima-claude/skills/livecanvas/references/picostrap.md +227 -0
- package/plugins/ima-claude/skills/mcp-atlassian/SKILL.md +339 -0
- package/plugins/ima-claude/skills/mcp-context7/SKILL.md +109 -0
- package/plugins/ima-claude/skills/mcp-memory/SKILL.md +182 -0
- package/plugins/ima-claude/skills/mcp-qdrant/SKILL.md +233 -0
- package/plugins/ima-claude/skills/mcp-sequential/SKILL.md +149 -0
- package/plugins/ima-claude/skills/mcp-serena/SKILL.md +174 -0
- package/plugins/ima-claude/skills/mcp-tavily/SKILL.md +118 -0
- package/plugins/ima-claude/skills/mcp-vestige/SKILL.md +259 -0
- package/plugins/ima-claude/skills/php-authnet/SKILL.md +275 -0
- package/plugins/ima-claude/skills/php-authnet/references/api-reference.md +624 -0
- package/plugins/ima-claude/skills/php-authnet/references/sandbox-testing.md +424 -0
- package/plugins/ima-claude/skills/php-fp/SKILL.md +333 -0
- package/plugins/ima-claude/skills/php-fp/examples/pure-functions.php +403 -0
- package/plugins/ima-claude/skills/php-fp/examples/tests/PureFunctionsTest.php +515 -0
- package/plugins/ima-claude/skills/php-fp/references/core-principles.md +277 -0
- package/plugins/ima-claude/skills/php-fp/references/testing-patterns.md +374 -0
- package/plugins/ima-claude/skills/php-fp-wordpress/SKILL.md +216 -0
- package/plugins/ima-claude/skills/php-fp-wordpress/references/fp-patterns.md +275 -0
- package/plugins/ima-claude/skills/php-fp-wordpress/references/plugin-architecture.md +295 -0
- package/plugins/ima-claude/skills/php-fp-wordpress/references/security-examples.md +203 -0
- package/plugins/ima-claude/skills/php-fp-wordpress/references/testing-strategy.md +259 -0
- package/plugins/ima-claude/skills/phpunit-wp/SKILL.md +716 -0
- package/plugins/ima-claude/skills/playwright/SKILL.md +434 -0
- package/plugins/ima-claude/skills/playwright/references/accessibility-testing.md +153 -0
- package/plugins/ima-claude/skills/playwright/references/ci-cd.md +268 -0
- package/plugins/ima-claude/skills/playwright/references/network-mocking.md +270 -0
- package/plugins/ima-claude/skills/playwright/references/visual-regression.md +215 -0
- package/plugins/ima-claude/skills/py-fp/SKILL.md +663 -0
- package/plugins/ima-claude/skills/py-fp/examples/pure-functions.py +185 -0
- package/plugins/ima-claude/skills/py-fp/examples/tests/test_pure_functions.py +244 -0
- package/plugins/ima-claude/skills/py-fp/references/core-principles.md +381 -0
- package/plugins/ima-claude/skills/py-fp/references/testing-patterns.md +283 -0
- package/plugins/ima-claude/skills/quasar-fp/SKILL.md +327 -0
- package/plugins/ima-claude/skills/quasar-fp/metadata.json +85 -0
- package/plugins/ima-claude/skills/quasar-fp/references/component-patterns.md +257 -0
- package/plugins/ima-claude/skills/quasar-fp/references/theme-integration.md +233 -0
- package/plugins/ima-claude/skills/quasar-fp/references/utility-classes.md +237 -0
- package/plugins/ima-claude/skills/quickstart/SKILL.md +129 -0
- package/plugins/ima-claude/skills/rails/SKILL.md +359 -0
- package/plugins/ima-claude/skills/resume-session/SKILL.md +68 -0
- package/plugins/ima-claude/skills/rg/SKILL.md +205 -0
- package/plugins/ima-claude/skills/ruby-fp/SKILL.md +336 -0
- package/plugins/ima-claude/skills/save-session/SKILL.md +81 -0
- package/plugins/ima-claude/skills/scorecard/SKILL.md +96 -0
- package/plugins/ima-claude/skills/skill-analyzer/SKILL.md +127 -0
- package/plugins/ima-claude/skills/skill-analyzer/references/advanced-checklist.md +44 -0
- package/plugins/ima-claude/skills/skill-analyzer/references/core-checklist.md +60 -0
- package/plugins/ima-claude/skills/skill-analyzer/scripts/analyze_skill.py +418 -0
- package/plugins/ima-claude/skills/skill-creator/LICENSE.txt +202 -0
- package/plugins/ima-claude/skills/skill-creator/SKILL.md +343 -0
- package/plugins/ima-claude/skills/skill-creator/references/output-patterns.md +82 -0
- package/plugins/ima-claude/skills/skill-creator/references/workflows.md +28 -0
- package/plugins/ima-claude/skills/skill-creator/scripts/init_skill.py +303 -0
- package/plugins/ima-claude/skills/skill-creator/scripts/package_skill.py +110 -0
- package/plugins/ima-claude/skills/skill-creator/scripts/quick_validate.py +103 -0
- package/plugins/ima-claude/skills/task-master/SKILL.md +51 -0
- package/plugins/ima-claude/skills/task-planner/SKILL.md +228 -0
- package/plugins/ima-claude/skills/task-runner/SKILL.md +192 -0
- package/plugins/ima-claude/skills/unit-testing/SKILL.md +198 -0
- package/plugins/ima-claude/skills/unit-testing/references/mock-patterns.md +181 -0
- package/plugins/ima-claude/skills/unit-testing/references/tdd-workflow.md +177 -0
- package/plugins/ima-claude/skills/unit-testing/references/test-strategy.md +126 -0
- package/plugins/ima-claude/skills/wp-local/SKILL.md +246 -0
- package/plugins/ima-claude/skills/wp-local/references/configuration.md +198 -0
- package/plugins/ima-claude/skills/wp-local/references/wp-cli-reference.md +406 -0
- package/plugins/ima-claude/skills/wp-local/scripts/wp-local.sh +61 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
# Reactivity Patterns
|
|
2
|
+
|
|
3
|
+
Deep reactivity patterns, performance optimization, and memory management for Vue.js.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
1. [Reactive vs Ref](#reactive-vs-ref)
|
|
8
|
+
2. [Computed Optimization](#computed-optimization)
|
|
9
|
+
3. [Watch Anti-Patterns](#watch-anti-patterns)
|
|
10
|
+
4. [ShallowRef for Performance](#shallowref-for-performance)
|
|
11
|
+
5. [Memory Management](#memory-management)
|
|
12
|
+
6. [Common Anti-Patterns](#common-anti-patterns)
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Reactive vs Ref
|
|
17
|
+
|
|
18
|
+
### When to Use ref()
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
import { ref, computed } from 'vue'
|
|
22
|
+
|
|
23
|
+
// Primitives - always ref
|
|
24
|
+
const count = ref(0)
|
|
25
|
+
const name = ref('')
|
|
26
|
+
const isActive = ref(false)
|
|
27
|
+
|
|
28
|
+
// Simple objects - prefer ref for consistency
|
|
29
|
+
const user = ref<UserData>({ id: '1', name: 'Alice', email: 'alice@test.com' })
|
|
30
|
+
|
|
31
|
+
// Access with .value in script
|
|
32
|
+
console.log(user.value.name)
|
|
33
|
+
|
|
34
|
+
// Template auto-unwraps
|
|
35
|
+
// <p>{{ user.name }}</p>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### When to Use reactive()
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
import { reactive } from 'vue'
|
|
42
|
+
|
|
43
|
+
// Complex nested objects with many properties
|
|
44
|
+
const state = reactive({
|
|
45
|
+
users: [] as UserData[],
|
|
46
|
+
filters: {
|
|
47
|
+
search: '',
|
|
48
|
+
minAge: 0,
|
|
49
|
+
status: 'active' as 'active' | 'inactive'
|
|
50
|
+
},
|
|
51
|
+
pagination: {
|
|
52
|
+
page: 1,
|
|
53
|
+
perPage: 20,
|
|
54
|
+
total: 0
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
// No .value needed
|
|
59
|
+
console.log(state.filters.search)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Decision Guide
|
|
63
|
+
|
|
64
|
+
| Scenario | Use |
|
|
65
|
+
|----------|-----|
|
|
66
|
+
| Primitive values | `ref()` |
|
|
67
|
+
| Single object | `ref()` |
|
|
68
|
+
| Form state with many fields | `reactive()` |
|
|
69
|
+
| Complex nested state | `reactive()` |
|
|
70
|
+
| Needs to be replaced entirely | `ref()` |
|
|
71
|
+
| Need to pass by reference | `ref()` |
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Computed Optimization
|
|
76
|
+
|
|
77
|
+
### Always Computed for Derived State
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
import { ref, computed } from 'vue'
|
|
81
|
+
|
|
82
|
+
const users = ref<UserData[]>([])
|
|
83
|
+
const searchTerm = ref('')
|
|
84
|
+
|
|
85
|
+
// Good: computed automatically caches
|
|
86
|
+
const filteredUsers = computed(() =>
|
|
87
|
+
users.value.filter(u =>
|
|
88
|
+
u.name.toLowerCase().includes(searchTerm.value.toLowerCase())
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
// Good: computed chain
|
|
93
|
+
const activeFilteredUsers = computed(() =>
|
|
94
|
+
filteredUsers.value.filter(u => u.isActive)
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
const userCount = computed(() => activeFilteredUsers.value.length)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Avoid Expensive Recomputation
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
// Bad: recomputes on every access
|
|
104
|
+
const getExpensiveData = () => {
|
|
105
|
+
return users.value.map(u => ({
|
|
106
|
+
...u,
|
|
107
|
+
computed: expensiveCalculation(u)
|
|
108
|
+
}))
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Good: computed caches until dependencies change
|
|
112
|
+
const expensiveData = computed(() =>
|
|
113
|
+
users.value.map(u => ({
|
|
114
|
+
...u,
|
|
115
|
+
computed: expensiveCalculation(u)
|
|
116
|
+
}))
|
|
117
|
+
)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Computed with Getters and Setters
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
const firstName = ref('John')
|
|
124
|
+
const lastName = ref('Doe')
|
|
125
|
+
|
|
126
|
+
const fullName = computed({
|
|
127
|
+
get: () => `${firstName.value} ${lastName.value}`,
|
|
128
|
+
set: (value: string) => {
|
|
129
|
+
const [first, ...rest] = value.split(' ')
|
|
130
|
+
firstName.value = first
|
|
131
|
+
lastName.value = rest.join(' ')
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
// Usage
|
|
136
|
+
fullName.value = 'Jane Smith' // Sets firstName and lastName
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Watch Anti-Patterns
|
|
142
|
+
|
|
143
|
+
### Bad: Watch for Derived State
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
// Bad: watch to update derived state
|
|
147
|
+
const fullName = ref('')
|
|
148
|
+
watch([firstName, lastName], () => {
|
|
149
|
+
fullName.value = `${firstName.value} ${lastName.value}`
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
// Good: computed for derived state
|
|
153
|
+
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Good: Watch for Side Effects Only
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
import { watch, watchEffect } from 'vue'
|
|
160
|
+
|
|
161
|
+
// Good: watch for API calls
|
|
162
|
+
watch(userId, async (newId) => {
|
|
163
|
+
if (newId) {
|
|
164
|
+
user.value = await fetchUser(newId)
|
|
165
|
+
}
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
// Good: watchEffect for multiple dependencies
|
|
169
|
+
watchEffect(() => {
|
|
170
|
+
if (user.value && isActive.value) {
|
|
171
|
+
analytics.track('user_active', { userId: user.value.id })
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
// Good: watch with cleanup
|
|
176
|
+
watch(source, (newValue, oldValue, onCleanup) => {
|
|
177
|
+
const controller = new AbortController()
|
|
178
|
+
fetchData(newValue, { signal: controller.signal })
|
|
179
|
+
|
|
180
|
+
onCleanup(() => controller.abort())
|
|
181
|
+
})
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## ShallowRef for Performance
|
|
187
|
+
|
|
188
|
+
Use shallowRef when you have large objects that are replaced, not mutated.
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
import { shallowRef, triggerRef } from 'vue'
|
|
192
|
+
|
|
193
|
+
// Large dataset that gets replaced entirely
|
|
194
|
+
const largeDataset = shallowRef<DataItem[]>([])
|
|
195
|
+
|
|
196
|
+
// Replacing works fine
|
|
197
|
+
const refreshData = async () => {
|
|
198
|
+
largeDataset.value = await fetchLargeDataset()
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Mutation requires manual trigger
|
|
202
|
+
const updateItem = (index: number, updates: Partial<DataItem>) => {
|
|
203
|
+
largeDataset.value[index] = { ...largeDataset.value[index], ...updates }
|
|
204
|
+
triggerRef(largeDataset) // Manual trigger needed
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### When to Use shallowRef
|
|
209
|
+
|
|
210
|
+
| Scenario | Use |
|
|
211
|
+
|----------|-----|
|
|
212
|
+
| Large arrays (1000+ items) | `shallowRef()` |
|
|
213
|
+
| Data replaced, not mutated | `shallowRef()` |
|
|
214
|
+
| Deeply nested objects | `shallowRef()` or `shallowReactive()` |
|
|
215
|
+
| Small objects with mutations | `ref()` |
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Memory Management
|
|
220
|
+
|
|
221
|
+
### Cleanup Subscriptions
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
import { onUnmounted } from 'vue'
|
|
225
|
+
|
|
226
|
+
export const useEventListener = (
|
|
227
|
+
target: EventTarget,
|
|
228
|
+
event: string,
|
|
229
|
+
handler: EventListener
|
|
230
|
+
) => {
|
|
231
|
+
target.addEventListener(event, handler)
|
|
232
|
+
|
|
233
|
+
onUnmounted(() => {
|
|
234
|
+
target.removeEventListener(event, handler)
|
|
235
|
+
})
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Avoid Memory Leaks with watchEffect
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
import { watchEffect } from 'vue'
|
|
243
|
+
|
|
244
|
+
// watchEffect auto-stops when component unmounts
|
|
245
|
+
// But manual stop is available if needed
|
|
246
|
+
const stop = watchEffect(() => {
|
|
247
|
+
// Side effect
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
// Manual stop if needed
|
|
251
|
+
// stop()
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Cleanup in Async Operations
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
import { ref, onUnmounted } from 'vue'
|
|
258
|
+
|
|
259
|
+
export const useAsyncData = <T>(fetcher: () => Promise<T>) => {
|
|
260
|
+
const data = ref<T | null>(null)
|
|
261
|
+
const loading = ref(false)
|
|
262
|
+
let cancelled = false
|
|
263
|
+
|
|
264
|
+
const fetch = async () => {
|
|
265
|
+
cancelled = false
|
|
266
|
+
loading.value = true
|
|
267
|
+
try {
|
|
268
|
+
const result = await fetcher()
|
|
269
|
+
if (!cancelled) {
|
|
270
|
+
data.value = result
|
|
271
|
+
}
|
|
272
|
+
} finally {
|
|
273
|
+
if (!cancelled) {
|
|
274
|
+
loading.value = false
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
onUnmounted(() => {
|
|
280
|
+
cancelled = true
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
return { data, loading, fetch }
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## Common Anti-Patterns
|
|
290
|
+
|
|
291
|
+
### Reactive Over-Engineering
|
|
292
|
+
|
|
293
|
+
```typescript
|
|
294
|
+
// Bad: Unnecessary nesting
|
|
295
|
+
const user = reactive({
|
|
296
|
+
profile: reactive({
|
|
297
|
+
settings: reactive({
|
|
298
|
+
theme: 'dark'
|
|
299
|
+
})
|
|
300
|
+
})
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
// Good: Simple structure
|
|
304
|
+
const userSettings = ref({ theme: 'dark', notifications: true })
|
|
305
|
+
const isDarkTheme = computed(() => userSettings.value.theme === 'dark')
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### Ref Unwrapping Confusion
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
// Bad: Inconsistent access
|
|
312
|
+
const count = ref(0)
|
|
313
|
+
console.log(count) // Ref object, not value
|
|
314
|
+
|
|
315
|
+
// Good: Always use .value in script
|
|
316
|
+
console.log(count.value)
|
|
317
|
+
|
|
318
|
+
// Template auto-unwraps, no .value needed
|
|
319
|
+
// <p>{{ count }}</p>
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Mutating Props
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
// Bad: Mutating prop
|
|
326
|
+
const props = defineProps<{ user: UserData }>()
|
|
327
|
+
props.user.name = 'New Name' // Mutation!
|
|
328
|
+
|
|
329
|
+
// Good: Emit update
|
|
330
|
+
const emit = defineEmits<{ update: [user: UserData] }>()
|
|
331
|
+
const updateName = (name: string) => {
|
|
332
|
+
emit('update', { ...props.user, name })
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
338
|
+
## Performance Checklist
|
|
339
|
+
|
|
340
|
+
Before optimizing, always measure first. Then:
|
|
341
|
+
|
|
342
|
+
1. Use `computed()` for derived state (caching)
|
|
343
|
+
2. Avoid `watch()` for derived values
|
|
344
|
+
3. Use `shallowRef()` for large datasets
|
|
345
|
+
4. Clean up subscriptions in `onUnmounted()`
|
|
346
|
+
5. Use `v-once` for static content in templates
|
|
347
|
+
6. Use `v-memo` for expensive list renders
|
|
348
|
+
7. Avoid reactive objects when plain objects suffice
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
# Testing Vue FP Components
|
|
2
|
+
|
|
3
|
+
Testing patterns for Vue.js functional programming architecture.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
1. [Testing Pure Composables](#testing-pure-composables)
|
|
8
|
+
2. [Testing Components](#testing-components)
|
|
9
|
+
3. [Testing Wrapper Components](#testing-wrapper-components)
|
|
10
|
+
4. [Mocking Dependencies](#mocking-dependencies)
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Testing Pure Composables
|
|
15
|
+
|
|
16
|
+
Composables with pure logic are trivially testable.
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
// composables/__tests__/useUserLogic.test.ts
|
|
20
|
+
import { ref } from 'vue'
|
|
21
|
+
import { useUserLogic } from '../useUserLogic'
|
|
22
|
+
|
|
23
|
+
describe('useUserLogic', () => {
|
|
24
|
+
it('computes display data correctly', () => {
|
|
25
|
+
const userData = ref({
|
|
26
|
+
id: '1',
|
|
27
|
+
name: ' John ',
|
|
28
|
+
email: 'john@test.com'
|
|
29
|
+
})
|
|
30
|
+
const config = ref({
|
|
31
|
+
showEmail: true,
|
|
32
|
+
variant: 'compact' as const
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const { displayData } = useUserLogic(userData, config)
|
|
36
|
+
|
|
37
|
+
expect(displayData.value).toEqual({
|
|
38
|
+
id: '1',
|
|
39
|
+
name: ' John ',
|
|
40
|
+
email: 'john@test.com',
|
|
41
|
+
displayName: 'John' // Trimmed
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('hides email when config.showEmail is false', () => {
|
|
46
|
+
const userData = ref({
|
|
47
|
+
id: '1',
|
|
48
|
+
name: 'John',
|
|
49
|
+
email: 'john@test.com'
|
|
50
|
+
})
|
|
51
|
+
const config = ref({
|
|
52
|
+
showEmail: false,
|
|
53
|
+
variant: 'compact' as const
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const { displayData } = useUserLogic(userData, config)
|
|
57
|
+
|
|
58
|
+
expect(displayData.value.email).toBeUndefined()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('generates correct CSS classes for variant', () => {
|
|
62
|
+
const userData = ref({ id: '1', name: 'John', email: 'john@test.com' })
|
|
63
|
+
const config = ref({ showEmail: true, variant: 'detailed' as const })
|
|
64
|
+
|
|
65
|
+
const { cssClasses } = useUserLogic(userData, config)
|
|
66
|
+
|
|
67
|
+
expect(cssClasses.value.card).toContain('detailed')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('reacts to config changes', () => {
|
|
71
|
+
const userData = ref({ id: '1', name: 'John', email: 'john@test.com' })
|
|
72
|
+
const config = ref({ showEmail: true, variant: 'compact' as const })
|
|
73
|
+
|
|
74
|
+
const { displayData } = useUserLogic(userData, config)
|
|
75
|
+
expect(displayData.value.email).toBe('john@test.com')
|
|
76
|
+
|
|
77
|
+
config.value = { showEmail: false, variant: 'compact' }
|
|
78
|
+
expect(displayData.value.email).toBeUndefined()
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Testing Components
|
|
86
|
+
|
|
87
|
+
Use Vue Test Utils for component testing.
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
// components/__tests__/UserCard.test.ts
|
|
91
|
+
import { mount } from '@vue/test-utils'
|
|
92
|
+
import UserCard from '../UserCard.vue'
|
|
93
|
+
|
|
94
|
+
describe('UserCard', () => {
|
|
95
|
+
const defaultProps = {
|
|
96
|
+
userData: { id: '1', name: 'John', email: 'john@test.com' },
|
|
97
|
+
config: { showEmail: true, variant: 'compact' as const }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
it('renders user data correctly', () => {
|
|
101
|
+
const wrapper = mount(UserCard, { props: defaultProps })
|
|
102
|
+
|
|
103
|
+
expect(wrapper.find('.user-card__name').text()).toBe('John')
|
|
104
|
+
expect(wrapper.find('p').text()).toBe('john@test.com')
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('hides email when config.showEmail is false', () => {
|
|
108
|
+
const wrapper = mount(UserCard, {
|
|
109
|
+
props: {
|
|
110
|
+
...defaultProps,
|
|
111
|
+
config: { showEmail: false, variant: 'compact' }
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
expect(wrapper.find('p').exists()).toBe(false)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('applies correct variant class', () => {
|
|
119
|
+
const wrapper = mount(UserCard, {
|
|
120
|
+
props: {
|
|
121
|
+
...defaultProps,
|
|
122
|
+
config: { showEmail: true, variant: 'detailed' }
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
expect(wrapper.find('.user-card').classes()).toContain('user-card--detailed')
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('emits update event with new data', async () => {
|
|
130
|
+
const wrapper = mount(UserCard, { props: defaultProps })
|
|
131
|
+
|
|
132
|
+
await wrapper.find('button').trigger('click')
|
|
133
|
+
|
|
134
|
+
expect(wrapper.emitted('update')).toBeTruthy()
|
|
135
|
+
expect(wrapper.emitted('update')![0]).toEqual([
|
|
136
|
+
expect.objectContaining({ id: '1' })
|
|
137
|
+
])
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Testing Wrapper Components
|
|
145
|
+
|
|
146
|
+
Test wrappers separately from pure components.
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
// components/__tests__/UserDisplayWrapper.test.ts
|
|
150
|
+
import { mount, flushPromises } from '@vue/test-utils'
|
|
151
|
+
import UserDisplayWrapper from '../UserDisplayWrapper.vue'
|
|
152
|
+
|
|
153
|
+
// Mock the API module
|
|
154
|
+
vi.mock('@/api/userApi', () => ({
|
|
155
|
+
userApi: {
|
|
156
|
+
getUser: vi.fn(),
|
|
157
|
+
updateUser: vi.fn()
|
|
158
|
+
}
|
|
159
|
+
}))
|
|
160
|
+
|
|
161
|
+
import { userApi } from '@/api/userApi'
|
|
162
|
+
|
|
163
|
+
describe('UserDisplayWrapper', () => {
|
|
164
|
+
beforeEach(() => {
|
|
165
|
+
vi.clearAllMocks()
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('shows loading state initially', () => {
|
|
169
|
+
(userApi.getUser as Mock).mockImplementation(() => new Promise(() => {}))
|
|
170
|
+
|
|
171
|
+
const wrapper = mount(UserDisplayWrapper, {
|
|
172
|
+
props: { userId: '1' }
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
expect(wrapper.text()).toContain('Loading')
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('renders user data after fetch', async () => {
|
|
179
|
+
(userApi.getUser as Mock).mockResolvedValue({
|
|
180
|
+
id: '1',
|
|
181
|
+
name: 'John',
|
|
182
|
+
email: 'john@test.com'
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
const wrapper = mount(UserDisplayWrapper, {
|
|
186
|
+
props: { userId: '1' }
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
await flushPromises()
|
|
190
|
+
|
|
191
|
+
expect(wrapper.text()).toContain('John')
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('shows error state on fetch failure', async () => {
|
|
195
|
+
(userApi.getUser as Mock).mockRejectedValue(new Error('Network error'))
|
|
196
|
+
|
|
197
|
+
const wrapper = mount(UserDisplayWrapper, {
|
|
198
|
+
props: { userId: '1' }
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
await flushPromises()
|
|
202
|
+
|
|
203
|
+
expect(wrapper.text()).toContain('Error')
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('calls updateUser on update event', async () => {
|
|
207
|
+
const mockUser = { id: '1', name: 'John', email: 'john@test.com' }
|
|
208
|
+
;(userApi.getUser as Mock).mockResolvedValue(mockUser)
|
|
209
|
+
;(userApi.updateUser as Mock).mockResolvedValue(undefined)
|
|
210
|
+
|
|
211
|
+
const wrapper = mount(UserDisplayWrapper, {
|
|
212
|
+
props: { userId: '1' }
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
await flushPromises()
|
|
216
|
+
|
|
217
|
+
// Trigger update from child component
|
|
218
|
+
await wrapper.findComponent({ name: 'UserDisplayPure' }).vm.$emit('update', {
|
|
219
|
+
...mockUser,
|
|
220
|
+
name: 'Updated'
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
expect(userApi.updateUser).toHaveBeenCalledWith(
|
|
224
|
+
expect.objectContaining({ name: 'Updated' })
|
|
225
|
+
)
|
|
226
|
+
})
|
|
227
|
+
})
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## Mocking Dependencies
|
|
233
|
+
|
|
234
|
+
### Using Provide/Inject for DI
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
// Component that uses injected API
|
|
238
|
+
// UserFeature.vue uses `const api = inject('api')`
|
|
239
|
+
|
|
240
|
+
describe('UserFeature with injected API', () => {
|
|
241
|
+
const mockApi = {
|
|
242
|
+
getUser: vi.fn().mockResolvedValue({ id: '1', name: 'Test' }),
|
|
243
|
+
updateUser: vi.fn().mockResolvedValue(undefined)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
it('uses injected API', async () => {
|
|
247
|
+
const wrapper = mount(UserFeature, {
|
|
248
|
+
global: {
|
|
249
|
+
provide: {
|
|
250
|
+
api: mockApi
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
props: { userId: '1' }
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
await flushPromises()
|
|
257
|
+
|
|
258
|
+
expect(mockApi.getUser).toHaveBeenCalledWith('1')
|
|
259
|
+
})
|
|
260
|
+
})
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Testing Composables with Mocked Dependencies
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
// composables/__tests__/useUserData.test.ts
|
|
267
|
+
import { ref } from 'vue'
|
|
268
|
+
import { useUserData } from '../useUserData'
|
|
269
|
+
|
|
270
|
+
describe('useUserData', () => {
|
|
271
|
+
it('fetches user with provided API', async () => {
|
|
272
|
+
const mockApi = {
|
|
273
|
+
getUser: vi.fn().mockResolvedValue({ id: '1', name: 'Test' })
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Composable accepts API as dependency
|
|
277
|
+
const { user, loading, fetchUser } = useUserData(mockApi)
|
|
278
|
+
|
|
279
|
+
expect(loading.value).toBe(false)
|
|
280
|
+
expect(user.value).toBeNull()
|
|
281
|
+
|
|
282
|
+
await fetchUser('1')
|
|
283
|
+
|
|
284
|
+
expect(mockApi.getUser).toHaveBeenCalledWith('1')
|
|
285
|
+
expect(user.value).toEqual({ id: '1', name: 'Test' })
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
it('handles fetch errors', async () => {
|
|
289
|
+
const mockApi = {
|
|
290
|
+
getUser: vi.fn().mockRejectedValue(new Error('Not found'))
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const { error, fetchUser } = useUserData(mockApi)
|
|
294
|
+
|
|
295
|
+
await fetchUser('1')
|
|
296
|
+
|
|
297
|
+
expect(error.value).toBeInstanceOf(Error)
|
|
298
|
+
expect(error.value?.message).toBe('Not found')
|
|
299
|
+
})
|
|
300
|
+
})
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
---
|
|
304
|
+
|
|
305
|
+
## Testing Best Practices
|
|
306
|
+
|
|
307
|
+
| Practice | Reason |
|
|
308
|
+
|----------|--------|
|
|
309
|
+
| Test composables in isolation | Faster, focused tests |
|
|
310
|
+
| Test components with minimal mocking | Verify integration |
|
|
311
|
+
| Test wrappers with API mocks | Isolate side effects |
|
|
312
|
+
| Use provide/inject for DI | Easy mock injection |
|
|
313
|
+
| Test reactive updates | Verify computed reactivity |
|
|
314
|
+
| Avoid snapshot tests for logic | Fragile, hard to review |
|