ngx-json-render 0.3.0 → 0.5.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 +146 -20
- package/fesm2022/ngx-json-render-testing.mjs +232 -56
- package/fesm2022/ngx-json-render-testing.mjs.map +1 -1
- package/fesm2022/ngx-json-render.mjs +516 -49
- package/fesm2022/ngx-json-render.mjs.map +1 -1
- package/package.json +1 -1
- package/types/ngx-json-render-testing.d.ts +110 -17
- package/types/ngx-json-render.d.ts +223 -14
package/README.md
CHANGED
|
@@ -371,6 +371,66 @@ The check waits for the spec to settle. While `loading` is true a missing child
|
|
|
371
371
|
is a patch that has not arrived yet, not a defect, so nothing is reported and
|
|
372
372
|
`strict` keeps rendering; the hooks check once, when the generation completes.
|
|
373
373
|
|
|
374
|
+
Pass the catalog too, and the same check covers what structure alone cannot
|
|
375
|
+
see — every `type` is one the catalog defines, and the props match its schema:
|
|
376
|
+
|
|
377
|
+
```html
|
|
378
|
+
<json-render [spec]="spec()" [registry]="registry" [catalog]="catalog" validate="strict" />
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### Capping what a spec may cost
|
|
382
|
+
|
|
383
|
+
`validate` describes a spec. `renderLimits` constrains it:
|
|
384
|
+
|
|
385
|
+
```html
|
|
386
|
+
<json-render
|
|
387
|
+
[spec]="spec()"
|
|
388
|
+
[registry]="registry"
|
|
389
|
+
[renderLimits]="{ maxElements: 500, maxDepth: 16, maxRepeatItems: 200 }"
|
|
390
|
+
/>
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
```ts
|
|
394
|
+
readonly ui = injectUIStream({
|
|
395
|
+
api: '/api/generate',
|
|
396
|
+
renderLimits: { maxElements: 500, maxDepth: 16 },
|
|
397
|
+
});
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
| Limit | What it caps | When it is passed |
|
|
401
|
+
| ---------------- | ------------------------------- | --------------------------------------------------------------- |
|
|
402
|
+
| `maxElements` | Elements in the spec | Nothing renders — there is no useful part of an oversized graph |
|
|
403
|
+
| `maxDepth` | Nesting, counting the root as 1 | Elements below the cap do not render; what fits still does |
|
|
404
|
+
| `maxRepeatItems` | Items one `repeat` expands | The surplus items do not render |
|
|
405
|
+
|
|
406
|
+
Every limit is opt-in and unset means unlimited; there are no defaults, so
|
|
407
|
+
upgrading changes nothing about what your app renders today.
|
|
408
|
+
|
|
409
|
+
An element a cap refuses does not act either. `watch` is the one thing an
|
|
410
|
+
element does without being on screen, and it stays unwired while the element is
|
|
411
|
+
capped — otherwise a spec could keep dispatching actions from behind a limit
|
|
412
|
+
that was supposed to have stopped it. The same holds for the element that
|
|
413
|
+
closes a cycle.
|
|
414
|
+
|
|
415
|
+
Two things follow from limits being a control rather than a report. They are
|
|
416
|
+
enforced in **every** mode, `validate="off"` included — the app already made
|
|
417
|
+
the decision by setting a number. And they apply while `loading`: a partial
|
|
418
|
+
spec is a subset of the finished one, so a cap can only ever fire early, never
|
|
419
|
+
falsely. Under `strict`, a spec over `maxDepth` is refused outright rather than
|
|
420
|
+
truncated, which is how an app says it would rather draw nothing than draw the
|
|
421
|
+
first sixteen levels of something hostile.
|
|
422
|
+
|
|
423
|
+
In the hooks, a limit fails the generation instead of calling `onComplete`, in
|
|
424
|
+
any mode — the same reasoning as `strict`: `onComplete` is where apps persist a
|
|
425
|
+
spec, and this is one the app has already refused.
|
|
426
|
+
|
|
427
|
+
One order is deliberate rather than incidental. The cheap caps run before the
|
|
428
|
+
structural check, because core's `validateSpec` walks the tree by recursion:
|
|
429
|
+
the specs that most need a limit are exactly the ones that would overflow the
|
|
430
|
+
stack proving they exceed it. If you accept specs you did not generate, set
|
|
431
|
+
`maxDepth` — with no cap there is nothing to stop the check from recursing as
|
|
432
|
+
deep as the spec asks.
|
|
433
|
+
|
|
374
434
|
## Testing
|
|
375
435
|
|
|
376
436
|
`ngx-json-render/testing` is a separate entry point, so nothing in it can reach
|
|
@@ -413,6 +473,40 @@ the harness: `text` / `texts` / `find` / `findAll` for the DOM, `click` /
|
|
|
413
473
|
Options mirror the renderer's inputs, plus `providers` for anything the
|
|
414
474
|
components under test inject.
|
|
415
475
|
|
|
476
|
+
### Mounting one component
|
|
477
|
+
|
|
478
|
+
`renderComponent` mounts a catalog component with no renderer and no spec. You
|
|
479
|
+
give it props and read back what it emitted and what it wrote to bound props.
|
|
480
|
+
It tests the component as a presentational one — what it draws, and what it
|
|
481
|
+
says when used — and leaves what the spec does with that to `renderSpec`:
|
|
482
|
+
|
|
483
|
+
```ts
|
|
484
|
+
import { renderComponent } from 'ngx-json-render/testing';
|
|
485
|
+
|
|
486
|
+
it('emits press, and stays quiet once disabled', async () => {
|
|
487
|
+
const button = await renderComponent(MyButton, { props: { label: 'Save' } });
|
|
488
|
+
|
|
489
|
+
expect(button.text('button')).toBe('Save');
|
|
490
|
+
await button.click('button');
|
|
491
|
+
expect(button.emitted).toEqual(['press']);
|
|
492
|
+
|
|
493
|
+
await button.patchProps({ disabled: true });
|
|
494
|
+
expect(button.find<HTMLButtonElement>('button').disabled).toBe(true);
|
|
495
|
+
});
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
`bindings` says which props are two-way bound (`{ checked: '/dark' }`): a
|
|
499
|
+
`setBound` on one of them is recorded in `writes` and lands back in the props,
|
|
500
|
+
and on any other prop it is the same no-op it is under the renderer. `on` lists
|
|
501
|
+
the events the spec would bind, for components that read `ctx.on(event).bound`;
|
|
502
|
+
`key`, `type` and `loading` fill in the rest of the context, and `setProps` /
|
|
503
|
+
`patchProps` / `setLoading` change it between frames. The DOM helpers are the
|
|
504
|
+
same as `renderSpec`'s.
|
|
505
|
+
|
|
506
|
+
A component that renders `<jr-children>`, registers field validation or
|
|
507
|
+
dispatches actions itself needs the renderer's own services, and there is no
|
|
508
|
+
honest fake for a subtree — it fails with a message pointing at `renderSpec`.
|
|
509
|
+
|
|
416
510
|
### Replaying a generation
|
|
417
511
|
|
|
418
512
|
`recordedTransport` is a `fetch` that answers from a recording, so a test runs
|
|
@@ -616,10 +710,36 @@ string to the `navigate` callback you provide, verbatim. Treat it as untrusted:
|
|
|
616
710
|
match it against known routes, and never hand it to `window.location` or
|
|
617
711
|
`router.navigateByUrl` unchecked.
|
|
618
712
|
|
|
619
|
-
**A spec
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
713
|
+
**A spec cannot render forever.** Two elements naming each other as children,
|
|
714
|
+
or one naming itself, would recurse until the tab died. The renderer refuses to
|
|
715
|
+
draw an element that is rendering itself again _without reading any deeper into
|
|
716
|
+
state_, so the cycle is broken where it closes and everything above it still
|
|
717
|
+
renders. This holds in every mode, including `validate="off"` and mid-stream:
|
|
718
|
+
it is a crash guard, not an opinion about spec quality. Core's `validateSpec`
|
|
719
|
+
does not report cycles, so nothing else in the stack catches this for you.
|
|
720
|
+
|
|
721
|
+
Recursion that goes somewhere is untouched. A tree — a comment thread, a file
|
|
722
|
+
browser, a nested menu — is an element repeating over a path relative to the
|
|
723
|
+
item it is already inside (`{"$item": "children"}`) and rendering itself for
|
|
724
|
+
each one, so every pass reads one level further in and the drawing ends where
|
|
725
|
+
the data does. What the guard stops is the pass that reads the same array
|
|
726
|
+
again: `repeat` over a fixed `/items` inside itself never runs out, however
|
|
727
|
+
much data there is.
|
|
728
|
+
|
|
729
|
+
**A spec sizes its own render tree, up to the caps you set.** `repeat` iterates
|
|
730
|
+
a state array the spec may itself have supplied, and nesting costs a component
|
|
731
|
+
per level, so specs are a denial-of-service surface against the browser tab.
|
|
732
|
+
`renderLimits` is the cap: `maxElements`, `maxDepth` and `maxRepeatItems`,
|
|
733
|
+
enforced in every mode. They are unset by default — a renderer cannot guess
|
|
734
|
+
what your catalog considers a reasonable page — so an app taking specs it did
|
|
735
|
+
not generate should set all three. See
|
|
736
|
+
[Capping what a spec may cost](#capping-what-a-spec-may-cost).
|
|
737
|
+
|
|
738
|
+
**A spec can name components you never built.** `validate` with a `catalog`
|
|
739
|
+
reports every `type` the catalog does not define and every prop its schema
|
|
740
|
+
rejects; `strict` refuses such a spec outright. Without a catalog the renderer
|
|
741
|
+
only warns and draws nothing in that element's place, which degrades well but
|
|
742
|
+
tells you nothing until you read the console.
|
|
623
743
|
|
|
624
744
|
**`confirm` is a UX affordance, not a security control.** It routes an action
|
|
625
745
|
through the confirmation dialog before the handler runs, but it is set on the
|
|
@@ -635,33 +755,39 @@ Injectables/helpers: `injectRenderContext`, `injectElementKey`, `injectRepeatSco
|
|
|
635
755
|
|
|
636
756
|
Tokens: `JR_CONFIRM_DIALOG` (replace the confirmation dialog), `JR_CONFIRM_LABELS` (its two words), `CONFIRM_CONTEXT`, `RENDER_CONTEXT`, `REPEAT_SCOPE`.
|
|
637
757
|
|
|
758
|
+
Spec checking: `checkSpec`, `formatSpecCheckIssues`, and the types
|
|
759
|
+
`RenderLimits`, `SpecCheck`, `SpecCheckIssue`, `SpecCheckIssueCode`,
|
|
760
|
+
`SpecCheckOptions`, `SpecCatalog`, `SpecValidationMode`.
|
|
761
|
+
|
|
638
762
|
`injectActions().execute()` rejects when the user dismisses a `confirm`
|
|
639
763
|
dialog, which is a normal gesture rather than a failure — `isActionCancelled(error)`
|
|
640
764
|
is how you tell the two apart.
|
|
641
765
|
|
|
642
766
|
Registry & schema: `defineRegistry`, `createStoreSetState`, `schema`.
|
|
643
767
|
|
|
644
|
-
Testing (`ngx-json-render/testing`): `renderSpec`, `recordedTransport`, `specStream`, `usageLine`.
|
|
768
|
+
Testing (`ngx-json-render/testing`): `renderSpec`, `renderComponent`, `recordedTransport`, `specStream`, `usageLine`.
|
|
645
769
|
|
|
646
770
|
Everything from `@json-render/core` (types, `createStateStore`, `nestedToFlat`, prompt builders, spec validators, SpecStream compiler) composes with this package; the most common symbols are re-exported.
|
|
647
771
|
|
|
648
772
|
## Renderer inputs
|
|
649
773
|
|
|
650
|
-
| Input | Type | Purpose
|
|
651
|
-
| --------------------- | ------------------------------------ |
|
|
652
|
-
| `spec` | `Spec \| null` | The UI spec (may be partial while streaming)
|
|
653
|
-
| `registry` | `ComponentRegistry` | Catalog type → Angular component
|
|
654
|
-
| `loading` | `boolean` | Suppress missing-element warnings while streaming
|
|
655
|
-
| `fallback` | `Type<unknown>` | Component for unknown types
|
|
656
|
-
| `validate` | `'off' \| 'warn' \| 'strict'` | Check the settled spec's structure (default `'off'`)
|
|
657
|
-
| `
|
|
658
|
-
| `
|
|
659
|
-
| `
|
|
660
|
-
| `
|
|
661
|
-
| `
|
|
662
|
-
| `
|
|
663
|
-
| `
|
|
664
|
-
| `
|
|
774
|
+
| Input | Type | Purpose |
|
|
775
|
+
| --------------------- | ------------------------------------ | ------------------------------------------------------- |
|
|
776
|
+
| `spec` | `Spec \| null` | The UI spec (may be partial while streaming) |
|
|
777
|
+
| `registry` | `ComponentRegistry` | Catalog type → Angular component |
|
|
778
|
+
| `loading` | `boolean` | Suppress missing-element warnings while streaming |
|
|
779
|
+
| `fallback` | `Type<unknown>` | Component for unknown types |
|
|
780
|
+
| `validate` | `'off' \| 'warn' \| 'strict'` | Check the settled spec's structure (default `'off'`) |
|
|
781
|
+
| `renderLimits` | `RenderLimits` | Cap elements, depth and repeat expansion (default none) |
|
|
782
|
+
| `catalog` | `Catalog` | Also check types and props against the catalog |
|
|
783
|
+
| `state` | `StateModel` | Initial state (uncontrolled; defaults to `spec.state`) |
|
|
784
|
+
| `store` | `StateStore` | External store (controlled mode) |
|
|
785
|
+
| `handlers` | `Record<string, ActionHandler>` | Action handlers |
|
|
786
|
+
| `onAction` | `(name, params) => unknown` | Catch-all action handler |
|
|
787
|
+
| `navigate` | `(path) => void` | Used by `onSuccess: { navigate }` |
|
|
788
|
+
| `validationFunctions` | `Record<string, ValidationFunction>` | Custom validation |
|
|
789
|
+
| `functions` | `Record<string, ComputedFunction>` | `$computed` functions |
|
|
790
|
+
| `directives` | `DirectiveDefinition[]` | Custom `$`-prefixed expressions |
|
|
665
791
|
|
|
666
792
|
Output: `(stateChange)` — batched `{ path, value }[]` in uncontrolled mode.
|
|
667
793
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { NgComponentOutlet } from '@angular/common';
|
|
1
2
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { signal, ChangeDetectionStrategy, Component,
|
|
3
|
+
import { provideZonelessChangeDetection, inject, Injector, signal, ChangeDetectionStrategy, Component, computed, isDevMode } from '@angular/core';
|
|
3
4
|
import { TestBed } from '@angular/core/testing';
|
|
4
|
-
import { JsonRenderer, JsonRenderValidationService } from 'ngx-json-render';
|
|
5
|
+
import { RENDER_CONTEXT, ELEMENT_KEY, JsonRenderer, JsonRenderValidationService } from 'ngx-json-render';
|
|
5
6
|
|
|
6
7
|
/** The rejection `fetch` raises when its signal fires. */
|
|
7
8
|
function abortError() {
|
|
@@ -163,6 +164,230 @@ function usageLine(usage) {
|
|
|
163
164
|
});
|
|
164
165
|
}
|
|
165
166
|
|
|
167
|
+
/**
|
|
168
|
+
* What `renderSpec` and `renderComponent` share: the TestBed setup, the settle
|
|
169
|
+
* loop, and the handful of DOM queries a test runs against what rendered.
|
|
170
|
+
*
|
|
171
|
+
* @internal Not exported from the entry point.
|
|
172
|
+
*/
|
|
173
|
+
/**
|
|
174
|
+
* Configure the TestBed, unless the test already did.
|
|
175
|
+
*
|
|
176
|
+
* A test that called `TestBed.runInInjectionContext` first — or that renders
|
|
177
|
+
* a second time — has an instantiated module, and configuring it again
|
|
178
|
+
* throws. Its own module stands; the only thing that cannot be salvaged is
|
|
179
|
+
* this call's `providers`, so that is the one case worth failing over.
|
|
180
|
+
*/
|
|
181
|
+
function configureTestBed(caller, providers) {
|
|
182
|
+
try {
|
|
183
|
+
TestBed.configureTestingModule({
|
|
184
|
+
providers: [provideZonelessChangeDetection(), ...providers],
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
if (providers.length > 0) {
|
|
189
|
+
throw new Error(`${caller}: the TestBed was already instantiated, so \`providers\` cannot be applied. Pass them to your own TestBed.configureTestingModule() call instead.`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/** Let effects, promises and change detection finish. */
|
|
194
|
+
function settler(fixture) {
|
|
195
|
+
return async () => {
|
|
196
|
+
// Twice: the first pass runs the effects that a render schedules, the
|
|
197
|
+
// second renders what those effects changed. One pass leaves a test
|
|
198
|
+
// asserting against a frame that no browser would ever show.
|
|
199
|
+
await fixture.whenStable();
|
|
200
|
+
fixture.detectChanges();
|
|
201
|
+
await fixture.whenStable();
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function domQueries(caller, element, settle) {
|
|
205
|
+
const resolve = (target) => {
|
|
206
|
+
if (typeof target !== 'string')
|
|
207
|
+
return target;
|
|
208
|
+
const found = element.querySelector(target);
|
|
209
|
+
if (!found) {
|
|
210
|
+
throw new Error(`${caller}: nothing matches ${JSON.stringify(target)}. Rendered HTML:\n${element.innerHTML}`);
|
|
211
|
+
}
|
|
212
|
+
return found;
|
|
213
|
+
};
|
|
214
|
+
return {
|
|
215
|
+
text: (selector) => ((selector ? resolve(selector) : element).textContent ?? '').trim(),
|
|
216
|
+
texts: (selector) => Array.from(element.querySelectorAll(selector)).map((el) => (el.textContent ?? '').trim()),
|
|
217
|
+
find: (selector) => resolve(selector),
|
|
218
|
+
findAll: (selector) => Array.from(element.querySelectorAll(selector)),
|
|
219
|
+
async click(target) {
|
|
220
|
+
resolve(target).click();
|
|
221
|
+
await settle();
|
|
222
|
+
},
|
|
223
|
+
async fill(target, value) {
|
|
224
|
+
const input = resolve(target);
|
|
225
|
+
input.value = value;
|
|
226
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
227
|
+
await settle();
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Mounted through `NgComponentOutlet` with an element injector, the way
|
|
234
|
+
* `<jr-element>` mounts a catalog component — so the context is the
|
|
235
|
+
* component's own, not the TestBed module's, and a test can mount twice.
|
|
236
|
+
*/
|
|
237
|
+
class JrComponentTestHost {
|
|
238
|
+
parent = inject(Injector);
|
|
239
|
+
component = signal(null, ...(ngDevMode ? [{ debugName: "component" }] : /* istanbul ignore next */ []));
|
|
240
|
+
injector = signal(this.parent, ...(ngDevMode ? [{ debugName: "injector" }] : /* istanbul ignore next */ []));
|
|
241
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: JrComponentTestHost, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
242
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: JrComponentTestHost, isStandalone: true, selector: "jr-component-test-host", ngImport: i0, template: `
|
|
243
|
+
@if (component(); as type) {
|
|
244
|
+
<ng-container *ngComponentOutlet="type; injector: injector()" />
|
|
245
|
+
}
|
|
246
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
247
|
+
}
|
|
248
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: JrComponentTestHost, decorators: [{
|
|
249
|
+
type: Component,
|
|
250
|
+
args: [{
|
|
251
|
+
selector: 'jr-component-test-host',
|
|
252
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
253
|
+
imports: [NgComponentOutlet],
|
|
254
|
+
template: `
|
|
255
|
+
@if (component(); as type) {
|
|
256
|
+
<ng-container *ngComponentOutlet="type; injector: injector()" />
|
|
257
|
+
}
|
|
258
|
+
`,
|
|
259
|
+
}]
|
|
260
|
+
}] });
|
|
261
|
+
/** The renderer-owned services a component can only have from a renderer. */
|
|
262
|
+
const RENDERER_ONLY = /JsonRender(RootContext|StateService|ValidationService|ActionsService)/;
|
|
263
|
+
/**
|
|
264
|
+
* Turn "no provider for the renderer's services" into what to do about it.
|
|
265
|
+
* `<jr-children>`, `injectFieldValidation` and `injectActions` all need a
|
|
266
|
+
* real renderer behind them; there is no honest fake for a subtree.
|
|
267
|
+
*/
|
|
268
|
+
function explainMissingRenderer(error) {
|
|
269
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
270
|
+
if (RENDERER_ONLY.test(message)) {
|
|
271
|
+
throw new Error(`renderComponent: this component needs a real <json-render> — it renders <jr-children>, validates a field or dispatches actions itself. Test it with renderSpec instead.\n\n${message}`);
|
|
272
|
+
}
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Mount a catalog component with no renderer and no spec: hand it props, and
|
|
277
|
+
* read back the events it emitted and the values it wrote to bound props.
|
|
278
|
+
*
|
|
279
|
+
* This is the test of a component as a presentational one — what it draws
|
|
280
|
+
* for these props, and what it says when used. What the spec then does with
|
|
281
|
+
* that is the renderer's business, and `renderSpec` is where to test it.
|
|
282
|
+
*
|
|
283
|
+
* `props` is not inferred from the first value, so a later `patchProps` can
|
|
284
|
+
* set a prop the first render left out. Pass `P` to have the props typed.
|
|
285
|
+
*
|
|
286
|
+
* A component that renders `<jr-children>`, registers field validation or
|
|
287
|
+
* dispatches actions directly needs the renderer's services and fails with a
|
|
288
|
+
* message pointing at `renderSpec`.
|
|
289
|
+
*
|
|
290
|
+
* @example
|
|
291
|
+
* ```ts
|
|
292
|
+
* const button = await renderComponent(MyButton, { props: { label: 'Save' } });
|
|
293
|
+
*
|
|
294
|
+
* expect(button.text('button')).toBe('Save');
|
|
295
|
+
* await button.click('button');
|
|
296
|
+
* expect(button.emitted).toEqual(['press']);
|
|
297
|
+
*
|
|
298
|
+
* await button.patchProps({ disabled: true });
|
|
299
|
+
* expect(button.find<HTMLButtonElement>('button').disabled).toBe(true);
|
|
300
|
+
* ```
|
|
301
|
+
*/
|
|
302
|
+
async function renderComponent(component, options = {}) {
|
|
303
|
+
const emitted = [];
|
|
304
|
+
const writes = [];
|
|
305
|
+
const props = signal(options.props ?? {}, ...(ngDevMode ? [{ debugName: "props" }] : /* istanbul ignore next */ []));
|
|
306
|
+
const loading = signal(options.loading ?? false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
307
|
+
const bindings = options.bindings;
|
|
308
|
+
const boundEvents = new Set(options.on ?? []);
|
|
309
|
+
const key = signal(options.key ?? 'element', ...(ngDevMode ? [{ debugName: "key" }] : /* istanbul ignore next */ []));
|
|
310
|
+
const element = computed(() => ({
|
|
311
|
+
type: options.type ?? component.name,
|
|
312
|
+
props: props(),
|
|
313
|
+
children: [],
|
|
314
|
+
}), ...(ngDevMode ? [{ debugName: "element" }] : /* istanbul ignore next */ []));
|
|
315
|
+
const emit = (event) => {
|
|
316
|
+
emitted.push(event);
|
|
317
|
+
};
|
|
318
|
+
const context = {
|
|
319
|
+
element,
|
|
320
|
+
props: props.asReadonly(),
|
|
321
|
+
emit,
|
|
322
|
+
on: (event) => ({
|
|
323
|
+
emit: () => emit(event),
|
|
324
|
+
shouldPreventDefault: false,
|
|
325
|
+
bound: boundEvents.has(event),
|
|
326
|
+
}),
|
|
327
|
+
bindings: signal(bindings).asReadonly(),
|
|
328
|
+
loading: loading.asReadonly(),
|
|
329
|
+
setBound: (prop, value) => {
|
|
330
|
+
if (!bindings?.[prop]) {
|
|
331
|
+
if (isDevMode()) {
|
|
332
|
+
console.warn(`[ngx-json-render] setBound("${prop}"): prop has no $bindState/$bindItem binding`);
|
|
333
|
+
}
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
writes.push({ prop, value });
|
|
337
|
+
// What the renderer's round trip amounts to: the write lands in state,
|
|
338
|
+
// and the bound prop resolves to it on the next read.
|
|
339
|
+
props.update((current) => ({ ...current, [prop]: value }));
|
|
340
|
+
},
|
|
341
|
+
};
|
|
342
|
+
configureTestBed('renderComponent', options.providers ?? []);
|
|
343
|
+
const fixture = TestBed.createComponent(JrComponentTestHost);
|
|
344
|
+
const host = fixture.componentInstance;
|
|
345
|
+
host.injector.set(Injector.create({
|
|
346
|
+
providers: [
|
|
347
|
+
{ provide: RENDER_CONTEXT, useValue: context },
|
|
348
|
+
{ provide: ELEMENT_KEY, useValue: key.asReadonly() },
|
|
349
|
+
],
|
|
350
|
+
parent: host.parent,
|
|
351
|
+
}));
|
|
352
|
+
host.component.set(component);
|
|
353
|
+
const settle = settler(fixture);
|
|
354
|
+
try {
|
|
355
|
+
await settle();
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
explainMissingRenderer(error);
|
|
359
|
+
}
|
|
360
|
+
const mounted = fixture.debugElement.query((candidate) => candidate.componentInstance instanceof component);
|
|
361
|
+
if (!mounted) {
|
|
362
|
+
throw new Error(`renderComponent: ${component.name} did not mount. Is it a component?`);
|
|
363
|
+
}
|
|
364
|
+
const root = fixture.nativeElement;
|
|
365
|
+
return {
|
|
366
|
+
fixture,
|
|
367
|
+
element: root,
|
|
368
|
+
component: mounted.componentInstance,
|
|
369
|
+
context,
|
|
370
|
+
emitted,
|
|
371
|
+
writes,
|
|
372
|
+
props: () => props(),
|
|
373
|
+
async setProps(next) {
|
|
374
|
+
props.set(next);
|
|
375
|
+
await settle();
|
|
376
|
+
},
|
|
377
|
+
async patchProps(patch) {
|
|
378
|
+
props.update((current) => ({ ...current, ...patch }));
|
|
379
|
+
await settle();
|
|
380
|
+
},
|
|
381
|
+
async setLoading(next) {
|
|
382
|
+
loading.set(next);
|
|
383
|
+
await settle();
|
|
384
|
+
},
|
|
385
|
+
settle,
|
|
386
|
+
...domQueries('renderComponent', root, settle),
|
|
387
|
+
destroy: () => fixture.destroy(),
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
166
391
|
class JrTestHost {
|
|
167
392
|
spec = signal(null, ...(ngDevMode ? [{ debugName: "spec" }] : /* istanbul ignore next */ []));
|
|
168
393
|
registry = signal({}, ...(ngDevMode ? [{ debugName: "registry" }] : /* istanbul ignore next */ []));
|
|
@@ -194,7 +419,7 @@ class JrTestHost {
|
|
|
194
419
|
[directives]="directives()"
|
|
195
420
|
(stateChange)="record($event)"
|
|
196
421
|
/>
|
|
197
|
-
`, isInline: true, dependencies: [{ kind: "component", type: JsonRenderer, selector: "json-render", inputs: ["spec", "registry", "loading", "fallback", "validate", "state", "store", "handlers", "onAction", "navigate", "validationFunctions", "functions", "directives"], outputs: ["stateChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
422
|
+
`, isInline: true, dependencies: [{ kind: "component", type: JsonRenderer, selector: "json-render", inputs: ["spec", "registry", "loading", "fallback", "validate", "renderLimits", "catalog", "state", "store", "handlers", "onAction", "navigate", "validationFunctions", "functions", "directives"], outputs: ["stateChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
198
423
|
}
|
|
199
424
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: JrTestHost, decorators: [{
|
|
200
425
|
type: Component,
|
|
@@ -238,26 +463,6 @@ function recordingHandlers(handlers, dispatched) {
|
|
|
238
463
|
}
|
|
239
464
|
return wrapped;
|
|
240
465
|
}
|
|
241
|
-
/**
|
|
242
|
-
* Configure the TestBed, unless the test already did.
|
|
243
|
-
*
|
|
244
|
-
* A test that called `TestBed.runInInjectionContext` first — or that renders
|
|
245
|
-
* a second spec — has an instantiated module, and configuring it again
|
|
246
|
-
* throws. Its own module stands; the only thing that cannot be salvaged is
|
|
247
|
-
* this call's `providers`, so that is the one case worth failing over.
|
|
248
|
-
*/
|
|
249
|
-
function configureTestBed(providers) {
|
|
250
|
-
try {
|
|
251
|
-
TestBed.configureTestingModule({
|
|
252
|
-
providers: [provideZonelessChangeDetection(), ...providers],
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
catch {
|
|
256
|
-
if (providers.length > 0) {
|
|
257
|
-
throw new Error('renderSpec: the TestBed was already instantiated, so `providers` cannot be applied. Pass them to your own TestBed.configureTestingModule() call instead.');
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
466
|
/**
|
|
262
467
|
* Render a spec against a registry and hand back the few things a test does
|
|
263
468
|
* to it.
|
|
@@ -287,7 +492,7 @@ function configureTestBed(providers) {
|
|
|
287
492
|
async function renderSpec(spec, options) {
|
|
288
493
|
const dispatched = [];
|
|
289
494
|
const changes = [];
|
|
290
|
-
configureTestBed(options.providers ?? []);
|
|
495
|
+
configureTestBed('renderSpec', options.providers ?? []);
|
|
291
496
|
const fixture = TestBed.createComponent(JrTestHost);
|
|
292
497
|
const host = fixture.componentInstance;
|
|
293
498
|
host.registry.set(options.registry);
|
|
@@ -306,14 +511,7 @@ async function renderSpec(spec, options) {
|
|
|
306
511
|
host.directives.set(options.directives);
|
|
307
512
|
host.record = (batch) => changes.push(...batch);
|
|
308
513
|
host.spec.set(spec);
|
|
309
|
-
const settle =
|
|
310
|
-
// Twice: the first pass runs the effects that a render schedules, the
|
|
311
|
-
// second renders what those effects changed. One pass leaves a test
|
|
312
|
-
// asserting against a frame that no browser would ever show.
|
|
313
|
-
await fixture.whenStable();
|
|
314
|
-
fixture.detectChanges();
|
|
315
|
-
await fixture.whenStable();
|
|
316
|
-
};
|
|
514
|
+
const settle = settler(fixture);
|
|
317
515
|
await settle();
|
|
318
516
|
const element = fixture.nativeElement;
|
|
319
517
|
// Found by predicate rather than with `By.directive`, which would make
|
|
@@ -324,15 +522,6 @@ async function renderSpec(spec, options) {
|
|
|
324
522
|
// From the renderer's own injector: these services are provided by the
|
|
325
523
|
// component, so the root TestBed injector has never heard of them.
|
|
326
524
|
const validation = rendererElement.injector.get(JsonRenderValidationService);
|
|
327
|
-
const resolve = (target) => {
|
|
328
|
-
if (typeof target !== 'string')
|
|
329
|
-
return target;
|
|
330
|
-
const found = element.querySelector(target);
|
|
331
|
-
if (!found) {
|
|
332
|
-
throw new Error(`renderSpec: nothing matches ${JSON.stringify(target)}. Rendered HTML:\n${element.innerHTML}`);
|
|
333
|
-
}
|
|
334
|
-
return found;
|
|
335
|
-
};
|
|
336
525
|
return {
|
|
337
526
|
fixture,
|
|
338
527
|
element,
|
|
@@ -356,20 +545,7 @@ async function renderSpec(spec, options) {
|
|
|
356
545
|
await settle();
|
|
357
546
|
},
|
|
358
547
|
state: () => store.state(),
|
|
359
|
-
|
|
360
|
-
texts: (selector) => Array.from(element.querySelectorAll(selector)).map((el) => (el.textContent ?? '').trim()),
|
|
361
|
-
find: (selector) => resolve(selector),
|
|
362
|
-
findAll: (selector) => Array.from(element.querySelectorAll(selector)),
|
|
363
|
-
async click(target) {
|
|
364
|
-
resolve(target).click();
|
|
365
|
-
await settle();
|
|
366
|
-
},
|
|
367
|
-
async fill(target, value) {
|
|
368
|
-
const input = resolve(target);
|
|
369
|
-
input.value = value;
|
|
370
|
-
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
371
|
-
await settle();
|
|
372
|
-
},
|
|
548
|
+
...domQueries('renderSpec', element, settle),
|
|
373
549
|
destroy: () => fixture.destroy(),
|
|
374
550
|
};
|
|
375
551
|
}
|
|
@@ -385,5 +561,5 @@ async function renderSpec(spec, options) {
|
|
|
385
561
|
* Generated bundle index. Do not edit.
|
|
386
562
|
*/
|
|
387
563
|
|
|
388
|
-
export { recordedTransport, renderSpec, specStream, usageLine };
|
|
564
|
+
export { recordedTransport, renderComponent, renderSpec, specStream, usageLine };
|
|
389
565
|
//# sourceMappingURL=ngx-json-render-testing.mjs.map
|