arckode-ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,122 @@
1
+ # SKILL: Compiler — parser, template-compiler, vite-plugin
2
+
3
+ > Cargá este skill cuando trabajés en `src/compiler/` o cuando necesites agregar/modificar una directiva.
4
+
5
+ ## Parser (`src/compiler/parser.ts`)
6
+
7
+ Convierte un `.ark` (HTML con secciones) en `{ template, script, style }`.
8
+
9
+ ### Validaciones que tira excepciones (`ArkParseError`)
10
+
11
+ - `MISSING_TEMPLATE` — no hay `<template>`
12
+ - `MISSING_SCRIPT` — no hay `<script>`
13
+ - `WRONG_ORDER` — `<script>` aparece antes que `<template>`
14
+ - `MISSING_LANG_TS` — `<script>` sin `lang="ts"`
15
+ - `DUPLICATE_SECTION` — más de un `<template>` o más de un `<script>`
16
+
17
+ Estos son distintos de las violations del analyzer — el parser tira excepción, el build se rompe.
18
+
19
+ ## Template compiler (`src/compiler/template-compiler.ts`)
20
+
21
+ Convierte el `<template>` en una render function que retorna `VNode`.
22
+
23
+ ### Directivas soportadas (verificadas en tests)
24
+
25
+ | Directiva | Sintaxis | Output |
26
+ |-----------|----------|--------|
27
+ | Interpolación | `{{ expr }}` | `h('span', null, expr)` o template literal |
28
+ | Atributo dinámico | `:attr="expr"` | `attr: expr` |
29
+ | Evento | `@event="handler"` | `onEvent: handler` (capitaliza primera letra) |
30
+ | Tecla | `@keydown.enter="x"` | wrapper `(e) => e.key === 'Enter' && x(e)` |
31
+ | Condicional | `v-if/v-else-if/v-else` | ternario `cond ? a : cond2 ? b : c` |
32
+ | Lista | `v-for="item in coll"` | `coll.map((item) => ...)` |
33
+ | Lista con índice | `v-for="(item, idx) in coll"` | `coll.map((item, idx) => ...)` |
34
+ | Visibilidad | `v-show="expr"` | `style: { display: expr ? '' : 'none' }` |
35
+ | Slot | `<slot />` | `(props.__slot_default ? props.__slot_default() : [])` |
36
+ | Componente hijo | `<PascalCase :prop="x" />` | `h(PascalCase, { prop: x })` |
37
+
38
+ ### Modificadores de teclado (`KEY_MAP`)
39
+
40
+ ```
41
+ enter | escape/esc | tab | space | up | down | left | right | delete | backspace
42
+ ```
43
+
44
+ Otros se pasan capitalizados: `@keydown.foo` → `e.key === 'Foo'`.
45
+
46
+ ### Cómo agregar una directiva nueva
47
+
48
+ 1. Si es atributo simple: ya funciona via `:attr` o `@event`
49
+ 2. Si es estructural (como `v-if`): hay que agregar manejo especial en `genChildren()` o `genElement()`
50
+ 3. Si es modificador de tecla: agregar al `KEY_MAP`
51
+
52
+ Después:
53
+ - Agregar test en `template-compiler.test.ts`
54
+ - Si afecta el contrato de predictabilidad, agregar regla al analyzer
55
+ - Documentar en `~/.claude/skills/arckode-ui/SKILL.md` (skill principal)
56
+
57
+ ### Edge cases conocidos
58
+
59
+ - `>` dentro de valores de atributo: el `TOKEN_RE` los respeta (caso testeado)
60
+ - Self-closing tags (`<slot />`, `<input />`): manejados con detección del trailing `/`
61
+ - Whitespace entre `v-if/v-else-if/v-else`: el lookahead skipea text nodes blank
62
+
63
+ ## Vite plugin (`src/compiler/vite-plugin.ts`)
64
+
65
+ ```typescript
66
+ import { arkcodeUi } from 'arckode-ui/vite' // NOMBRE EXACTO: arkcodeUi
67
+ arkcodeUi({
68
+ analyzer: {
69
+ failOnWarnings: false,
70
+ ignore: [],
71
+ },
72
+ })
73
+ ```
74
+
75
+ ### Comportamiento dev vs prod
76
+
77
+ | Modo | Errors del analyzer | Warnings del analyzer |
78
+ |------|---------------------|----------------------|
79
+ | `dev` | log + continúa | log + continúa |
80
+ | `prod` | **bloquea el build** | log (a menos que `failOnWarnings: true`) |
81
+
82
+ ### Lo que hace el transform
83
+
84
+ 1. Lee `.ark` source
85
+ 2. Parse → `{ template, script, style }`
86
+ 3. Corre `analyze()`, formatea violations
87
+ 4. Genera `__scopeId` (hash determinístico del path)
88
+ 5. Compila el template a render function
89
+ 6. Concatena: imports + script preamble + render function + injection (`__component.__render = render`)
90
+ 7. Pasa por `transformWithEsbuild` para strip TS
91
+
92
+ ### Output del módulo compilado
93
+
94
+ ```javascript
95
+ import { defineComponent, h } from 'arckode-ui'
96
+ // ... imports del script original
97
+ // ... script preamble
98
+ function render(props, state, computed, actions) { return ... }
99
+ const __scopeId = 'abc123'
100
+ const __component = defineComponent({ ... })
101
+ __component.__scopeId = __scopeId
102
+ __component.__render = render
103
+ export default __component
104
+ ```
105
+
106
+ El renderer lee `component.__render` para saber qué renderizar.
107
+
108
+ ## Anti-patterns
109
+
110
+ ```typescript
111
+ // ❌ usar nombres distintos a 'arkcodeUi'
112
+ import { arckodeUi } from 'arckode-ui/vite' // wrong
113
+ import { arckodePlugin } from 'arckode-ui/vite' // wrong
114
+ // → import { arkcodeUi } from 'arckode-ui/vite'
115
+
116
+ // ❌ habilitar failOnWarnings en dev (rompe DX)
117
+ arkcodeUi({ analyzer: { failOnWarnings: true } }) // bloquea hasta en dev
118
+
119
+ // ❌ poner shebang en src/cli/index.ts
120
+ // esbuild lo rechaza. El shebang se inyecta vía rollupOptions.output.banner
121
+ // solo en el chunk cli.
122
+ ```
@@ -0,0 +1,233 @@
1
+ # SKILL: Components — patrones canónicos de UI
2
+
3
+ > Cargá este skill cuando escribas archivos `.ark` para usuarios finales del framework (no para desarrollar el framework en sí).
4
+
5
+ ## Estructura siempre igual de un componente
6
+
7
+ ```html
8
+ <template>
9
+ <!-- HTML con directivas -->
10
+ </template>
11
+
12
+ <script lang="ts">
13
+ import { defineComponent, signal, computed } from 'arckode-ui'
14
+
15
+ export default defineComponent({
16
+ name: 'NombrePascal', // OBLIGATORIO
17
+ props: { /* tipos nativos */ },
18
+ emits: [ /* kebab-case */ ],
19
+ setup(props, { emit }) {
20
+ // 1. signals
21
+ // 2. computed
22
+ // 3. lifecycle (onMount)
23
+ // 4. watch
24
+ // 5. actions (function declarations)
25
+ // 6. return { state, computed, actions }
26
+ },
27
+ })
28
+ </script>
29
+ ```
30
+
31
+ ## Patrón 1 — `:class` con ternario reactivo
32
+
33
+ ❌ NO ponés el ternario en template (lo flagea el analyzer):
34
+ ```html
35
+ <button :class="state.active.value ? 'on' : 'off'"> <!-- LOGIC_IN_TEMPLATE -->
36
+ ```
37
+
38
+ ✅ Computed simple cuando depende solo de un signal:
39
+ ```typescript
40
+ const buttonClass = computed(() => state.active.value ? 'on' : 'off')
41
+ ```
42
+ ```html
43
+ <button :class="computed.buttonClass.value">
44
+ ```
45
+
46
+ ✅ Computed que retorna closure cuando depende de un item del loop:
47
+ ```typescript
48
+ // SNAPSHOT PATTERN — leer la dep en el getter, no en la closure
49
+ const tabClass = computed(() => {
50
+ const active = activeTab.value // ← lee suscriptamente
51
+ return (tab: string) => tab === active ? CLS_ON : CLS_OFF
52
+ })
53
+ ```
54
+ ```html
55
+ <button v-for="tab in computed.tabs.value" :class="computed.tabClass.value(tab)">
56
+ ```
57
+
58
+ **Por qué snapshot**: si leés `activeTab.value` adentro de la closure, el `computed` no se invalida cuando cambia activeTab (la closure se crea una vez y se cachea). Leerlo en el getter registra la dep.
59
+
60
+ ## Patrón 2 — Input controlado
61
+
62
+ `v-model` NO EXISTE. Siempre explícito:
63
+
64
+ ```html
65
+ <input
66
+ :value="state.email.value"
67
+ @input="actions.onEmailInput"
68
+ @keydown.escape="actions.clearEmail"
69
+ />
70
+ ```
71
+ ```typescript
72
+ function onEmailInput(e: Event) {
73
+ email.value = (e.target as HTMLInputElement).value
74
+ }
75
+ function clearEmail() { email.value = '' }
76
+ ```
77
+
78
+ ## Patrón 3 — Curried action para v-for
79
+
80
+ Cuando un handler en un loop necesita el item:
81
+
82
+ ```typescript
83
+ function toggleTodo(id: string) {
84
+ return () => {
85
+ todos.value = todos.value.map(t => t.id === id ? { ...t, done: !t.done } : t)
86
+ }
87
+ }
88
+ ```
89
+ ```html
90
+ <button v-for="todo in state.todos.value" @click="actions.toggleTodo(todo.id)">
91
+ {{ todo.done ? '✓' : '○' }}
92
+ </button>
93
+ ```
94
+
95
+ `actions.toggleTodo(todo.id)` se evalúa en cada render → devuelve la función real → se asigna como handler.
96
+
97
+ ## Patrón 4 — Comunicación hijo→padre
98
+
99
+ **Funciona vía `@event` con emit (FIX del v0.1.0 corregido):**
100
+
101
+ ```typescript
102
+ // Child
103
+ emits: ['user-saved'],
104
+ setup(props, { emit }) {
105
+ function save() { emit('user-saved', { id: '1', name: 'X' }) }
106
+ return { state: {}, computed: {}, actions: { save } }
107
+ }
108
+ ```
109
+ ```html
110
+ <!-- Padre template -->
111
+ <UserCard @user-saved="actions.onUserSaved" />
112
+ ```
113
+ ```typescript
114
+ // Padre setup
115
+ function onUserSaved(payload: { id: string; name: string }) {
116
+ console.log(payload) // ← recibe e.detail directo, no el Event
117
+ }
118
+ ```
119
+
120
+ Detalles:
121
+ - El payload llega como **primer argumento** del handler (no como `e.detail` — el wrapper ya lo extrae)
122
+ - El nombre del evento debe estar en `emits: []` (sino warning runtime)
123
+ - Eventos en kebab-case obligatorio
124
+
125
+ **Patrón alternativo (callback como prop):** sigue funcionando, útil cuando el child no tiene "ownership" del evento conceptualmente:
126
+ ```html
127
+ <TaskForm :onSubmit="actions.handleCreate" :onCancel="actions.closeModal" />
128
+ ```
129
+
130
+ ## Patrón 5 — Modal con slot
131
+
132
+ ```html
133
+ <!-- Modal.ark -->
134
+ <template>
135
+ <div v-show="props.visible" class="fixed inset-0 ...">
136
+ <div class="modal-content">
137
+ <header>
138
+ <h2>{{ props.title }}</h2>
139
+ <button @click="actions.close">✕</button>
140
+ </header>
141
+ <div class="body"><slot /></div>
142
+ </div>
143
+ </div>
144
+ </template>
145
+
146
+ <script lang="ts">
147
+ export default defineComponent({
148
+ name: 'Modal',
149
+ props: {
150
+ visible: { type: Boolean, required: true },
151
+ title: { type: String, required: true },
152
+ onClose: { type: Function, required: true },
153
+ },
154
+ emits: [],
155
+ setup(props) {
156
+ function close() { (props.onClose as () => void)() }
157
+ return { state: {}, computed: {}, actions: { close } }
158
+ },
159
+ })
160
+ </script>
161
+ ```
162
+
163
+ Uso:
164
+ ```html
165
+ <Modal :visible="state.modalOpen.value" title="Confirmar" :onClose="actions.closeModal">
166
+ <p>¿Estás seguro?</p>
167
+ <button @click="actions.confirm">Sí</button>
168
+ </Modal>
169
+ ```
170
+
171
+ ## Patrón 6 — Form con validación
172
+
173
+ ```typescript
174
+ const text = signal('')
175
+ const priority = signal<'low' | 'medium' | 'high'>('medium')
176
+
177
+ const textError = computed(() => {
178
+ const v = text.value.trim()
179
+ if (v.length === 0) return ''
180
+ if (v.length < 2) return 'Mínimo 2 caracteres.'
181
+ return ''
182
+ })
183
+
184
+ const formInvalid = computed(() => text.value.trim().length < 2)
185
+
186
+ function submit(e: Event) {
187
+ e.preventDefault()
188
+ if (formInvalid.value) return
189
+ // ... emit o callback
190
+ }
191
+ ```
192
+ ```html
193
+ <form @submit="actions.submit">
194
+ <input :value="state.text.value" @input="actions.onTextInput" />
195
+ <p v-show="computed.textError.value">{{ computed.textError.value }}</p>
196
+ <button type="submit" :disabled="computed.formInvalid.value">Enviar</button>
197
+ </form>
198
+ ```
199
+
200
+ ## Anti-patterns universales
201
+
202
+ ```typescript
203
+ // ❌ ref/reactive
204
+ const x = ref(0) // → signal(0)
205
+
206
+ // ❌ v-model
207
+ <input v-model="state.x" /> // → :value + @input explícito
208
+
209
+ // ❌ provide/inject
210
+ provide('key', val) // → defineStore o prop
211
+
212
+ // ❌ fetch en componente
213
+ onMount(() => fetch('/x')) // → createService
214
+
215
+ // ❌ arrow function en actions
216
+ actions: { submit: () => {} } // → function submit() {}
217
+
218
+ // ❌ llaves extra en return
219
+ return { state, computed, actions, methods } // → solo las 3
220
+
221
+ // ❌ handler sin actions.
222
+ @click="handleClick" // → @click="actions.handleClick"
223
+
224
+ // ❌ v-for sin namespace
225
+ v-for="x in items" // → v-for="x in state.items.value"
226
+ ```
227
+
228
+ ## Antes de declarar listo
229
+
230
+ ```bash
231
+ npx ark analyze # 0 errores
232
+ npx vitest run # cuando trabajás en el framework
233
+ ```
@@ -0,0 +1,145 @@
1
+ # SKILL: Runtime — signals, componentes, renderer
2
+
3
+ > Cargá este skill cuando trabajés en `src/runtime/` o cuando uses signals/lifecycle/h/mount en código de usuario.
4
+
5
+ ## Primitivas reactivas (`src/runtime/signals.ts`)
6
+
7
+ ```typescript
8
+ const count = signal(0)
9
+ count.value = 5 // set — dispara re-render
10
+ count.value // get — suscribe al efecto activo
11
+ count.peek // get SIN suscribirse (lectura no reactiva)
12
+
13
+ const double = computed(() => count.value * 2)
14
+ double.value // readonly
15
+
16
+ const stop = watch(count, (newVal, oldVal) => { ... })
17
+ watch(count, cb, { immediate: true }) // dispara cb al inicio
18
+
19
+ const stopE = effect(() => { document.title = `${count.value}` })
20
+ ```
21
+
22
+ **Reglas no negociables:**
23
+ - `computed.value` NUNCA mutar (es readonly)
24
+ - Dentro de `computed`, NO mutar otros signals (eso es side effect → usar `watch` o `effect`)
25
+ - `signal.peek` es la única forma de leer sin suscribirse — útil para evitar loops o leer al instante
26
+
27
+ ## defineComponent (`src/runtime/define-component.ts`)
28
+
29
+ ```typescript
30
+ export default defineComponent({
31
+ name: 'MiComp', // string PascalCase, OBLIGATORIO
32
+ props: {
33
+ label: { type: String, required: true },
34
+ count: { type: Number, default: 0 },
35
+ },
36
+ emits: ['save', 'cancel'], // SIEMPRE kebab-case
37
+ setup(props, { emit }) {
38
+ // ...
39
+ return { state, computed, actions } // SOLO estas 3 llaves
40
+ },
41
+ })
42
+ ```
43
+
44
+ **Tipos válidos de prop:** `String | Number | Boolean | Array | Object | Function` (clases nativas, no strings)
45
+
46
+ **Validaciones en dev** (sin afectar producción):
47
+ - Prop sin `type` → warning
48
+ - Emit no declarado en `emits[]` → warning
49
+ - Emit en camelCase → warning
50
+ - Return con llave distinta a `state|computed|actions` → warning
51
+
52
+ ## Lifecycle hooks
53
+
54
+ ```typescript
55
+ setup(props, { emit }) {
56
+ onMount(() => {
57
+ // ejecuta al montar en el DOM
58
+ return () => { /* cleanup opcional al unmount */ }
59
+ })
60
+ onUnmount(() => { /* ejecuta al desmontar */ })
61
+ onUpdate(() => { /* ejecuta en cada re-render */ })
62
+ }
63
+ ```
64
+
65
+ **Importante:** los hooks usan un estado global del módulo (`currentMountHooks`). Solo funcionan si se llaman SINCRÓNICAMENTE dentro de `setup()`. No los llames en callbacks async ni en setTimeout.
66
+
67
+ ## Renderer (`src/runtime/renderer.ts`)
68
+
69
+ ### `mount(component: Component, selector: string)`
70
+
71
+ ```typescript
72
+ import { mount } from 'arckode-ui'
73
+ import App from './App.ark'
74
+ mount(App, '#app') // toma un selector CSS string, NO HTMLElement
75
+ ```
76
+
77
+ Retorna una función de unmount.
78
+
79
+ ### Cómo funciona el renderer
80
+
81
+ 1. Llama `component.setup(props, ctx)` → obtiene `{ state, computed, actions }`
82
+ 2. Busca `component.__render` (lo inyecta el Vite plugin desde el `<template>`)
83
+ 3. Envuelve el render en un `effect()` que se re-ejecuta cuando cualquier signal usado cambia
84
+ 4. Crea VNodes y los aplica al DOM via `createNode` + `patch`
85
+
86
+ ### Componentes hijos en el VDOM
87
+
88
+ Cuando `h(ChildComp, props, ...children)`:
89
+ - Crea un wrapper `<div style="display:contents">` (transparente al layout)
90
+ - Pasa props al child via `mountComponent`
91
+ - Las props `onX` (function) **también** se registran como event listeners en el wrapper
92
+
93
+ ### Comunicación hijo→padre (verificado)
94
+
95
+ ```html
96
+ <!-- Padre -->
97
+ <Child @save="actions.onSave" />
98
+ ```
99
+ ```typescript
100
+ // Padre
101
+ function onSave(payload: { id: string }) { console.log(payload) }
102
+
103
+ // Child
104
+ emit('save', { id: '1' }) // dispatch CustomEvent('save', { detail: payload })
105
+ ```
106
+
107
+ El emit del child dispatcha `CustomEvent(eventName)` SIN prefijo. Bubblea hasta el wrapper que tiene el listener. El handler recibe `e.detail` directo como primer argumento (no el `Event`).
108
+
109
+ **Anti-pattern:** dispatcher con prefijo `ark:event` — eso rompía el binding `@event`. NO volver a poner el prefijo.
110
+
111
+ ## Anti-patterns
112
+
113
+ ```typescript
114
+ // ❌ mutar dentro de computed
115
+ const x = computed(() => { signal2.value = 5; return signal1.value * 2 })
116
+
117
+ // ❌ llamar hooks dentro de async
118
+ onMount(async () => {
119
+ await fetch('/x')
120
+ onUnmount(() => {}) // NO — onUnmount fuera del setup sync
121
+ })
122
+
123
+ // ❌ pasar HTMLElement a mount
124
+ mount(App, document.getElementById('app')!)
125
+ // → mount(App, '#app')
126
+
127
+ // ❌ leer signal en lugar de .value
128
+ console.log(count) // imprime el objeto
129
+ console.log(count.value) // ✓
130
+ ```
131
+
132
+ ## Tests del runtime
133
+
134
+ Los tests usan `@vitest-environment happy-dom`. Para crear container:
135
+
136
+ ```typescript
137
+ function makeContainer(): HTMLElement {
138
+ const div = document.createElement('div')
139
+ div.id = 'app'
140
+ document.body.appendChild(div)
141
+ return div
142
+ }
143
+ ```
144
+
145
+ Helper `setEffectMicrotaskFlush` no existe — el `effect()` se ejecuta sincrónicamente.
@@ -0,0 +1,169 @@
1
+ # SKILL: Testing — Vitest + happy-dom
2
+
3
+ > Cargá este skill cuando escribas o modifiques tests del framework o de proyectos que usan arckode-ui.
4
+
5
+ ## Setup base
6
+
7
+ Tests con `vitest`. DOM tests usan `happy-dom` (más rápido que jsdom).
8
+
9
+ ```typescript
10
+ // @vitest-environment happy-dom ← agregar al tope del archivo para tests con DOM
11
+ import { describe, test, expect, beforeEach, vi } from 'vitest'
12
+ ```
13
+
14
+ ## Test de signals / computed / watch
15
+
16
+ ```typescript
17
+ import { signal, computed, watch } from 'arckode-ui'
18
+
19
+ test('computed reacciona', () => {
20
+ const a = signal(2)
21
+ const double = computed(() => a.value * 2)
22
+ expect(double.value).toBe(4)
23
+ a.value = 5
24
+ expect(double.value).toBe(10)
25
+ })
26
+
27
+ test('watch callback recibe new + old', async () => {
28
+ const a = signal(0)
29
+ let captured: [number, number] | null = null
30
+ watch(a, (nv, ov) => { captured = [nv, ov] })
31
+ a.value = 7
32
+ await new Promise(r => queueMicrotask(r as () => void))
33
+ expect(captured).toEqual([7, 0])
34
+ })
35
+ ```
36
+
37
+ **Nota:** `watch` dispara el callback en `queueMicrotask`. Esperar una microtask antes de assertear.
38
+
39
+ ## Test del store (`defineStore`)
40
+
41
+ El registry global es singleton. Limpiar entre tests:
42
+
43
+ ```typescript
44
+ import { defineStore, _clearRegistry } from 'arckode-ui'
45
+ // _clearRegistry NO está exportado del barrel — importarlo del path directo
46
+ // para tests internos del framework.
47
+
48
+ beforeEach(() => { _clearRegistry() })
49
+
50
+ test('singleton', () => {
51
+ const useStore = defineStore('test', { state: { x: signal(0) }, actions: {} })
52
+ expect(useStore()).toBe(useStore()) // misma instancia
53
+ })
54
+ ```
55
+
56
+ **Para tests de consumidores:** no necesitan `_clearRegistry` si cada test usa un `id` distinto.
57
+
58
+ ## Test del service (`createService`)
59
+
60
+ Mockear `fetch` con vi:
61
+
62
+ ```typescript
63
+ import { createService } from 'arckode-ui'
64
+
65
+ function mockFetchOk(body: unknown, status = 200) {
66
+ vi.spyOn(global, 'fetch').mockResolvedValueOnce(
67
+ new Response(JSON.stringify(body), {
68
+ status,
69
+ headers: { 'Content-Type': 'application/json' },
70
+ }),
71
+ )
72
+ }
73
+
74
+ beforeEach(() => { vi.restoreAllMocks() })
75
+
76
+ test('GET retorna JSON parseado', async () => {
77
+ mockFetchOk({ id: '1' })
78
+ const svc = createService(
79
+ { baseUrl: '/api' },
80
+ { async getById(id: string) { return this.get<{ id: string }>(`/${id}`) } },
81
+ )
82
+ const result = await svc.getById('1')
83
+ expect(result).toEqual({ id: '1' })
84
+ })
85
+ ```
86
+
87
+ ## Test de componentes (`.ark` compilado)
88
+
89
+ Los `.ark` necesitan el Vite plugin para compilarse. Para tests unitarios del runtime/renderer, **NO** importes `.ark` files — construí el component manualmente:
90
+
91
+ ```typescript
92
+ import { defineComponent, h } from 'arckode-ui'
93
+ import { mountComponent } from 'arckode-ui'
94
+
95
+ const TestComp = defineComponent({
96
+ name: 'Test',
97
+ setup: () => ({ state: {}, computed: {}, actions: {} }),
98
+ })
99
+ ;(TestComp as any).__render = () => h('div', null, 'hello')
100
+
101
+ const container = document.createElement('div')
102
+ const unmount = mountComponent(TestComp, {}, container, null)
103
+ expect(container.textContent).toBe('hello')
104
+ unmount()
105
+ ```
106
+
107
+ ## Test del analyzer
108
+
109
+ ```typescript
110
+ import { analyze } from 'arckode-ui' // bueno, el analyzer no se exporta — usar path directo
111
+ import { analyze } from '../analyzer'
112
+
113
+ function findViolation(violations, code) {
114
+ return violations.find(v => v.code === code)
115
+ }
116
+
117
+ test('detecta el patrón X', () => {
118
+ const source = `<template>...</template><script lang="ts">...</script>`
119
+ const violations = analyze(source, 'Test.ark')
120
+ const v = findViolation(violations, 'MY_CODE')
121
+ expect(v).toBeDefined()
122
+ expect(v?.severity).toBe('error')
123
+ expect(v?.fix).toContain('lo que esperás como sugerencia')
124
+ })
125
+ ```
126
+
127
+ ## Test de templates del CLI
128
+
129
+ ```typescript
130
+ import { componentTemplate } from '../templates'
131
+ import { analyze } from '../../compiler/analyzer'
132
+
133
+ test('scaffold genera código válido', () => {
134
+ const content = componentTemplate('Foo')
135
+ const violations = analyze(content, 'Foo.ark')
136
+ const errors = violations.filter(v => v.severity === 'error')
137
+ expect(errors).toHaveLength(0)
138
+ })
139
+ ```
140
+
141
+ Hay test que valida que **TODAS** las plantillas del CLI pasen el analyzer con 0 errors (`src/cli/__tests__/templates.test.ts`). Si agregás un tipo nuevo al CLI, agregar test ahí.
142
+
143
+ ## Patrones a evitar
144
+
145
+ ```typescript
146
+ // ❌ assertear === en async sin await
147
+ const x = signal(0)
148
+ watch(x, ...)
149
+ x.value = 5
150
+ expect(captured).toBe(...) // ← falla, watch es async
151
+
152
+ // ❌ NO importar .ark files en tests del runtime
153
+ import App from '../App.ark' // requeriría el Vite plugin
154
+
155
+ // ❌ NO compartir state global entre tests
156
+ const sharedState = signal(0)
157
+ test('a', () => { sharedState.value = 1; ... })
158
+ test('b', () => { ... }) // contaminado por 'a'
159
+ ```
160
+
161
+ ## Comandos
162
+
163
+ ```bash
164
+ bun test # corre todos los tests (vitest run)
165
+ bun run test:watch # modo watch
166
+ bun run type-check # tsc --noEmit (excluye __tests__/)
167
+ ```
168
+
169
+ **Nota sobre type-check:** los tests están excluidos del strict TS check via `tsconfig.json` para evitar fricción con `noUncheckedIndexedAccess + exactOptionalPropertyTypes`. Vitest los corre OK pero `tsc` no los valida estrictamente. Si querés strict en tests, crear un `tsconfig.test.json` separado.