codeforge-dev 1.4.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/.devcontainer/.env +22 -0
- package/.devcontainer/CHANGELOG.md +197 -0
- package/.devcontainer/CLAUDE.md +117 -0
- package/.devcontainer/README.md +222 -0
- package/.devcontainer/config/main-system-prompt.md +502 -0
- package/.devcontainer/config/settings.json +47 -0
- package/.devcontainer/devcontainer.json +94 -0
- package/.devcontainer/features/README.md +113 -0
- package/.devcontainer/features/agent-browser/README.md +65 -0
- package/.devcontainer/features/agent-browser/devcontainer-feature.json +23 -0
- package/.devcontainer/features/agent-browser/install.sh +79 -0
- package/.devcontainer/features/ast-grep/README.md +24 -0
- package/.devcontainer/features/ast-grep/devcontainer-feature.json +24 -0
- package/.devcontainer/features/ast-grep/install.sh +51 -0
- package/.devcontainer/features/ccstatusline/README.md +296 -0
- package/.devcontainer/features/ccstatusline/devcontainer-feature.json +19 -0
- package/.devcontainer/features/ccstatusline/install.sh +290 -0
- package/.devcontainer/features/ccusage/README.md +205 -0
- package/.devcontainer/features/ccusage/devcontainer-feature.json +38 -0
- package/.devcontainer/features/ccusage/install.sh +132 -0
- package/.devcontainer/features/claude-code/README.md +498 -0
- package/.devcontainer/features/claude-code/config/settings.json +36 -0
- package/.devcontainer/features/claude-code/config/system-prompt.md +118 -0
- package/.devcontainer/features/claude-code/config/world-building-sp.md +1432 -0
- package/.devcontainer/features/claude-code/devcontainer-feature.json +42 -0
- package/.devcontainer/features/claude-code/install.sh +466 -0
- package/.devcontainer/features/claude-monitor/README.md +74 -0
- package/.devcontainer/features/claude-monitor/devcontainer-feature.json +38 -0
- package/.devcontainer/features/claude-monitor/install.sh +99 -0
- package/.devcontainer/features/lsp-servers/README.md +85 -0
- package/.devcontainer/features/lsp-servers/devcontainer-feature.json +40 -0
- package/.devcontainer/features/lsp-servers/install.sh +116 -0
- package/.devcontainer/features/mcp-qdrant/CHANGES.md +399 -0
- package/.devcontainer/features/mcp-qdrant/README.md +474 -0
- package/.devcontainer/features/mcp-qdrant/devcontainer-feature.json +57 -0
- package/.devcontainer/features/mcp-qdrant/install.sh +295 -0
- package/.devcontainer/features/mcp-qdrant/poststart-hook.sh +129 -0
- package/.devcontainer/features/mcp-reasoner/README.md +177 -0
- package/.devcontainer/features/mcp-reasoner/devcontainer-feature.json +20 -0
- package/.devcontainer/features/mcp-reasoner/install.sh +177 -0
- package/.devcontainer/features/mcp-reasoner/poststart-hook.sh +67 -0
- package/.devcontainer/features/notify-hook/README.md +86 -0
- package/.devcontainer/features/notify-hook/devcontainer-feature.json +23 -0
- package/.devcontainer/features/notify-hook/install.sh +38 -0
- package/.devcontainer/features/splitrail/README.md +140 -0
- package/.devcontainer/features/splitrail/devcontainer-feature.json +34 -0
- package/.devcontainer/features/splitrail/install.sh +129 -0
- package/.devcontainer/features/tree-sitter/README.md +138 -0
- package/.devcontainer/features/tree-sitter/devcontainer-feature.json +52 -0
- package/.devcontainer/features/tree-sitter/install.sh +173 -0
- package/.devcontainer/plugins/devs-marketplace/.claude-plugin/marketplace.json +106 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/hooks/hooks.json +17 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/scripts/format-file.py +101 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/hooks/hooks.json +17 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/scripts/lint-file.py +137 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/.claude-plugin/plugin.json +8 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/claude-code-headless/SKILL.md +387 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/claude-code-headless/references/cli-flags-and-output.md +312 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/claude-code-headless/references/sdk-and-mcp.md +569 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker/SKILL.md +309 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker/references/compose-services.md +438 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker/references/dockerfile-patterns.md +340 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker-py/SKILL.md +412 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker-py/references/container-lifecycle.md +388 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker-py/references/resources-and-security.md +444 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/fastapi/SKILL.md +344 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/fastapi/references/middleware-and-lifespan.md +254 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/fastapi/references/pydantic-models.md +245 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/fastapi/references/routing-and-dependencies.md +255 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/fastapi/references/sse-and-streaming.md +318 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/pydantic-ai/SKILL.md +345 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/pydantic-ai/references/agents-and-tools.md +271 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/pydantic-ai/references/models-and-streaming.md +422 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/skill-building/SKILL.md +220 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/skill-building/references/cross-vendor-principles.md +139 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/skill-building/references/patterns-and-antipatterns.md +376 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/skill-building/references/skill-authoring-patterns.md +356 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/sqlite/SKILL.md +329 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/sqlite/references/advanced-queries.md +314 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/sqlite/references/javascript-patterns.md +323 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/sqlite/references/python-patterns.md +354 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/sqlite/references/schema-and-pragmas.md +326 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/SKILL.md +356 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/ai-sdk-svelte.md +128 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/component-patterns.md +332 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/layercake.md +203 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/migration-guide.md +350 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/runes-and-reactivity.md +328 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/spa-and-routing.md +262 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/svelte-dnd-action.md +181 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/testing/SKILL.md +414 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/testing/references/fastapi-testing.md +411 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/testing/references/svelte-testing.md +538 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codeforge-lsp/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/dangerous-command-blocker/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/dangerous-command-blocker/hooks/hooks.json +17 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/dangerous-command-blocker/scripts/block-dangerous.py +110 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/notify-hook/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/notify-hook/hooks/hooks.json +17 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/planning-reminder/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/planning-reminder/hooks/hooks.json +17 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/hooks/hooks.json +17 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/scripts/guard-protected.py +108 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/commands/ticket/357/200/272create-pr.md +337 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/commands/ticket/357/200/272new.md +166 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/commands/ticket/357/200/272review-commit.md +290 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/commands/ticket/357/200/272work.md +257 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/plugin.json +8 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/system-prompt.md +184 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/.claude-plugin/plugin.json +6 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/config/planning-instructions.md +14 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/functional-conjuring-map.md +989 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/hooks/hooks.json +33 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/__pycache__/post-enhance-task.cpython-314.pyc +0 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/enhance-planning.py +71 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/enhancers/enhance-plan.sh +68 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/enhancers/enhance-task.sh +120 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/post-enhance-plan.py +133 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/post-enhance-task.py +253 -0
- package/.devcontainer/scripts/setup-aliases.sh +80 -0
- package/.devcontainer/scripts/setup-config.sh +28 -0
- package/.devcontainer/scripts/setup-irie-claude.sh +32 -0
- package/.devcontainer/scripts/setup-plugins.sh +80 -0
- package/.devcontainer/scripts/setup.sh +58 -0
- package/LICENSE.txt +674 -0
- package/README.md +267 -0
- package/package.json +44 -0
- package/setup.js +83 -0
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
# Svelte Testing -- Deep Dive
|
|
2
|
+
|
|
3
|
+
## 1. Vitest Configuration for SvelteKit
|
|
4
|
+
|
|
5
|
+
### Full Setup
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -D vitest jsdom @testing-library/svelte @testing-library/jest-dom @testing-library/user-event
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```javascript
|
|
12
|
+
// vite.config.js
|
|
13
|
+
import { defineConfig } from 'vitest/config'
|
|
14
|
+
import { sveltekit } from '@sveltejs/kit/vite'
|
|
15
|
+
import { svelteTesting } from '@testing-library/svelte/vite'
|
|
16
|
+
|
|
17
|
+
export default defineConfig({
|
|
18
|
+
plugins: [sveltekit(), svelteTesting()],
|
|
19
|
+
test: {
|
|
20
|
+
environment: 'jsdom',
|
|
21
|
+
setupFiles: ['./vitest-setup.js'],
|
|
22
|
+
include: ['tests/**/*.test.ts', 'tests/**/*.svelte.test.ts'],
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```javascript
|
|
28
|
+
// vitest-setup.js
|
|
29
|
+
import '@testing-library/jest-dom/vitest'
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### TypeScript Support
|
|
33
|
+
|
|
34
|
+
Add to `tsconfig.json`:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"compilerOptions": {
|
|
39
|
+
"types": ["@testing-library/jest-dom"]
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Runes in Test Files
|
|
45
|
+
|
|
46
|
+
Svelte 5 runes (`$state`, `$derived`, `$effect`) are only available inside `.svelte` files. To use them directly in tests, name the file with `.svelte.test.ts`:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
tests/
|
|
50
|
+
counter.svelte.test.ts # Can use $state, $derived
|
|
51
|
+
api.test.ts # Standard test file (no runes)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 2. @testing-library/svelte Render API
|
|
57
|
+
|
|
58
|
+
### render() Signature
|
|
59
|
+
|
|
60
|
+
```javascript
|
|
61
|
+
import { render, screen } from '@testing-library/svelte'
|
|
62
|
+
|
|
63
|
+
// Short form: props directly
|
|
64
|
+
const result = render(MyComponent, { name: 'World', count: 0 })
|
|
65
|
+
|
|
66
|
+
// Long form: with additional options
|
|
67
|
+
const result = render(MyComponent, {
|
|
68
|
+
props: { name: 'World', count: 0 },
|
|
69
|
+
context: new Map([['theme', 'dark']]),
|
|
70
|
+
target: document.getElementById('custom-root'),
|
|
71
|
+
})
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Render Result
|
|
75
|
+
|
|
76
|
+
```javascript
|
|
77
|
+
const {
|
|
78
|
+
container, // wrapping DOM element
|
|
79
|
+
baseElement, // base element for queries (document.body)
|
|
80
|
+
component, // Svelte component instance
|
|
81
|
+
debug, // pretty-print DOM: debug() or debug(element)
|
|
82
|
+
rerender, // update props: await rerender({ newProp: 'value' })
|
|
83
|
+
unmount, // destroy component
|
|
84
|
+
|
|
85
|
+
// All query functions bound to baseElement:
|
|
86
|
+
getByRole, getByText, getByLabelText,
|
|
87
|
+
queryByRole, queryByText,
|
|
88
|
+
findByRole, findByText,
|
|
89
|
+
// ... and all other query variants
|
|
90
|
+
} = render(MyComponent, { name: 'World' })
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Updating Props
|
|
94
|
+
|
|
95
|
+
```javascript
|
|
96
|
+
const { rerender } = render(Counter, { count: 0 })
|
|
97
|
+
expect(screen.getByText('0')).toBeInTheDocument()
|
|
98
|
+
|
|
99
|
+
await rerender({ count: 5 })
|
|
100
|
+
expect(screen.getByText('5')).toBeInTheDocument()
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Debug Output
|
|
104
|
+
|
|
105
|
+
```javascript
|
|
106
|
+
screen.debug() // logs entire document.body
|
|
107
|
+
screen.debug(screen.getByRole('button')) // logs specific element
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## 3. User Event Simulation
|
|
113
|
+
|
|
114
|
+
### Setup
|
|
115
|
+
|
|
116
|
+
Always call `.setup()` before using userEvent:
|
|
117
|
+
|
|
118
|
+
```javascript
|
|
119
|
+
import userEvent from '@testing-library/user-event'
|
|
120
|
+
|
|
121
|
+
const user = userEvent.setup()
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Click Events
|
|
125
|
+
|
|
126
|
+
```javascript
|
|
127
|
+
test('handles click', async () => {
|
|
128
|
+
const user = userEvent.setup()
|
|
129
|
+
render(Button, { label: 'Submit' })
|
|
130
|
+
|
|
131
|
+
await user.click(screen.getByRole('button', { name: 'Submit' }))
|
|
132
|
+
expect(screen.getByText('Submitted')).toBeInTheDocument()
|
|
133
|
+
})
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Typing
|
|
137
|
+
|
|
138
|
+
```javascript
|
|
139
|
+
test('handles text input', async () => {
|
|
140
|
+
const user = userEvent.setup()
|
|
141
|
+
render(SearchForm)
|
|
142
|
+
|
|
143
|
+
const input = screen.getByRole('textbox')
|
|
144
|
+
await user.type(input, 'hello world')
|
|
145
|
+
expect(input).toHaveValue('hello world')
|
|
146
|
+
|
|
147
|
+
await user.clear(input)
|
|
148
|
+
expect(input).toHaveValue('')
|
|
149
|
+
})
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Special Keys
|
|
153
|
+
|
|
154
|
+
```javascript
|
|
155
|
+
await user.type(input, '{Enter}')
|
|
156
|
+
await user.type(input, '{Backspace}')
|
|
157
|
+
await user.keyboard('{Shift>}A{/Shift}') // hold Shift, press A
|
|
158
|
+
await user.tab()
|
|
159
|
+
await user.tab({ shift: true })
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Select and Hover
|
|
163
|
+
|
|
164
|
+
```javascript
|
|
165
|
+
await user.selectOptions(screen.getByRole('combobox'), ['option1'])
|
|
166
|
+
await user.hover(screen.getByRole('button'))
|
|
167
|
+
await user.unhover(screen.getByRole('button'))
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### userEvent vs fireEvent
|
|
171
|
+
|
|
172
|
+
`userEvent` simulates full browser interaction sequences (focus, keydown, keypress, input, keyup). `fireEvent` dispatches a single event. Prefer `userEvent` -- it catches bugs that `fireEvent` misses (e.g., event handlers that depend on focus state).
|
|
173
|
+
|
|
174
|
+
```javascript
|
|
175
|
+
import { fireEvent } from '@testing-library/svelte'
|
|
176
|
+
|
|
177
|
+
// fireEvent is async in svelte-testing-library
|
|
178
|
+
await fireEvent.click(button)
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## 4. Async State Updates
|
|
184
|
+
|
|
185
|
+
### findBy Queries (Wait for Elements)
|
|
186
|
+
|
|
187
|
+
```javascript
|
|
188
|
+
test('loads data asynchronously', async () => {
|
|
189
|
+
render(AsyncList)
|
|
190
|
+
|
|
191
|
+
// findByText polls until the element appears (default timeout: 1000ms)
|
|
192
|
+
const item = await screen.findByText('Loaded Item', {}, { timeout: 3000 })
|
|
193
|
+
expect(item).toBeInTheDocument()
|
|
194
|
+
})
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### waitFor (Wait for Assertions)
|
|
198
|
+
|
|
199
|
+
```javascript
|
|
200
|
+
import { waitFor } from '@testing-library/svelte'
|
|
201
|
+
|
|
202
|
+
test('counter updates after delay', async () => {
|
|
203
|
+
render(DelayedCounter)
|
|
204
|
+
|
|
205
|
+
await waitFor(() => {
|
|
206
|
+
expect(screen.getByText('Updated: 42')).toBeInTheDocument()
|
|
207
|
+
}, { timeout: 2000 })
|
|
208
|
+
})
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### act() -- Flush Pending Updates
|
|
212
|
+
|
|
213
|
+
```javascript
|
|
214
|
+
import { act } from '@testing-library/svelte'
|
|
215
|
+
|
|
216
|
+
test('programmatic state change', async () => {
|
|
217
|
+
const { component } = render(Counter)
|
|
218
|
+
|
|
219
|
+
await act(() => {
|
|
220
|
+
component.increment()
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
expect(screen.getByText('1')).toBeInTheDocument()
|
|
224
|
+
})
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### flushSync -- Synchronous Flush (Svelte Native)
|
|
228
|
+
|
|
229
|
+
```javascript
|
|
230
|
+
import { flushSync, mount, unmount } from 'svelte'
|
|
231
|
+
|
|
232
|
+
test('counter with flushSync', () => {
|
|
233
|
+
const component = mount(Counter, {
|
|
234
|
+
target: document.body,
|
|
235
|
+
props: { initial: 0 },
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
document.body.querySelector('button').click()
|
|
239
|
+
flushSync()
|
|
240
|
+
|
|
241
|
+
expect(document.body.innerHTML).toContain('1')
|
|
242
|
+
unmount(component)
|
|
243
|
+
})
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Testing $effect with $effect.root
|
|
247
|
+
|
|
248
|
+
In `.svelte.test.ts` files:
|
|
249
|
+
|
|
250
|
+
```javascript
|
|
251
|
+
import { flushSync } from 'svelte'
|
|
252
|
+
|
|
253
|
+
test('derived state tracks changes', () => {
|
|
254
|
+
const cleanup = $effect.root(() => {
|
|
255
|
+
let count = $state(0)
|
|
256
|
+
let doubled = $derived(count * 2)
|
|
257
|
+
|
|
258
|
+
flushSync()
|
|
259
|
+
expect(doubled).toBe(0)
|
|
260
|
+
|
|
261
|
+
count = 5
|
|
262
|
+
flushSync()
|
|
263
|
+
expect(doubled).toBe(10)
|
|
264
|
+
})
|
|
265
|
+
cleanup()
|
|
266
|
+
})
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## 5. Mocking Fetch and SSE
|
|
272
|
+
|
|
273
|
+
### Simple Fetch Mock with vi.fn()
|
|
274
|
+
|
|
275
|
+
```javascript
|
|
276
|
+
import { vi, beforeEach, afterEach } from 'vitest'
|
|
277
|
+
|
|
278
|
+
beforeEach(() => {
|
|
279
|
+
globalThis.fetch = vi.fn()
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
afterEach(() => {
|
|
283
|
+
vi.restoreAllMocks()
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
test('fetches and displays data', async () => {
|
|
287
|
+
globalThis.fetch.mockResolvedValueOnce(
|
|
288
|
+
new Response(JSON.stringify({ items: ['Apple', 'Banana'] }), {
|
|
289
|
+
headers: { 'Content-Type': 'application/json' },
|
|
290
|
+
})
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
render(ItemList)
|
|
294
|
+
|
|
295
|
+
expect(await screen.findByText('Apple')).toBeInTheDocument()
|
|
296
|
+
expect(screen.getByText('Banana')).toBeInTheDocument()
|
|
297
|
+
expect(fetch).toHaveBeenCalledWith('/api/items', expect.any(Object))
|
|
298
|
+
})
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### MSW (Mock Service Worker) -- Recommended
|
|
302
|
+
|
|
303
|
+
MSW intercepts requests at the network level, keeping test code decoupled from implementation:
|
|
304
|
+
|
|
305
|
+
```bash
|
|
306
|
+
npm install -D msw
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
```javascript
|
|
310
|
+
// tests/mocks/handlers.js
|
|
311
|
+
import { http, HttpResponse } from 'msw'
|
|
312
|
+
|
|
313
|
+
export const handlers = [
|
|
314
|
+
http.get('/api/items', () => {
|
|
315
|
+
return HttpResponse.json({ items: ['Apple', 'Banana'] })
|
|
316
|
+
}),
|
|
317
|
+
|
|
318
|
+
http.post('/api/items', async ({ request }) => {
|
|
319
|
+
const body = await request.json()
|
|
320
|
+
return HttpResponse.json({ id: 1, ...body }, { status: 201 })
|
|
321
|
+
}),
|
|
322
|
+
]
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
```javascript
|
|
326
|
+
// tests/mocks/server.js
|
|
327
|
+
import { setupServer } from 'msw/node'
|
|
328
|
+
import { handlers } from './handlers'
|
|
329
|
+
|
|
330
|
+
export const server = setupServer(...handlers)
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
```javascript
|
|
334
|
+
// vitest-setup.js
|
|
335
|
+
import '@testing-library/jest-dom/vitest'
|
|
336
|
+
import { server } from './tests/mocks/server'
|
|
337
|
+
import { beforeAll, afterAll, afterEach } from 'vitest'
|
|
338
|
+
|
|
339
|
+
beforeAll(() => server.listen())
|
|
340
|
+
afterEach(() => server.resetHandlers())
|
|
341
|
+
afterAll(() => server.close())
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
```javascript
|
|
345
|
+
test('displays fetched items', async () => {
|
|
346
|
+
render(ItemList)
|
|
347
|
+
expect(await screen.findByText('Apple')).toBeInTheDocument()
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
test('handles server error', async () => {
|
|
351
|
+
server.use(
|
|
352
|
+
http.get('/api/items', () => {
|
|
353
|
+
return new HttpResponse(null, { status: 500 })
|
|
354
|
+
})
|
|
355
|
+
)
|
|
356
|
+
render(ItemList)
|
|
357
|
+
expect(await screen.findByText('Error loading items')).toBeInTheDocument()
|
|
358
|
+
})
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
### MSW for SSE Streams
|
|
362
|
+
|
|
363
|
+
```javascript
|
|
364
|
+
import { http, HttpResponse } from 'msw'
|
|
365
|
+
|
|
366
|
+
const handlers = [
|
|
367
|
+
http.get('/api/stream', () => {
|
|
368
|
+
const encoder = new TextEncoder()
|
|
369
|
+
const stream = new ReadableStream({
|
|
370
|
+
start(controller) {
|
|
371
|
+
controller.enqueue(encoder.encode('data: {"token": "Hello"}\n\n'))
|
|
372
|
+
controller.enqueue(encoder.encode('data: {"token": " World"}\n\n'))
|
|
373
|
+
controller.enqueue(encoder.encode('event: done\ndata: \n\n'))
|
|
374
|
+
controller.close()
|
|
375
|
+
},
|
|
376
|
+
})
|
|
377
|
+
return new HttpResponse(stream, {
|
|
378
|
+
headers: { 'Content-Type': 'text/event-stream' },
|
|
379
|
+
})
|
|
380
|
+
}),
|
|
381
|
+
]
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### Mocking EventSource Directly
|
|
385
|
+
|
|
386
|
+
```javascript
|
|
387
|
+
class MockEventSource {
|
|
388
|
+
constructor(url) {
|
|
389
|
+
this.url = url
|
|
390
|
+
this.readyState = 0
|
|
391
|
+
MockEventSource.instances.push(this)
|
|
392
|
+
setTimeout(() => {
|
|
393
|
+
this.readyState = 1
|
|
394
|
+
this.onopen?.()
|
|
395
|
+
}, 0)
|
|
396
|
+
}
|
|
397
|
+
close() { this.readyState = 2 }
|
|
398
|
+
emitMessage(data) {
|
|
399
|
+
this.onmessage?.({ data: JSON.stringify(data) })
|
|
400
|
+
}
|
|
401
|
+
static instances = []
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
vi.stubGlobal('EventSource', MockEventSource)
|
|
405
|
+
|
|
406
|
+
test('receives SSE messages', async () => {
|
|
407
|
+
render(StreamComponent)
|
|
408
|
+
|
|
409
|
+
// Wait for connection
|
|
410
|
+
await vi.waitFor(() => expect(MockEventSource.instances).toHaveLength(1))
|
|
411
|
+
|
|
412
|
+
const es = MockEventSource.instances[0]
|
|
413
|
+
es.emitMessage({ token: 'Hello' })
|
|
414
|
+
|
|
415
|
+
expect(await screen.findByText('Hello')).toBeInTheDocument()
|
|
416
|
+
})
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
---
|
|
420
|
+
|
|
421
|
+
## 6. Mocking SvelteKit Modules
|
|
422
|
+
|
|
423
|
+
### $app/navigation
|
|
424
|
+
|
|
425
|
+
```javascript
|
|
426
|
+
import { vi } from 'vitest'
|
|
427
|
+
|
|
428
|
+
vi.mock('$app/navigation', () => ({
|
|
429
|
+
goto: vi.fn(),
|
|
430
|
+
invalidate: vi.fn(),
|
|
431
|
+
invalidateAll: vi.fn(),
|
|
432
|
+
beforeNavigate: vi.fn(),
|
|
433
|
+
afterNavigate: vi.fn(),
|
|
434
|
+
onNavigate: vi.fn(),
|
|
435
|
+
}))
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### $app/environment
|
|
439
|
+
|
|
440
|
+
```javascript
|
|
441
|
+
vi.mock('$app/environment', () => ({
|
|
442
|
+
browser: true,
|
|
443
|
+
dev: true,
|
|
444
|
+
building: false,
|
|
445
|
+
version: 'test',
|
|
446
|
+
}))
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
### $app/stores
|
|
450
|
+
|
|
451
|
+
```javascript
|
|
452
|
+
import { readable, writable } from 'svelte/store'
|
|
453
|
+
|
|
454
|
+
vi.mock('$app/stores', () => ({
|
|
455
|
+
page: readable({
|
|
456
|
+
url: new URL('http://localhost/items'),
|
|
457
|
+
params: { id: '1' },
|
|
458
|
+
route: { id: '/items/[id]' },
|
|
459
|
+
status: 200,
|
|
460
|
+
error: null,
|
|
461
|
+
data: {},
|
|
462
|
+
form: null,
|
|
463
|
+
}),
|
|
464
|
+
navigating: readable(null),
|
|
465
|
+
updated: {
|
|
466
|
+
check: vi.fn(),
|
|
467
|
+
subscribe: readable(false).subscribe,
|
|
468
|
+
},
|
|
469
|
+
}))
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
### Assert Navigation
|
|
473
|
+
|
|
474
|
+
```javascript
|
|
475
|
+
import { goto } from '$app/navigation'
|
|
476
|
+
|
|
477
|
+
test('navigates on button click', async () => {
|
|
478
|
+
const user = userEvent.setup()
|
|
479
|
+
render(NavButton, { href: '/dashboard' })
|
|
480
|
+
|
|
481
|
+
await user.click(screen.getByRole('button'))
|
|
482
|
+
expect(goto).toHaveBeenCalledWith('/dashboard')
|
|
483
|
+
})
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
---
|
|
487
|
+
|
|
488
|
+
## 7. Vitest Mocking API Reference
|
|
489
|
+
|
|
490
|
+
| Function | Purpose |
|
|
491
|
+
|----------|---------|
|
|
492
|
+
| `vi.fn()` | Create a mock function |
|
|
493
|
+
| `vi.fn(() => impl)` | Mock with implementation |
|
|
494
|
+
| `vi.mock('./module')` | Auto-mock entire module (hoisted) |
|
|
495
|
+
| `vi.mock('./module', () => ({...}))` | Mock with factory (hoisted) |
|
|
496
|
+
| `vi.spyOn(obj, 'method')` | Spy on existing method |
|
|
497
|
+
| `vi.stubGlobal('name', value)` | Stub a global variable |
|
|
498
|
+
| `vi.stubEnv('VAR', 'value')` | Stub an environment variable |
|
|
499
|
+
| `vi.useFakeTimers()` | Mock Date/setTimeout/setInterval |
|
|
500
|
+
| `vi.setSystemTime(date)` | Set mocked system time |
|
|
501
|
+
| `vi.advanceTimersByTime(ms)` | Advance fake timers |
|
|
502
|
+
| `vi.restoreAllMocks()` | Restore all mocks/spies |
|
|
503
|
+
| `vi.resetAllMocks()` | Reset mock call/result state |
|
|
504
|
+
| `vi.clearAllMocks()` | Clear mock call history |
|
|
505
|
+
| `vi.waitFor(callback)` | Wait for callback to not throw |
|
|
506
|
+
|
|
507
|
+
---
|
|
508
|
+
|
|
509
|
+
## 8. Snapshot Testing
|
|
510
|
+
|
|
511
|
+
### Component Snapshots
|
|
512
|
+
|
|
513
|
+
```javascript
|
|
514
|
+
test('matches snapshot', () => {
|
|
515
|
+
const { container } = render(Badge, { label: 'New', variant: 'primary' })
|
|
516
|
+
expect(container.innerHTML).toMatchSnapshot()
|
|
517
|
+
})
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
### Inline Snapshots
|
|
521
|
+
|
|
522
|
+
```javascript
|
|
523
|
+
test('renders badge HTML', () => {
|
|
524
|
+
const { container } = render(Badge, { label: 'New' })
|
|
525
|
+
expect(container.innerHTML).toMatchInlineSnapshot(`
|
|
526
|
+
"<span class="badge badge-default">New</span>"
|
|
527
|
+
`)
|
|
528
|
+
})
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
### When to Snapshot
|
|
532
|
+
|
|
533
|
+
Snapshots are useful for detecting unintended DOM changes in presentational components. Avoid snapshots for:
|
|
534
|
+
- Components with dynamic data (timestamps, random IDs).
|
|
535
|
+
- Complex interactive components (assertions on specific behavior are more maintainable).
|
|
536
|
+
- Components that change frequently (snapshots become noise).
|
|
537
|
+
|
|
538
|
+
Prefer explicit assertions (`expect(element).toHaveTextContent(...)`) over snapshots for behavioral tests.
|
package/.devcontainer/plugins/devs-marketplace/plugins/dangerous-command-blocker/hooks/hooks.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "Block dangerous bash commands before execution",
|
|
3
|
+
"hooks": {
|
|
4
|
+
"PreToolUse": [
|
|
5
|
+
{
|
|
6
|
+
"matcher": "Bash",
|
|
7
|
+
"hooks": [
|
|
8
|
+
{
|
|
9
|
+
"type": "command",
|
|
10
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/block-dangerous.py",
|
|
11
|
+
"timeout": 5
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Block dangerous bash commands before execution.
|
|
4
|
+
|
|
5
|
+
Reads tool input from stdin, checks against dangerous patterns.
|
|
6
|
+
Exit code 2 blocks the command with error message.
|
|
7
|
+
Exit code 0 allows the command to proceed.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import re
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
DANGEROUS_PATTERNS = [
|
|
15
|
+
# Destructive filesystem deletion
|
|
16
|
+
(r'\brm\s+.*-[^\s]*r[^\s]*f[^\s]*\s+[/~](?:\s|$)',
|
|
17
|
+
"Blocked: rm -rf on root or home directory"),
|
|
18
|
+
(r'\brm\s+.*-[^\s]*f[^\s]*r[^\s]*\s+[/~](?:\s|$)',
|
|
19
|
+
"Blocked: rm -rf on root or home directory"),
|
|
20
|
+
(r'\brm\s+-rf\s+/(?:\s|$)',
|
|
21
|
+
"Blocked: rm -rf /"),
|
|
22
|
+
(r'\brm\s+-rf\s+~(?:\s|$)',
|
|
23
|
+
"Blocked: rm -rf ~"),
|
|
24
|
+
|
|
25
|
+
# Root-level file removal
|
|
26
|
+
(r'\bsudo\s+rm\b',
|
|
27
|
+
"Blocked: sudo rm - use caution with privileged deletion"),
|
|
28
|
+
|
|
29
|
+
# World-writable permissions
|
|
30
|
+
(r'\bchmod\s+777\b',
|
|
31
|
+
"Blocked: chmod 777 creates security vulnerability"),
|
|
32
|
+
(r'\bchmod\s+-R\s+777\b',
|
|
33
|
+
"Blocked: recursive chmod 777 creates security vulnerability"),
|
|
34
|
+
|
|
35
|
+
# Force push to main/master
|
|
36
|
+
(r'\bgit\s+push\s+.*--force.*\s+(origin\s+)?(main|master)\b',
|
|
37
|
+
"Blocked: force push to main/master destroys history"),
|
|
38
|
+
(r'\bgit\s+push\s+.*-f\s+.*\s+(origin\s+)?(main|master)\b',
|
|
39
|
+
"Blocked: force push to main/master destroys history"),
|
|
40
|
+
(r'\bgit\s+push\s+-f\s+(origin\s+)?(main|master)\b',
|
|
41
|
+
"Blocked: force push to main/master destroys history"),
|
|
42
|
+
(r'\bgit\s+push\s+--force\s+(origin\s+)?(main|master)\b',
|
|
43
|
+
"Blocked: force push to main/master destroys history"),
|
|
44
|
+
|
|
45
|
+
# System directory modification
|
|
46
|
+
(r'>\s*/usr/',
|
|
47
|
+
"Blocked: writing to /usr system directory"),
|
|
48
|
+
(r'>\s*/etc/',
|
|
49
|
+
"Blocked: writing to /etc system directory"),
|
|
50
|
+
(r'>\s*/bin/',
|
|
51
|
+
"Blocked: writing to /bin system directory"),
|
|
52
|
+
(r'>\s*/sbin/',
|
|
53
|
+
"Blocked: writing to /sbin system directory"),
|
|
54
|
+
|
|
55
|
+
# Disk formatting
|
|
56
|
+
(r'\bmkfs\.\w+',
|
|
57
|
+
"Blocked: disk formatting command"),
|
|
58
|
+
(r'\bdd\s+.*of=/dev/',
|
|
59
|
+
"Blocked: dd writing to device"),
|
|
60
|
+
|
|
61
|
+
# History manipulation
|
|
62
|
+
(r'\bgit\s+reset\s+--hard\s+origin/(main|master)\b',
|
|
63
|
+
"Blocked: hard reset to remote main/master - destructive operation"),
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def check_command(command: str) -> tuple[bool, str]:
|
|
68
|
+
"""Check if command matches any dangerous pattern.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
(is_dangerous, message)
|
|
72
|
+
"""
|
|
73
|
+
for pattern, message in DANGEROUS_PATTERNS:
|
|
74
|
+
if re.search(pattern, command, re.IGNORECASE):
|
|
75
|
+
return True, message
|
|
76
|
+
return False, ""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def main():
|
|
80
|
+
try:
|
|
81
|
+
input_data = json.load(sys.stdin)
|
|
82
|
+
tool_input = input_data.get("tool_input", {})
|
|
83
|
+
command = tool_input.get("command", "")
|
|
84
|
+
|
|
85
|
+
if not command:
|
|
86
|
+
sys.exit(0)
|
|
87
|
+
|
|
88
|
+
is_dangerous, message = check_command(command)
|
|
89
|
+
|
|
90
|
+
if is_dangerous:
|
|
91
|
+
# Output error message and exit 2 to block
|
|
92
|
+
print(json.dumps({
|
|
93
|
+
"error": message
|
|
94
|
+
}))
|
|
95
|
+
sys.exit(2)
|
|
96
|
+
|
|
97
|
+
# Allow command to proceed
|
|
98
|
+
sys.exit(0)
|
|
99
|
+
|
|
100
|
+
except json.JSONDecodeError:
|
|
101
|
+
# If we can't parse input, allow by default
|
|
102
|
+
sys.exit(0)
|
|
103
|
+
except Exception as e:
|
|
104
|
+
# Log error but don't block on hook failure
|
|
105
|
+
print(f"Hook error: {e}", file=sys.stderr)
|
|
106
|
+
sys.exit(0)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
if __name__ == "__main__":
|
|
110
|
+
main()
|