ngx-speculoos 12.0.1 → 13.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,6 +18,7 @@ how to write Angular unit tests.
18
18
  - [Getting started](#getting-started)
19
19
  - [Features in details](#features-in-details)
20
20
  - [ComponentTester](#componenttester)
21
+ - [Automatic change detection](#automatic-change-detection)
21
22
  - [Queries](#queries)
22
23
  - [Queries for elements](#queries-for-elements)
23
24
  - [CSS and Type selectors](#css-and-type-selectors)
@@ -37,6 +38,7 @@ how to write Angular unit tests.
37
38
  - [Can I use the TestElement methods to act on the component element itself, rather than a sub-element?](#can-i-use-the-testelement-methods-to-act-on-the-component-element-itself-rather-than-a-sub-element)
38
39
  - [Issues, questions](#issues-questions)
39
40
  - [Complete example](#complete-example)
41
+ - [Upgrading to v13](#upgrading-to-v13)
40
42
 
41
43
  ## Quick presentation
42
44
 
@@ -181,7 +183,7 @@ describe('My component', () => {
181
183
  });
182
184
 
183
185
  tester = new MyComponentTester();
184
- tester.detectChanges();
186
+ tester.change();
185
187
  });
186
188
 
187
189
  it('should ...', () => {
@@ -189,6 +191,74 @@ describe('My component', () => {
189
191
  });
190
192
  ```
191
193
 
194
+ ### Automatic change detection
195
+
196
+ The future of Angular is zoneless. Without ZoneJS, components have to make sure to properly notify
197
+ Angular that they must be checked for changes, typically by updating signals.
198
+ Instead of imperatively triggering change detections in tests, it's thus a better idea to let
199
+ Angular decide if change detection must be run, in order to spot bugs where the component doesn't
200
+ properly handle its state changes.
201
+
202
+ This can be done by:
203
+
204
+ - adding a provider in the testing module to configure the fixtures to be in _automatic_ mode
205
+ - awaiting the component fixture stability when the test *thinks* that a change detection should
206
+ automatically happen.
207
+
208
+ When the `provideAutomaticChangeDetection()` provider is added, the `ComponentTester` will run in
209
+ _automatic_ mode. In this mode, calling `detectChanges()` throws an error, because you should always
210
+ let Angular decide if change detection is necessary.
211
+
212
+ Here's an example of a test that uses this technique:
213
+
214
+ ```ts
215
+ class AppComponentTester extends ComponentTester<AppComponent> {
216
+ constructor() {
217
+ super(AppComponent);
218
+ }
219
+
220
+ get incrementButton() {
221
+ return this.button('button');
222
+ }
223
+
224
+ get count() {
225
+ return this.element('#count');
226
+ }
227
+ }
228
+
229
+ describe('AppComponent', () => {
230
+ let tester: AppComponentTester;
231
+
232
+ beforeEach(async () => {
233
+ TestBed.configureTestingModule({
234
+ providers: [
235
+ provideComponentFixtureAutoDetection(),
236
+ provideExperimentalZonelessChangeDetection() // if you already uses zoneless also add this provider
237
+ ]
238
+ });
239
+
240
+ jasmine.addMatchers(speculoosMatchers);
241
+
242
+ tester = new AppComponentTester();
243
+ // a first call to change() is necessary to let Angular run its first change detection
244
+ await tester.change();
245
+ });
246
+
247
+ it('should display the counter value and increment it', async () => {
248
+ expect(tester.count).toHaveText('0');
249
+
250
+ // this clicks the button and then lets Angular decide if a CD is necessary, and waits until
251
+ // the DOM has been updated (or not)
252
+ await tester.incrementButton.click();
253
+
254
+ expect(tester.count).toHaveText('1');
255
+ });
256
+ });
257
+ ```
258
+
259
+ In _automatic_ mode, your test functions should be `async`, and each action you do with the elements
260
+ (`click()`, `dispatchEvent`, etc.) should be awaited.
261
+
192
262
  ### Queries
193
263
 
194
264
  #### Queries for elements
@@ -281,12 +351,12 @@ class TestDatepicker extends TestHtmlElement<HTMLElement> {
281
351
  return this.input('input');
282
352
  }
283
353
 
284
- setDate(year: number, month: number, day: number) {
285
- this.inputField.fillWith(`${year}-${month}-${day}`);
354
+ async setDate(year: number, month: number, day: number) {
355
+ await this.inputField.fillWith(`${year}-${month}-${day}`);
286
356
  }
287
357
 
288
- toggleDropdown() {
289
- this.button('button').click();
358
+ async toggleDropdown() {
359
+ await this.button('button').click();
290
360
  }
291
361
  }
292
362
  ```
@@ -301,14 +371,26 @@ get birthDate() {
301
371
  ```
302
372
 
303
373
  ```typescript
304
- it('should not save if birth date is in the future') {
374
+ it('should not save if birth date is in the future', () =>) {
305
375
  // ...
306
376
  tester.birthDate.setDate(2200, 1, 1);
307
377
  tester.save.click();
308
378
  expect(userService.create).not.toHaveBenCalled();
309
- }
379
+ });
310
380
  ```
311
381
 
382
+ or, in _automatic_ mode
383
+
384
+ ```typescript
385
+ it('should not save if birth date is in the future'), async () => {
386
+ // ...
387
+ await tester.birthDate.setDate(2200, 1, 1);
388
+ await tester.save.click();
389
+ expect(userService.create).not.toHaveBenCalled();
390
+ });
391
+ ```
392
+
393
+
312
394
  #### Subqueries
313
395
 
314
396
  A query is made from the root `ComponentTester`. But `TestElement` themselves also support queries.
@@ -540,10 +622,10 @@ using the usual queries.
540
622
 
541
623
  ## Gotchas
542
624
 
543
- ### When do I need to call `detectChanges()`?
625
+ ### When do I need to call `change` or `detectChanges()`?
544
626
 
545
- Any event dispatched through a `TestElement` automatically calls `detectChanges()` for you.
546
- But you still need to call `detectChanges()` by yourself in the other cases:
627
+ In _imperative_ mode, any event dispatched through a `TestElement` automatically calls `detectChanges()` for you.
628
+ But you still need to call `change()` or `detectChanges()` by yourself in the other cases:
547
629
 
548
630
  - to actually initialize your component. Sometimes, you want to configure some mocks before the `ngOnInit()`
549
631
  method of your component is called. That's why creating a `ComponentTester` does not automatically call
@@ -553,6 +635,15 @@ But you still need to call `detectChanges()` by yourself in the other cases:
553
635
  by changing the state, or emitting an event through a subject, or triggering a navigation
554
636
  from the `ActivatedRouteStub`
555
637
 
638
+ Note that, in _imperative_ mode, `change()` calls `detectChanges()`. So you can call either one of the other
639
+ when you want to trigger a change detection.
640
+
641
+ In _automatic_ mode, any event dispatched through a `TestElement` automatically calls `await change()` for you.
642
+ But you still need to call `await change()` by yourself in the same other cases as in the _imperative_ mode:
643
+
644
+ - to actually initialize your component.
645
+ - to force change detection once you've changed the state of your component without dispatching an event.
646
+
556
647
  ### Can I use the `TestElement` methods to act on the component element itself, rather than a sub-element?
557
648
 
558
649
  Yes. The `ComponentTester` has a `testElement` property, which is the `TestHtmlElement` wrapping the component's element.
@@ -564,3 +655,53 @@ Please, provide feedback by filing issues, or by submitting pull requests, to th
564
655
  ## Complete example
565
656
 
566
657
  You can look at a minimal complete example in the [demo](https://github.com/Ninja-Squad/ngx-speculoos/tree/master/projects/demo/src/app) project.
658
+
659
+ ## Upgrading to v13
660
+
661
+ Version 13 of `ngx-speculoos` introduces the _automatic_ mode, consisting in using automatic change detection
662
+ instead of imperatively running change detections. See the [Automatic change detection](#automatic-change-detection)
663
+ section above for details.
664
+
665
+ As a result, all the methods that used to call `detectChanges()` for you now return a `Promise` instead of returning
666
+ `void`. In _imperative_ mode (the default), they are in fact synchronous and call `detectChanges()`, just as before.
667
+ In _automatic_ mode however, they call `await change()` and should thus be awaited.
668
+
669
+ Your tests should generally keep compiling and running without changes.
670
+ But if you created custom test elements which override methods that now return a promise, and return something
671
+ other than `void`, for example:
672
+
673
+ ```typescript
674
+ class CustomInput extends TestInput {
675
+ //...
676
+ fillWith(s: string): CustomInput {
677
+ super.fillWith(s);
678
+ return this;
679
+ }
680
+ }
681
+ ```
682
+
683
+ Then that won't compile anymore.
684
+
685
+ And in general, if you want your custom test element to be usable in both modes, all their method that explicitly
686
+ or indirectly called `detectChanges()` should now return a promise and explicitly of indirectly call `await change()`.
687
+ For example:
688
+
689
+ ```typescript
690
+ class CustomInput extends TestHtmlElement {
691
+ //...
692
+ async fillInput(s: string): Promise<void> {
693
+ await this.element('input').fillWith(s);
694
+ // ...
695
+ }
696
+
697
+ async clickButton(): Promise<void> {
698
+ await this.element('button').click();
699
+ // ...
700
+ }
701
+
702
+ async changeState(): Promise<void> {
703
+ this.component(Foo).doSomething();
704
+ await this.change();
705
+ }
706
+ }
707
+ ```
@@ -1,13 +1,19 @@
1
- import { TestBed, ComponentFixture } from '@angular/core/testing';
1
+ import { TestBed, ComponentFixture, ComponentFixtureAutoDetect } from '@angular/core/testing';
2
2
  import { By } from '@angular/platform-browser';
3
3
  import { RouterTestingHarness } from '@angular/router/testing';
4
4
  import { Router, ActivatedRouteSnapshot, convertToParamMap, ActivatedRoute } from '@angular/router';
5
5
  import { BehaviorSubject } from 'rxjs';
6
+ import { makeEnvironmentProviders } from '@angular/core';
7
+
8
+ "use strict";
6
9
 
7
10
  /**
8
11
  * A wrapped DOM element, providing additional methods and attributes helping with writing tests
9
12
  */
10
13
  class TestElement {
14
+ tester;
15
+ debugElement;
16
+ querier;
11
17
  constructor(tester,
12
18
  /**
13
19
  * the wrapped debug element
@@ -29,16 +35,16 @@ class TestElement {
29
35
  /**
30
36
  * dispatches an event of the given type from the wrapped element, then triggers a change detection
31
37
  */
32
- dispatchEventOfType(type) {
38
+ async dispatchEventOfType(type) {
33
39
  this.nativeElement.dispatchEvent(new Event(type));
34
- this.tester.detectChanges();
40
+ await this.tester.change();
35
41
  }
36
42
  /**
37
43
  * dispatches the given event from the wrapped element, then triggers a change detection
38
44
  */
39
- dispatchEvent(event) {
45
+ async dispatchEvent(event) {
40
46
  this.nativeElement.dispatchEvent(event);
41
- this.tester.detectChanges();
47
+ await this.tester.change();
42
48
  }
43
49
  /**
44
50
  * Gets the CSS classes of the wrapped element, as an array
@@ -155,9 +161,9 @@ class TestHtmlElement extends TestElement {
155
161
  /**
156
162
  * Clicks on the wrapped element, then triggers a change detection
157
163
  */
158
- click() {
164
+ async click() {
159
165
  this.nativeElement.click();
160
- this.tester.detectChanges();
166
+ await this.tester.change();
161
167
  }
162
168
  /**
163
169
  * Tests if the element is visible, in the same meaning (and implementation) as in jQuery, i.e.
@@ -200,7 +206,7 @@ class TestSelect extends TestHtmlElement {
200
206
  throw new Error(`The index ${index} is out of bounds`);
201
207
  }
202
208
  this.nativeElement.selectedIndex = index;
203
- this.dispatchEventOfType('change');
209
+ return this.dispatchEventOfType('change');
204
210
  }
205
211
  /**
206
212
  * Selects the first option with the given value, then dispatches an event of type change and triggers a change detection.
@@ -209,7 +215,7 @@ class TestSelect extends TestHtmlElement {
209
215
  selectValue(value) {
210
216
  const index = this.optionValues.indexOf(value);
211
217
  if (index >= 0) {
212
- this.selectIndex(index);
218
+ return this.selectIndex(index);
213
219
  }
214
220
  else {
215
221
  throw new Error(`The value ${value} is not part of the option values (${this.optionValues.join(', ')})`);
@@ -222,7 +228,7 @@ class TestSelect extends TestHtmlElement {
222
228
  selectLabel(label) {
223
229
  const index = this.optionLabels.indexOf(label);
224
230
  if (index >= 0) {
225
- this.selectIndex(index);
231
+ return this.selectIndex(index);
226
232
  }
227
233
  else {
228
234
  throw new Error(`The label ${label} is not part of the option labels (${this.optionLabels.join(', ')})`);
@@ -289,9 +295,9 @@ class TestTextArea extends TestHtmlElement {
289
295
  * Sets the value of the wrapped textarea, then dispatches an event of type input and triggers a change detection
290
296
  * @param value the new value of the textarea
291
297
  */
292
- fillWith(value) {
298
+ async fillWith(value) {
293
299
  this.nativeElement.value = value;
294
- this.dispatchEventOfType('input');
300
+ await this.dispatchEventOfType('input');
295
301
  }
296
302
  /**
297
303
  * the value of the wrapped textarea
@@ -318,9 +324,9 @@ class TestInput extends TestHtmlElement {
318
324
  * Sets the value of the wrapped input, then dispatches an event of type input and triggers a change detection
319
325
  * @param value the new value of the input
320
326
  */
321
- fillWith(value) {
327
+ async fillWith(value) {
322
328
  this.nativeElement.value = value;
323
- this.dispatchEventOfType('input');
329
+ await this.dispatchEventOfType('input');
324
330
  }
325
331
  /**
326
332
  * the value of the wrapped input
@@ -343,16 +349,16 @@ class TestInput extends TestHtmlElement {
343
349
  /**
344
350
  * Checks the wrapped input, then dispatches an event of type change and triggers a change detection
345
351
  */
346
- check() {
352
+ async check() {
347
353
  this.nativeElement.checked = true;
348
- this.dispatchEventOfType('change');
354
+ await this.dispatchEventOfType('change');
349
355
  }
350
356
  /**
351
357
  * Unchecks the wrapped input, then dispatches an event of type change and triggers a change detection
352
358
  */
353
- uncheck() {
359
+ async uncheck() {
354
360
  this.nativeElement.checked = false;
355
- this.dispatchEventOfType('change');
361
+ await this.dispatchEventOfType('change');
356
362
  }
357
363
  }
358
364
 
@@ -361,6 +367,8 @@ class TestInput extends TestHtmlElement {
361
367
  * @internal
362
368
  */
363
369
  class TestElementQuerier {
370
+ tester;
371
+ root;
364
372
  constructor(tester, root) {
365
373
  this.tester = tester;
366
374
  this.root = root;
@@ -497,6 +505,18 @@ class TestElementQuerier {
497
505
  * @param <C> the type of the component to test
498
506
  */
499
507
  class ComponentTester {
508
+ /**
509
+ * The test element of the component
510
+ */
511
+ testElement;
512
+ /**
513
+ * The component fixture of the component
514
+ */
515
+ fixture;
516
+ /**
517
+ * The mode used by the ComponentTester
518
+ */
519
+ mode;
500
520
  /**
501
521
  * Creates a component fixture of the given type with the TestBed and wraps it into a ComponentTester
502
522
  */
@@ -517,7 +537,9 @@ class ComponentTester {
517
537
  */
518
538
  constructor(arg) {
519
539
  this.fixture = arg instanceof ComponentFixture ? arg : TestBed.createComponent(arg);
540
+ const autoDetect = TestBed.inject(ComponentFixtureAutoDetect, false);
520
541
  this.testElement = TestElementQuerier.wrap(this.debugElement, this);
542
+ this.mode = autoDetect ? 'automatic' : 'imperative';
521
543
  }
522
544
  /**
523
545
  * The native DOM host element of the component
@@ -627,17 +649,36 @@ class ComponentTester {
627
649
  return this.testElement.customs(selector, customTestElementType);
628
650
  }
629
651
  /**
630
- * Triggers a change detection using the wrapped fixture
652
+ * Triggers a change detection using the wrapped fixture in imperative mode.
653
+ * Throws an error in autodetection mode.
654
+ * You should generally prever
631
655
  */
632
656
  detectChanges(checkNoChanges) {
657
+ if (this.mode === 'automatic') {
658
+ throw new Error('In automatic mode, you should not call detectChanges');
659
+ }
633
660
  this.fixture.detectChanges(checkNoChanges);
634
661
  }
635
662
  /**
636
- * Delegates to the wrapped fixture whenStable and then detect changes
663
+ * In imperative mode, runs change detection.
664
+ * In implicit mode, awaits stability.
665
+ */
666
+ async change() {
667
+ if (this.mode === 'automatic') {
668
+ await this.stable();
669
+ }
670
+ else {
671
+ this.fixture.detectChanges();
672
+ }
673
+ }
674
+ /**
675
+ * Delegates to the wrapped fixture whenStable and, in imperative mode, detect changes
637
676
  */
638
677
  async stable() {
639
678
  await this.fixture.whenStable();
640
- this.detectChanges();
679
+ if (this.mode === 'imperative') {
680
+ this.detectChanges();
681
+ }
641
682
  }
642
683
  }
643
684
 
@@ -649,6 +690,7 @@ class ComponentTester {
649
690
  * for example.
650
691
  */
651
692
  class RoutingTester extends ComponentTester {
693
+ harness;
652
694
  constructor(harness) {
653
695
  super(harness.fixture);
654
696
  this.harness = harness;
@@ -681,6 +723,12 @@ class RoutingTester extends ComponentTester {
681
723
  }
682
724
 
683
725
  class ActivatedRouteSnapshotStub extends ActivatedRouteSnapshot {
726
+ _parent = null;
727
+ _root;
728
+ _firstChild = null;
729
+ _children = [];
730
+ _pathFromRoot = [];
731
+ _title;
684
732
  get parent() {
685
733
  return this._parent;
686
734
  }
@@ -725,10 +773,6 @@ class ActivatedRouteSnapshotStub extends ActivatedRouteSnapshot {
725
773
  }
726
774
  constructor() {
727
775
  super();
728
- this._parent = null;
729
- this._firstChild = null;
730
- this._children = [];
731
- this._pathFromRoot = [];
732
776
  this._root = this;
733
777
  }
734
778
  }
@@ -749,6 +793,17 @@ class ActivatedRouteSnapshotStub extends ActivatedRouteSnapshot {
749
793
  * on the stub route. So if the code keeps a reference to params or paramMaps, it won't see the changes.
750
794
  */
751
795
  class ActivatedRouteStub extends ActivatedRoute {
796
+ _firstChild;
797
+ _children;
798
+ paramsSubject;
799
+ queryParamsSubject;
800
+ dataSubject;
801
+ fragmentSubject;
802
+ urlSubject;
803
+ titleSubject;
804
+ _parent;
805
+ _root;
806
+ _pathFromRoot;
752
807
  /**
753
808
  * Constructs a new instance, based on the given options.
754
809
  * If an option is not provided (or if no option is provided at all), then the route has a default value for this option
@@ -1249,15 +1304,30 @@ function createMock(type) {
1249
1304
  return jasmine.createSpyObj(type.name, collectMethodNames(type.prototype));
1250
1305
  }
1251
1306
 
1307
+ const COMPONENT_FIXTURE_AUTO_DETECTION = makeEnvironmentProviders([
1308
+ { provide: ComponentFixtureAutoDetect, useValue: true }
1309
+ ]);
1310
+ /**
1311
+ * Provide function which returns the provider `{ provide: ComponentFixtureAutoDetect, useValue: true }`.
1312
+ * This provider can be added to the testing module to configure the component testers
1313
+ * (and the underlying ComponentFixture) in automatic mode:
1314
+ *
1315
+ * ```
1316
+ * TestBed.configureTestingModule({ providers: [provideAutomaticChangeDetection()] });
1317
+ * ```
1318
+ */
1319
+ function provideAutomaticChangeDetection() {
1320
+ return COMPONENT_FIXTURE_AUTO_DETECTION;
1321
+ }
1322
+
1252
1323
  /* eslint-disable */
1253
1324
  /*
1254
1325
  * Public API Surface of ngx-speculoos
1255
1326
  */
1256
- /// <reference path="./jasmine-matchers.ts" />
1257
1327
 
1258
1328
  /**
1259
1329
  * Generated bundle index. Do not edit.
1260
1330
  */
1261
1331
 
1262
- export { ActivatedRouteStub, ComponentTester, RoutingTester, TestButton, TestElement, TestHtmlElement, TestInput, TestSelect, TestTextArea, createMock, speculoosMatchers, stubRoute };
1332
+ export { ActivatedRouteStub, ComponentTester, RoutingTester, TestButton, TestElement, TestHtmlElement, TestInput, TestSelect, TestTextArea, createMock, provideAutomaticChangeDetection, speculoosMatchers, stubRoute };
1263
1333
  //# sourceMappingURL=ngx-speculoos.mjs.map