Skip to content

Commit

Permalink
feat(spectator): support for Function-based outputs (#671)
Browse files Browse the repository at this point in the history
* feat(spectator): support for Function-based outputs

* feat(spectator): limit the output keys at compile time to outputs only

* fix(spectator): properly infer the type when passing a T argument for the output

* chore(spectator): cleanup the output method overloads and used types

---------

Co-authored-by: Anatolie Darii <[email protected]>
  • Loading branch information
anatolie-darii and Anatolie Darii authored Sep 11, 2024
1 parent defdedd commit 02ed01d
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { createComponentFactory, createHostFactory, Spectator, SpectatorHost } from '@ngneat/spectator/jest';
import { FunctionOutputComponent } from '../../../test/function-output/function-output.component';

describe('FunctionOutputComponent', () => {
describe('with Spectator', () => {
let spectator: Spectator<FunctionOutputComponent>;

const createComponent = createComponentFactory({
component: FunctionOutputComponent,
});

beforeEach(() => {
spectator = createComponent();
});

it('should emit the event on button click', () => {
let output;
spectator.output('buttonClick').subscribe((result) => (output = result));

spectator.click('button');

expect(output).toEqual(true);
});
});

describe('with SpectatorHost', () => {
let host: SpectatorHost<FunctionOutputComponent>;

const createHost = createHostFactory({
component: FunctionOutputComponent,
template: `<app-function-output/>`,
});

beforeEach(() => {
host = createHost();
});

it('should emit the event on button click', () => {
let output;
host.output('buttonClick').subscribe((result) => (output = result));

host.click('button');

expect(output).toEqual(true);
});
});
});
20 changes: 14 additions & 6 deletions projects/spectator/src/lib/base/dom-spectator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DebugElement, ElementRef, EventEmitter, Type } from '@angular/core';
import { DebugElement, ElementRef, EventEmitter, OutputEmitterRef, Type } from '@angular/core';
import { ComponentFixture, tick } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { Observable } from 'rxjs';
Expand All @@ -18,6 +18,11 @@ import { BaseSpectator } from './base-spectator';

const KEY_UP = 'keyup';

type KeysMatchingReturnType<T, V> = keyof { [P in keyof T as T[P] extends V ? P : never]: P } & keyof T;
type KeysMatchingOutputFunction<T> = KeysMatchingReturnType<T, OutputEmitterRef<any>>;
type KeysMatchingClassicOutput<T> = KeysMatchingReturnType<T, EventEmitter<any>>;
type KeysMatchingOutput<T> = KeysMatchingOutputFunction<T> | KeysMatchingClassicOutput<T>;

/**
* @internal
*/
Expand Down Expand Up @@ -159,14 +164,17 @@ export abstract class DomSpectator<I> extends BaseSpectator {
return null;
}

public output<T, K extends keyof I = keyof I>(output: K): Observable<T> {
const observable = this.instance[output];
public output<K extends KeysMatchingOutput<I> = KeysMatchingOutput<I>>(output: K): I[K];
public output<T, K extends KeysMatchingClassicOutput<I> = KeysMatchingClassicOutput<I>>(output: K): Observable<T>;
public output<T, K extends KeysMatchingOutputFunction<I> = KeysMatchingOutputFunction<I>>(output: K): OutputEmitterRef<T>;
public output<T, K extends KeysMatchingOutput<I>>(output: K): I[K] | Observable<T> | OutputEmitterRef<T> {
const eventEmitter = this.instance[output];

if (!(observable instanceof Observable)) {
throw new Error(`${String(output)} is not an @Output`);
if (!(eventEmitter instanceof Observable) && !(eventEmitter instanceof OutputEmitterRef)) {
throw new Error(`${String(output)} is not an @Output or an output function`);
}

return observable as Observable<T>;
return eventEmitter;
}

public tick(millis?: number): void {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { createComponentFactory, createHostFactory, Spectator, SpectatorHost } from '@ngneat/spectator';
import { FunctionOutputComponent } from './function-output.component';

describe('FunctionOutputComponent', () => {
describe('with Spectator', () => {
let spectator: Spectator<FunctionOutputComponent>;

const createComponent = createComponentFactory({
component: FunctionOutputComponent,
});

beforeEach(() => {
spectator = createComponent();
});

it('should emit the event on button click', () => {
let output;
spectator.output('buttonClick').subscribe((result) => (output = result));

spectator.click('button');

expect(output).toEqual(true);
});
});

describe('with SpectatorHost', () => {
let host: SpectatorHost<FunctionOutputComponent>;

const createHost = createHostFactory({
component: FunctionOutputComponent,
template: `<app-function-output/>`,
});

beforeEach(() => {
host = createHost();
});

it('should emit the event on button click', () => {
let output;
host.output('buttonClick').subscribe((result) => (output = result));

host.click('button');

expect(output).toEqual(true);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Component, input, output, ɵINPUT_SIGNAL_BRAND_WRITE_TYPE } from '@angular/core';

@Component({
selector: 'app-function-output',
template: ` <button (click)="buttonClick.emit(true)">Emit function output</button> `,
standalone: true,
})
export class FunctionOutputComponent {
public buttonClick = output<boolean>();
}

0 comments on commit 02ed01d

Please sign in to comment.