@uistate/examples 1.0.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/README.md +40 -0
- package/cssState/.gitkeep +0 -0
- package/eventState/001-counter/README.md +44 -0
- package/eventState/001-counter/index.html +33 -0
- package/eventState/002-counter-improved/README.md +44 -0
- package/eventState/002-counter-improved/index.html +47 -0
- package/eventState/003-input-reactive/README.md +44 -0
- package/eventState/003-input-reactive/index.html +33 -0
- package/eventState/004-computed-state/README.md +45 -0
- package/eventState/004-computed-state/index.html +65 -0
- package/eventState/005-conditional-rendering/README.md +42 -0
- package/eventState/005-conditional-rendering/index.html +39 -0
- package/eventState/006-list-rendering/README.md +49 -0
- package/eventState/006-list-rendering/index.html +63 -0
- package/eventState/007-form-validation/README.md +52 -0
- package/eventState/007-form-validation/index.html +102 -0
- package/eventState/008-undo-redo/README.md +70 -0
- package/eventState/008-undo-redo/index.html +108 -0
- package/eventState/009-localStorage-side-effects/README.md +72 -0
- package/eventState/009-localStorage-side-effects/index.html +57 -0
- package/eventState/010-decoupled-components/README.md +74 -0
- package/eventState/010-decoupled-components/index.html +93 -0
- package/eventState/011-async-patterns/README.md +98 -0
- package/eventState/011-async-patterns/index.html +132 -0
- package/eventState/028-counter-improved-eventTest/LICENSE +55 -0
- package/eventState/028-counter-improved-eventTest/README.md +131 -0
- package/eventState/028-counter-improved-eventTest/app/store.js +9 -0
- package/eventState/028-counter-improved-eventTest/index.html +49 -0
- package/eventState/028-counter-improved-eventTest/runtime/core/behaviors.runtime.js +282 -0
- package/eventState/028-counter-improved-eventTest/runtime/core/eventState.js +100 -0
- package/eventState/028-counter-improved-eventTest/runtime/core/eventStateNew.js +149 -0
- package/eventState/028-counter-improved-eventTest/runtime/core/helpers.js +212 -0
- package/eventState/028-counter-improved-eventTest/runtime/core/router.js +271 -0
- package/eventState/028-counter-improved-eventTest/store.d.ts +8 -0
- package/eventState/028-counter-improved-eventTest/style.css +170 -0
- package/eventState/028-counter-improved-eventTest/tests/README.md +208 -0
- package/eventState/028-counter-improved-eventTest/tests/counter.test.js +116 -0
- package/eventState/028-counter-improved-eventTest/tests/eventTest.js +176 -0
- package/eventState/028-counter-improved-eventTest/tests/generateTypes.js +168 -0
- package/eventState/028-counter-improved-eventTest/tests/run.js +20 -0
- package/eventState/030-todo-app-with-eventTest/LICENSE +55 -0
- package/eventState/030-todo-app-with-eventTest/README.md +121 -0
- package/eventState/030-todo-app-with-eventTest/app/router.js +25 -0
- package/eventState/030-todo-app-with-eventTest/app/store.js +16 -0
- package/eventState/030-todo-app-with-eventTest/app/views/home.js +11 -0
- package/eventState/030-todo-app-with-eventTest/app/views/todoDemo.js +88 -0
- package/eventState/030-todo-app-with-eventTest/index.html +65 -0
- package/eventState/030-todo-app-with-eventTest/runtime/core/behaviors.runtime.js +282 -0
- package/eventState/030-todo-app-with-eventTest/runtime/core/eventState.js +100 -0
- package/eventState/030-todo-app-with-eventTest/runtime/core/eventStateNew.js +149 -0
- package/eventState/030-todo-app-with-eventTest/runtime/core/helpers.js +212 -0
- package/eventState/030-todo-app-with-eventTest/runtime/core/router.js +271 -0
- package/eventState/030-todo-app-with-eventTest/store.d.ts +18 -0
- package/eventState/030-todo-app-with-eventTest/style.css +170 -0
- package/eventState/030-todo-app-with-eventTest/tests/README.md +208 -0
- package/eventState/030-todo-app-with-eventTest/tests/eventTest.js +176 -0
- package/eventState/030-todo-app-with-eventTest/tests/generateTypes.js +189 -0
- package/eventState/030-todo-app-with-eventTest/tests/run.js +20 -0
- package/eventState/030-todo-app-with-eventTest/tests/todos.test.js +167 -0
- package/eventState/031-todo-app-with-eventTest/LICENSE +55 -0
- package/eventState/031-todo-app-with-eventTest/README.md +54 -0
- package/eventState/031-todo-app-with-eventTest/TUTORIAL.md +390 -0
- package/eventState/031-todo-app-with-eventTest/WHY_EVENTSTATE.md +777 -0
- package/eventState/031-todo-app-with-eventTest/app/bridges.js +113 -0
- package/eventState/031-todo-app-with-eventTest/app/router.js +26 -0
- package/eventState/031-todo-app-with-eventTest/app/store.js +15 -0
- package/eventState/031-todo-app-with-eventTest/app/views/home.js +46 -0
- package/eventState/031-todo-app-with-eventTest/app/views/todoDemo.js +69 -0
- package/eventState/031-todo-app-with-eventTest/devtools/dock.js +41 -0
- package/eventState/031-todo-app-with-eventTest/devtools/stateTracker.dock.js +10 -0
- package/eventState/031-todo-app-with-eventTest/devtools/stateTracker.js +246 -0
- package/eventState/031-todo-app-with-eventTest/devtools/telemetry.js +104 -0
- package/eventState/031-todo-app-with-eventTest/devtools/typeGenerator.js +339 -0
- package/eventState/031-todo-app-with-eventTest/index.html +103 -0
- package/eventState/031-todo-app-with-eventTest/package-lock.json +2184 -0
- package/eventState/031-todo-app-with-eventTest/package.json +24 -0
- package/eventState/031-todo-app-with-eventTest/runtime/core/behaviors.runtime.js +282 -0
- package/eventState/031-todo-app-with-eventTest/runtime/core/eventState.js +100 -0
- package/eventState/031-todo-app-with-eventTest/runtime/core/eventStateNew.js +149 -0
- package/eventState/031-todo-app-with-eventTest/runtime/core/helpers.js +212 -0
- package/eventState/031-todo-app-with-eventTest/runtime/core/router.js +271 -0
- package/eventState/031-todo-app-with-eventTest/runtime/extensions/boundary.js +36 -0
- package/eventState/031-todo-app-with-eventTest/runtime/extensions/converge.js +63 -0
- package/eventState/031-todo-app-with-eventTest/runtime/extensions/eventState.plus.js +210 -0
- package/eventState/031-todo-app-with-eventTest/runtime/extensions/hydrate.js +157 -0
- package/eventState/031-todo-app-with-eventTest/runtime/extensions/queryBinding.js +69 -0
- package/eventState/031-todo-app-with-eventTest/runtime/forms/computed.js +78 -0
- package/eventState/031-todo-app-with-eventTest/runtime/forms/meta.js +51 -0
- package/eventState/031-todo-app-with-eventTest/runtime/forms/submitWithBoundary.js +28 -0
- package/eventState/031-todo-app-with-eventTest/runtime/forms/validators.js +55 -0
- package/eventState/031-todo-app-with-eventTest/store.d.ts +23 -0
- package/eventState/031-todo-app-with-eventTest/style.css +170 -0
- package/eventState/031-todo-app-with-eventTest/tests/README.md +208 -0
- package/eventState/031-todo-app-with-eventTest/tests/eventTest.js +176 -0
- package/eventState/031-todo-app-with-eventTest/tests/generateTypes.js +191 -0
- package/eventState/031-todo-app-with-eventTest/tests/run.js +20 -0
- package/eventState/031-todo-app-with-eventTest/tests/todos.test.js +192 -0
- package/eventState/032-todo-app-with-eventTest/LICENSE +55 -0
- package/eventState/032-todo-app-with-eventTest/README.md +54 -0
- package/eventState/032-todo-app-with-eventTest/TUTORIAL.md +390 -0
- package/eventState/032-todo-app-with-eventTest/WHY_EVENTSTATE.md +777 -0
- package/eventState/032-todo-app-with-eventTest/app/actions/index.js +153 -0
- package/eventState/032-todo-app-with-eventTest/app/bridges.js +113 -0
- package/eventState/032-todo-app-with-eventTest/app/router.js +26 -0
- package/eventState/032-todo-app-with-eventTest/app/store.js +15 -0
- package/eventState/032-todo-app-with-eventTest/app/views/home.js +46 -0
- package/eventState/032-todo-app-with-eventTest/app/views/todoDemo.js +69 -0
- package/eventState/032-todo-app-with-eventTest/devtools/dock.js +41 -0
- package/eventState/032-todo-app-with-eventTest/devtools/stateTracker.dock.js +10 -0
- package/eventState/032-todo-app-with-eventTest/devtools/stateTracker.js +246 -0
- package/eventState/032-todo-app-with-eventTest/devtools/telemetry.js +104 -0
- package/eventState/032-todo-app-with-eventTest/devtools/typeGenerator.js +339 -0
- package/eventState/032-todo-app-with-eventTest/index.html +87 -0
- package/eventState/032-todo-app-with-eventTest/package-lock.json +2184 -0
- package/eventState/032-todo-app-with-eventTest/package.json +24 -0
- package/eventState/032-todo-app-with-eventTest/runtime/core/behaviors.runtime.js +282 -0
- package/eventState/032-todo-app-with-eventTest/runtime/core/eventState.js +100 -0
- package/eventState/032-todo-app-with-eventTest/runtime/core/eventStateNew.js +149 -0
- package/eventState/032-todo-app-with-eventTest/runtime/core/helpers.js +212 -0
- package/eventState/032-todo-app-with-eventTest/runtime/core/router.js +271 -0
- package/eventState/032-todo-app-with-eventTest/runtime/extensions/boundary.js +36 -0
- package/eventState/032-todo-app-with-eventTest/runtime/extensions/converge.js +63 -0
- package/eventState/032-todo-app-with-eventTest/runtime/extensions/eventState.plus.js +210 -0
- package/eventState/032-todo-app-with-eventTest/runtime/extensions/hydrate.js +157 -0
- package/eventState/032-todo-app-with-eventTest/runtime/extensions/queryBinding.js +69 -0
- package/eventState/032-todo-app-with-eventTest/runtime/forms/computed.js +78 -0
- package/eventState/032-todo-app-with-eventTest/runtime/forms/meta.js +51 -0
- package/eventState/032-todo-app-with-eventTest/runtime/forms/submitWithBoundary.js +28 -0
- package/eventState/032-todo-app-with-eventTest/runtime/forms/validators.js +55 -0
- package/eventState/032-todo-app-with-eventTest/store.d.ts +23 -0
- package/eventState/032-todo-app-with-eventTest/style.css +170 -0
- package/eventState/032-todo-app-with-eventTest/tests/README.md +208 -0
- package/eventState/032-todo-app-with-eventTest/tests/eventTest.js +176 -0
- package/eventState/032-todo-app-with-eventTest/tests/generateTypes.js +191 -0
- package/eventState/032-todo-app-with-eventTest/tests/run.js +20 -0
- package/eventState/032-todo-app-with-eventTest/tests/todos.test.js +192 -0
- package/package.json +27 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
body {
|
|
2
|
+
font: 14px/1.4 system-ui, sans-serif;
|
|
3
|
+
margin: 0;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/* Hide focus outline on route container (set programmatically for a11y) */
|
|
7
|
+
[data-route-root]:focus {
|
|
8
|
+
outline: none;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
nav {
|
|
12
|
+
padding: 10px 14px;
|
|
13
|
+
border-bottom: 1px solid #eee;
|
|
14
|
+
position: sticky;
|
|
15
|
+
top: 0;
|
|
16
|
+
background: #fff;
|
|
17
|
+
z-index: 10;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
nav a {
|
|
21
|
+
margin-right: 12px;
|
|
22
|
+
text-decoration: none;
|
|
23
|
+
color: #0366d6;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
nav a.active {
|
|
27
|
+
font-weight: 600;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
main {
|
|
31
|
+
padding: 16px;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.loading-badge {
|
|
35
|
+
margin-left: 8px;
|
|
36
|
+
font-size: 12px;
|
|
37
|
+
color: #666;
|
|
38
|
+
display: none;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
html[data-transitioning="on"] .loading-badge {
|
|
42
|
+
display: inline;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* 404 view demo: tint the top bar only on notfound */
|
|
46
|
+
html[data-view="notfound"] nav {
|
|
47
|
+
background: #fff7f7;
|
|
48
|
+
border-bottom-color: #f5c2c2;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
html[data-view="notfound"] nav a {
|
|
52
|
+
color: #b91c1c;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
html[data-view="notfound"] .loading-badge {
|
|
56
|
+
color: #b91c1c;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* Tabs styling (used by views/demo.js) */
|
|
60
|
+
.tabs {
|
|
61
|
+
margin-top: 12px;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.tab-bar {
|
|
65
|
+
display: flex;
|
|
66
|
+
gap: 8px;
|
|
67
|
+
border-bottom: 1px solid #eee;
|
|
68
|
+
padding-bottom: 6px;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.tab {
|
|
72
|
+
appearance: none;
|
|
73
|
+
border: 1px solid #ddd;
|
|
74
|
+
background: #fafafa;
|
|
75
|
+
border-radius: 6px;
|
|
76
|
+
padding: 6px 10px;
|
|
77
|
+
cursor: pointer;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.tab:hover {
|
|
81
|
+
background: #f3f4f6;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.tab.active {
|
|
85
|
+
background: #e5efff;
|
|
86
|
+
border-color: #bcd1ff;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.tab-panels {
|
|
90
|
+
padding-top: 10px;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.tab-panel {
|
|
94
|
+
display: none;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.tab-panel[data-active] {
|
|
98
|
+
display: block;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
:root {
|
|
102
|
+
--bg: #ffffff;
|
|
103
|
+
--fg: #111111;
|
|
104
|
+
--muted: #555;
|
|
105
|
+
--card: #f6f6f6;
|
|
106
|
+
--border: #e5e5e5;
|
|
107
|
+
--btn-bg: #f8f8f8;
|
|
108
|
+
--btn-fg: #111;
|
|
109
|
+
--btn-border: #ddd;
|
|
110
|
+
--input-bg: #fff;
|
|
111
|
+
--input-fg: #222;
|
|
112
|
+
--input-border: #d6d6d6;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
body[data-theme='dark'] {
|
|
116
|
+
--bg: #0f1115;
|
|
117
|
+
--fg: #cfd3dc;
|
|
118
|
+
--muted: #9aa3b2;
|
|
119
|
+
--card: #151923;
|
|
120
|
+
--border: #242a36;
|
|
121
|
+
--btn-bg: #1a2030;
|
|
122
|
+
--btn-fg: #cfd3dc;
|
|
123
|
+
--btn-border: #2b3242;
|
|
124
|
+
--input-bg: #1b2130;
|
|
125
|
+
--input-fg: #cfd3dc;
|
|
126
|
+
--input-border: #2b3242;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
body {
|
|
130
|
+
background: var(--bg);
|
|
131
|
+
color: var(--fg);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
a {
|
|
135
|
+
color: inherit;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
nav {
|
|
139
|
+
background: var(--card);
|
|
140
|
+
border-bottom: 1px solid var(--border);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.btn {
|
|
144
|
+
appearance: none;
|
|
145
|
+
border: 1px solid var(--btn-border);
|
|
146
|
+
background: var(--btn-bg);
|
|
147
|
+
color: var(--btn-fg);
|
|
148
|
+
border-radius: 6px;
|
|
149
|
+
padding: 6px 10px;
|
|
150
|
+
cursor: pointer;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
input,
|
|
154
|
+
select,
|
|
155
|
+
textarea {
|
|
156
|
+
background: var(--input-bg);
|
|
157
|
+
color: var(--input-fg);
|
|
158
|
+
border: 1px solid var(--input-border);
|
|
159
|
+
border-radius: 6px;
|
|
160
|
+
padding: 6px 8px;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
input::placeholder,
|
|
164
|
+
textarea::placeholder {
|
|
165
|
+
color: color-mix(in srgb, var(--input-fg) 55%, transparent);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.btn:hover {
|
|
169
|
+
filter: brightness(1.05);
|
|
170
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# EventTest - Event-Sequence Testing
|
|
2
|
+
|
|
3
|
+
Event-driven TDD for EventState applications.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ✅ **Event-sequence testing** - Test state changes through event flows
|
|
8
|
+
- ✅ **Type extraction** - Generate `.d.ts` files from test assertions
|
|
9
|
+
- ✅ **No DOM required** - Tests run in Node.js
|
|
10
|
+
- ✅ **Fluent API** - Chainable assertions
|
|
11
|
+
- ✅ **Type-safe** - Tests define types, not manual definitions
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
### Writing Tests
|
|
16
|
+
|
|
17
|
+
```javascript
|
|
18
|
+
import { createEventTest, test } from './eventTest.js';
|
|
19
|
+
|
|
20
|
+
test('add todo', () => {
|
|
21
|
+
const t = createEventTest({
|
|
22
|
+
domain: { todos: { items: [] } }
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Trigger intent
|
|
26
|
+
t.trigger('intent.todo.add', { text: 'Buy milk' });
|
|
27
|
+
|
|
28
|
+
// Assert types (for .d.ts generation)
|
|
29
|
+
t.assertArrayOf('domain.todos.items', {
|
|
30
|
+
id: 'number',
|
|
31
|
+
text: 'string',
|
|
32
|
+
done: 'boolean'
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Assert values
|
|
36
|
+
t.assertArrayLength('domain.todos.items', 1);
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Running Tests
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# Run all tests
|
|
44
|
+
node tests/run.js
|
|
45
|
+
|
|
46
|
+
# Or run specific test file
|
|
47
|
+
node tests/todos.test.js
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Generating Types
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# Generate store.d.ts from test assertions
|
|
54
|
+
node tests/generateTypes.js
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## API Reference
|
|
58
|
+
|
|
59
|
+
### `createEventTest(initialState)`
|
|
60
|
+
|
|
61
|
+
Creates a test instance with isolated store.
|
|
62
|
+
|
|
63
|
+
**Returns:** Test API object
|
|
64
|
+
|
|
65
|
+
### Test API
|
|
66
|
+
|
|
67
|
+
#### `.trigger(path, value)`
|
|
68
|
+
|
|
69
|
+
Set a value in the store (trigger state change).
|
|
70
|
+
|
|
71
|
+
```javascript
|
|
72
|
+
t.trigger('intent.todo.add', { text: 'Buy milk' });
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
#### `.assertPath(path, expected)`
|
|
76
|
+
|
|
77
|
+
Assert exact value at path.
|
|
78
|
+
|
|
79
|
+
```javascript
|
|
80
|
+
t.assertPath('ui.theme', 'dark');
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
#### `.assertType(path, type)`
|
|
84
|
+
|
|
85
|
+
Assert primitive type. Stores type info for `.d.ts` generation.
|
|
86
|
+
|
|
87
|
+
```javascript
|
|
88
|
+
t.assertType('ui.theme', 'string');
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
#### `.assertArrayOf(path, elementShape)`
|
|
92
|
+
|
|
93
|
+
Assert array with element shape. Stores type info for `.d.ts` generation.
|
|
94
|
+
|
|
95
|
+
```javascript
|
|
96
|
+
t.assertArrayOf('domain.todos.items', {
|
|
97
|
+
id: 'number',
|
|
98
|
+
text: 'string',
|
|
99
|
+
done: 'boolean'
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
#### `.assertShape(path, objectShape)`
|
|
104
|
+
|
|
105
|
+
Assert object shape. Stores type info for `.d.ts` generation.
|
|
106
|
+
|
|
107
|
+
```javascript
|
|
108
|
+
t.assertShape('ui.route', {
|
|
109
|
+
path: 'string',
|
|
110
|
+
view: 'string'
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
#### `.assertArrayLength(path, length)`
|
|
115
|
+
|
|
116
|
+
Assert array length.
|
|
117
|
+
|
|
118
|
+
```javascript
|
|
119
|
+
t.assertArrayLength('domain.todos.items', 3);
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
#### `.assertEventFired(path, times)`
|
|
123
|
+
|
|
124
|
+
Assert event fired N times.
|
|
125
|
+
|
|
126
|
+
```javascript
|
|
127
|
+
t.assertEventFired('domain.todos.items', 1);
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
#### `.getEventLog()`
|
|
131
|
+
|
|
132
|
+
Get all captured events.
|
|
133
|
+
|
|
134
|
+
```javascript
|
|
135
|
+
const log = t.getEventLog();
|
|
136
|
+
// [{ timestamp: 123, path: 'domain.todos.items', value: [...] }]
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
#### `.getTypeAssertions()`
|
|
140
|
+
|
|
141
|
+
Get all type assertions (for type generation).
|
|
142
|
+
|
|
143
|
+
```javascript
|
|
144
|
+
const assertions = t.getTypeAssertions();
|
|
145
|
+
// [{ path: 'domain.todos.items', type: 'array', elementShape: {...} }]
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Workflow
|
|
149
|
+
|
|
150
|
+
### 1. Write Tests (TDD)
|
|
151
|
+
|
|
152
|
+
```javascript
|
|
153
|
+
// tests/todos.test.js
|
|
154
|
+
test('add todo', () => {
|
|
155
|
+
const t = createEventTest({ domain: { todos: { items: [] } } });
|
|
156
|
+
t.trigger('intent.todo.add', { text: 'Buy milk' });
|
|
157
|
+
t.assertArrayOf('domain.todos.items', {
|
|
158
|
+
id: 'number',
|
|
159
|
+
text: 'string',
|
|
160
|
+
done: 'boolean'
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### 2. Implement Bridges
|
|
166
|
+
|
|
167
|
+
```javascript
|
|
168
|
+
// app/bridges.js
|
|
169
|
+
store.subscribe('intent.todo.add', ({ text }) => {
|
|
170
|
+
const items = store.get('domain.todos.items') || [];
|
|
171
|
+
const id = items.length + 1;
|
|
172
|
+
store.set('domain.todos.items', [...items, { id, text, done: false }]);
|
|
173
|
+
});
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### 3. Generate Types
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
node tests/generateTypes.js
|
|
180
|
+
# Creates store.d.ts with perfect types!
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### 4. Build UI
|
|
184
|
+
|
|
185
|
+
```javascript
|
|
186
|
+
// Now you have autocomplete!
|
|
187
|
+
const items = store.get('domain.todos.items');
|
|
188
|
+
// TypeScript knows: Array<{ id: number, text: string, done: boolean }>
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Benefits
|
|
192
|
+
|
|
193
|
+
- **Tests define behavior** - TDD workflow
|
|
194
|
+
- **Types derived from tests** - No manual type definitions
|
|
195
|
+
- **Perfect coverage** - Every test adds type information
|
|
196
|
+
- **No DOM needed** - Fast feedback loop
|
|
197
|
+
- **Deterministic** - Same tests = same types
|
|
198
|
+
- **Watcher-friendly** - Auto-regenerate on test changes
|
|
199
|
+
|
|
200
|
+
## Philosophy
|
|
201
|
+
|
|
202
|
+
EventTest follows EventState's core principles:
|
|
203
|
+
|
|
204
|
+
1. **State-first** - Tests work with state paths
|
|
205
|
+
2. **Dot-paths everywhere** - No special syntax
|
|
206
|
+
3. **Types are optional** - Tests work without TypeScript
|
|
207
|
+
4. **Types are derived** - Not hand-written
|
|
208
|
+
5. **JS-first** - Clean, readable code
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* eventTest.js - Event-Sequence Testing for EventState
|
|
3
|
+
*
|
|
4
|
+
* Provides TDD-style testing with type extraction capabilities
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createEventState } from '../runtime/core/eventStateNew.js';
|
|
8
|
+
|
|
9
|
+
export function createEventTest(initialState = {}) {
|
|
10
|
+
const store = createEventState(initialState);
|
|
11
|
+
const eventLog = [];
|
|
12
|
+
const typeAssertions = [];
|
|
13
|
+
|
|
14
|
+
// Spy on all events
|
|
15
|
+
store.subscribe('*', (detail) => {
|
|
16
|
+
const { path, value } = detail;
|
|
17
|
+
eventLog.push({ timestamp: Date.now(), path, value });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const api = {
|
|
21
|
+
store,
|
|
22
|
+
|
|
23
|
+
// Trigger a state change
|
|
24
|
+
trigger(path, value) {
|
|
25
|
+
store.set(path, value);
|
|
26
|
+
return this;
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
// Assert exact value
|
|
30
|
+
assertPath(path, expected) {
|
|
31
|
+
const actual = store.get(path);
|
|
32
|
+
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
|
|
33
|
+
throw new Error(`Expected ${path} to be ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
|
|
34
|
+
}
|
|
35
|
+
return this;
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
// Assert type (for type generation)
|
|
39
|
+
assertType(path, expectedType) {
|
|
40
|
+
const actual = store.get(path);
|
|
41
|
+
const actualType = typeof actual;
|
|
42
|
+
|
|
43
|
+
if (actualType !== expectedType) {
|
|
44
|
+
throw new Error(`Expected ${path} to be type ${expectedType}, got ${actualType}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Store for type generation
|
|
48
|
+
typeAssertions.push({ path, type: expectedType });
|
|
49
|
+
return this;
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
// Assert array with element shape (for type generation)
|
|
53
|
+
assertArrayOf(path, elementShape) {
|
|
54
|
+
const actual = store.get(path);
|
|
55
|
+
|
|
56
|
+
if (!Array.isArray(actual)) {
|
|
57
|
+
throw new Error(`Expected ${path} to be an array, got ${typeof actual}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Validate first element matches shape (if array not empty)
|
|
61
|
+
if (actual.length > 0) {
|
|
62
|
+
validateShape(actual[0], elementShape, path);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Store for type generation
|
|
66
|
+
typeAssertions.push({ path, type: 'array', elementShape });
|
|
67
|
+
return this;
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
// Assert object shape (for type generation)
|
|
71
|
+
assertShape(path, objectShape) {
|
|
72
|
+
const actual = store.get(path);
|
|
73
|
+
|
|
74
|
+
if (typeof actual !== 'object' || actual === null || Array.isArray(actual)) {
|
|
75
|
+
throw new Error(`Expected ${path} to be an object, got ${typeof actual}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
validateShape(actual, objectShape, path);
|
|
79
|
+
|
|
80
|
+
// Store for type generation
|
|
81
|
+
typeAssertions.push({ path, type: 'object', shape: objectShape });
|
|
82
|
+
return this;
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
// Assert array length
|
|
86
|
+
assertArrayLength(path, expectedLength) {
|
|
87
|
+
const actual = store.get(path);
|
|
88
|
+
|
|
89
|
+
if (!Array.isArray(actual)) {
|
|
90
|
+
throw new Error(`Expected ${path} to be an array`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (actual.length !== expectedLength) {
|
|
94
|
+
throw new Error(`Expected ${path} to have length ${expectedLength}, got ${actual.length}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return this;
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
// Assert event fired N times
|
|
101
|
+
assertEventFired(path, times) {
|
|
102
|
+
const count = eventLog.filter(e => e.path === path).length;
|
|
103
|
+
if (times !== undefined && count !== times) {
|
|
104
|
+
throw new Error(`Expected ${path} to fire ${times} times, fired ${count}`);
|
|
105
|
+
}
|
|
106
|
+
return this;
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
// Get event log
|
|
110
|
+
getEventLog() {
|
|
111
|
+
return [...eventLog];
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
// Get type assertions (for type generation)
|
|
115
|
+
getTypeAssertions() {
|
|
116
|
+
return [...typeAssertions];
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
return api;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Helper to validate object shape
|
|
124
|
+
function validateShape(actual, shape, path) {
|
|
125
|
+
for (const [key, expectedType] of Object.entries(shape)) {
|
|
126
|
+
if (!(key in actual)) {
|
|
127
|
+
throw new Error(`Expected ${path} to have property ${key}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const actualValue = actual[key];
|
|
131
|
+
|
|
132
|
+
// Handle nested objects
|
|
133
|
+
if (typeof expectedType === 'object' && !Array.isArray(expectedType)) {
|
|
134
|
+
validateShape(actualValue, expectedType, `${path}.${key}`);
|
|
135
|
+
} else {
|
|
136
|
+
// Primitive type check
|
|
137
|
+
const actualType = typeof actualValue;
|
|
138
|
+
if (actualType !== expectedType) {
|
|
139
|
+
throw new Error(`Expected ${path}.${key} to be type ${expectedType}, got ${actualType}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Simple test runner
|
|
146
|
+
export function test(name, fn) {
|
|
147
|
+
try {
|
|
148
|
+
fn();
|
|
149
|
+
console.log(`✓ ${name}`);
|
|
150
|
+
return true;
|
|
151
|
+
} catch (error) {
|
|
152
|
+
console.error(`✗ ${name}`);
|
|
153
|
+
console.error(` ${error.message}`);
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Run multiple tests
|
|
159
|
+
export function runTests(tests) {
|
|
160
|
+
console.log('\n🧪 Running tests...\n');
|
|
161
|
+
|
|
162
|
+
let passed = 0;
|
|
163
|
+
let failed = 0;
|
|
164
|
+
|
|
165
|
+
for (const [name, fn] of Object.entries(tests)) {
|
|
166
|
+
if (test(name, fn)) {
|
|
167
|
+
passed++;
|
|
168
|
+
} else {
|
|
169
|
+
failed++;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
console.log(`\n📊 Results: ${passed} passed, ${failed} failed\n`);
|
|
174
|
+
|
|
175
|
+
return { passed, failed };
|
|
176
|
+
}
|