objs-core 2.3.0 → 2.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/README.md CHANGED
@@ -1,618 +1,561 @@
1
- # Objs
2
- > Fast and simple library to speed up developing by AI context friendly architecture, auto-tests recording, cache control and other. Develop new features without rewriting anything. Works standalone or alongside React. Examples and full documentation: [Full Documentation](https://en.fous.name/objs/documentation)
3
-
4
- **AI-friendly** — one file, `SKILL.md` primer (~6,000 tokens). An LLM generates correct Objs code from a description without JSX, virtual DOM, or React lifecycle knowledge.
5
-
6
- **React-developer-friendly** — familiar `className`, `ref`/`refs`, `o.createStore`. Add one script tag to an existing React app and get Playwright test generation without touching any components.
7
-
8
- **Live examples** — real patterns in [`examples/`](https://foggysq.github.io/objs/examples/), narrative walkthroughs in EXAMPLES.md. For AI assistants: use SKILL.md as `@SKILL.md` or system prompt.
9
-
10
- ---
11
-
12
- ## Why Objs
13
-
14
- **Core functionality** Record user actions in the browser, export ready-to-commit tests, and run them in CI. One script; no separate recorder or test-ID maintenance.
15
-
16
- - **Record → Playwright in one pipeline** — `o.startRecording()` captures click, input, change, scroll; `o.stopRecording()` returns actions and auto-generated assertions; `o.exportPlaywrightTest(recording)` outputs a `.spec.ts` with locators and network mocks. Paste into your repo and run `npx playwright test`.
17
- - **CI support** — Export runs in all builds (including prod). QA or assessors record on staging; paste the generated Playwright test into your test suite; CI runs it with no extra config. Optional `o.exportTest(recording)` for Objs-style tests.
18
- - **Store and update model** — `o.createStore()` + `subscribe`/`notify`: no virtual DOM, no re-render cascade; only subscribed components update their own DOM (O(1) per subscriber).
19
- - **One library, many roles** — DOM + state + routing + AJAX + cache (`o.inc`) + tests + recording + SSR. No extra test runner or recorder product. Built-in `o.route()` / `o.router()` no separate router dependency.
20
- - **Stable selectors and UI checks** — `{...o.reactQA('ComponentName')}` or `o.autotag`; recorder uses `data-qa` and list indices. `o.assertSize(el, { w, h, padding, margin })` for design system verification; `o.testConfirm()` for manual/hover checks after replay.
21
- - **Works standalone or with React** — Add one script tag to an existing React app; no architecture change. Familiar `className`, `ref`/`refs`, `o.createStore`. Built-in SSR (Node) with `DocumentMVP` and in-browser hydration.
22
- - **AI-friendly** One file, ~6,000-token `SKILL.md` primer. No JSX, virtual DOM, or React lifecycle; fewer tokens than typical React context for runnable output. No stale closures, dependency arrays, or re-render cascades. Same code runs in Node (SSR) so tools can verify output without a browser — **verify generated code without user review**: run `o.init(states).render()` in Node, serialize or assert structure before returning to the user.
23
-
24
- [Full comparison and live demo](https://foggysq.github.io/objs/examples/ai-workflow/index.html)
25
-
26
- ---
27
-
28
- ### Update v2.3: New features
29
- - **Recording: extended events** — Default events now include `submit`, `keydown`, `focus`, `blur` in addition to click, input, change, scroll, mouseover.
30
- - **Recording: extended attributes** — MutationObserver records `style`, `hidden`, `disabled`, `aria-expanded`, `aria-checked` (not just class). Assertions and Playwright export support all types.
31
- - **Playwright export: real expect()** — All assertion types emit `expect()` calls: `toHaveClass`, `toHaveCSS`, `toBeHidden`, `toBeDisabled`, `toHaveAttribute` for aria-*.
32
- - **Network: XHR interception** — Captures `XMLHttpRequest` alongside `fetch`; GET/POST/PUT with request and response body stored in mocks.
33
- - **Playwright export: route verification** Generated route handlers verify request method and body (POST/PUT) before fulfilling.
34
- - **WebSocket monitoring** — Records WebSocket connections and messages (in/out); Playwright export includes `framereceived`/`framesent` assertions.
35
- - **Recording: blur/focus on removed elements** — Blur and focus events on elements removed by the previous action (e.g. click delete) are not recorded.
36
- - **Test overlay: async steps** — `onComplete` callback now runs when manual check (Promise) resolves; overlay counter and panel display correctly.
37
-
38
- ---
39
-
40
- ### Update v2.2: New features
41
- - **<div>${objInstance}</div>** — Objs instance has `toString()` and `Symbol.toPrimitive`; use in template literals without `.html()` call. The HTML is inserted and auto-hydrated when the parent sets `innerHTML` (e.g. html: &#96;&lt;div&gt;${child}&lt;/div&gt;&#96; in render()).
42
- - **o.playRecording(recording, opts)** — Extended options: `runAssertions`, `root`, `actionDelay`, `manualChecks`, `onComplete`. Assertions verification and manual checks are natively supported. [Recording example](https://foggysq.github.io/objs/examples/recording/index.html) updated.
43
- - **o.test(title, ..., { confirmOnFailure, confirmOnFailureTimeout })** — If a step fails, show overlay "Continue?" / "Stop" instead of aborting. Use `confirmOnFailure: true` and optionally `confirmOnFailureTimeout` (ms).
44
- - **o.test(title, ..., { sync: true })** — Run steps synchronously (one after another) instead of async; useful for playRecording.
45
- - **o.sleep(ms)** — Returns a Promise that resolves after `ms` milliseconds. Used by exportTest and playRecording for action delays.
46
- - **o.exportTest(recording, options?)** — New `options.delay` (default 16ms). Emits `await o.sleep(delay)` at end of each action step. Pass `{ delay: 0 }` to omit.
47
- - **o.testConfirm(label, items?, opts?)** — `opts.timeout` added for countdown before auto-close.
48
- - **o.overlay(opts)** — Common draggable overlay (used by testConfirm, testOverlay, confirmOnFailure). Public API for custom overlays.
49
- - **o.runRecordingAssertions(recording, root?, actionIdx?)** — Public API to run recording assertions against the DOM.
50
- - **Recording: removedElements** — MutationObserver now records removed nodes; playback skips assertions for removed elements.
51
- - **Test overlay** — Cursor `grab` only on draggable areas; `o-overlay-bar` max-height limited to 90vh.
52
-
53
- #### Breaking changes (migrate if affected)
54
- - **o.exportTest(recording)** — Default delay is now 16ms (was 0). Generated code includes `async` and `await o.sleep(16)` in action steps. To restore old behavior: `o.exportTest(recording, { delay: 0 })`.
55
- - **o.playRecording(recording, { runAssertions: true })** — Return value is now `{ testId }` instead of `testId` (number). Use `const { testId } = o.playRecording(...)` when using runAssertions.
56
-
57
-
58
-
59
- ### Update v2.1: New features
60
- - **`refs` on ObjsInstance** — array data support for auto-collect `ref="name"` child elements. Use `.select(i)` to choose not only element but also its refs. The default `.refs` contains the first element children.
61
- - **self.select(e).refs...** — use `.select(e)` in render and other actions in event handlers to get Objs instance with the e.target from self and controll refs.
62
- - **Auto HTML hydration** — when render sets `innerHTML` with markup built from inited children (e.g. `html: header.html() + field.html()` in the parent’s render), Objs automatically binds those same instances to the new DOM nodes inside the container. The parent’s stored references (e.g. `self.store.field`) then point at the real elements, and events/refs work. It makes JSX-like code from native HTML sample strings.
63
-
64
-
65
- ### Update v2.0: New features for micro-service architecture and AI development
66
-
67
- #### Breaking changes (migrate first)
68
- - **Build output** — Distribution files are now `objs.built.js` and `objs.built.min.js` (from `node build.js`). `objs.prod.js` / `objs.dev.js` are no longer produced; use the built files or `objs.js` for development.
69
- - **`attr(name, value)`** `null` removes the attribute; `""` sets it to empty string. Previously `""` could remove; now use `attr(name, null)` to remove.
70
- - **`style(value)`** — `null` removes the `style` attribute entirely. Use `style(null)` or `css(null)` to clear; empty string no longer removes.
71
- - **`css(object)`** — `null` removes the `style` attribute entirely. Use `css(null)` to clear; `css({})` or `style('')` no longer remove.
72
- - **`addClass(...cls)`** / **`removeClass(...cls)`** — Now accept multiple arguments: `addClass('a', 'b', 'c')`. Single-string usage remains valid.
73
-
74
- #### New
75
- - **`o.startRecording()`** — QA testers can start actions recording on staging
76
- - **`o.stopRecording()`** — Stop recording
77
- - **`o.exportTest()`** — Export tests code as ready to review and use
78
- - **Build** `objs.js` is the source (development); `node build.js` produces `objs.built.js` and `objs.built.min.js` (ESM + window.o) for distribution.
79
- - **`o.assertSize(el, expected)`** — Can compare padding and margin (design system / UI verification): `padding`, `paddingTop`/`Right`/`Bottom`/`Left`, `margin`, `marginTop`/`Right`/`Bottom`/`Left` in addition to `w` and `h`
80
- - **`val([value])`** Get/set `.value` on `input`/`textarea`/`select`
81
- - **`refs` on ObjsInstance** — Auto-collects `ref="name"` child elements as ObjsInstances on `init`
82
- - **`className` in render** — Alias for `class` in render descriptors (React familiarity)
83
- - **`o.createStore(obj)`** — Reactive store with `subscribe` / `notify` / `reset`
84
- - **`o.reactQA(name)`** — Returns `{ 'data-qa': 'kebab-name' }` for React JSX spread
85
- - **`o.exportPlaywrightTest(recording, options?)`** — Generates a ready-to-run Playwright `.spec.ts` from a recording
86
- - **Tests auto-generation from user recordings** — record interactions, export as committed test code
87
- - **Redux / MobX / React Context adapters** with granular-update listener pattern
88
- - **TypeScript definitions** (`objs.d.ts`) covering the full public API
89
- - **QA autotag** (`o.autotag`) — auto-sets `data-qa` from component name
90
- - **Test overlay** — fixed UI panel with results and JSON export
91
- - Data loaders/stores and connection to component state updates
92
- - SSR (Server side render) and in-browser hydration
93
- - Tests with page reload, Cookies and SS/LS deletion for e2e
94
-
95
- #### Fixes
96
- - Object.assign() for state props to save input consistency
97
- - `attr(name, null)` now correctly removes attributes; `attr(name, '')` sets empty string
98
- - if test() gets a verification title without test function, it logs it as a text divider
99
- - `.html('')` sets innerHTML to `''`
100
- - append attribute in state adds child nodes, childNodes/children — replace and add children
101
- - `.add(element)` works only for got elements (not inited)
102
- - all Objs Cookies/LS/SS names start with `'oTest-'` or `'oInc-'`
103
- - use `o().store = {}` to save component data instead of root properties
104
- - `console.error()` for error output by default
105
- - `o.inc()` can cache files from urls starting with protocol, if `o.incCors` is false (default)
106
- - `o.first()` gets one element and runs `select(0)` automatically
107
- - added parent check for `remove()` method
108
-
109
-
110
- ## Get started
111
-
112
- **Browser** source with test tools:
113
- ```html
114
- <script src="objs.js" type="text/javascript"></script>
115
- ```
116
-
117
- **Browser (smaller)** minified `objs.built.min.js` for production. Use `type="module"`:
118
- ```html
119
- <script src="objs.min.js" type="module"></script>
120
- ```
121
-
122
- **npm / bundler** — correct file chosen automatically via `package.json` exports:
123
- ```js
124
- import o from 'objs-core'; // resolves to objs.built.js
125
- ```
126
-
127
- ```
128
- npm i objs-core
129
- ```
130
-
131
-
132
-
133
-
134
- ## Features
135
-
136
- #### Develop
137
- - Samples and state control
138
- - Data store, Cookies and LS/SS control
139
- - Events delegation
140
-
141
- #### Test
142
- - Sync/async, tests with reload
143
- - Console & HTML output
144
- - Autotests
145
-
146
- #### Optimize
147
- - Separate logic and samples
148
- - Native micro-service architecture
149
- - Async loading and preloading, cache
150
-
151
-
152
-
153
-
154
- ## Main principles
155
-
156
- ### Dynamic content
157
-
158
- #### Create sample
159
-
160
- To control elements Objs uses states. State - it's an information how to create or change DOM element. To create an element use `render` state with html (inner HTML) and tag attributes:
161
- ```
162
- // state called render for timer example
163
- const timerStates = {
164
- render: {
165
- class: 'timer',
166
- html: 'Seconds: <span ref="n">0</span>',
167
- }
168
- }
169
- ```
170
- - `render` could be a string if you use HTML samples (see [documentation](https://fous.name/objs/documentation/?parameter=value#states)):
171
- `'<div class="timer">Seconds:<span>0</span></div>'`
172
- - default tag is `div` (if tag is undefined)
173
- - attributes `dataset` and `style` can be object type
174
- - to append elements inside - use `append` with DOM element/Objs or an array of them as a value
175
-
176
- #### States
177
-
178
- Then add a new state that will start and finish counting. Number will be stored in the object itself - `self` object. So the state will be a function that gets `self`, creates a variable, increments it by interval and shows as innerHTML of `span`:
179
- ```
180
- // new timer states object
181
- const timerStates = {
182
- render: {
183
- class: 'timer',
184
- html: 'Seconds: <span ref="n">0</span>',
185
- },
186
- start: ({self}) => {
187
- self.n = self.n || 0;
188
- self.interval = setInterval(() => {
189
- self.n++;
190
- self.refs.n.html(self.n);
191
- }, 1000);
192
- },
193
- stop: ({self}) => {
194
- clearInterval(self.interval);
195
- }
196
- }
197
- ```
198
- - every state gets object with
199
- `self` - Objs object
200
- `o` - o-function to use inside
201
- `i` - index of the current element in Objs object
202
-
203
- #### Append in DOM
204
-
205
- The last thing is to create and append element on the page. To do this - init states, render object and start timer... And also - append it.
206
- ```
207
- // create and start timer
208
- const timer = o.init(timerStates)
209
- .render()
210
- .start()
211
- .appendInside('#simpleTimer');
212
-
213
- // stop timer
214
- timer.stop();
215
- ```
216
-
217
- #### Main settings
218
-
219
- `o.showErrors` – turn on/off showing errors (false)
220
- `o.errors` – an array of all hidden errors, can be logged by `o.logErrors()` for debug
221
- `o.onError` – a function than will be called with an error as an argument
222
-
223
- > This and some more complex live examples are in the [full documentation](https://fous.name/objs/documentation). There are lots of useful methods and settings.
224
-
225
-
226
- ### Tests - unit tests, e2e, recording etc.
227
-
228
- Testing is a first-class part of Objs: use `o.test()` and `o.addTest()` for sync and async unit tests, including tests with page reload and autorun. Record user sessions with `o.startRecording()` / `o.stopRecording()`, then export to Objs-style tests (`o.exportTest()`) or Playwright (`.spec.ts`) with `o.exportPlaywrightTest()` for e2e in CI. Replay with `o.playRecording()` (all builds); call `o.testOverlay()` to show a results panel so assessors can see if all auto tests passed and which manual checks failed. Use `o.testConfirm()` for manual checks (e.g. hover effects). Dev-only: `o.assertSize()` / `o.assertVisible()` for layout assertions. See the [recording example](https://foggysq.github.io/objs/examples/recording/index.html) and the full documentation for details.
229
-
230
-
231
-
232
-
233
- ## Functions
234
- Almost all functions return control object with methods, let's call it **Objs**. Full API and TypeScript types: [objs.d.ts](objs.d.ts).
235
-
236
- ### Element selection
237
- `o(q)` – gets elements to control object. If [string] - by **querySelectorAll(q)** into control object, if DOM element or an array of them - gets them, if [number] - gets control object from **o.inits[q]**.
238
-
239
- `o.first(q)` – gets element to control by **querySelector(q)**.
240
-
241
- `o.take(q)` – gets elements like **o(q)** from DOM but if there is just one element or equal number of elements to inited in **o.inits[]** before, gets all inited elements and their methods.
242
-
243
- ### Component control
244
- `o.init(states)` – returns **Objs**, creates method(s) for each state to create, change elements. State called **render** is reserved for creation elements. **states** can be [string], [object], [function] that returns [string] or [object]. After **init()** **Objs** gets a **initID** parameter for a saved object in **o.inits**. More info about structure and features [here](https://fous.name/objs).
245
-
246
- `o.initState(state, [props])` – inite method and call it with props, e.g. to render/create element. **Objs** gets a **.initID** parameter for a saved object in **o.inits[]**.
247
-
248
- `o.inits[initID]` – an array of all inited objects. Available by index **initID** or **o.take()**.
249
-
250
- Instance: `o().init()` – equal to **o.init()** but with elements to control. `o().initState()` – equal to **o.initState()** but with elements to control. `o().sample()` – returns states object with render state for creation such elements. `o().getSSR(initId, [fromEls])` – bind this instance to DOM nodes by initId; optional **fromEls** (e.g. from a container) skips document query; used by auto-hydration when parent sets innerHTML. `o().saveState([id])`, `o().revertState([id])`, `o().loseState(id)` – save/restore DOM state. `o().unmount()` – remove from DOM and **o.inits**. `o().connect(loader, state, fail)` – connect a loader to this instance (state/fail method names). `o().initID` – undefined or number in **o.inits[]**. `o().html([html])` – returns html string of all elements or sets innerHTML as **html**; when **html** is set, any `[data-o-init]` nodes inside are auto-hydrated (inited instances bound to those nodes).
251
-
252
- ### DOM manipulation
253
- `o().reset(q)` clears **Objs** and get new elements by **q**, works as **o()**.
254
-
255
- `o().select([i])` – selects number **i** element from 0 to change only it, if **i** is undefined selects the last index element.
256
-
257
- `o().all()` – selects all elements to operate again.
258
-
259
- `o().remove([i])` – removes all or **i** element from DOM.
260
-
261
- `o().skip(i)` – removes **i** element from control set of this **Objs**.
262
-
263
- `o().add()` adds element to control set.
264
-
265
- `o().find(q)` – finds all children elements by q-query in each element.
266
-
267
- `o().first(q)` – finds only the first child element by q-query in each element.
268
-
269
- `o().length` number of elements of control set.
270
-
271
- `o().el` the first DOM element in the set.
272
-
273
- `o().els` – all DOM elements of the set.
274
-
275
- `o().last` the last DOM element in the set.
276
-
277
- `o().attr(attribute, [value])` – `UPDATED` sets **attribute** to **value**. Pass `null` to remove the attribute. Pass `""` to set an empty string. Returns **attribute** value if **value** is undefined. If **.select()** was not used before — returns an array of values.
278
-
279
- `o().attrs()` – returns an array of all elements attributes, if **.select()** was used before - returns an object with values of one element.
280
-
281
- `o().dataset([object])` – Sets dataset values due to the **object** data. It will not delete other dataset values. If **.select()** was used before - returns an object with dataset of one element or changes just one element.
282
-
283
- `o().style(value)` – `UPDATED` sets style attribute to [string] **value**. Pass `null` to remove the `style` attribute entirely.
284
-
285
- `o().css(object|null)` – `UPDATED` sets style from **object** like `{width: '100px', 'font-family': 'Arial'}`. Pass `null` to remove the `style` attribute entirely.
286
-
287
- `o().val([value])` – `NEW` gets or sets the `.value` property of `input`/`textarea`/`select`. Returns current value when called without argument; sets and returns `Objs` for chaining when called with argument.
288
-
289
- `o().setClass(value)` – sets class attribute to **value**.
290
-
291
- `o().addClass(...cls)` – `UPDATED` adds one or more classes: `addClass('foo', 'bar', 'baz')`.
292
-
293
- `o().removeClass(...cls)` – `UPDATED` removes one or more classes: `removeClass('foo', 'bar')`.
294
-
295
- `o().toggleClass(class, rule)` – switch having and not having **class** by **rule**. If **rule** set **class**.
296
-
297
- `o().haveClass(class)` – returns true if all elements have **class**.
298
-
299
- `o().innerHTML([html])` – if **html** is set, sets innerHTML of all elements. If not set, returns array with innerHTML of each element.
300
-
301
- `o().innerText(text)` – sets innerText for all elements.
302
-
303
- `o().textContent(content)` sets textContent for all elements.
304
-
305
- `o().refs` – `NEW` object populated on `init` — every child element with a `ref="name"` attribute is available as `component.refs.name` (an ObjsInstance wrapper). Use for direct access without selectors.
306
-
307
- `o().forEach(function)` – runs **function** with an object as the first parameter: {o, self, i, el} where is o-function, self Objs object, i-index of current element and el - DOM element.
308
-
309
- ### Events
310
- `o().on(events, function, [options])` – adds **events** listeners separated by ', ' to elements.
311
-
312
- `o().off(events, function, [options])` – removes **events** listeners separated by ', ' to elements.
313
-
314
- `o().offAll([event])` – removes all listeners or for special **event** from elements.
315
-
316
- `o().onAll([event])` – adds all inited listeners from cache for all or for special **event**.
317
-
318
- `o().ie` – object with all ever added listeners like {click: [[function, options], ...], ...}.
319
-
320
- ### DOM insert
321
- `o().appendInside(q)` – append elements inside element **q** or got by **q** query.
322
-
323
- `o().appendBefore(q)` – append elements before element **q** or got by **q** query.
324
-
325
- `o().appendAfter(q)` – append elements after element **q** or got by **q** query.
326
-
327
- `o().prepareFor(React.createElement, [React.Component])` – clones and returns React element or JSX Component if React.Component is given. Allows to use Objs in React Apps. Objs states should be inited on rendered elements.
328
-
329
- ### State and store
330
- `o.createStore(defaults)` – `NEW` creates a reactive plain-object store. Returns the defaults object extended with `subscribe(component, stateName)`, `notify()`, and `reset()`. Subscribed components receive `{ ...storeProps, self, o, i }` merged into their state context on every `notify()`.
331
-
332
- ```
333
- Objs update cycle (vs React):
334
-
335
- React: setState(newVal)
336
- → component function re-runs entirely
337
- virtual DOM diff
338
- → patch (1–N nodes, including unchanged ones)
339
-
340
- Objs: store.notify()
341
- each subscribed component's sync() fires
342
- each sync() writes only its own DOM nodes
343
- → O(1) per subscriber — no diff, no cascade
344
- ```
345
-
346
- `o.connectRedux(store, selector, component, [state])` – connects a Redux store slice to a component state method. Fires immediately and on every store change. Returns unsubscribe function.
347
-
348
- `o.connectMobX(mobx, observable, accessor, component, [state])` – wraps `mobx.autorun()` to connect a MobX observable to a component state method. Returns disposer.
349
-
350
- `o.withReactContext(React, Context, selector, component, [state])` – returns a React bridge component that calls `component[state](selector(contextValue))` on every context change. Mount it inside the Provider to connect.
351
-
352
- `o.ObjsContext` – default context value placeholder for `React.createContext()`.
353
-
354
- ### Routing
355
- `o.route(path, task)` – register a route: **path** is string, boolean, or function(path); **task** is function or object. Returns match result. Built-in; no separate router dependency.
356
-
357
- `o.router(routes)` – run routing: **routes** is object of path → task. Returns true if a route matched.
358
-
359
- Use **o.getParams([key])** to read GET (query) parameters in route callbacks or when initialising components—e.g. pass `o.getParams()` to `render(data)` or read `o.getParams('id')` for component state or data loading.
360
-
361
- ### HTTP and parameters
362
- `o.get(url, [props])` – returns promise for GET AJAX, **data** in **props** as an [object] will be converted to string parameters.
363
-
364
- `o.post(url, props)` – returns promise for POST AJAX, **data** in **props** as an [object] will be converted to body.
365
-
366
- `o.ajax(url, props)` – returns propmise for AJAX, needs **method** in **props** equal to GET or POST, **data** will be converted for GET/POST format.
367
-
368
- `o.getParams([key])` returns GET **key** value or an object with all GET parameters.
369
-
370
- ### Include and cache
371
- `o.inc(sources, [callBack, callBad])` returns [number] **setID**, gets **souces** is an object like {nameID: url, ...} where **nameID** is unique ID, **url** link to JS, CSS or image, **callBack** – function to run after everything is loaded successfully, **callBad** - function to run on failure. Functions gets **setN** as the first argument.
372
-
373
- `o.incCheck(setID)` – true if include files set number **setID** is loaded.
374
-
375
- `o.incCacheClear([all])` – true. Clears localStorage JS, CSS cache. If **all** is true, removes DOM elements of include and clears all include data.
376
-
377
- `o.newLoader(promise)` – create a loader for async data; use with `o().connect(loader, state, fail)`.
378
-
379
- `o.incCache` – true, cache in localStorage enabled.
380
-
381
- `o.incCacheExp` – 1000 * 60 * 60 * 24, cache for 24 hours.
382
-
383
- `o.incTimeout` – 6000, ms timeout to load function.
384
-
385
- `o.incSource` '', prefix for urls.
386
-
387
- `o.incForce` – false, do not load already loaded files.
388
-
389
- `o.incAsync` – true, async loading, set to false for in order loading.
390
-
391
- `o.incCors` – false, do not allow loading from other domains
392
-
393
- `o.incFns` – object, array of name:status for all loaded functions.
394
-
395
- ### Cookies and storage
396
- `o.setCookie(name, value, [options])` – set a cookie.
397
-
398
- `o.getCookie(name)` – get cookie value.
399
-
400
- `o.deleteCookie(name)` – delete a cookie.
401
-
402
- `o.clearCookies()` – clear all cookies.
403
-
404
- `o.clearLocalStorage()`, `o.clearSessionStorage()`, `o.clearTestsStorage()` – clear respective storage.
405
-
406
- `o.clearAfterTests()` clear cookies and test-related storage after test run (e.g. in tAfterEach).
407
-
408
- ### Testing
409
- `o.test(title, test1, test2, ..., callBack)` – returns [number] **testID**, gets [string] **title** and tests like ["Test title", testFunction], where **testFunction** should return true for success and false or string for failure. If test is async, **testFunction** should get the first parameter and use it in **o.testUpdate()**.
410
-
411
- `o.addTest(title, ...cases)` – add a test suite; returns handle for **o.runTest()**.
412
-
413
- `o.runTest(testId?, autoRun?, savePrev?)` – run test(s). **savePrev** true keeps existing sessionStorage for that testId so the run can resume.
414
-
415
- `o.testUpdate(info, result, [description])` – returns undefined, gets **info** object (the first parameter of any **testFunction**) to update test status and set it to **result** (true or false/string), **description** - additional text if needed. Used for test status update for async tests. More info [here](https://fous.name/objs).
416
-
417
- `o.updateLogs()` – return test log lines (e.g. for assertions).
418
-
419
- `o.tLog[testID]` – test sessions and text results.
420
-
421
- `o.tRes[testID]` test sets results as true/false.
422
-
423
- `o.tStatus[testID: [functionID: true/false],...]` – an array of set test functions statuses.
424
-
425
- `o.tShowOk` – false, success tests are hidden, only errors. Set to **true** to see success results before **o.test()**.
426
-
427
- `o.tStyled` – false, logs are in console view. Set to **true** to make logs HTML styled before **o.test()**.
428
-
429
- `o.tTime` 2000, milliseconds timeout for async tests.
430
-
431
- `o.tBeforeEach` / `o.tAfterEach` – global hooks called before/after each test case. Set to a function.
432
-
433
- ### Recording and export
434
- Available in all builds so QA testers/assessors can record on staging or production environments.
435
-
436
- > **Security note:** `o.startRecording()` intercepts `window.fetch` and captures request/response bodies including auth tokens. Appropriate for staging environments; review before enabling on production.
437
-
438
- `o.startRecording(observe?, events?, timeouts?)` – `UPDATED` starts capturing user interactions and network requests as mocks. Optional `observe` is a CSS selector to scope the MutationObserver (e.g. `'#task-app'`). Defaults: events `['click','mouseover','scroll','input','change']`, timeouts `{click:100, mouseover:50}`. Check **o.recorder.active** to see if recording is on.
439
-
440
- `o.stopRecording()` – `UPDATED` stops recording, returns `{actions, mocks, initialData, assertions, observeRoot, stepDelays}`. When scoped recording was used, `assertions` is an array of `{actionIdx, type, selector, text?|className?}` (from the MutationObserver), and `observeRoot` is the selector string or null. `stepDelays` is the per-event delay map (from `timeouts`) used when replaying.
441
-
442
- `o.exportTest(recording)` – `UPDATED` returns generated `o.addTest()` source code string ready to review and commit.
443
-
444
- `o.exportPlaywrightTest(recording, [options])` – `NEW` returns a complete Playwright `.spec.ts` file string with network mocks, `page.goto()`, typed locator steps, and TODO assertion comments. `options.testName` and `options.baseUrl` are optional.
445
-
446
- ```js
447
- o.startRecording();
448
- // QA tester uses the app normally
449
- const rec = o.stopRecording();
450
- console.log(o.exportPlaywrightTest(rec, { testName: 'Checkout flow' }));
451
- // paste tests/checkout.spec.ts npx playwright test
452
- ```
453
-
454
- `o.clearRecording([id])` – removes recording from sessionStorage.
455
-
456
- `o.playRecording(recording, [mockOverrides])` – Replays recording as a test with intercepted fetch. Available in all builds (for assessors on staging).
457
-
458
- ### QA and selectors
459
- `o.autotag` – set to a string (e.g. `"qa"`) to auto-add `data-{autotag}="component-name"` to all rendered elements. Component name comes from `states.name` (camelCase → kebab-case). Ships in all builds — QA teams can target stable selectors with Playwright/Cypress.
460
-
461
- `o.reactQA(componentName)` – `NEW` returns a `{ 'data-qa': 'kebab-name' }` object for spreading onto React JSX elements. Converts CamelCase to kebab-case. Respects `o.autotag` value. Ships in all builds.
462
-
463
- ```jsx
464
- <button {...o.reactQA('CheckoutButton')} onClick={fn}>Checkout</button>
465
- //<button data-qa="checkout-button">
466
- ```
467
-
468
- ### Measurement and UI assertions (dev)
469
- All builds include the full API (test framework, playRecording, testOverlay, testConfirm, measure/assertVisible/assertSize). Only the debug flag and debug logging are behind `__DEV__`.
470
-
471
- `o.measure(el)` returns `{width, height, top, left, visible, opacity, zIndex}`. Use in test assertions.
472
-
473
- `o.assertVisible(el)` returns `true/false` for use inside `o.test()`.
474
-
475
- `o.assertSize(el, expected)` returns `true` or a descriptive error string. `expected` can include:
476
- - `w`, `h` – width and height (px)
477
- - `padding` same value for all four sides (px), or `paddingTop`, `paddingRight`, `paddingBottom`, `paddingLeft` individually
478
- - `margin` – same value for all four sides (px), or `marginTop`, `marginRight`, `marginBottom`, `marginLeft` individually
479
-
480
- Use for design system or UI verification tests (e.g. button height 24px, container padding 20px).
481
-
482
- `o.testOverlay()` – Renders a fixed overlay button (🧪 Tests). Click to see pass/fail results for all test runs (auto steps and manual checks). For assessors: after replay, open the overlay to see if all auto tests passed and which manual checks failed. Available in all builds.
483
-
484
- `o.testConfirm(label, items?, opts?)` – Shows a draggable overlay titled "Label: Paused" with an optional checklist; returns `Promise<{ ok: boolean, errors?: string[] }>`. Use after replay for manual checks (e.g. hover effects). Available in all builds. See the [recording example](https://foggysq.github.io/objs/examples/recording/index.html) for a live demo.
485
-
486
- ### SSR and Node
487
- In Node, **o.D** is **o.DocumentMVP** (no real DOM); **o.init().render()** builds a virtual tree and you can serialize with the same code path that produces HTML for SSR. See full docs for getSSR and hydration.
488
-
489
- ### Utilities and debug
490
- **o.verify / o.safeVerify / o.specialTypes** — Runtime type checking for arguments, config, or API responses. Useful in projects to fail fast at API boundaries, validate options before use, or keep generated code safe.
491
-
492
- - **o.verify(pairs, safe?)** **pairs** is an array of `[value, expectedTypes]`, where **expectedTypes** is a string or array of strings (e.g. `'string'`, `['number','undefined']`). Uses built-in `typeof` checks plus **o.specialTypes**. On failure: throws (default) or returns an Error if **safe** is true. On success: returns `true`.
493
- - **o.safeVerify(pairs)** Same as **o.verify(pairs, true)**; returns `true` or `false` (no throw).
494
- - **o.specialTypes** Object of custom validators used by **o.verify()**. Built-in: `notEmptyString`, `array`, `promise`. **Developers can add global validators here**: assign a function `(value, typeofValue) => boolean` to **o.specialTypes.myType**. That validator is then available everywhere—in your app and inside Objs—so you can use `o.verify([x, ['myType']])` consistently.
495
-
496
- `o.showErrors` false as default, but all errors are saved in **o.errors[]**.
497
-
498
- `o.errors` – an array of all errors.
499
-
500
- `o.logErrors()` – log all hidden errors in console.
501
-
502
- `o.onError` set a function that is called when an error happens.
503
-
504
- `o.getStates()` – returns array of state info per init.
505
-
506
- `o.getStores()` – returns array of store refs.
507
-
508
- `o.getListeners()` returns array of listener refs.
509
-
510
- `o.camelToKebab(str)`, `o.kebabToCamel(str)` – convert between naming conventions.
511
-
512
- `o.C(obj, key)` – safe `Object.hasOwn`-style check; returns whether `obj` has own property `key`. Used internally; available for app code. `o.F` and `o.U` are internal constants (false, undefined). `o.W` and `o.H` exist but are reserved; do not rely on them.
513
-
514
-
515
-
516
-
517
- ## Why Objs for AI-assisted development
518
-
519
- ### The complete loop in one script
520
-
521
- ```
522
- develop → o.autotag / o.reactQA → o.startRecording() → o.stopRecording()
523
- o.exportPlaywrightTest() paste npx playwright test
524
- ```
525
-
526
- No Playwright config to set up manually. No test IDs to maintain. The entire pipeline — component, QA tag, behavior capture, and Playwright test generation — runs inside the same library. Works in React projects too: add one script tag, sprinkle `{...o.reactQA('MyComponent')}`, record.
527
-
528
- ### Dev/prod build split
529
-
530
- `objs.js` is the source for development or script tag. `objs.built.js` and `objs.built.min.js` are produced by `node build.js` (ESM + window.o). Only the debug flag is behind `__DEV__`.
531
-
532
- The **recording pipeline** (`startRecording`, `stopRecording`, `exportTest`, `exportPlaywrightTest`, `reactQA`) ships in all builds so QA assessors can use it on staging.
533
-
534
- Bundlers pick the right file automatically via `package.json` exports conditions:
535
-
536
- ```js
537
- // Vite, webpack, esbuild no config needed
538
- import o from 'objs-core'; // dev server objs.js, build objs.built.js
539
-
540
- // Script tag
541
- <script src="objs.js"></script>
542
- ```
543
-
544
- ### States as AI-natural data structures
545
-
546
- Every Objs component is a plain JS object. An LLM can generate correct components from a description without knowing JSX, virtual DOM, or React lifecycle rules:
547
-
548
- ```js
549
- // AI prompt: "create a counter with increment and reset"
550
- // Hack: render as function sets the entity store and returns the init object (no globals, no post-init wiring)
551
- const counterStates = {
552
- name: 'Counter',
553
- render: ({ self }) => {
554
- self.store.n = self.store.n ?? 0;
555
- return {
556
- html: '<span ref="n">0</span> <button ref="inc">+</button> <button ref="rst">Reset</button>',
557
- events: {
558
- click: (e) => {
559
- if (e.target === self.refs?.inc?.el) self.updateCount(++self.store.n);
560
- else if (e.target === self.refs?.rst?.el) self.updateCount(self.store.n = 0);
561
- },
562
- },
563
- };
564
- },
565
- updateCount: ({ self }, num) => {
566
- self.store.n = num;
567
- self.refs.n.html(num);
568
- },
569
- };
570
- o.init(counterStates).render().appendInside('#app');
571
- ```
572
-
573
- No compiler. No build step to try the above. No framework knowledge needed to generate it.
574
-
575
- ### Granular reactive updates — no virtual DOM diff
576
-
577
- Each store subscription calls exactly one targeted DOM write:
578
-
579
- ```js
580
- // React with Redux: entire subtree re-renders, React diffs it
581
- // Objs: one function call, one innerHTML assignment
582
- o.connectRedux(store, s => s.userName, profileCard, 'updateName');
583
- o.connectRedux(store, s => s.score, profileCard, 'updateScore');
584
- ```
585
-
586
- Similar philosophy to Solid.js signals — but the update logic is a plain function, not a reactive primitive. An AI generates it without any framework knowledge.
587
-
588
- ### Comparison
589
-
590
- | | Objs v2.0 | React ecosystem |
591
- |---|---|---|
592
- | **Setup** | `<script src="objs.js">` or `npm i objs-core` | React + Babel/Vite + config |
593
- | **State management** | Built-in states + loaders | Redux / Zustand / MobX (separate) |
594
- | **Routing** | `o.route()` built-in | React Router (separate) |
595
- | **Testing** | Built-in `o.test()` + recording | Jest + Testing Library + Playwright |
596
- | **Dev tools** | Built-in overlay, recording | React DevTools extension |
597
- | **TypeScript** | `objs.d.ts` included | @types/react + separate config |
598
- | **SSR** | Built-in DocumentMVP | Next.js / separate hydration setup |
599
- | **AI context size** | ~2500 lines, one file | Dozens of packages, thousands of files |
600
- | **Prod bundle overhead** | Dev code fully stripped | Depends on tree-shaking config |
601
-
602
- ### Real-world patterns
603
-
604
- See [EXAMPLES.md](EXAMPLES.md) for the architecture guide and runnable examples (aligned with [SKILL.md](SKILL.md)):
605
- 1. **How render works** — plain object, function, HTML string, multi-instance, `append`, `children`, `ref`/`refs`
606
- 2. **Single components (atoms)** — Button, Badge, Field with `val()`, `css(null)`, `addClass` spread
607
- 3. **Nesting & composition** — slot pattern, `append` in render, factory with dynamic children
608
- 4. **Design system** — Atoms → Molecules → Organisms, `self.store`, update efficiency
609
- 5. **Real-world** — menu, cart+cards, dialog, drawer+URL, complex form
610
- 6. **React integration** — four modes including bolt-on Playwright recording with `o.reactQA`
611
-
612
- **Rule (from SKILL):** Define one state method per data slice; never call `.render()` to update — use targeted state methods. In event handlers use **self.select(e)** and **refs** (e.g. `row.refs.input.val()`), not `e.target`/class selectors or raw DOM.
613
-
614
-
615
-
616
-
617
- ## License
618
- Apache 2.0
1
+ # Objs
2
+ > Fast and simple library to speed up developing by AI context friendly architecture, auto-tests recording, cache control and other. Develop new features without rewriting anything. Works standalone or alongside React. Examples and full documentation: [Full Documentation](https://en.fous.name/objs/documentation)
3
+
4
+ **AI-friendly** — one file, `SKILL.md` primer (~6,000 tokens). An LLM generates correct Objs code from a description without JSX, virtual DOM, or React lifecycle knowledge.
5
+
6
+ **React-developer-friendly** — familiar `className`, `ref`/`refs`, `o.createStore`. Add one script tag to an existing React app and get Playwright test generation without touching any components.
7
+
8
+ **Live examples** — real patterns in [`examples/`](https://foggysq.github.io/objs/examples/), narrative walkthroughs in EXAMPLES.md. For AI assistants: use SKILL.md as `@SKILL.md` or system prompt.
9
+
10
+ ---
11
+
12
+ ## Why Objs
13
+
14
+ **Objs is built so one dependency spans the whole loop—UI, reactive state, in-browser recording, replay with mocks, and export to standard Playwright**—instead of stitching together a framework, a separate recorder product, and a second test stack just to lock regressions in CI.
15
+
16
+ **Core functionality** — Record user actions in the browser, export ready-to-commit tests, and run them in CI or **run in the browser extension**. One script; no separate recorder or test-ID maintenance.
17
+
18
+ - **Record Playwright in one pipeline** — `o.startRecording()` captures click, input, change, scroll; `o.stopRecording()` returns actions and auto-generated assertions; `o.exportPlaywrightTest(recording)` outputs a `.spec.ts` with locators and network mocks. Paste into your repo and run `npx playwright test`.
19
+ - **CI support** — Export runs in all builds (including prod). QA or assessors record on staging; paste the generated Playwright test into your test suite; CI runs it with no extra config. Optional `o.exportTest(recording)` for Objs-style tests.
20
+ - **Store and update model** — `o.createStore()` + `subscribe`/`notify`: no virtual DOM, no re-render cascade; only subscribed components update their own DOM (O(1) per subscriber).
21
+ - **One library, many roles** — DOM + state + routing + AJAX + cache (`o.inc`) + tests + recording + SSR. No extra test runner or recorder product. Built-in `o.route()` / `o.router()` no separate router dependency.
22
+ - **Stable selectors and UI checks** `{...o.reactQA('ComponentName')}` or `o.autotag`; recorder uses `data-qa` and list indices. `o.assertSize(el, { w, h, padding, margin })` for design system verification; `o.testConfirm()` for manual/hover checks after replay.
23
+ - **Works standalone or with React** — Add one script tag to an existing React app; no architecture change. Familiar `className`, `ref`/`refs`, `o.createStore`. Built-in SSR (Node) with `DocumentMVP` and in-browser hydration.
24
+ - **AI-friendly** One file, ~6,000-token `SKILL.md` primer. No JSX, virtual DOM, or React lifecycle; fewer tokens than typical React context for runnable output. No stale closures, dependency arrays, or re-render cascades. Same code runs in Node (SSR) so tools can verify output without a browser — **verify generated code without user review**: run `o.init(states).render()` in Node, serialize or assert structure before returning to the user.
25
+
26
+ → [Full comparison and live demo](https://foggysq.github.io/objs/examples/ai-workflow/index.html)
27
+
28
+ ---
29
+
30
+ ### Update v2.4: Chrome extension + native WebSocket replay
31
+
32
+ - **Strict record / replay** — `o.startRecording({ strictCaptureAssertions?, strictCaptureNetwork?, strictCaptureWebSocket? })` stores **`strictCapture`** on the recording; **`o.playRecording`** accepts **`strictPlay`** and per-feature **`strictAssertions`**, **`strictNetwork`**, **`strictWebSocket`**, **`strictRemoved`** (see README “Recording and export”). The extension **Recording settings** accordion includes matching toggles for JSON replay.
33
+ - **Chrome extension (`objs-extension/`)** — Manifest V3 toolbar popup with an **accordion** per test: edit **`o.exportTest()` JS** (same as the recording example’s “Export Objs test”), **Play** runs `addTest`/`run`, **Stop** after recording fills the script + snapshot for Playwright. Legacy **JSON** recordings still **Play** via `o.playRecording` (replay with network mocks). Import/export `.js` / `.json`, download Playwright. Load unpacked from `chrome://extensions` (Developer mode) or **package and sign it yourself** for internal distribution.
34
+ - **Distribution** — The Objs project does **not** publish this extension to the **public Chrome Web Store**. Enterprises and teams zip or policy-deploy the folder to match their **host permissions, signing, and compliance** requirements.
35
+ - **Native WebSocket mocking** — During `o.playRecording`, when `recording.websocketEvents` is present, Objs installs a mock `WebSocket` that replays captured in/out messages (same teardown as fetch/XHR mocks). Use `skipWebSocketMock: true` in play options to force a live connection.
36
+ - **Extension setup** — See [`objs-extension/README.md`](objs-extension/README.md) for load-unpacked steps and packaging notes.
37
+ - **`o().cssMerge(object|null)`** — Merges into the existing inline `style` attribute instead of replacing it (unlike `css()`, which overwrites the whole attribute). Properties in the object **add** or **replace**; pass **`null`** or **`undefined`** for a property to **remove** that property only. Pass **`null`** for the whole argument to clear the style attribute (same as `css(null)`). Keys may be **camelCase** or **kebab-case**; they are normalized to kebab-case when serializing.
38
+
39
+ ---
40
+
41
+ ## Get started
42
+
43
+ **Browser** — source with test tools:
44
+ ```html
45
+ <script src="objs.js" type="text/javascript"></script>
46
+ ```
47
+
48
+ **Browser (smaller)** — minified `objs.built.min.js` for production. Use `type="module"`:
49
+ ```html
50
+ <script src="objs.min.js" type="module"></script>
51
+ ```
52
+
53
+ **npm / bundler** correct file chosen automatically via `package.json` exports:
54
+ ```js
55
+ import o from 'objs-core'; // resolves to objs.built.js
56
+ ```
57
+
58
+ ```
59
+ npm i objs-core
60
+ ```
61
+
62
+
63
+
64
+
65
+ ## Features
66
+
67
+ #### Develop
68
+ - Samples and state control
69
+ - Data store, Cookies and LS/SS control
70
+ - Events delegation
71
+
72
+ #### Test
73
+ - Sync/async, tests with reload
74
+ - Console & HTML output
75
+ - Autotests
76
+
77
+ #### Optimize
78
+ - Separate logic and samples
79
+ - Native micro-service architecture
80
+ - Async loading and preloading, cache
81
+
82
+
83
+
84
+
85
+ ## Main principles
86
+
87
+ ### Dynamic content
88
+
89
+ #### Create sample
90
+
91
+ To control elements Objs uses states. State - it's an information how to create or change DOM element. To create an element use `render` state with html (inner HTML) and tag attributes:
92
+ ```
93
+ // state called render for timer example
94
+ const timerStates = {
95
+ render: {
96
+ class: 'timer',
97
+ html: 'Seconds: <span ref="n">0</span>',
98
+ }
99
+ }
100
+ ```
101
+ - `render` could be a string if you use HTML samples (see [documentation](https://fous.name/objs/documentation/?parameter=value#states)):
102
+ `'<div class="timer">Seconds:<span>0</span></div>'`
103
+ - default tag is `div` (if tag is undefined)
104
+ - attributes `dataset` and `style` can be object type
105
+ - to append elements inside - use `append` with DOM element/Objs or an array of them as a value
106
+
107
+ #### States
108
+
109
+ Then add a new state that will start and finish counting. Number will be stored in the object itself - `self` object. So the state will be a function that gets `self`, creates a variable, increments it by interval and shows as innerHTML of `span`:
110
+ ```
111
+ // new timer states object
112
+ const timerStates = {
113
+ render: {
114
+ class: 'timer',
115
+ html: 'Seconds: <span ref="n">0</span>',
116
+ },
117
+ start: ({self}) => {
118
+ self.n = self.n || 0;
119
+ self.interval = setInterval(() => {
120
+ self.n++;
121
+ self.refs.n.html(self.n);
122
+ }, 1000);
123
+ },
124
+ stop: ({self}) => {
125
+ clearInterval(self.interval);
126
+ }
127
+ }
128
+ ```
129
+ - every state gets object with
130
+ `self` - Objs object
131
+ `o` - o-function to use inside
132
+ `i` - index of the current element in Objs object
133
+
134
+ #### Append in DOM
135
+
136
+ The last thing is to create and append element on the page. To do this - init states, render object and start timer... And also - append it.
137
+ ```
138
+ // create and start timer
139
+ const timer = o.init(timerStates)
140
+ .render()
141
+ .start()
142
+ .appendInside('#simpleTimer');
143
+
144
+ // stop timer
145
+ timer.stop();
146
+ ```
147
+
148
+ #### Main settings
149
+
150
+ `o.showErrors` – turn on/off showing errors (false)
151
+ `o.errors` – an array of all hidden errors, can be logged by `o.logErrors()` for debug
152
+ `o.onError` – a function than will be called with an error as an argument
153
+
154
+ > This and some more complex live examples are in the [full documentation](https://fous.name/objs/documentation). There are lots of useful methods and settings.
155
+
156
+
157
+ ### Tests - unit tests, e2e, recording etc.
158
+
159
+ Testing is a first-class part of Objs: use `o.test()` and `o.addTest()` for sync and async unit tests, including tests with page reload and autorun. Record user sessions with `o.startRecording()` / `o.stopRecording()`, then export to Objs-style tests (`o.exportTest()`) or Playwright (`.spec.ts`) with `o.exportPlaywrightTest()` for e2e in CI. Replay with `o.playRecording()` (all builds); call `o.testOverlay()` to show a results panel so assessors can see if all auto tests passed and which manual checks failed. Use `o.testConfirm()` for manual checks (e.g. hover effects). Dev-only: `o.assertSize()` / `o.assertVisible()` for layout assertions. See the [recording example](https://foggysq.github.io/objs/examples/recording/index.html) and the full documentation for details.
160
+
161
+
162
+
163
+
164
+ ## Functions
165
+ Almost all functions return control object with methods, let's call it **Objs**. Full API and TypeScript types: [objs.d.ts](objs.d.ts).
166
+
167
+ ### Element selection
168
+ `o(q)` – gets elements to control object. If [string] - by **querySelectorAll(q)** into control object, if DOM element or an array of them - gets them, if [number] - gets control object from **o.inits[q]**.
169
+
170
+ `o.first(q)` gets element to control by **querySelector(q)**.
171
+
172
+ `o.take(q)` gets elements like **o(q)** from DOM but if there is just one element or equal number of elements to inited in **o.inits[]** before, gets all inited elements and their methods.
173
+
174
+ ### Component control
175
+ `o.init(states)` – returns **Objs**, creates method(s) for each state to create, change elements. State called **render** is reserved for creation elements. **states** can be [string], [object], [function] that returns [string] or [object]. After **init()** **Objs** gets a **initID** parameter for a saved object in **o.inits**. More info about structure and features [here](https://fous.name/objs).
176
+
177
+ `o.initState(state, [props])` – inite method and call it with props, e.g. to render/create element. **Objs** gets a **.initID** parameter for a saved object in **o.inits[]**.
178
+
179
+ `o.inits[initID]` – an array of all inited objects. Available by index **initID** or **o.take()**.
180
+
181
+ Instance: `o().init()` equal to **o.init()** but with elements to control. `o().initState()` – equal to **o.initState()** but with elements to control. `o().sample()` – returns states object with render state for creation such elements. `o().getSSR(initId, [fromEls])` – bind this instance to DOM nodes by initId; optional **fromEls** (e.g. from a container) skips document query; used by auto-hydration when parent sets innerHTML. `o().saveState([id])`, `o().revertState([id])`, `o().loseState(id)` – save/restore DOM state. `o().unmount()` – remove from DOM and **o.inits**. `o().connect(loader, state, fail)` – connect a loader to this instance (state/fail method names). `o().initID` – undefined or number in **o.inits[]**. **`toString()` / `Symbol.toPrimitive`** — an Objs instance stringifies to its HTML (same as **`.html()`**), so you can use **`${child}`** in template literals and when the parent sets **innerHTML** from composed instance markup, children auto-hydrate. `o().html([html])` – returns html string of all elements or sets innerHTML as **html**; when **html** is set, any `[data-o-init]` nodes inside are auto-hydrated (inited instances bound to those nodes).
182
+
183
+ ### DOM manipulation
184
+ `o().reset(q)` clears **Objs** and get new elements by **q**, works as **o()**.
185
+
186
+ `o().select([i])` – selects number **i** element from 0 to change only it, if **i** is undefined selects the last index element. Pass an **Event** to select the element in the set that contains **event.target** (use in handlers to get **`self.select(e).refs…`**).
187
+
188
+ `o().all()` selects all elements to operate again.
189
+
190
+ `o().remove([i])` – removes all or **i** element from DOM.
191
+
192
+ `o().skip(i)` – removes **i** element from control set of this **Objs**.
193
+
194
+ `o().add()` – adds element to control set.
195
+
196
+ `o().find(q)` – finds all children elements by q-query in each element.
197
+
198
+ `o().first(q)` finds only the first child element by q-query in each element.
199
+
200
+ `o().length` number of elements of control set.
201
+
202
+ `o().el` – the first DOM element in the set.
203
+
204
+ `o().els` – all DOM elements of the set.
205
+
206
+ `o().last` – the last DOM element in the set.
207
+
208
+ `o().attr(attribute, [value])` `UPDATED` sets **attribute** to **value**. Pass `null` to remove the attribute. Pass `""` to set an empty string. Returns **attribute** value if **value** is undefined. If **.select()** was not used before — returns an array of values.
209
+
210
+ `o().attrs()` – returns an array of all elements attributes, if **.select()** was used before - returns an object with values of one element.
211
+
212
+ `o().dataset([object])` – Sets dataset values due to the **object** data. It will not delete other dataset values. If **.select()** was used before - returns an object with dataset of one element or changes just one element.
213
+
214
+ `o().style(value)` – `UPDATED` sets style attribute to [string] **value**. Pass `null` to remove the `style` attribute entirely.
215
+
216
+ `o().css(object|null)` – `UPDATED` sets style from **object** like `{width: '100px', 'font-family': 'Arial'}`. Pass `null` to remove the `style` attribute entirely.
217
+
218
+ `o().cssMerge(object|null)` – `NEW` merges into the existing inline `style`: properties add or replace; `null` or `undefined` for a property removes that property only. Pass `null` for the whole argument to remove the `style` attribute (same as `css(null)`). Keys may be camelCase or kebab-case.
219
+
220
+ `o().val([value])` – `NEW` gets or sets the `.value` property of `input`/`textarea`/`select`. Returns current value when called without argument; sets and returns `Objs` for chaining when called with argument.
221
+
222
+ `o().setClass(value)` – sets class attribute to **value**.
223
+
224
+ `o().addClass(...cls)` – `UPDATED` adds one or more classes: `addClass('foo', 'bar', 'baz')`.
225
+
226
+ `o().removeClass(...cls)` `UPDATED` removes one or more classes: `removeClass('foo', 'bar')`.
227
+
228
+ `o().toggleClass(class, rule)` switch having and not having **class** by **rule**. If **rule** set **class**.
229
+
230
+ `o().haveClass(class)` – returns true if all elements have **class**.
231
+
232
+ `o().innerHTML([html])` – if **html** is set, sets innerHTML of all elements. If not set, returns array with innerHTML of each element.
233
+
234
+ `o().innerText(text)` sets innerText for all elements.
235
+
236
+ `o().textContent(content)` sets textContent for all elements.
237
+
238
+ `o().refs` – `NEW` object populated on `init` — every child element with a `ref="name"` attribute is available as `component.refs.name` (an ObjsInstance wrapper). Use for direct access without selectors.
239
+
240
+ `o().forEach(function)` – runs **function** with an object as the first parameter: {o, self, i, el} where is o-function, self Objs object, i-index of current element and el - DOM element.
241
+
242
+ ### Events
243
+ `o().on(events, function, [options])` – adds **events** listeners separated by ', ' to elements.
244
+
245
+ `o().off(events, function, [options])` – removes **events** listeners separated by ', ' to elements.
246
+
247
+ `o().offAll([event])` – removes all listeners or for special **event** from elements.
248
+
249
+ `o().onAll([event])` – adds all inited listeners from cache for all or for special **event**.
250
+
251
+ `o().ie` – object with all ever added listeners like {click: [[function, options], ...], ...}.
252
+
253
+ ### DOM insert
254
+ `o().appendInside(q)` – append elements inside element **q** or got by **q** query.
255
+
256
+ `o().appendBefore(q)` – append elements before element **q** or got by **q** query.
257
+
258
+ `o().appendAfter(q)` – append elements after element **q** or got by **q** query.
259
+
260
+ `o().prepareFor(React.createElement, [React.Component])` – clones and returns React element or JSX Component if React.Component is given. Allows to use Objs in React Apps. Objs states should be inited on rendered elements.
261
+
262
+ ### State and store
263
+ `o.createStore(defaults)` – `NEW` creates a reactive plain-object store. Returns the defaults object extended with `subscribe(component, stateName)`, `notify()`, and `reset()`. Subscribed components receive `{ ...storeProps, self, o, i }` merged into their state context on every `notify()`.
264
+
265
+ ```
266
+ Objs update cycle (vs React):
267
+
268
+ React: setState(newVal)
269
+ component function re-runs entirely
270
+ → virtual DOM diff
271
+ → patch (1N nodes, including unchanged ones)
272
+
273
+ Objs: store.notify()
274
+ → each subscribed component's sync() fires
275
+ → each sync() writes only its own DOM nodes
276
+ → O(1) per subscriber — no diff, no cascade
277
+ ```
278
+
279
+ `o.connectRedux(store, selector, component, [state])` – connects a Redux store slice to a component state method. Fires immediately and on every store change. Returns unsubscribe function.
280
+
281
+ `o.connectMobX(mobx, observable, accessor, component, [state])` – wraps `mobx.autorun()` to connect a MobX observable to a component state method. Returns disposer.
282
+
283
+ `o.withReactContext(React, Context, selector, component, [state])` – returns a React bridge component that calls `component[state](selector(contextValue))` on every context change. Mount it inside the Provider to connect.
284
+
285
+ `o.ObjsContext` – default context value placeholder for `React.createContext()`.
286
+
287
+ ### Routing
288
+ `o.route(path, task)` – register a route: **path** is string, boolean, or function(path); **task** is function or object. Returns match result. Built-in; no separate router dependency.
289
+
290
+ `o.router(routes)` – run routing: **routes** is object of path → task. Returns true if a route matched.
291
+
292
+ Use **o.getParams([key])** to read GET (query) parameters in route callbacks or when initialising components—e.g. pass `o.getParams()` to `render(data)` or read `o.getParams('id')` for component state or data loading.
293
+
294
+ ### HTTP and parameters
295
+ `o.get(url, [props])` – returns promise for GET AJAX, **data** in **props** as an [object] will be converted to string parameters.
296
+
297
+ `o.post(url, props)` – returns promise for POST AJAX, **data** in **props** as an [object] will be converted to body.
298
+
299
+ `o.ajax(url, props)` – returns propmise for AJAX, needs **method** in **props** equal to GET or POST, **data** will be converted for GET/POST format.
300
+
301
+ `o.getParams([key])` – returns GET **key** value or an object with all GET parameters.
302
+
303
+ ### Include and cache
304
+ `o.inc(sources, [callBack, callBad])` – returns [number] **setID**, gets **souces** is an object like {nameID: url, ...} where **nameID** is unique ID, **url** link to JS, CSS or image, **callBack** – function to run after everything is loaded successfully, **callBad** - function to run on failure. Functions gets **setN** as the first argument.
305
+
306
+ `o.incCheck(setID)` – true if include files set number **setID** is loaded.
307
+
308
+ `o.incCacheClear([all])` – true. Clears localStorage JS, CSS cache. If **all** is true, removes DOM elements of include and clears all include data.
309
+
310
+ `o.newLoader(promise)` – create a loader for async data; use with `o().connect(loader, state, fail)`.
311
+
312
+ `o.incCache` – true, cache in localStorage enabled.
313
+
314
+ `o.incCacheExp` – 1000 * 60 * 60 * 24, cache for 24 hours.
315
+
316
+ `o.incTimeout` – 6000, ms timeout to load function.
317
+
318
+ `o.incSource` – '', prefix for urls.
319
+
320
+ `o.incForce` false, do not load already loaded files.
321
+
322
+ `o.incAsync` – true, async loading, set to false for in order loading.
323
+
324
+ `o.incCors` – false, do not allow loading from other domains
325
+
326
+ `o.incFns` – object, array of name:status for all loaded functions.
327
+
328
+ ### Cookies and storage
329
+ `o.setCookie(name, value, [options])` – set a cookie.
330
+
331
+ `o.getCookie(name)` – get cookie value.
332
+
333
+ `o.deleteCookie(name)` delete a cookie.
334
+
335
+ `o.clearCookies()` – clear all cookies.
336
+
337
+ `o.clearLocalStorage()`, `o.clearSessionStorage()`, `o.clearTestsStorage()` – clear respective storage.
338
+
339
+ `o.clearAfterTests()` – clear cookies and test-related storage after test run (e.g. in tAfterEach).
340
+
341
+ ### Testing
342
+ `o.test(title, test1, test2, ..., callBack)` – returns [number] **testID**, gets [string] **title** and tests like ["Test title", testFunction], where **testFunction** should return true for success and false or string for failure. If test is async, **testFunction** should get the first parameter and use it in **o.testUpdate()**. Optional **options object** (same argument list as a test case): **`{ sync: true }`** runs steps one after another synchronously (typical with **o.playRecording**); **`{ confirmOnFailure: true, confirmOnFailureTimeout?: number }`** shows a Continue/Stop overlay when a step fails instead of stopping immediately.
343
+
344
+ `o.sleep(ms)` – returns a **Promise** that resolves after **ms** milliseconds (used by **o.exportTest** and **o.playRecording** action delays).
345
+
346
+ `o.addTest(title, ...cases)` – add a test suite; returns handle for **o.runTest()**.
347
+
348
+ `o.runTest(testId?, autoRun?, savePrev?)` – run test(s). **savePrev** true keeps existing sessionStorage for that testId so the run can resume.
349
+
350
+ `o.testUpdate(info, result, [description])` – returns undefined, gets **info** object (the first parameter of any **testFunction**) to update test status and set it to **result** (true or false/string), **description** - additional text if needed. Used for test status update for async tests. More info [here](https://fous.name/objs).
351
+
352
+ `o.updateLogs()` – return test log lines (e.g. for assertions).
353
+
354
+ `o.tLog[testID]` – test sessions and text results.
355
+
356
+ `o.tRes[testID]` – test sets results as true/false.
357
+
358
+ `o.tStatus[testID: [functionID: true/false],...]` – an array of set test functions statuses.
359
+
360
+ `o.tShowOk` – false, success tests are hidden, only errors. Set to **true** to see success results before **o.test()**.
361
+
362
+ `o.tStyled` – false, logs are in console view. Set to **true** to make logs HTML styled before **o.test()**.
363
+
364
+ `o.tTime` – 2000, milliseconds timeout for async tests.
365
+
366
+ `o.tBeforeEach` / `o.tAfterEach`global hooks called before/after each test case. Set to a function.
367
+
368
+ ### Recording and export
369
+ Available in all builds so QA testers/assessors can record on staging or production environments.
370
+
371
+ > **Security note:** `o.startRecording()` intercepts `window.fetch` and captures request/response bodies including auth tokens. Appropriate for staging environments; review before enabling on production.
372
+
373
+ `o.startRecording(observe?, events?, timeouts?)` – `UPDATED` starts capturing user interactions and network requests as mocks (**fetch** and **XMLHttpRequest**). Optional `observe` is a CSS selector to scope the MutationObserver (e.g. `'#task-app'`). Default events: **`click`**, **`mouseover`**, **`scroll`**, **`input`**, **`change`**, **`submit`**, **`keydown`**, **`focus`**, **`blur`** (override with the **`events`** array). Default per-event debounce/step delays include **`{ click: 100, mouseover: 50, scroll: 30, input: 50, change: 50, submit: 100, keydown: 50, focus: 50, blur: 50 }`** (merge with **`timeouts`**). **Blur**/**focus** on a target removed by the **immediately preceding** recorded action are not captured. Check **o.recorder.active** to see if recording is on.
374
+
375
+ `o.startRecording({ observe?, events?, timeouts?, strictCaptureAssertions?, strictCaptureNetwork?, strictCaptureWebSocket? })` – Same as above using an options object. The optional **strictCapture\*** booleans are stored on the returned recording as **`strictCapture`** and used as defaults for **`o.playRecording`** strict modes when you do not override them in play options.
376
+
377
+ `o.stopRecording()` – `UPDATED` stops recording, returns `{actions, mocks, initialData, assertions, observeRoot, stepDelays, removedElements?, websocketEvents?, strictCapture?}`. Assertions are driven by the MutationObserver: types include **`visible`**, **`class`**, **`style`**, **`hidden`**, **`disabled`**, **`aria-expanded`**, **`aria-checked`**, with fields matching the type (e.g. **`text`**, **`className`**, **`style`**, **`listSelector`**, **`index`**). **`removedElements`** records removed nodes for lenient replay (skip or **strictRemoved**). **`websocketEvents`** holds captured WebSocket URLs and in/out messages when used. **`observeRoot`** is the selector string or null. **`stepDelays`** is the per-event delay map used when replaying.
378
+
379
+ `o.exportTest(recording, options?)` – `UPDATED` returns generated **`o.addTest()`** (or extension-oriented **`o.test`** when **`extensionExport: true`**) source string ready to review and commit. **`options.delay`** is the pause in ms after each action (default **16**; use **`{ delay: 0 }`** to omit **`o.sleep`** in emitted steps).
380
+
381
+ `o.exportPlaywrightTest(recording, [options])` – `NEW` returns a complete Playwright `.spec.ts` file string with network **route** mocks (method/body checks for POST/PUT where applicable), **`page.goto()`**, typed locator steps, real **`expect()`** for DOM (**toHaveClass**, **toHaveCSS**, **toBeHidden**, **toBeDisabled**, **toHaveAttribute**, etc.), and **WebSocket** **`framereceived`/`framesent`** expectations when messages were recorded. **`options.testName`** and **`options.baseUrl`** are optional.
382
+
383
+ ```js
384
+ o.startRecording();
385
+ // QA tester uses the app normally
386
+ const rec = o.stopRecording();
387
+ console.log(o.exportPlaywrightTest(rec, { testName: 'Checkout flow' }));
388
+ // paste → tests/checkout.spec.ts → npx playwright test
389
+ ```
390
+
391
+ `o.clearRecording([id])` – removes recording from sessionStorage.
392
+
393
+ `o.playRecording(recording, [mockOverrides])` – Replays recording as a test with intercepted fetch. Available in all builds (for assessors on staging).
394
+
395
+ `o.playRecording(recording, { … })` – Options include **`runAssertions`**, **`root`**, **`actionDelay`**, **`manualChecks`**, **`mockOverrides`**, **`skipWebSocketMock`**, **`skipNetworkMocks`**, **`recordingAssertionDebug`**, **`onComplete`**, and strict replay: **`strictPlay`** (shorthand for all strict toggles below), **`strictAssertions`** (exact list index and text, normalized style/class match, no fuzzy list rescan), **`strictNetwork`** (mocked fetch/XHR body must match **`mock.request`**), **`strictWebSocket`** (outbound frames must match recording order and payload), **`strictRemoved`** (assertions tied to **`removedElements`** verify absence instead of auto-pass; defaults to **`strictAssertions`** when omitted). With **`runAssertions: true`**, the return value is **`{ testId }`** (object), not a bare numeric id.
396
+
397
+ `o.runRecordingAssertions(recording, root?, actionIdx?, opts?)` – **`opts`** may include **`assertions`**, **`removedElements`**, **`strictAssertions`**, **`strictRemoved`** (same semantics as play). **`removedElements`** / skip logic applies only when **`actionIdx`** is set (as **o.playRecording** does per step); omitting **`actionIdx`** runs all matching assertions without removed-element bypass.
398
+
399
+ ### QA and selectors
400
+ `o.autotag` – set to a string (e.g. `"qa"`) to auto-add `data-{autotag}="component-name"` to all rendered elements. Component name comes from `states.name` (camelCase → kebab-case). Ships in all builds — QA teams can target stable selectors with Playwright/Cypress.
401
+
402
+ `o.reactQA(componentName)` – `NEW` returns a `{ 'data-qa': 'kebab-name' }` object for spreading onto React JSX elements. Converts CamelCase to kebab-case. Respects `o.autotag` value. Ships in all builds.
403
+
404
+ ```jsx
405
+ <button {...o.reactQA('CheckoutButton')} onClick={fn}>Checkout</button>
406
+ // <button data-qa="checkout-button">
407
+ ```
408
+
409
+ ### Measurement and UI assertions (dev)
410
+ All builds include the full API (test framework, playRecording, testOverlay, testConfirm, measure/assertVisible/assertSize). Only the debug flag and debug logging are behind `__DEV__`.
411
+
412
+ `o.measure(el)` – returns `{width, height, top, left, visible, opacity, zIndex}`. Use in test assertions.
413
+
414
+ `o.assertVisible(el)` – returns `true/false` for use inside `o.test()`.
415
+
416
+ `o.assertSize(el, expected)` – returns `true` or a descriptive error string. `expected` can include:
417
+ - `w`, `h`width and height (px)
418
+ - `padding` – same value for all four sides (px), or `paddingTop`, `paddingRight`, `paddingBottom`, `paddingLeft` individually
419
+ - `margin` – same value for all four sides (px), or `marginTop`, `marginRight`, `marginBottom`, `marginLeft` individually
420
+
421
+ Use for design system or UI verification tests (e.g. button height 24px, container padding 20px).
422
+
423
+ `o.testOverlay()` – Renders a fixed overlay button (🧪 Tests). Click to see pass/fail results for all test runs (auto steps and manual checks). Drag handle uses **`grab`**; panel height is capped (**e.g. 90vh**). For assessors: after replay, open the overlay to see if all auto tests passed and which manual checks failed. **`onComplete`** from **o.playRecording** runs after async manual checks (**testConfirm** promises) settle so counts stay accurate. Available in all builds.
424
+
425
+ `o.testConfirm(label, items?, opts?)` – Shows a draggable overlay titled "Label: Paused" with an optional checklist; returns `Promise<{ ok: boolean, errors?: string[] }>`. **`opts.timeout`** (ms) enables a countdown before auto-close. Use after replay for manual checks (e.g. hover effects). Available in all builds. See the [recording example](https://foggysq.github.io/objs/examples/recording/index.html) for a live demo.
426
+
427
+ `o.overlay(opts)` – Low-level draggable overlay: **`innerHTML`**, **`onClose`**, **`timeout`**, **`excludeDragSelector`**, **`removeExisting`**, **`className`**, **`id`**. Shared by **testConfirm**, **testOverlay**, and **confirmOnFailure** test options.
428
+
429
+ ### SSR and Node
430
+ In Node, **o.D** is **o.DocumentMVP** (no real DOM); **o.init().render()** builds a virtual tree and you can serialize with the same code path that produces HTML for SSR. See full docs for getSSR and hydration.
431
+
432
+ ### Utilities and debug
433
+ **o.verify / o.safeVerify / o.specialTypes** — Runtime type checking for arguments, config, or API responses. Useful in projects to fail fast at API boundaries, validate options before use, or keep generated code safe.
434
+
435
+ - **o.verify(pairs, safe?)** — **pairs** is an array of `[value, expectedTypes]`, where **expectedTypes** is a string or array of strings (e.g. `'string'`, `['number','undefined']`). Uses built-in `typeof` checks plus **o.specialTypes**. On failure: throws (default) or returns an Error if **safe** is true. On success: returns `true`.
436
+ - **o.safeVerify(pairs)** Same as **o.verify(pairs, true)**; returns `true` or `false` (no throw).
437
+ - **o.specialTypes** — Object of custom validators used by **o.verify()**. Built-in: `notEmptyString`, `array`, `promise`. **Developers can add global validators here**: assign a function `(value, typeofValue) => boolean` to **o.specialTypes.myType**. That validator is then available everywhere—in your app and inside Objs—so you can use `o.verify([x, ['myType']])` consistently.
438
+
439
+ `o.showErrors` – false as default, but all errors are saved in **o.errors[]**.
440
+
441
+ `o.errors` – an array of all errors.
442
+
443
+ `o.logErrors()` – log all hidden errors in console.
444
+
445
+ `o.onError` – set a function that is called when an error happens.
446
+
447
+ `o.getStates()` – returns array of state info per init.
448
+
449
+ `o.getStores()` returns array of store refs.
450
+
451
+ `o.getListeners()` returns array of listener refs.
452
+
453
+ `o.camelToKebab(str)`, `o.kebabToCamel(str)` – convert between naming conventions.
454
+
455
+ `o.C(obj, key)` – safe `Object.hasOwn`-style check; returns whether `obj` has own property `key`. Used internally; available for app code. `o.F` and `o.U` are internal constants (false, undefined). `o.W` and `o.H` exist but are reserved; do not rely on them.
456
+
457
+
458
+
459
+
460
+ ## Why Objs for AI-assisted development
461
+
462
+ ### The complete loop in one script
463
+
464
+ ```
465
+ developo.autotag / o.reactQA → o.startRecording() → o.stopRecording()
466
+ → o.exportPlaywrightTest() → paste → npx playwright test
467
+ ```
468
+
469
+ No Playwright config to set up manually. No test IDs to maintain. The entire pipeline — component, QA tag, behavior capture, and Playwright test generation — runs inside the same library. Works in React projects too: add one script tag, sprinkle `{...o.reactQA('MyComponent')}`, record.
470
+
471
+ ### Dev/prod build split
472
+
473
+ `objs.js` is the source for development or script tag. `objs.built.js` and `objs.built.min.js` are produced by `node build.js` (ESM + window.o). Only the debug flag is behind `__DEV__`.
474
+
475
+ The **recording pipeline** (`startRecording`, `stopRecording`, `exportTest`, `exportPlaywrightTest`, `reactQA`) ships in all builds so QA assessors can use it on staging.
476
+
477
+ Bundlers pick the right file automatically via `package.json` exports conditions:
478
+
479
+ ```js
480
+ // Vite, webpack, esbuild no config needed
481
+ import o from 'objs-core'; // dev server → objs.js, build → objs.built.js
482
+
483
+ // Script tag
484
+ <script src="objs.js"></script>
485
+ ```
486
+
487
+ ### States as AI-natural data structures
488
+
489
+ Every Objs component is a plain JS object. An LLM can generate correct components from a description without knowing JSX, virtual DOM, or React lifecycle rules:
490
+
491
+ ```js
492
+ // AI prompt: "create a counter with increment and reset"
493
+ // Hack: render as function sets the entity store and returns the init object (no globals, no post-init wiring)
494
+ const counterStates = {
495
+ name: 'Counter',
496
+ render: ({ self }) => {
497
+ self.store.n = self.store.n ?? 0;
498
+ return {
499
+ html: '<span ref="n">0</span> <button ref="inc">+</button> <button ref="rst">Reset</button>',
500
+ events: {
501
+ click: (e) => {
502
+ if (e.target === self.refs?.inc?.el) self.updateCount(++self.store.n);
503
+ else if (e.target === self.refs?.rst?.el) self.updateCount(self.store.n = 0);
504
+ },
505
+ },
506
+ };
507
+ },
508
+ updateCount: ({ self }, num) => {
509
+ self.store.n = num;
510
+ self.refs.n.html(num);
511
+ },
512
+ };
513
+ o.init(counterStates).render().appendInside('#app');
514
+ ```
515
+
516
+ No compiler. No build step to try the above. No framework knowledge needed to generate it.
517
+
518
+ ### Granular reactive updates — no virtual DOM diff
519
+
520
+ Each store subscription calls exactly one targeted DOM write:
521
+
522
+ ```js
523
+ // React with Redux: entire subtree re-renders, React diffs it
524
+ // Objs: one function call, one innerHTML assignment
525
+ o.connectRedux(store, s => s.userName, profileCard, 'updateName');
526
+ o.connectRedux(store, s => s.score, profileCard, 'updateScore');
527
+ ```
528
+
529
+ Similar philosophy to Solid.js signals — but the update logic is a plain function, not a reactive primitive. An AI generates it without any framework knowledge.
530
+
531
+ ### Comparison
532
+
533
+ | | Objs v2.0 | React ecosystem |
534
+ |---|---|---|
535
+ | **Setup** | `<script src="objs.js">` or `npm i objs-core` | React + Babel/Vite + config |
536
+ | **State management** | Built-in states + loaders | Redux / Zustand / MobX (separate) |
537
+ | **Routing** | `o.route()` built-in | React Router (separate) |
538
+ | **Testing** | Built-in `o.test()` + recording | Jest + Testing Library + Playwright |
539
+ | **Dev tools** | Built-in overlay, recording | React DevTools extension |
540
+ | **TypeScript** | `objs.d.ts` included | @types/react + separate config |
541
+ | **SSR** | Built-in DocumentMVP | Next.js / separate hydration setup |
542
+ | **AI context size** | ~2500 lines, one file | Dozens of packages, thousands of files |
543
+ | **Prod bundle overhead** | Dev code fully stripped | Depends on tree-shaking config |
544
+
545
+ ### Real-world patterns
546
+
547
+ See [EXAMPLES.md](EXAMPLES.md) for the architecture guide and runnable examples (aligned with [SKILL.md](SKILL.md)):
548
+ 1. **How render works** — plain object, function, HTML string, multi-instance, `append`, `children`, `ref`/`refs`
549
+ 2. **Single components (atoms)** Button, Badge, Field with `val()`, `css(null)`, `addClass` spread
550
+ 3. **Nesting & composition** slot pattern, `append` in render, factory with dynamic children
551
+ 4. **Design system** — Atoms → Molecules → Organisms, `self.store`, update efficiency
552
+ 5. **Real-world** — menu, cart+cards, dialog, drawer+URL, complex form
553
+ 6. **React integration** four modes including bolt-on Playwright recording with `o.reactQA`
554
+
555
+ **Rule (from SKILL):** Define one state method per data slice; never call `.render()` to update — use targeted state methods. In event handlers use **self.select(e)** and **refs** (e.g. `row.refs.input.val()`), not `e.target`/class selectors or raw DOM.
556
+
557
+
558
+
559
+
560
+ ## License
561
+ Apache 2.0