import { InjectionToken, Type } from '@angular/core'; import { NgtxElement, NgtxFixture, NgtxMultiElement } from '../core'; import { NgtxTestState } from './symbols'; import type { NgtxTestEnv } from './test-env'; type ChainedStatement = (testStatement: TestStateExporter) => ExpectApi; type PredicateReceiver = (...predicates: ExtensionFn>[]) => ExpectApi; interface EmitPredicate { (resolver: EventDispatcher, arg?: any): ExpectApi; (eventName: Events, arg?: any): ExpectApi; } type CallPredicate = (resolver: CallSiteResolver, methodName: keyof Out, args?: any[]) => ExpectApi; type AllowType = { [Key in keyof Base]: Base[Key] extends Type ? Key : never; }; type AllowedNames = AllowType[keyof Base]; type OmitType = Pick>; export type ExtensionFnMarker = { __ngtxExtensionFn: true; }; export type ExtensionFnSignature = (target: ElementListRef, env: NgtxTestEnv, fixture: NgtxFixture) => void; export type ExtensionFn = ExtensionFnSignature & ExtensionFnMarker; export type TestStateExporter = { [NgtxTestState]: NgtxTestEnv; }; export type CssClass = string | undefined; export type CallSiteResolver = (target: NgtxElement) => Output; export interface SpyFactorySetter { setSpyFactory(fn: any): void; } export type SubjectStatement = (subject: TargetRef) => PredicateApi; export type WhenStatement = SpyFactorySetter & SubjectStatement & ChainedStatement; export type AndStatement = PredicateReceiver & // and(...extFns)... WhenStatement; export type EventDispatcher = (subject: NgtxElement) => void; export interface PredicateApi { /** * Predicate emitting the specified event on the pre-defined target: * * > **Please note:** The `emits` method is just an alias for `emit`. They can be used interchangeably. * > ~~~ts * > When(host).emit(...)... * > // is the same as: * > When(host).emits(...)... * > ~~~ * * #### Example 1 - without event-arg: * * ~~~ts * When(the.IncreaseButton).emits('click')... * ~~~ * * #### Example 2 - with event-arg: * * ~~~ts * When(the.Tabs).emit('tabIndexChange', 1)... * ~~~ */ emit: EmitPredicate; /** * Predicate emitting the specified event on the pre-defined target: * * > **Please note:** The `emits` method is just an alias for `emit`. They can be used interchangeably. * > ~~~ts * > When(host).emit(...)... * > // is the same as: * > When(host).emits(...)... * > ~~~ * * #### Example 1 - without event-arg: * * ~~~ts * When(the.IncreaseButton).emits('click')... * ~~~ * * #### Example 2 - with event-arg: * * ~~~ts * When(the.Tabs).emit('tabIndexChange', 1)... * ~~~ */ emits: EmitPredicate; /** * Predicate calling the specified method. The call-target as well as the method-name will be selected via parameters. * * > **Please note:** The examples uses the concept of test-harness classes. You can read more about it [here](https://github.com/Centigrade/ngtx/blob/experimental/capabilities/docs/GOOD_TESTS.md). * * > **Please note:** The `calls` method is just an alias for `call`. They can be used interchangeably. * > ~~~ts * > When(host).call(...)... * > // is the same as: * > When(host).calls(...)... * > ~~~ * * #### Example 1 - method on the target's nativeElement: * * ~~~ts * import { nativeMethod } from '@centigrade/ngtx'; * // target is the.Button's nativeElement, * // only arg is the options object: * When(the.Button) * .calls(nativeMethod, 'scrollIntoView', [{ behavior: 'smooth' }])... * ~~~ * * #### Example 2 - method on a componentInstance: * * ~~~ts * import { componentMethod } from '@centigrade/ngtx'; * // target is the.Button's componentInstance * When(the.Button).calls(componentMethod, 'click')... * ~~~ * * #### Example 3 - method on a service or injected token: * * ~~~ts * import { injected } from '@centigrade/ngtx'; * // target is the host's injector: * When(host) * .calls(injected(SomeService), 'someMethodOnService')... * ~~~ */ call: CallPredicate; /** * Predicate calling the specified method. The call-target as well as the method-name will be selected via parameters. * * > **Please note:** The examples uses the concept of test-harness classes. You can read more about it [here](https://github.com/Centigrade/ngtx/blob/experimental/capabilities/docs/GOOD_TESTS.md). * * > **Please note:** The `calls` method is just an alias for `call`. They can be used interchangeably. * > ~~~ts * > When(host).call(...)... * > // is the same as: * > When(host).calls(...)... * > ~~~ * * #### Example 1 - method on the target's nativeElement: * * ~~~ts * import { nativeMethod } from '@centigrade/ngtx'; * // target is the.Button's nativeElement, * // only arg is the options object: * When(the.Button) * .calls(nativeMethod, 'scrollIntoView', [{ behavior: 'smooth' }])... * ~~~ * * #### Example 2 - method on a componentInstance: * * ~~~ts * import { componentMethod } from '@centigrade/ngtx'; * // target is the.Button's componentInstance * When(the.Button).calls(componentMethod, 'click')... * ~~~ * * #### Example 3 - method on a service or injected token: * * ~~~ts * import { injected } from '@centigrade/ngtx'; * // target is the host's injector: * When(host) * .calls(injected(SomeService), 'someMethodOnService')... * ~~~ */ calls: CallPredicate; /** A predicate doing nothing. You can use this if the test is already set-up from the very beginning. */ rendered(): ExpectApi; /** * A predicate accepting [predicate extension-functions](https://github.com/Centigrade/ngtx/blob/experimental/capabilities/docs/declarative-api/built-in.md) * * @include alias: call, calls * > **Please note:** The `calls` method is just an alias for `call`. They can be used interchangeably. * > ~~~ts * > When(host).call(...)... * > // is the same as: * > When(host).calls(...)... * > ~~~ */ has: PredicateReceiver; have: PredicateReceiver; does: PredicateReceiver; do: PredicateReceiver; gets: PredicateReceiver; get: PredicateReceiver; is: PredicateReceiver; are: PredicateReceiver; } export interface ExpectApi extends TestStateExporter { /** * Allows to chain additional statement(s) to the test-expression: * * Example 1 - with targeted statement: * * ~~~ts * When(host) * .has(state({ text: 'Hi' })) * .and(the.ClearButton) * .gets(clicked()) * .expect(host) * .to(haveState({ text: '' })); * ~~~ * * Example 2 - with extension-function(s) only: * * ~~~ts * When(host) * .has(state({ label: 'Welcome' })) * .and(detectChanges({ viaChangeDetectorRef: true })) * .expect(the.Label) * .to(haveText({ label: 'Welcome' })); * ~~~ * * Example 3 - with capability api: * * ~~~ts * When(host) * .has(state({ text: 'Hi' })) * .and(the.ClearButton.getsClicked()) * .expect(host) * .to(haveText({ text: '' })); * ~~~ * */ and: AndStatement; expect(object: TargetRef): AssertionApi; expect(...assertions: NgtxTestEnv[]): void; } export interface AssertionApi extends TestStateExporter { not: AssertionApi; /** * Accepts [asserting extension-functions](https://github.com/Centigrade/ngtx/blob/main/docs/built-in.md), * and immediately triggers the test expression to run. You want to use this behavior inside ngtx test-cases: * * **Example** * * ~~~ts * it('should render the label text', () => { * When(host).has(state({ label: 'Welcome' })).expect(the.Label).to(haveText('Welcome')); * }); * ~~~ * @param assertions The assertion(s) to register for this test. */ to(...assertions: ExtensionFn>[]): void; /** * Accepts [asserting extension-functions](https://github.com/Centigrade/ngtx/blob/main/docs/built-in.md), but does not immediately trigger the test expression to run. * You want to use this behavior for ngtx' capabilities classes: * * **Example** * * ~~~ts * import { Capabilities } from '@centigrade/ngtx'; * * export class ButtonCapability extends Capabilities { * public toHaveText(expectedText: string | string[]) { * return this.expectComponents.will(haveText(expectedText)); * } * } * ~~~ * @param assertions The assertion(s) to register for this test. */ will(...assertions: ExtensionFn>[]): NgtxTestEnv; } export type IHaveLifeCycleHook = { ngAfterViewInit?: Function; ngAfterContentInit?: Function; ngOnInit?: Function; ngOnChanges?: Function; ngOnDestroy?: Function; }; export type ElementList = NgtxElement[]; export type ElementListRef = () => ElementList; export type Token = Type | Function | InjectionToken; export type PropertiesOf = Partial>; export type HtmlPropertiesOf = PropertiesOf | Record<`data-${string}`, any>; export type Events = keyof Type | HtmlEvents; export interface CallBaseOptions { /** The number of times the spy was called. */ times?: number | null; } export interface CallOptions extends CallBaseOptions { /** The values that were passed as arguments to the spy. */ args?: any[]; /** The return-value that the spy should return when being called. */ whichReturns?: any; } /** * Defines options what aspects of a spy should be asserted. * * **Example:** * ~~~ts * ...expect(...).toHaveCalledService(AuthService, 'isLoggedIn', { * args: 'user.name', * times: 1, * whichReturns: true * }); * ~~~ */ export interface EmissionOptions extends CallBaseOptions { arg?: any; } export type TargetRef = () => NgtxElement | NgtxMultiElement; export type NgtxElementRef = () => NgtxElement; export type NgtxMultiElementRef = () => NgtxMultiElement; export type HtmlEvents = T extends `on${infer Suffix}` ? Suffix : never; /** * Contains the current test definition state. Ngtx declarative * tests are structured similar to a sentence in human language: * * ` .` * * Thus, the declarative test state includes exactly these mentioned parts. * There is always a subject that has or does something (-> predicate), and * an object that will react on it and thus has an assertion defined. * * This state can be mutated by setting a subject or object to a `PartRef` * or by overriding or wrapping the `predicate` or `assertion` function. */ export interface DeclarativeTestState { /** Whether the assertion is preceded by a ".not" and will be negated. */ negateAssertion?: boolean; /** * The `predicate` of the test case that describes what actions has to * be done, before assertions can be made. Override or wrap this function * in order to add actions to the test case body. In traditional (AAA) * testing this would map to the `arrange` and `act` sections of your test. * * The predicate will most likely use the before-hand defined `subject` of * the declarative test-state to execute the actions on it. * * **Example:** * ~~~ts * const disabled = (state: DeclarativeTest, fixture: NgtxFixture) => { * return { * // here we override the predicate of the test-state: * predicate: () => { * state.subject().componentInstance.disabled = true; * fixture.detectChanges(); * }, * }; * } * * // this will set the disabled property on host to true * // and detect changes afterwards: * When(host).is(disabled).expect(...).to(...); * ~~~ */ predicate: (() => void)[]; /** * The `assertion` part of the test case. Override or wrap this function in * order to run expectations on the test's `object`. In traditional (AAA) * testing this would map to the `assert` part of your test. * * **Example:** * ~~~ts * const haveFocus = (state: DeclarativeTestState): DeclarativeTestState => { * return { * // here we override the assertion of the test-state: * assertion: () => { * const target = state.object().nativeElement; * expect(document.activeElement).toBe(target); * } * } * } * * const Input = () => get('input'); * // this will assert that the Input is the activeElement * // of the document at the end of the test. * When(...).has(...).expect(Input).to(haveFocus); * ~~~ */ assertion: (() => void)[]; spyRegistry: SpyRegisterEntry[]; } export interface SpyRegisterEntry { done: boolean; host: () => any; methodName: string; spy: any; } export type SpyOnFn = (host: () => T, methodName: keyof T, spyReturnValue?: any) => any; export type PublicApi = OmitType; export type PublicMembers = OmitType; export {};