ngx-speculoos
Version:
Helps writing Angular unit tests
1,264 lines (1,253 loc) • 52.5 kB
JavaScript
import { TestBed, ComponentFixture, ComponentFixtureAutoDetect } from '@angular/core/testing';
import { NgZone, makeEnvironmentProviders } from '@angular/core';
import { By } from '@angular/platform-browser';
import { RouterTestingHarness } from '@angular/router/testing';
import { Router, ActivatedRouteSnapshot, convertToParamMap, ActivatedRoute } from '@angular/router';
import { BehaviorSubject } from 'rxjs';
/**
* A wrapped DOM element, providing additional methods and attributes helping with writing tests
*/
class TestElement {
tester;
debugElement;
querier;
constructor(tester,
/**
* the wrapped debug element
*/
debugElement) {
this.tester = tester;
this.debugElement = debugElement;
this.querier = new TestElementQuerier(tester, debugElement);
}
get nativeElement() {
return this.debugElement.nativeElement;
}
/**
* the text content of this element
*/
get textContent() {
return this.nativeElement.textContent;
}
/**
* dispatches an event of the given type from the wrapped element, then triggers a change detection
*/
async dispatchEventOfType(type) {
this.nativeElement.dispatchEvent(new Event(type));
await this.tester.change();
}
/**
* dispatches the given event from the wrapped element, then triggers a change detection
*/
async dispatchEvent(event) {
this.nativeElement.dispatchEvent(event);
await this.tester.change();
}
/**
* Gets the CSS classes of the wrapped element, as an array
*/
get classes() {
return Array.prototype.slice.call(this.nativeElement.classList);
}
/**
* Gets the attribute of the wrapped element with the given name
* @param name the name of the attribute to get
*/
attr(name) {
return this.nativeElement.getAttribute(name);
}
element(selector) {
return this.querier.element(selector);
}
elements(selector) {
return this.querier.elements(selector);
}
/**
* Gets the first input matched by the given selector. Throws an Error if the matched element isn't actually an input.
* @param selector a CSS or directive selector
* @returns the wrapped input, or null if no element was matched
*/
input(selector) {
return this.querier.input(selector);
}
/**
* Gets the first select matched by the given selector. Throws an Error if the matched element isn't actually a select.
* @param selector a CSS or directive selector
* @returns the wrapped select, or null if no element was matched
*/
select(selector) {
return this.querier.select(selector);
}
/**
* Gets the first textarea matched by the given selector
* @param selector a CSS or directive selector
* @returns the wrapped textarea, or null if no element was matched. Throws an Error if the matched element isn't actually a textarea.
* @throws {Error} if the matched element isn't actually a textarea
*/
textarea(selector) {
return this.querier.textarea(selector);
}
/**
* Gets the first button matched by the given selector. Throws an Error if the matched element isn't actually a button.
* @param selector a CSS or directive selector
* @returns the wrapped button, or null if no element was matched
*/
button(selector) {
return this.querier.button(selector);
}
/**
* Gets the first directive matching the given component directive selector and returns its component instance
* @param selector the selector of a component directive
*/
component(selector) {
return this.querier.element(selector)?.debugElement?.componentInstance ?? null;
}
/**
* Gets the directives matching the given component directive selector and returns their component instance
* @param selector the selector of a component directive
*/
components(selector) {
return this.querier.elements(selector).map(e => e.debugElement.componentInstance);
}
/**
* Gets the first element matching the given selector, then gets the given token from its injector, or null if there is no such token
* @param selector a CSS or directive selector
* @param token the token to get from the matched element injector
*/
token(selector, token) {
return this.querier.element(selector)?.debugElement?.injector?.get(token, null) ?? null;
}
/**
* Gets the elements matching the given selector, then gets their given token from their injector, or null if there is no such token
* @param selector a CSS or directive selector
* @param token the token to get from the matched element injector
*/
tokens(selector, token) {
return this.querier.elements(selector).map(e => e.debugElement.injector.get(token, null) ?? null);
}
/**
* Gets the element matching the given selector, and if found, creates and returns a custom TestElement of the provided
* type. This is useful to create custom higher-level abstractions similar to TestInput, TestSelect, etc. for
* custom elements or components.
* @param selector a CSS or directive selector
* @param customTestElementType the type of the TestElement subclass that will wrap the found element
*/
custom(selector, customTestElementType) {
const element = this.querier.element(selector);
return element && new customTestElementType(this.tester, element.debugElement);
}
/**
* Gets the elements matching the given selector, and creates and returns custom TestElements of the provided
* type. This is useful to create custom higher-level abstractions similar to TestInput, TestSelect, etc. for
* custom elements or components.
* @param selector a CSS or directive selector
* @param customTestElementType the type of the TestElement subclass that will wrap the found elements
*/
customs(selector, customTestElementType) {
return this.querier.elements(selector).map(element => new customTestElementType(this.tester, element.debugElement));
}
}
/**
* A wrapped DOM HTML element, providing additional methods and attributes helping with writing tests
*/
class TestHtmlElement extends TestElement {
constructor(tester, debugElement) {
super(tester, debugElement);
}
/**
* Clicks on the wrapped element, then triggers a change detection
*/
async click() {
this.nativeElement.click();
await this.tester.change();
}
/**
* Tests if the element is visible, in the same meaning (and implementation) as in jQuery, i.e.
* present anywhere in the DOM, and visible.
* An element is not visible typically, if its display style or any of its ancestors display style is none.
*/
get visible() {
return !!(this.nativeElement.offsetWidth || this.nativeElement.offsetHeight || this.nativeElement.getClientRects().length);
}
}
/**
* A wrapped button element, providing additional methods and attributes helping with writing tests
*/
class TestButton extends TestHtmlElement {
constructor(tester, debugElement) {
super(tester, debugElement);
}
/**
* the disabled flag of the button
*/
get disabled() {
return this.nativeElement.disabled;
}
}
/**
* A wrapped DOM HTML select element, providing additional methods and attributes helping with writing tests
*/
class TestSelect extends TestHtmlElement {
constructor(tester, debugElement) {
super(tester, debugElement);
}
/**
* Selects the option at the given index, then dispatches an event of type change and triggers a change detection.
* If the index is out of bounds and is not -1, then throws an error.
*/
selectIndex(index) {
if (index < -1 || index >= this.nativeElement.options.length) {
throw new Error(`The index ${index} is out of bounds`);
}
this.nativeElement.selectedIndex = index;
return this.dispatchEventOfType('change');
}
/**
* Selects the first option with the given value, then dispatches an event of type change and triggers a change detection.
* If there is no option with the given value, then throws an error.
*/
selectValue(value) {
const index = this.optionValues.indexOf(value);
if (index >= 0) {
return this.selectIndex(index);
}
else {
throw new Error(`The value ${value} is not part of the option values (${this.optionValues.join(', ')})`);
}
}
/**
* Selects the first option with the given label (or text), then dispatches an event of type change and triggers a change detection.
* If there is no option with the given label, then throws an error.
*/
selectLabel(label) {
const index = this.optionLabels.indexOf(label);
if (index >= 0) {
return this.selectIndex(index);
}
else {
throw new Error(`The label ${label} is not part of the option labels (${this.optionLabels.join(', ')})`);
}
}
/**
* the selected index of the wrapped select
*/
get selectedIndex() {
return this.nativeElement.selectedIndex;
}
/**
* the value of the selected option of the wrapped select, or null if there is no selected option
*/
get selectedValue() {
if (this.selectedIndex < 0) {
return null;
}
return this.nativeElement.options[this.selectedIndex].value;
}
/**
* the label (or text if no label) of the selected option of the wrapped select, or null if there is no selected option
*/
get selectedLabel() {
if (this.selectedIndex < 0) {
return null;
}
return this.nativeElement.options[this.selectedIndex].label;
}
/**
* the values of the options, as an array
*/
get optionValues() {
return Array.prototype.slice.call(this.nativeElement.options).map(option => option.value);
}
/**
* the labels (or texts if no label) of the options, as an array
*/
get optionLabels() {
return Array.prototype.slice.call(this.nativeElement.options).map(option => option.label);
}
/**
* the number of options in the select
*/
get size() {
return this.nativeElement.options.length;
}
/**
* the disabled property of the wrapped select
*/
get disabled() {
return this.nativeElement.disabled;
}
}
/**
* A wrapped DOM HTML textarea element, providing additional methods and attributes helping with writing tests
*/
class TestTextArea extends TestHtmlElement {
constructor(tester, debugElement) {
super(tester, debugElement);
}
/**
* Sets the value of the wrapped textarea, then dispatches an event of type input and triggers a change detection
* @param value the new value of the textarea
*/
async fillWith(value) {
this.nativeElement.value = value;
await this.dispatchEventOfType('input');
}
/**
* the value of the wrapped textarea
*/
get value() {
return this.nativeElement.value;
}
/**
* the disabled property of the wrapped textarea
*/
get disabled() {
return this.nativeElement.disabled;
}
}
/**
* A wrapped DOM HTML input element, providing additional methods and attributes helping with writing tests
*/
class TestInput extends TestHtmlElement {
constructor(tester, debugElement) {
super(tester, debugElement);
}
/**
* Sets the value of the wrapped input, then dispatches an event of type input and triggers a change detection
* @param value the new value of the input
*/
async fillWith(value) {
this.nativeElement.value = value;
await this.dispatchEventOfType('input');
}
/**
* the value of the wrapped input
*/
get value() {
return this.nativeElement.value;
}
/**
* the checked property of the wrapped input
*/
get checked() {
return this.nativeElement.checked;
}
/**
* the disabled property of the wrapped input
*/
get disabled() {
return this.nativeElement.disabled;
}
/**
* Checks the wrapped input, then dispatches an event of type change and triggers a change detection
*/
async check() {
this.nativeElement.checked = true;
await this.dispatchEventOfType('change');
}
/**
* Unchecks the wrapped input, then dispatches an event of type change and triggers a change detection
*/
async uncheck() {
this.nativeElement.checked = false;
await this.dispatchEventOfType('change');
}
}
/* eslint-disable @typescript-eslint/no-explicit-any */
/**
* @internal
*/
class TestElementQuerier {
tester;
root;
constructor(tester, root) {
this.tester = tester;
this.root = root;
}
static wrap(childDebugElement, tester) {
const childElement = childDebugElement.nativeElement;
if (childElement instanceof HTMLButtonElement) {
return new TestButton(tester, childDebugElement);
}
else if (childElement instanceof HTMLInputElement) {
return new TestInput(tester, childDebugElement);
}
else if (childElement instanceof HTMLSelectElement) {
return new TestSelect(tester, childDebugElement);
}
else if (childElement instanceof HTMLTextAreaElement) {
return new TestTextArea(tester, childDebugElement);
}
else if (childElement instanceof HTMLElement) {
return new TestHtmlElement(tester, childDebugElement);
}
else {
return new TestElement(tester, childDebugElement);
}
}
/**
* Gets the first element matching the given selector and wraps it into a TestElement. The actual type
* of the returned value is the TestElement subclass matching the type of the found element. So, if the
* matched element is an input for example, the method will return a TestInput. You can thus use
* `tester.element('#some-input') as TestInput`.
* @param selector a CSS or directive selector
* @returns the wrapped element, or null if no element matches the selector.
*/
element(selector) {
const childElement = this.query(selector);
return childElement && TestElementQuerier.wrap(childElement, this.tester);
}
/**
* Gets all the elements matching the given selector and wraps them into a TestElement. The actual type
* of the returned elements is the TestElement subclass matching the type of the found element. So, if the
* matched elements are inputs for example, the method will return an array of TestInput. You can thus use
* `tester.elements('input') as Array<TestInput>`.
* @param selector a CSS or directive selector
* @returns the array of matched elements, empty if no element was matched
*/
elements(selector) {
const childElements = this.queryAll(selector);
return childElements.map(debugElement => TestElementQuerier.wrap(debugElement, this.tester));
}
/**
* Gets the first input matched by the given selector. Throws an Error if the matched element isn't actually an input.
* @param selector a CSS or directive selector
* @returns the wrapped input, or null if no element was matched
*/
input(selector) {
const childElement = this.query(selector);
if (!childElement) {
return null;
}
else if (!(childElement.nativeElement instanceof HTMLInputElement)) {
throw new Error(`Element with selector ${selector} is not an HTMLInputElement`);
}
return new TestInput(this.tester, childElement);
}
/**
* Gets the first select matched by the given selector. Throws an Error if the matched element isn't actually a select.
* @param selector a CSS or directive selector
* @returns the wrapped select, or null if no element was matched
*/
select(selector) {
const childElement = this.query(selector);
if (!childElement) {
return null;
}
else if (!(childElement.nativeElement instanceof HTMLSelectElement)) {
throw new Error(`Element with selector ${selector} is not an HTMLSelectElement`);
}
return new TestSelect(this.tester, childElement);
}
/**
* Gets the first textarea matched by the given selector
* @param selector a CSS or directive selector
* @returns the wrapped textarea, or null if no element was matched. Throws an Error if the matched element isn't actually a textarea.
* @throws {Error} if the matched element isn't actually a textarea
*/
textarea(selector) {
const childElement = this.query(selector);
if (!childElement) {
return null;
}
else if (!(childElement.nativeElement instanceof HTMLTextAreaElement)) {
throw new Error(`Element with selector ${selector} is not an HTMLTextAreaElement`);
}
return new TestTextArea(this.tester, childElement);
}
/**
* Gets the first button matched by the given selector. Throws an Error if the matched element isn't actually a button.
* @param selector a CSS or directive selector
* @returns the wrapped button, or null if no element was matched
*/
button(selector) {
const childElement = this.query(selector);
if (!childElement) {
return null;
}
else if (!(childElement.nativeElement instanceof HTMLButtonElement)) {
throw new Error(`Element with selector ${selector} is not an HTMLButtonElement`);
}
return new TestButton(this.tester, childElement);
}
query(selector) {
if (typeof selector === 'string') {
return this.root.query(By.css(selector));
}
else {
return this.root.query(By.directive(selector));
}
}
queryAll(selector) {
if (typeof selector === 'string') {
return this.root.queryAll(By.css(selector));
}
else {
return this.root.queryAll(By.directive(selector));
}
}
}
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */
/**
* The main entry point of the API. It wraps an Angular ComponentFixture<T>, and gives access to its
* most used properties and methods. It also allows getting elements wrapped in TestElement (and its subclasses)
* @param <C> the type of the component to test
*/
class ComponentTester {
/**
* The test element of the component
*/
testElement;
/**
* The component fixture of the component
*/
fixture;
/**
* The mode used by the ComponentTester
*/
mode;
/**
* Creates a component fixture of the given type with the TestBed and wraps it into a ComponentTester
*/
static create(componentType) {
const fixture = TestBed.createComponent(componentType);
return new ComponentTester(fixture);
}
/**
* Creates a ComponentFixture for the given component type using the TestBed, and creates a ComponentTester
* wrapping (and delegating) to this fixture. If a fixture is passed, then delegates to this fixture directly.
*
* Note that no `detectChanges()` call is made by this constructor. It's up to the subclass constructor,
* or to the user of the created ComponentTester, to call `detectChanges()` at least once to trigger change
* detection. This is necessary because some component templates can only be evaluated once inputs
* have been set on the component instance.
*
* @param arg the type of the component to wrap, or a component fixture to wrap
*/
constructor(arg) {
this.fixture = arg instanceof ComponentFixture ? arg : TestBed.createComponent(arg);
const autoDetect = TestBed.inject(ComponentFixtureAutoDetect, false);
const zoneless = !(TestBed.inject(NgZone) instanceof NgZone);
this.testElement = TestElementQuerier.wrap(this.debugElement, this);
this.mode = autoDetect || zoneless ? 'automatic' : 'imperative';
}
/**
* The native DOM host element of the component
*/
get nativeElement() {
return this.fixture.nativeElement;
}
/**
* Gets the instance of the tested component from the wrapped fixture
*/
get componentInstance() {
return this.fixture.componentInstance;
}
/**
* Gets the debug element from the wrapped fixture
*/
get debugElement() {
return this.fixture.debugElement;
}
element(selector) {
return this.testElement.element(selector);
}
elements(selector) {
return this.testElement.elements(selector);
}
/**
* Gets the first input matched by the given selector. Throws an Error if the matched element isn't actually an input.
* @param selector a CSS or directive selector
* @returns the wrapped input, or null if no element was matched
*/
input(selector) {
return this.testElement.input(selector);
}
/**
* Gets the first select matched by the given selector. Throws an Error if the matched element isn't actually a select.
* @param selector a CSS or directive selector
* @returns the wrapped select, or null if no element was matched
*/
select(selector) {
return this.testElement.select(selector);
}
/**
* Gets the first textarea matched by the given selector
* @param selector a CSS or directive selector
* @returns the wrapped textarea, or null if no element was matched. Throws an Error if the matched element isn't actually a textarea.
* @throws {Error} if the matched element isn't actually a textarea
*/
textarea(selector) {
return this.testElement.textarea(selector);
}
/**
* Gets the first button matched by the given selector. Throws an Error if the matched element isn't actually a button.
* @param selector a CSS or directive selector
* @returns the wrapped button, or null if no element was matched
*/
button(selector) {
return this.testElement.button(selector);
}
/**
* Gets the first directive matching the given component directive selector and returns its component instance
* @param selector the selector of a component directive
*/
component(selector) {
return this.testElement.component(selector);
}
/**
* Gets the directives matching the given component directive selector and returns their component instance
* @param selector the selector of a component directive
*/
components(selector) {
return this.testElement.components(selector);
}
/**
* Gets the first element matching the given selector, then gets the given token from its injector, or null if there is no such token
* @param selector a CSS or directive selector
* @param token the token to get from the matched element injector
*/
token(selector, token) {
return this.testElement.token(selector, token);
}
/**
* Gets the elements matching the given selector, then gets their given token from their injector, or null if there is no such token
* @param selector a CSS or directive selector
* @param token the token to get from the matched element injector
*/
tokens(selector, token) {
return this.testElement.tokens(selector, token);
}
/**
* Gets the element matching the given selector, and if found, creates and returns a custom TestElement of the provided
* type. This is useful to create custom higher-level abstractions similar to TestInput, TestSelect, etc. for
* custom elements or components.
* @param selector a CSS or directive selector
* @param customTestElementType the type of the TestElement subclass that will wrap the found element
*/
custom(selector, customTestElementType) {
return this.testElement.custom(selector, customTestElementType);
}
/**
* Gets the elements matching the given selector, and creates and returns custom TestElements of the provided
* type. This is useful to create custom higher-level abstractions similar to TestInput, TestSelect, etc. for
* custom elements or components.
* @param selector a CSS or directive selector
* @param customTestElementType the type of the TestElement subclass that will wrap the found elements
*/
customs(selector, customTestElementType) {
return this.testElement.customs(selector, customTestElementType);
}
/**
* Triggers a change detection using the wrapped fixture in imperative mode.
* Throws an error in autodetection mode.
* You should generally prever
*/
detectChanges(checkNoChanges) {
if (this.mode === 'automatic') {
throw new Error('In automatic mode, you should not call detectChanges');
}
this.fixture.detectChanges(checkNoChanges);
}
/**
* In imperative mode, runs change detection.
* In automatic mode, awaits stability.
*/
async change() {
if (this.mode === 'automatic') {
await this.stable();
}
else {
this.fixture.detectChanges();
}
}
/**
* Delegates to the wrapped fixture whenStable and, in imperative mode, detect changes
*/
async stable() {
await this.fixture.whenStable();
if (this.mode === 'imperative') {
this.detectChanges();
}
}
}
/**
* A thin wrapper around Angular RouterTestingHarness which helps testing routed components.
* It allows, based on a configured testing module where the router is provided, to initially navigate
* to a given route, then test the routed component. It's then possible to either navigate again with
* the wrapped harness, or to use the component to trigger navigation, and test that the URL has changed
* for example.
*/
class RoutingTester extends ComponentTester {
harness;
constructor(harness) {
super(harness.fixture);
this.harness = harness;
}
/**
* Creates a RouterTestngHarness and uses it to navigate to the given URL
* @param url the URL to initially navigate to.
* @return a promise which resolves to a RoutingTester which wraps the harness
* and its fixture.
*/
static async forUrl(url) {
const harness = await RouterTestingHarness.create(url);
return new RoutingTester(harness);
}
/**
* Gets the current URL of the Router service as a string
*/
get url() {
const router = TestBed.inject(Router);
return router.url;
}
/**
* Gets the current URL of the Router service as an UrlTree, to be able to test its
* elements (query params, etc.)
*/
get urlTree() {
const router = TestBed.inject(Router);
return router.parseUrl(router.url);
}
}
class ActivatedRouteSnapshotStub extends ActivatedRouteSnapshot {
_parent = null;
_root;
_firstChild = null;
_children = [];
_pathFromRoot = [];
_title;
get parent() {
return this._parent;
}
set parent(value) {
this._parent = value;
}
get root() {
return this._root;
}
set root(value) {
this._root = value;
}
get firstChild() {
return this._firstChild;
}
set firstChild(value) {
this._firstChild = value;
}
get children() {
return this._children;
}
set children(value) {
this._children = value;
}
get pathFromRoot() {
return this._pathFromRoot;
}
set pathFromRoot(value) {
this._pathFromRoot = value;
}
get title() {
return this._title;
}
set title(value) {
this._title = value;
}
get paramMap() {
return convertToParamMap(this.params);
}
get queryParamMap() {
return convertToParamMap(this.queryParams);
}
constructor() {
super();
this._root = this;
}
}
/**
* A stub for ActivatedRoute. It behaves almost the same way as the actual ActivatedRoute, exposing a snapshot
* and observables for the params, query params etc., which are kept in sync.
*
* In addition, this stub allows simulating a navigation by changing the params, the query params, the fragment, etc.
* When that happens, the snapshot is modified, then the relevant observables emit the new values.
*
* There are some things that don't really work the same way as the real ActivatedRoute though:
* - the handling of the firstChild and of the children is entirely under the tester's responsibility. Setting the parent
* of a route stub does not add this route to the children of its parent, for example.
* - when changing the params, query params, fragment, etc., their associated observable emits unconditionally, instead of
* first checking if the value is actually different from before. It's thus the responsibility of the tester to not
* change the values if they're the same as before.
* - the params, paramMap, queryParams and queryParamMap objects of the route snapshot change when params or query params are set
* on the stub route. So if the code keeps a reference to params or paramMaps, it won't see the changes.
*/
class ActivatedRouteStub extends ActivatedRoute {
_firstChild;
_children;
paramsSubject;
queryParamsSubject;
dataSubject;
fragmentSubject;
urlSubject;
titleSubject;
_parent;
_root;
_pathFromRoot;
/**
* Constructs a new instance, based on the given options.
* If an option is not provided (or if no option is provided at all), then the route has a default value for this option
* (empty parameters for example, null fragment, etc.)
* If no parent is passed, then this route has no parent and is thus set as the root. Otherwise, the root and the path
* from root are created based on the root and path from root of the given parent route.
*/
constructor(options) {
super();
const snapshot = new ActivatedRouteSnapshotStub();
this.snapshot = snapshot;
this._firstChild = options?.firstChild ?? null;
this._children = options?.children ?? [];
this._parent = options?.parent ?? null;
this._root = this.parent?.root ?? this;
this._pathFromRoot = this.parent ? [...this.parent.pathFromRoot, this] : [this];
snapshot.params = options?.params ?? {};
snapshot.queryParams = options?.queryParams ?? {};
snapshot.data = options?.data ?? {};
snapshot.title = options?.title;
snapshot.fragment = options?.fragment ?? null;
snapshot.url = options?.url ?? [];
// @ts-expect-error the routeConfig is readonly, but we need to overwrite it here
snapshot.routeConfig = options?.routeConfig ?? null;
snapshot.firstChild = this.firstChild?.snapshot ?? null;
snapshot.children = this.children?.map(route => route.snapshot) ?? [];
snapshot.parent = this.parent?.snapshot ?? null;
snapshot.root = this.root.snapshot;
snapshot.pathFromRoot = this.pathFromRoot.map(route => route.snapshot);
this.paramsSubject = new BehaviorSubject(this.snapshot.params);
this.queryParamsSubject = new BehaviorSubject(this.snapshot.queryParams);
this.dataSubject = new BehaviorSubject(this.snapshot.data);
this.titleSubject = new BehaviorSubject(this.snapshot.title);
this.fragmentSubject = new BehaviorSubject(this.snapshot.fragment);
this.urlSubject = new BehaviorSubject(this.snapshot.url);
this.params = this.paramsSubject.asObservable();
this.queryParams = this.queryParamsSubject.asObservable();
this.data = this.dataSubject.asObservable();
this.fragment = this.fragmentSubject.asObservable();
this.url = this.urlSubject.asObservable();
// @ts-expect-error the title is readonly, but we need to be able to initialize it here
this.title = this.titleSubject.asObservable();
}
get root() {
return this._root;
}
get parent() {
return this._parent;
}
get pathFromRoot() {
return this._pathFromRoot;
}
get firstChild() {
return this._firstChild;
}
get children() {
return this._children;
}
get routeConfig() {
return this.snapshot.routeConfig;
}
/**
* Triggers a navigation with the given new parameters. All the other parts (query params etc.) stay as they are.
* This is a shortcut to `triggerNavigation` that can be used to only change the parameters.
*/
setParams(params) {
this.triggerNavigation({ params });
}
/**
* Triggers a navigation with the given new parameter. The other parameters, as well as all the other parts (query params etc.)
* stay as they are.
* This is a shortcut to `triggerNavigation` that can be used to only change one parameter.
*/
setParam(name, value) {
this.setParams({ ...this.snapshot.params, [name]: value });
}
/**
* Triggers a navigation with the given new query parameters. All the other parts (params etc.) stay as the are.
* This is a shortcut to `triggerNavigation` that can be used to only change the query parameters.
*/
setQueryParams(queryParams) {
this.triggerNavigation({ queryParams });
}
/**
* Triggers a navigation with the given new parameter. The other query parameters, as well as all the other parts (params etc.)
* stay as the are.
* This is a shortcut to `triggerNavigation` that can be used to only change one query parameter.
*/
setQueryParam(name, value) {
this.setQueryParams({ ...this.snapshot.queryParams, [name]: value });
}
/**
* Triggers a navigation with the given new data. The other parameters, as well as all the other parts (params etc.)
* stay as the are.
* This is a shortcut to `triggerNavigation` that can be used to only change the data.
*/
setData(data) {
this.triggerNavigation({ data });
}
/**
* Triggers a navigation with the given new data item. The other data, as well as all the other parts (params etc.)
* stay as the are.
* This is a shortcut to `triggerNavigation` that can be used to only change one data item.
*/
setDataItem(name, value) {
this.setData({ ...this.snapshot.data, [name]: value });
}
/**
* Triggers a navigation with the given new title. The other parameters, as well as all the other parts (params etc.)
* stay as the are.
* This is a shortcut to `triggerNavigation` that can be used to only change the title.
*/
setTitle(title) {
this.triggerNavigation({ title: { value: title } });
}
/**
* Triggers a navigation with the given new fragment. The other parts (params etc.) stay as the are.
* This is a shortcut to `triggerNavigation` that can be used to only change the fragment.
*/
setFragment(fragment) {
this.triggerNavigation({ fragment });
}
/**
* Triggers a navigation with the given new url. The other parts (params etc.) stay as the are.
* This is a shortcut to `triggerNavigation` that can be used to only change the url.
*/
setUrl(url) {
this.triggerNavigation({ url });
}
/**
* Triggers a navigation based on the given options. If an option is undefined or null, it's ignored. Except for fragment, which is only
* ignored if it's undefined, because null is a valid value for a fragment.
*
* The non-ignored values are used to change the snapshot of the route. Once the snapshot has been modified,
* the observables corresponding to the updated parts emit the new value.
*
* So, setting params and query params will make the params and queryParams observables emit, but not the fragment, data and
* url observables for example. This is consistent to how the router behaves.
*
* Note: since the title of a route can become undefined, in order to be able to distinguish between a navigation which leaves the title
* as it is and a navigation that sets the title to undefined, a wrapper object is used for the title. So
*
* - `triggerNavigation({ params:... })` leaves the title as is because it's undefined in the options
* - `triggerNavigation({ title: { value: 'test' } })` sets the title to 'test'
* - `triggerNavigation({ title: { value: undefined } })` sets the title to undefined
*/
triggerNavigation(options) {
// set the snapshot first
if (options.params) {
this.snapshot.params = options.params;
}
if (options.queryParams) {
this.snapshot.queryParams = options.queryParams;
}
if (options.fragment !== undefined) {
this.snapshot.fragment = options.fragment;
}
if (options.data) {
this.snapshot.data = options.data;
}
if (options.title) {
// @ts-expect-error the title is readonly, but we need to be able to overwrite it here
this.snapshot.title = options.title.value;
}
if (options.url) {
this.snapshot.url = options.url;
}
// then emit everything that has changed
if (options.params) {
this.paramsSubject.next(this.snapshot.params);
}
if (options.queryParams) {
this.queryParamsSubject.next(this.snapshot.queryParams);
}
if (options.fragment !== undefined) {
this.fragmentSubject.next(this.snapshot.fragment);
}
if (options.data) {
this.dataSubject.next(this.snapshot.data);
}
if (options.title) {
this.titleSubject.next(this.snapshot.title);
}
if (options.url) {
this.urlSubject.next(this.snapshot.url);
}
}
toString() {
return 'ActivatedRouteStub';
}
}
/**
* Creates a new ActivatedRouteStub, by calling its constructor.
*/
function stubRoute(options) {
return new ActivatedRouteStub(options);
}
const speculoosMatchers = {
/**
* Checks that the receiver is a TestElement wrapping a DOM element and as the given CSS class
*/
toHaveClass() {
const assert = (isNegative, el, expected) => {
if (!el) {
return { pass: false, message: `Expected to check class '${expected}' on element, but element was falsy` };
}
if (!(el instanceof TestElement)) {
return { pass: false, message: `Expected to check class '${expected}' on element, but element was not a TestElement` };
}
const actual = el.classes;
const pass = actual.indexOf(expected) !== -1;
const message = `Expected element to ${isNegative ? 'not ' : ''}have class '${expected}', ` +
`but had ${actual.length ? "'" + actual.join(', ') + "'" : 'none'}`;
return { pass: isNegative ? !pass : pass, message };
};
return {
compare(el, expected) {
return assert(false, el, expected);
},
negativeCompare(el, expected) {
return assert(true, el, expected);
}
};
},
/**
* Checks that the receiver is a TestInput or a TestTextArea and has the given value
*/
toHaveValue() {
const assert = (isNegative, el, expected) => {
if (!el) {
return { pass: false, message: `Expected to check value '${expected}' on element, but element was falsy` };
}
if (!(el instanceof TestInput) && !(el instanceof TestTextArea)) {
return {
pass: false,
message: `Expected to check value '${expected}' on element, but element was neither a TestInput nor a TestTextArea`
};
}
const actual = el.value;
const pass = actual === expected;
const message = `Expected element to ${isNegative ? 'not ' : ''}have value '${expected}', but had value '${actual}'`;
return { pass: isNegative ? !pass : pass, message };
};
return {
compare(el, expected) {
return assert(false, el, expected);
},
negativeCompare(el, expected) {
return assert(true, el, expected);
}
};
},
/**
* Checks that the receiver is a TestElement wrapping a DOM element and has the exact given textContent
*/
toHaveText() {
const assert = (isNegative, el, expected) => {
if (!el) {
return { pass: false, message: `Expected to check text '${expected}' on element, but element was falsy` };
}
if (!(el instanceof TestElement)) {
return { pass: false, message: `Expected to check text '${expected}' on element, but element was not a TestElement` };
}
const actual = el.textContent;
const pass = actual === expected;
const message = `Expected element to ${isNegative ? 'not ' : ''}have text '${expected}', but had '${actual}'`;
return { pass: isNegative ? !pass : pass, message };
};
return {
compare(el, expected) {
return assert(false, el, expected);
},
negativeCompare(el, expected) {
return assert(true, el, expected);
}
};
},
/**
* Checks that the receiver is a TestElement wrapping a DOM element and has the given textContent, after both have been trimmed.
* So, An element such as
* ```
* <h1>
* Some title
* </h1>
* ```
* will pass the test
* ```
* expect(tester.title).toHaveTrimmedText('Some title')
* ```
*/
toHaveTrimmedText() {
const assert = (isNegative, el, expected) => {
const trimmedExpected = expected.trim();
if (!el) {
return { pass: false, message: `Expected to check trimmed text '${trimmedExpected}' on element, but element was falsy` };
}
if (!(el instanceof TestElement)) {
return {
pass: false,
message: `Expected to check trimmed text '${trimmedExpected}' on element, but element was not a TestElement`
};
}
const actual = el.textContent?.trim();
const pass = actual === trimmedExpected;
const message = `Expected element to ${isNegative ? 'not ' : ''}have trimmed text '${trimmedExpected}', but had '${actual}'`;
return { pass: isNegative ? !pass : pass, message };
};
return {
compare(el, expected) {
return assert(false, el, expected);
},
negativeCompare(el, expected) {
return assert(true, el, expected);
}
};
},
/**
* Checks that the receiver is a TestElement wrapping a DOM element and contains the given textContent
*/
toContainText() {
const assert = (isNegative, el, expected) => {
if (!el) {
return { pass: false, message: `Expected to check text '${expected}' on element, but element was falsy` };
}
if (!(el instanceof TestElement)) {
return { pass: false, message: `Expected to check text '${expected}' on element, but element was not a TestElement` };
}
const actual = el.textContent;
if (!actual) {
return {
pass: isNegative,
message: `Expected element to ${isNegative ? 'not ' : ''}contain text '${expected}', but had no text`
};
}
const pass = actual.indexOf(expected) !== -1;
const message = `Expected element to ${isNegative ? 'not ' : ''}contain text '${expected}', but had text '${actual}'`;
return { pass: isNegative ? !pass : pass, message };
};
return {
compare(el, expected) {
return assert(false, el, expected);
},
negativeCompare(el, expected) {
return assert(true, el, expected);
}
};
},
/**
* Checks that the receiver is a TestElement wrapping a DOM element and contains the given textContent
*/
toBeChecked() {
const assert = (isNegative, el) => {
if (!el) {
return { pass: false, message: `Expected to check if element was checked, but element was falsy` };
}
if (!(el instanceof TestInput)) {
return { pass: false, message: `Expected to check if element was checked, but element was not a TestInput` };
}
const pass = el.checked;
const message = `Expected element to be ${isNegative ? 'not ' : ''}checked, but was${!isNegative ? ' not' : ''}`;
return { pass: isNegative ? !pass : pass, message };
};
return {
compare(el) {
return assert(false, el);
},
negativeCompare(el) {
return assert(true, el);
}
};
},
/**
* Checks that the receiver is a TestSelect wrapping a DOM element and has the given selected index
*/
toHaveSelectedIndex() {
const assert = (isNegative, el, expected) => {
if (!el) {
return { pass: false, message: `Expected to check selected index ${expected} on element, but element was falsy` };
}
if (!(el instanceof TestSelect)) {
return { pass: false, message: `Expected to check selected index ${expected} on element, but element was not a TestSelect` };
}
const actual = el.selectedIndex;
const pass = actual === expected;
const message = `Expected element to ${isNegative ? 'not ' : ''}have selected index ${expected}, but had ${actual}`;
return { pass: isNegative ? !pass : pass, message };
};
return {
compare(el, expected) {
return assert(false, el, expected);
},
negativeCompare(el, expected) {
return assert(true, el, expected);
}
};
},
/**
* Checks that the receiver is a TestSelect wrapping a DOM element with the selected option's value equal to the given value
*/
toHaveSelectedValue() {
const assert = (isNegative, el, expected) => {
if (!el) {
return { pass: false, message: `Expected to check selected value '${expected}' on element, but element was falsy` };
}
if (!(el instanceof TestSelect)) {
return { pass: false, message: `Expected to check selected value '${expected}' on element, but element was not a TestSelect` };
}
const actual = el.selectedValue;
const pass = actual === expected;
const message = `Expected element to ${isNegative ? 'not ' : ''}have selected value '${expected}', but had '${actual}'`;
return { pass: isNegative ? !pass : pass, message };
};
return {
compare(el, expected) {
return assert(false, el, expected);
},
negativeCompare(el, expected) {
return assert(true, el, expected);
}
};
},
/**
* Checks that the receiver is a TestSelect wrapping a DOM element with the selected option's label equal to the given label
*/
toHaveSelectedLabel() {
const assert = (isNegative, el, expected) => {
if (!el) {
return { pass: false, message: `Expected to check selected label '${expected}' on element, but element was falsy` };
}
if (!(el instanceof TestSelect)) {
return { pass: false, message: `Expected to check selected label '${expected}' on element, but element was not a TestSelect` };
}
const actual = el.selectedLabel;
const pass = actual === expected;
const message = `Expected element to ${isNegative ? 'not ' : ''}have selected label '${expected}', but had '${actual}'`;
return { pass: isNegative ? !pass : pass, message };
};
return {
compare(el, expected) {
return assert(false, el, expected);
},
negativeCompare(el, expected) {
return assert(true, el, expected);
}
};
},
/**
* Checks that the receiver is a TestHtmlElement which is visible
*/
toBeVisible() {
const assert = (isNegative, el) => {
const expectedState = `${isNegative ? 'in' : ''}visible`;
const inverseState = `${isNegative ? '' : 'in'}visible`;
if (!el) {
return { pass: false, message: `Expected to check if element was ${expectedState}, but element was falsy` };
}
if (!(el instanceof TestHtmlElement)) {
return { pass: false, message: