UNPKG

ngx-speculoos

Version:
1,264 lines (1,253 loc) 52.5 kB
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: