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 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 sizes its own render tree.** `repeat` iterates a state array the spec
620
- may itself have supplied, so specs are a denial-of-service surface against the
621
- browser tab. Cap spec size and array lengths at the boundary where you accept
622
- one.
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
- | `state` | `StateModel` | Initial state (uncontrolled; defaults to `spec.state`) |
658
- | `store` | `StateStore` | External store (controlled mode) |
659
- | `handlers` | `Record<string, ActionHandler>` | Action handlers |
660
- | `onAction` | `(name, params) => unknown` | Catch-all action handler |
661
- | `navigate` | `(path) => void` | Used by `onSuccess: { navigate }` |
662
- | `validationFunctions` | `Record<string, ValidationFunction>` | Custom validation |
663
- | `functions` | `Record<string, ComputedFunction>` | `$computed` functions |
664
- | `directives` | `DirectiveDefinition[]` | Custom `$`-prefixed expressions |
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, provideZonelessChangeDetection } from '@angular/core';
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 = async () => {
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
- text: (selector) => ((selector ? resolve(selector) : element).textContent ?? '').trim(),
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