objs-core 1.1.1 → 2.0.1
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/EXAMPLES.md +1637 -0
- package/README.md +346 -75
- package/SKILL.md +500 -0
- package/objs.built.js +2657 -0
- package/objs.built.min.js +67 -0
- package/objs.d.ts +455 -0
- package/objs.js +3805 -0
- package/package.json +70 -37
- package/objs.1.1.js +0 -1205
- package/objs.1.1.js.zip +0 -0
- package/objs.1.1.min.js +0 -2
- package/objs.1.1.min.js.zip +0 -0
- package/objs.npm.1.1.js +0 -16
package/SKILL.md
ADDED
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
# Objs v2.0 — AI Skill File
|
|
2
|
+
|
|
3
|
+
Use this file as a `.cursorrules` attachment, system prompt, or `@SKILL.md` reference to teach an AI assistant how to work with the Objs library.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Library basics
|
|
8
|
+
|
|
9
|
+
### Loading
|
|
10
|
+
|
|
11
|
+
```html
|
|
12
|
+
<!-- Browser — dev source (includes test tools) -->
|
|
13
|
+
<script src="objs.js"></script>
|
|
14
|
+
|
|
15
|
+
<!-- Browser — distribution (node build.js → objs.built.js, objs.built.min.js) -->
|
|
16
|
+
<script src="objs.built.js"></script>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```js
|
|
20
|
+
// npm / bundler — correct file chosen automatically by package.json exports
|
|
21
|
+
import o from 'objs-core'; // resolves to objs.built.js
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### The `o()` function
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
o('#id') // → ObjsInstance wrapping all matching elements
|
|
28
|
+
o('.class') // → ObjsInstance wrapping all matching elements
|
|
29
|
+
o(domElement) // → ObjsInstance wrapping one DOM element
|
|
30
|
+
o([el1, el2]) // → ObjsInstance wrapping element array
|
|
31
|
+
o(2) // → ObjsInstance from o.inits[2] (previously inited component)
|
|
32
|
+
o() // → empty ObjsInstance, used to start init chains
|
|
33
|
+
o.first('#id') // → ObjsInstance, single element, same as querySelector
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Running Objs in Node (SSR)
|
|
39
|
+
|
|
40
|
+
In Node, **o.D** is **o.DocumentMVP** — there is no real `document` or `window`. You can run Objs in Node to render components to HTML (e.g. for SSR or for verification): use `o.init(states).render()`; the result is a tree of plain objects; serialize with the same SSR path the app uses (e.g. `o.D.parseElement`). To self-check generated Objs code, you can run a Node script that requires Objs, calls `o.init(...).render()`, and inspects the output or HTML string — no browser or user review required for structure verification. Call **`.html()`** (no arguments) on the rendered ObjsInstance to get that HTML string (in Node it uses `o.D.parseElement` under the hood).
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Component model
|
|
45
|
+
|
|
46
|
+
A component is a **states object** passed to `o.init()`. Every key becomes a method on the component instance.
|
|
47
|
+
|
|
48
|
+
### State object keys
|
|
49
|
+
|
|
50
|
+
| Key | Meaning |
|
|
51
|
+
|---|---|
|
|
52
|
+
| `name` | Component name string — used for `o.autotag` data attribute |
|
|
53
|
+
| `render` | Reserved: defines the DOM element to create (tag, attributes, html) |
|
|
54
|
+
| `tag` / `tagName` | HTML element type (default: `div`) |
|
|
55
|
+
| `html` / `innerHTML` | Inner HTML of the element |
|
|
56
|
+
| `class` / `className` | CSS class (`className` is a React-familiar alias) |
|
|
57
|
+
| `style` | Inline style string or object |
|
|
58
|
+
| `dataset` | Object of `data-*` attributes |
|
|
59
|
+
| `events` | Object of `{eventName: handler}` added on creation |
|
|
60
|
+
| any other key | HTML attribute set via `setAttribute` |
|
|
61
|
+
|
|
62
|
+
**ObjsInstance properties after init:**
|
|
63
|
+
|
|
64
|
+
| Property | Description |
|
|
65
|
+
|---|---|
|
|
66
|
+
| `refs` | Object of `{ name: ObjsInstance }` — auto-populated from `ref="name"` child elements on `init` |
|
|
67
|
+
| `store` | Plain object for storing child components and other per-instance data |
|
|
68
|
+
|
|
69
|
+
### State function signature
|
|
70
|
+
|
|
71
|
+
Every non-render state is a function:
|
|
72
|
+
|
|
73
|
+
```js
|
|
74
|
+
stateName: ({ self, o, i, parent }, data) => {
|
|
75
|
+
// self — the ObjsInstance (has all methods: .first(), .html(), .attr(), .on(), etc.)
|
|
76
|
+
// o — the o() function for creating new instances or querying the global DOM
|
|
77
|
+
// i — index of current element in multi-element instances
|
|
78
|
+
// parent — the ObjsInstance this component was appendInside() into, or null
|
|
79
|
+
// data — argument passed when the state is called: component.stateName(data)
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Inside a state function, `self` is the ObjsInstance. Use `self.first()`, `self.html()`, `self.attr()` etc. directly — no need to re-wrap it with `o(self)`.
|
|
84
|
+
|
|
85
|
+
### Creating a component
|
|
86
|
+
|
|
87
|
+
```js
|
|
88
|
+
const buttonStates = {
|
|
89
|
+
name: 'SubmitButton', // sets data-qa="submit-button" if o.autotag = "qa"
|
|
90
|
+
render: { tag: 'button', class: 'btn', html: 'Submit' },
|
|
91
|
+
disable: ({ self }) => { self.attr('disabled', 'true'); },
|
|
92
|
+
enable: ({ self }) => { self.attr('disabled', null); }, // null removes the attribute
|
|
93
|
+
setLabel:({ self }, text) => { self.html(text); },
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const btn = o.init(buttonStates).render(); // creates the DOM element
|
|
97
|
+
btn.appendInside('#form'); // inserts it
|
|
98
|
+
btn.disable(); // calls the disable state
|
|
99
|
+
btn.setLabel('Saving...'); // calls with data
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Shorthand for simple elements
|
|
103
|
+
|
|
104
|
+
```js
|
|
105
|
+
// Render a single element without explicit states object
|
|
106
|
+
o.initState({ tag: 'span', class: 'badge', html: '3' }).appendInside('.nav');
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Selecting and querying elements
|
|
112
|
+
|
|
113
|
+
```js
|
|
114
|
+
o('.card').first('h3').html('New title'); // find first h3 inside each .card, set text
|
|
115
|
+
o('.card').find('button').on('click', fn); // find all buttons inside all .cards
|
|
116
|
+
o('.card').select(0).addClass('featured'); // operate only on first .card
|
|
117
|
+
|
|
118
|
+
const el = o.first('.card').el; // raw DOM element
|
|
119
|
+
const els = o('.card').els; // raw DOM array
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Events
|
|
125
|
+
|
|
126
|
+
```js
|
|
127
|
+
o(component).on('click', handler);
|
|
128
|
+
o(component).on('click, mouseover', handler); // multiple events
|
|
129
|
+
o(component).off('click', handler);
|
|
130
|
+
o(component).offAll(); // remove all listeners
|
|
131
|
+
o(component).offAll('click'); // remove all click listeners
|
|
132
|
+
|
|
133
|
+
// Event delegation (listen on parent, match children)
|
|
134
|
+
o('#list').onDelegate('click', '.item', handler);
|
|
135
|
+
o('#list').offDelegate('click'); // removes all delegated listeners for event type
|
|
136
|
+
|
|
137
|
+
// Parent listener (one parent element; listener runs when event.target is inside Objs elements)
|
|
138
|
+
o('#list').onParent('click', '.parentBlock', handler);
|
|
139
|
+
o('#list').offParent('click', '.parentBlock');
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Granular store updates — the key pattern
|
|
145
|
+
|
|
146
|
+
**Rule: define one state method per data slice. Never call `.render()` to update — it recreates the element. Use targeted state methods for updates.**
|
|
147
|
+
|
|
148
|
+
```js
|
|
149
|
+
// WRONG — recreates the entire DOM element on every store change
|
|
150
|
+
o.connectRedux(store, s => s, card, 'render');
|
|
151
|
+
|
|
152
|
+
// CORRECT — each update only touches the affected part
|
|
153
|
+
const cardStates = {
|
|
154
|
+
render: { tag: 'div', html: '<span class="name"></span> <span class="score"></span>' },
|
|
155
|
+
updateName: ({ self }, data) => { self.first('.name').html(data); },
|
|
156
|
+
updateScore: ({ self }, data) => { self.first('.score').html(data); },
|
|
157
|
+
};
|
|
158
|
+
const card = o.init(cardStates).render();
|
|
159
|
+
o.connectRedux(store, s => s.userName, card, 'updateName'); // only .name updates
|
|
160
|
+
o.connectRedux(store, s => s.userScore, card, 'updateScore'); // only .score updates
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
The `transform()` function inside the library already skips attributes whose value hasn't changed. Targeted state methods give you direct writes — O(1) per update, no diff.
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Store adapters
|
|
168
|
+
|
|
169
|
+
### Redux
|
|
170
|
+
|
|
171
|
+
```js
|
|
172
|
+
// Returns an unsubscribe function
|
|
173
|
+
const unsub = o.connectRedux(
|
|
174
|
+
store, // Redux store
|
|
175
|
+
state => state.cartItems, // selector
|
|
176
|
+
menuCart, // Objs component
|
|
177
|
+
'updateCount' // state method to call
|
|
178
|
+
);
|
|
179
|
+
unsub(); // disconnect
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### MobX
|
|
183
|
+
|
|
184
|
+
```js
|
|
185
|
+
// Returns a disposer function
|
|
186
|
+
const dispose = o.connectMobX(
|
|
187
|
+
mobx, // MobX instance
|
|
188
|
+
appStore, // observable
|
|
189
|
+
obs => obs.cartItems, // accessor
|
|
190
|
+
menuCart,
|
|
191
|
+
'updateCount'
|
|
192
|
+
);
|
|
193
|
+
dispose();
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### o.newLoader (built-in, for fetch/promise)
|
|
197
|
+
|
|
198
|
+
```js
|
|
199
|
+
const loader = o.newLoader(o.get('/api/data')); // fires the request
|
|
200
|
+
component.connect(loader, 'render'); // calls component.render(data) when ready
|
|
201
|
+
component.connect(loader, 'render', 'showError'); // optional fail state
|
|
202
|
+
loader.reload(o.get('/api/data?page=2')); // refetch with new request
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### o.createStore (built-in reactive store)
|
|
206
|
+
|
|
207
|
+
```js
|
|
208
|
+
const cartStore = o.createStore({ items: [], total: 0 });
|
|
209
|
+
|
|
210
|
+
// Subscribe components — they receive store props merged into their state context
|
|
211
|
+
cartStore.subscribe(cartBadge, 'sync'); // cartBadge.sync({ items, total, self, o, i }) on each notify
|
|
212
|
+
cartStore.subscribe(cartDrawer, 'sync');
|
|
213
|
+
|
|
214
|
+
// Update and notify all subscribers
|
|
215
|
+
cartStore.items.push(product);
|
|
216
|
+
cartStore.total += product.price;
|
|
217
|
+
cartStore.notify(); // fires both cartBadge.sync() and cartDrawer.sync()
|
|
218
|
+
|
|
219
|
+
// Reset to original defaults
|
|
220
|
+
cartStore.reset();
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Plain callback (no adapter needed)
|
|
224
|
+
|
|
225
|
+
```js
|
|
226
|
+
const store = { value: 0, listeners: [] };
|
|
227
|
+
const notify = () => store.listeners.forEach(fn => fn(store.value));
|
|
228
|
+
|
|
229
|
+
store.listeners.push((v) => counter.updateCount(v));
|
|
230
|
+
store.value++;
|
|
231
|
+
notify();
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## Component composition — nesting
|
|
237
|
+
|
|
238
|
+
A parent component stores child instances in its `.store` object. Children append inside the parent's element.
|
|
239
|
+
|
|
240
|
+
```js
|
|
241
|
+
const parentStates = {
|
|
242
|
+
name: 'ParentCard',
|
|
243
|
+
render: { tag: 'div', class: 'card', html: '<div class="header"></div><div class="body"></div>' },
|
|
244
|
+
init: ({ self }) => {
|
|
245
|
+
// Create children and store references
|
|
246
|
+
self.store.header = o.init(headerStates).render().appendInside(self.first('.header').el);
|
|
247
|
+
self.store.body = o.init(bodyStates).render().appendInside(self.first('.body').el);
|
|
248
|
+
},
|
|
249
|
+
// Update only the relevant child — no rerender of parent
|
|
250
|
+
updateTitle: ({ self }, title) => { self.store.header.setTitle(title); },
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const card = o.init(parentStates).render().appendInside('#app');
|
|
254
|
+
card.init(); // sets up children
|
|
255
|
+
card.updateTitle('New title');
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
To access all components:
|
|
259
|
+
```js
|
|
260
|
+
o.inits // array of all inited components
|
|
261
|
+
o.getStores() // array of all .store objects
|
|
262
|
+
o.getListeners() // array of all .ie (event listener maps)
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## React integration — three modes
|
|
268
|
+
|
|
269
|
+
### Mode 1: Objs inside a React ref (most common)
|
|
270
|
+
|
|
271
|
+
```jsx
|
|
272
|
+
function ProductSection() {
|
|
273
|
+
const ref = React.useRef(null);
|
|
274
|
+
|
|
275
|
+
React.useEffect(() => {
|
|
276
|
+
const grid = o.init(gridStates).render().appendInside(ref.current);
|
|
277
|
+
return () => grid.unmount(); // cleanup on unmount
|
|
278
|
+
}, []);
|
|
279
|
+
|
|
280
|
+
return <div ref={ref} />;
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### Mode 2: Objs element as a React element
|
|
285
|
+
|
|
286
|
+
```jsx
|
|
287
|
+
// In a React component:
|
|
288
|
+
const badge = o.init(badgeStates).render();
|
|
289
|
+
const reactEl = badge.prepareFor(React.createElement); // returns React element
|
|
290
|
+
return <div>{reactEl}</div>;
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Mode 3: Context bridge (shared state)
|
|
294
|
+
|
|
295
|
+
```jsx
|
|
296
|
+
const CartContext = React.createContext(null);
|
|
297
|
+
|
|
298
|
+
// In the React tree:
|
|
299
|
+
const CartBridge = o.withReactContext(React, CartContext, ctx => ctx.items, menuCart, 'updateCount');
|
|
300
|
+
// Mount anywhere inside <CartContext.Provider>:
|
|
301
|
+
return <CartContext.Provider value={cartState}><CartBridge /><OtherComponents /></CartContext.Provider>;
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
`CartBridge` renders nothing (`return null`). It calls `menuCart.updateCount(ctx.items)` on every context change — no React rerender of anything.
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
## QA autotag & React integration
|
|
309
|
+
|
|
310
|
+
```js
|
|
311
|
+
o.autotag = 'qa'; // set once, globally, before init() calls
|
|
312
|
+
|
|
313
|
+
// Each component with states.name gets data-qa="kebab-name" automatically
|
|
314
|
+
const btn = o.init({ name: 'SubmitButton', render: { tag: 'button' } }).render();
|
|
315
|
+
// Result: <button data-qa="submit-button" data-o-state="render" ...>
|
|
316
|
+
|
|
317
|
+
// Use in Playwright / Cypress:
|
|
318
|
+
// page.getByTestId('submit-button') (with testIdAttribute: 'data-qa')
|
|
319
|
+
// cy.get('[data-qa="submit-button"]')
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
`o.autotag` is present in all builds — QA teams need stable selectors in staging/production.
|
|
323
|
+
|
|
324
|
+
### o.reactQA — bolt-on for React projects
|
|
325
|
+
|
|
326
|
+
```jsx
|
|
327
|
+
// Returns { 'data-qa': 'kebab-name' } for spreading onto React JSX elements
|
|
328
|
+
// Converts CamelCase to kebab-case. Respects o.autotag value.
|
|
329
|
+
<button {...o.reactQA('CheckoutButton')} onClick={fn}>Checkout</button>
|
|
330
|
+
// → <button data-qa="checkout-button">
|
|
331
|
+
|
|
332
|
+
// Works even when o.autotag is undefined (defaults to 'data-qa')
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
## Dev/prod split
|
|
338
|
+
|
|
339
|
+
`objs.js` is the source; `node build.js` produces `objs.built.js` and `objs.built.min.js`. Only the debug flag and debug logging are behind `__DEV__`; all other API is present in every build.
|
|
340
|
+
|
|
341
|
+
**Behind `__DEV__` (stripped when `__DEV__` is false):**
|
|
342
|
+
- `o.debug` flag and debug logging in `returner()`, `result.debug()`
|
|
343
|
+
|
|
344
|
+
**Always present (all builds — objs.js, objs.built.js, objs.built.min.js):**
|
|
345
|
+
- All DOM manipulation methods, states, events (including `onDelegate`, `offDelegate`, `onParent`, `offParent`)
|
|
346
|
+
- `o.autotag`, `o.reactQA`
|
|
347
|
+
- `o.startRecording`, `o.stopRecording`, `o.exportTest`, `o.exportPlaywrightTest`, `o.clearRecording`, `o.playRecording`
|
|
348
|
+
- `o.test`, `o.addTest`, `o.runTest`, `o.testUpdate`, `o.testOverlay`, `o.testConfirm` (assessors on staging can replay and see auto + manual results)
|
|
349
|
+
- `o.measure`, `o.assertVisible`, `o.assertSize` (layout assertions for tests)
|
|
350
|
+
- `o.newLoader`, `o.connectRedux`, `o.connectMobX`, `o.withReactContext`
|
|
351
|
+
- `o.route`, `o.router`, `o.inc`, `o.ajax`, `o.get`, `o.post`
|
|
352
|
+
- `o.setCookie`, `o.getCookie`, storage helpers
|
|
353
|
+
- `o.verify`, `o.safeVerify`, `o.specialTypes`
|
|
354
|
+
|
|
355
|
+
> **Security note:** `o.startRecording()` intercepts `window.fetch` and captures request/response bodies. Appropriate for staging; review before enabling on production.
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
## Testing patterns
|
|
360
|
+
|
|
361
|
+
### Unit test
|
|
362
|
+
|
|
363
|
+
```js
|
|
364
|
+
o.addTest('Button states',
|
|
365
|
+
['renders correctly', () => {
|
|
366
|
+
const btn = o.init(buttonStates).render();
|
|
367
|
+
return btn.el?.tagName === 'BUTTON';
|
|
368
|
+
}],
|
|
369
|
+
['disable works', () => {
|
|
370
|
+
btn.disable();
|
|
371
|
+
return btn.el?.disabled === true;
|
|
372
|
+
}],
|
|
373
|
+
);
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### With lifecycle hooks
|
|
377
|
+
|
|
378
|
+
```js
|
|
379
|
+
o.addTest('Cart updates',
|
|
380
|
+
['adds item', () => { cartAdd({ id: 1 }); return cartStore.items.length === 1; }],
|
|
381
|
+
{ before: () => { cartStore.items = []; }, after: () => { cartStore.items = []; } }
|
|
382
|
+
);
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### Measurement assertions
|
|
386
|
+
|
|
387
|
+
```js
|
|
388
|
+
o.addTest('Layout',
|
|
389
|
+
['menu is visible', () => o.assertVisible(menu.el)],
|
|
390
|
+
['button has correct width', () => o.assertSize(btn.el, { w: 120 })],
|
|
391
|
+
);
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### Recording → Objs test
|
|
395
|
+
|
|
396
|
+
```js
|
|
397
|
+
// Available in all builds — record a session
|
|
398
|
+
o.startRecording();
|
|
399
|
+
// ... user interacts ...
|
|
400
|
+
const recording = o.stopRecording();
|
|
401
|
+
console.log(o.exportTest(recording)); // paste into your codebase as o.addTest()
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### Recording → Playwright CI test
|
|
405
|
+
|
|
406
|
+
```js
|
|
407
|
+
// Available in all builds — works on any DOM including React
|
|
408
|
+
o.startRecording('#app'); // optional: scope MutationObserver to selector; returns observeRoot in recording
|
|
409
|
+
// ... QA tester uses the app ...
|
|
410
|
+
const recording = o.stopRecording();
|
|
411
|
+
// recording.assertions, recording.observeRoot when scoped
|
|
412
|
+
console.log(o.exportPlaywrightTest(recording, { testName: 'Checkout flow' }));
|
|
413
|
+
// → paste into tests/checkout.spec.ts → npx playwright test
|
|
414
|
+
|
|
415
|
+
// Options:
|
|
416
|
+
// { testName: 'My flow' } — sets test() name
|
|
417
|
+
// { baseUrl: '/app' } — overrides page.goto() path
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
Generated output includes:
|
|
421
|
+
- `page.route()` mocks for every intercepted `fetch` call
|
|
422
|
+
- `page.goto(relativePath)` — needs `baseURL` in playwright.config.ts
|
|
423
|
+
- Typed locator steps: `.fill()`, `.click()`, `.check()`, `.selectOption()`, `.hover()`
|
|
424
|
+
- Auto-inserted `expect()` from `recording.assertions` (visible, toContainText, class comments)
|
|
425
|
+
|
|
426
|
+
### Manual check overlay
|
|
427
|
+
|
|
428
|
+
`o.testConfirm(label, items?, opts?)` — All builds. Shows a draggable overlay "Label: Paused" with an optional checklist; returns `Promise<{ ok, errors? }>`. Use after replay for items that can't be asserted automatically (e.g. hover).
|
|
429
|
+
|
|
430
|
+
```js
|
|
431
|
+
const r = await o.testConfirm('Manual check', ['Hover effect exists']);
|
|
432
|
+
if (!r.ok) console.warn(r.errors);
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
### Reload-based e2e
|
|
436
|
+
|
|
437
|
+
```js
|
|
438
|
+
// Survives page reloads — useful for testing navigation
|
|
439
|
+
const { autorun } = o.addTest('Page flow',
|
|
440
|
+
['page title is correct', () => document.title === 'Home'],
|
|
441
|
+
['nav link works', (info) => {
|
|
442
|
+
// mark as async, navigate, check on next load
|
|
443
|
+
o.testUpdate(info, document.title === 'Products');
|
|
444
|
+
}],
|
|
445
|
+
);
|
|
446
|
+
autorun(); // runs all tests in sequence, reloading between steps
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
### Test overlay (auto tests + manual results)
|
|
450
|
+
|
|
451
|
+
`o.testOverlay()` — All builds. Call once per page; shows a fixed 🧪 Tests button. For assessors: after replay, open the overlay to see if all auto tests passed and which manual checks failed.
|
|
452
|
+
|
|
453
|
+
```js
|
|
454
|
+
o.testOverlay(); // call once — shows a fixed button with live results (o.tLog / o.tRes)
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
---
|
|
458
|
+
|
|
459
|
+
## Runtime verification (o.verify, o.specialTypes)
|
|
460
|
+
|
|
461
|
+
Use **o.verify(pairs, safe?)** to check types at runtime—useful for function arguments, config, or API responses. **pairs** is an array of `[value, expectedTypes]` (e.g. `[[id, ['number']], [opts, ['object','undefined']]]`). Built-in: `typeof` names plus **o.specialTypes**: `notEmptyString`, `array`, `promise`. On failure: throws (or returns Error if **safe** true). **o.safeVerify(pairs)** returns boolean.
|
|
462
|
+
|
|
463
|
+
**Add global validators** by extending **o.specialTypes**: `o.specialTypes.myType = (val, type) => ...`. They are then available everywhere (your code and Objs internals), so you can use `o.verify([x, ['myType']])` consistently.
|
|
464
|
+
|
|
465
|
+
---
|
|
466
|
+
|
|
467
|
+
## Do / Don't
|
|
468
|
+
|
|
469
|
+
| Do | Don't |
|
|
470
|
+
|---|---|
|
|
471
|
+
| Use `self.first()`, `self.html()` etc. directly inside state functions | Re-wrap self: `o(self).first()` — works but is redundant |
|
|
472
|
+
| Call `.render()` once to create the element | Call `.render()` again to update — re-evaluates the render function, recreating any child components built inside it |
|
|
473
|
+
| Define one state method per data slice | Put all update logic in a single `update` state |
|
|
474
|
+
| Build child components in an `init` state or factory, store in `self.store` | Build child components inside the `render` function — they duplicate on every re-render |
|
|
475
|
+
| Set `o.autotag` before any `o.init()` calls | Set `o.autotag` after components have already rendered |
|
|
476
|
+
| Use `self.store` to hold child component references | Store children in external variables that may close over stale refs |
|
|
477
|
+
| Call `grid.unmount()` in React `useEffect` cleanup | Leave Objs components running after their React parent unmounts |
|
|
478
|
+
| Create Objs components inside `useEffect`, not in the React component body | Create Objs components in the React component body — they are recreated on every React render |
|
|
479
|
+
| Use `o.connectRedux` / `o.connectMobX` for live store connections | Manually subscribe and call `component.render()` on each change |
|
|
480
|
+
| Use `objs.built.js` or `objs.built.min.js` for distribution | Rely on `objs.prod.js` / `objs.dev.js` (no longer generated) |
|
|
481
|
+
| Use `states.name` for QA autotag | Manually add `data-qa` attributes — autotag keeps them in sync |
|
|
482
|
+
| Use `.val()` to get/set input value: `field.first('input').val('new')` | Access raw DOM: `field.first('input').el.value = 'new'` |
|
|
483
|
+
| Use `self.refs.name` to access named child elements | Use `self.first('[ref="name"]')` — refs gives ObjsInstance directly |
|
|
484
|
+
| Use `attr('disabled', null)` to remove an attribute | Use `attr('disabled', '')` — empty string now _sets_ the attribute to `""` |
|
|
485
|
+
| Use `css(null)` to remove the `style` attribute entirely | Use `css({})` or `style('')` — those no longer remove the style |
|
|
486
|
+
| Use `o.reactQA('ComponentName')` for stable React test selectors | Write `data-testid` manually — `reactQA` converts CamelCase to kebab automatically |
|
|
487
|
+
|
|
488
|
+
---
|
|
489
|
+
|
|
490
|
+
## Full examples
|
|
491
|
+
|
|
492
|
+
See [EXAMPLES.md](EXAMPLES.md) for architecture guide and complete walkthroughs, and [examples.js](examples.js) for paste-and-run code.
|
|
493
|
+
|
|
494
|
+
**EXAMPLES.md sections:**
|
|
495
|
+
1. How render works — plain object, function, HTML string, multi-instance, `append`, `children`, `ref`/`refs`
|
|
496
|
+
2. Single components — atoms (Button, Badge, Field) with `val()`, `css(null)`, `addClass` spread
|
|
497
|
+
3. Nesting & composition — slot pattern, `append` in render, factory with dynamic children
|
|
498
|
+
4. Design system architecture — Atoms → Molecules → Organisms, `self.store`, update efficiency table
|
|
499
|
+
5. Real-world examples — menu, cart+cards, dialog, drawer+URL, complex form
|
|
500
|
+
6. React integration — four modes including bolt-on Playwright recording with `o.reactQA`
|