mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
test(core): add runtime acceptance tests for model inputs (#54252)
Sets up the runtime tests for model inputs. PR Close #54252
This commit is contained in:
committed by
Jessica Janiuk
parent
0ea4d7086d
commit
8aac3c45b9
@@ -0,0 +1,546 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Component, Directive, EventEmitter, Input, model, OnChanges, Output, signal, SimpleChange, SimpleChanges, ViewChild} from '@angular/core';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
|
||||
describe('model inputs', () => {
|
||||
it('should support two-way binding to a signal', () => {
|
||||
@Directive({selector: '[dir]', standalone: true})
|
||||
class Dir {
|
||||
value = model(0);
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: '<div [(value)]="value" dir></div>',
|
||||
standalone: true,
|
||||
imports: [Dir],
|
||||
})
|
||||
class App {
|
||||
@ViewChild(Dir) dir!: Dir;
|
||||
value = signal(1);
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(App);
|
||||
const host = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
|
||||
// Initial value.
|
||||
expect(host.value()).toBe(1);
|
||||
expect(host.dir.value()).toBe(1);
|
||||
|
||||
// Changing the value from within the directive.
|
||||
host.dir.value.set(2);
|
||||
fixture.detectChanges();
|
||||
expect(host.value()).toBe(2);
|
||||
expect(host.dir.value()).toBe(2);
|
||||
|
||||
// Changing the value from the outside.
|
||||
host.value.set(3);
|
||||
fixture.detectChanges();
|
||||
expect(host.value()).toBe(3);
|
||||
expect(host.dir.value()).toBe(3);
|
||||
});
|
||||
|
||||
it('should support two-way binding to a non-signal value', () => {
|
||||
@Directive({selector: '[dir]', standalone: true})
|
||||
class Dir {
|
||||
value = model(0);
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: '<div [(value)]="value" dir></div>',
|
||||
standalone: true,
|
||||
imports: [Dir],
|
||||
})
|
||||
class App {
|
||||
@ViewChild(Dir) dir!: Dir;
|
||||
value = 1;
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(App);
|
||||
const host = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
|
||||
// Initial value.
|
||||
expect(host.value).toBe(1);
|
||||
expect(host.dir.value()).toBe(1);
|
||||
|
||||
// Changing the value from within the directive.
|
||||
host.dir.value.set(2);
|
||||
fixture.detectChanges();
|
||||
expect(host.value).toBe(2);
|
||||
expect(host.dir.value()).toBe(2);
|
||||
|
||||
// Changing the value from the outside.
|
||||
host.value = 3;
|
||||
fixture.detectChanges();
|
||||
expect(host.value).toBe(3);
|
||||
expect(host.dir.value()).toBe(3);
|
||||
});
|
||||
|
||||
it('should support two-way binding a signal to a non-model input/output pair', () => {
|
||||
@Directive({selector: '[dir]', standalone: true})
|
||||
class Dir {
|
||||
@Input() value = 0;
|
||||
@Output() valueChange = new EventEmitter<number>();
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: '<div [(value)]="value" dir></div>',
|
||||
standalone: true,
|
||||
imports: [Dir],
|
||||
})
|
||||
class App {
|
||||
@ViewChild(Dir) dir!: Dir;
|
||||
value = signal(1);
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(App);
|
||||
const host = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
|
||||
// Initial value.
|
||||
expect(host.value()).toBe(1);
|
||||
expect(host.dir.value).toBe(1);
|
||||
|
||||
// Changing the value from within the directive.
|
||||
host.dir.value = 2;
|
||||
host.dir.valueChange.emit(2);
|
||||
fixture.detectChanges();
|
||||
expect(host.value()).toBe(2);
|
||||
expect(host.dir.value).toBe(2);
|
||||
|
||||
// Changing the value from the outside.
|
||||
host.value.set(3);
|
||||
fixture.detectChanges();
|
||||
expect(host.value()).toBe(3);
|
||||
expect(host.dir.value).toBe(3);
|
||||
});
|
||||
|
||||
it('should support a one-way property binding to a model', () => {
|
||||
@Directive({selector: '[dir]', standalone: true})
|
||||
class Dir {
|
||||
value = model(0);
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: '<div [value]="value" dir></div>',
|
||||
standalone: true,
|
||||
imports: [Dir],
|
||||
})
|
||||
class App {
|
||||
@ViewChild(Dir) dir!: Dir;
|
||||
value = 1;
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(App);
|
||||
const host = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
|
||||
// Initial value.
|
||||
expect(host.value).toBe(1);
|
||||
expect(host.dir.value()).toBe(1);
|
||||
|
||||
// Changing the value from within the directive.
|
||||
host.dir.value.set(2);
|
||||
fixture.detectChanges();
|
||||
expect(host.value).toBe(1);
|
||||
expect(host.dir.value()).toBe(2);
|
||||
|
||||
// Changing the value from the outside.
|
||||
host.value = 3;
|
||||
fixture.detectChanges();
|
||||
expect(host.value).toBe(3);
|
||||
expect(host.dir.value()).toBe(3);
|
||||
});
|
||||
|
||||
it('should emit to the change output when the model changes', () => {
|
||||
const emittedValues: number[] = [];
|
||||
|
||||
@Directive({selector: '[dir]', standalone: true})
|
||||
class Dir {
|
||||
value = model(0);
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: '<div (valueChange)="changed($event)" dir></div>',
|
||||
standalone: true,
|
||||
imports: [Dir],
|
||||
})
|
||||
class App {
|
||||
@ViewChild(Dir) dir!: Dir;
|
||||
|
||||
changed(value: number) {
|
||||
emittedValues.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(App);
|
||||
const host = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(emittedValues).toEqual([]);
|
||||
|
||||
host.dir.value.set(1);
|
||||
fixture.detectChanges();
|
||||
expect(emittedValues).toEqual([1]);
|
||||
|
||||
// Same value should not emit.
|
||||
host.dir.value.set(1);
|
||||
fixture.detectChanges();
|
||||
expect(emittedValues).toEqual([1]);
|
||||
|
||||
host.dir.value.update(value => value * 5);
|
||||
fixture.detectChanges();
|
||||
expect(emittedValues).toEqual([1, 5]);
|
||||
});
|
||||
|
||||
it('should not emit to the change event when then property binding changes', () => {
|
||||
const emittedValues: number[] = [];
|
||||
|
||||
@Directive({selector: '[dir]', standalone: true})
|
||||
class Dir {
|
||||
value = model(0);
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: '<div [value]="value()" (valueChange)="changed($event)" dir></div>',
|
||||
standalone: true,
|
||||
imports: [Dir],
|
||||
})
|
||||
class App {
|
||||
@ViewChild(Dir) dir!: Dir;
|
||||
value = signal(1);
|
||||
|
||||
changed(value: number) {
|
||||
emittedValues.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(App);
|
||||
const host = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
expect(emittedValues).toEqual([]);
|
||||
|
||||
host.value.set(2);
|
||||
fixture.detectChanges();
|
||||
expect(emittedValues).toEqual([]);
|
||||
});
|
||||
|
||||
it('should support binding to the model input and output separately', () => {
|
||||
const emittedValues: number[] = [];
|
||||
|
||||
@Directive({selector: '[dir]', standalone: true})
|
||||
class Dir {
|
||||
value = model(0);
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: '<div [value]="value()" (valueChange)="changed($event)" dir></div>',
|
||||
standalone: true,
|
||||
imports: [Dir],
|
||||
})
|
||||
class App {
|
||||
@ViewChild(Dir) dir!: Dir;
|
||||
value = signal(1);
|
||||
|
||||
changed(value: number) {
|
||||
emittedValues.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(App);
|
||||
const host = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(host.value()).toBe(1);
|
||||
expect(host.dir.value()).toBe(1);
|
||||
expect(emittedValues).toEqual([]);
|
||||
|
||||
host.dir.value.set(2);
|
||||
fixture.detectChanges();
|
||||
expect(host.value()).toBe(1);
|
||||
expect(host.dir.value()).toBe(2);
|
||||
expect(emittedValues).toEqual([2]);
|
||||
|
||||
host.value.set(3);
|
||||
fixture.detectChanges();
|
||||
expect(host.value()).toBe(3);
|
||||
expect(host.dir.value()).toBe(3);
|
||||
expect(emittedValues).toEqual([2]);
|
||||
});
|
||||
|
||||
it('should support two-way binding to a model with an alias', () => {
|
||||
@Directive({selector: '[dir]', standalone: true})
|
||||
class Dir {
|
||||
value = model(0, {alias: 'alias'});
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: '<div [(alias)]="value" dir></div>',
|
||||
standalone: true,
|
||||
imports: [Dir],
|
||||
})
|
||||
class App {
|
||||
@ViewChild(Dir) dir!: Dir;
|
||||
value = signal(1);
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(App);
|
||||
const host = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
|
||||
// Initial value.
|
||||
expect(host.value()).toBe(1);
|
||||
expect(host.dir.value()).toBe(1);
|
||||
|
||||
// Changing the value from within the directive.
|
||||
host.dir.value.set(2);
|
||||
fixture.detectChanges();
|
||||
expect(host.value()).toBe(2);
|
||||
expect(host.dir.value()).toBe(2);
|
||||
|
||||
// Changing the value from the outside.
|
||||
host.value.set(3);
|
||||
fixture.detectChanges();
|
||||
expect(host.value()).toBe(3);
|
||||
expect(host.dir.value()).toBe(3);
|
||||
});
|
||||
|
||||
it('should support binding to an aliased model input and output separately', () => {
|
||||
const emittedValues: number[] = [];
|
||||
|
||||
@Directive({selector: '[dir]', standalone: true})
|
||||
class Dir {
|
||||
value = model(0, {alias: 'alias'});
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: '<div [alias]="value()" (aliasChange)="changed($event)" dir></div>',
|
||||
standalone: true,
|
||||
imports: [Dir],
|
||||
})
|
||||
class App {
|
||||
@ViewChild(Dir) dir!: Dir;
|
||||
value = signal(1);
|
||||
|
||||
changed(value: number) {
|
||||
emittedValues.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(App);
|
||||
const host = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(host.value()).toBe(1);
|
||||
expect(host.dir.value()).toBe(1);
|
||||
expect(emittedValues).toEqual([]);
|
||||
|
||||
host.dir.value.set(2);
|
||||
fixture.detectChanges();
|
||||
expect(host.value()).toBe(1);
|
||||
expect(host.dir.value()).toBe(2);
|
||||
expect(emittedValues).toEqual([2]);
|
||||
|
||||
host.value.set(3);
|
||||
fixture.detectChanges();
|
||||
expect(host.value()).toBe(3);
|
||||
expect(host.dir.value()).toBe(3);
|
||||
expect(emittedValues).toEqual([2]);
|
||||
});
|
||||
|
||||
it('should throw if a required model input is accessed too early', () => {
|
||||
@Directive({selector: '[dir]', standalone: true})
|
||||
class Dir {
|
||||
value = model.required<number>();
|
||||
|
||||
constructor() {
|
||||
this.value();
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: '<div [(value)]="value" dir></div>',
|
||||
standalone: true,
|
||||
imports: [Dir],
|
||||
})
|
||||
class App {
|
||||
value = 1;
|
||||
}
|
||||
|
||||
expect(() => TestBed.createComponent(App))
|
||||
.toThrowError(/Model is required but no value is available yet/);
|
||||
});
|
||||
|
||||
it('should stop emitting to the output on destroy', () => {
|
||||
let emittedEvents = 0;
|
||||
|
||||
@Directive({selector: '[dir]', standalone: true})
|
||||
class Dir {
|
||||
value = model(0);
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: '<div (valueChange)="changed()" dir></div>',
|
||||
standalone: true,
|
||||
imports: [Dir],
|
||||
})
|
||||
class App {
|
||||
@ViewChild(Dir) dir!: Dir;
|
||||
|
||||
changed() {
|
||||
emittedEvents++;
|
||||
}
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(App);
|
||||
fixture.detectChanges();
|
||||
|
||||
const modelRef = fixture.componentInstance.dir.value;
|
||||
expect(emittedEvents).toBe(0);
|
||||
|
||||
modelRef.set(1);
|
||||
fixture.detectChanges();
|
||||
expect(emittedEvents).toBe(1);
|
||||
|
||||
fixture.destroy();
|
||||
modelRef.set(2);
|
||||
expect(emittedEvents).toBe(1);
|
||||
});
|
||||
|
||||
it('should support inherited model inputs', () => {
|
||||
@Directive()
|
||||
abstract class BaseDir {
|
||||
value = model(0);
|
||||
}
|
||||
|
||||
@Directive({selector: '[dir]', standalone: true})
|
||||
class Dir extends BaseDir {
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: '<div [(value)]="value" dir></div>',
|
||||
standalone: true,
|
||||
imports: [Dir],
|
||||
})
|
||||
class App {
|
||||
@ViewChild(Dir) dir!: Dir;
|
||||
value = signal(1);
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(App);
|
||||
const host = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
|
||||
// Initial value.
|
||||
expect(host.value()).toBe(1);
|
||||
expect(host.dir.value()).toBe(1);
|
||||
|
||||
// Changing the value from within the directive.
|
||||
host.dir.value.set(2);
|
||||
fixture.detectChanges();
|
||||
expect(host.value()).toBe(2);
|
||||
expect(host.dir.value()).toBe(2);
|
||||
|
||||
// Changing the value from the outside.
|
||||
host.value.set(3);
|
||||
fixture.detectChanges();
|
||||
expect(host.value()).toBe(3);
|
||||
expect(host.dir.value()).toBe(3);
|
||||
});
|
||||
|
||||
it('should allow a two-way-bound signal to be bound in the template', () => {
|
||||
@Directive({
|
||||
selector: '[dir]',
|
||||
standalone: true,
|
||||
host: {
|
||||
'(click)': 'increment()',
|
||||
}
|
||||
})
|
||||
class Dir {
|
||||
value = model(0);
|
||||
|
||||
increment() {
|
||||
this.value.update(previous => previous + 1);
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: '<button [(value)]="value" dir></button> Current value: {{value()}}',
|
||||
standalone: true,
|
||||
imports: [Dir],
|
||||
})
|
||||
class App {
|
||||
value = signal(1);
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(App);
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.textContent).toContain('Current value: 1');
|
||||
|
||||
fixture.nativeElement.querySelector('button').click();
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.textContent).toContain('Current value: 2');
|
||||
|
||||
fixture.nativeElement.querySelector('button').click();
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.textContent).toContain('Current value: 3');
|
||||
});
|
||||
|
||||
it('should support ngOnChanges for two-way model bindings', () => {
|
||||
const changes: SimpleChange[] = [];
|
||||
|
||||
@Directive({selector: '[dir]', standalone: true})
|
||||
class Dir implements OnChanges {
|
||||
value = model(0);
|
||||
|
||||
ngOnChanges(allCahnges: SimpleChanges): void {
|
||||
if (allCahnges['value']) {
|
||||
changes.push(allCahnges['value']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: '<div [(value)]="value" dir></div>',
|
||||
standalone: true,
|
||||
imports: [Dir],
|
||||
})
|
||||
class App {
|
||||
@ViewChild(Dir) dir!: Dir;
|
||||
value = signal(1);
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(App);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(changes).toEqual([jasmine.objectContaining({
|
||||
previousValue: undefined,
|
||||
currentValue: 1,
|
||||
firstChange: true,
|
||||
})]);
|
||||
|
||||
fixture.componentInstance.value.set(2);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(changes).toEqual([
|
||||
jasmine.objectContaining({
|
||||
previousValue: undefined,
|
||||
currentValue: 1,
|
||||
firstChange: true,
|
||||
}),
|
||||
jasmine.objectContaining({
|
||||
previousValue: 1,
|
||||
currentValue: 2,
|
||||
firstChange: false,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Component, computed, Directive, effect, input} from '@angular/core';
|
||||
import {Component, computed, Directive, effect, EventEmitter, input, Output, ViewChild} from '@angular/core';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
|
||||
describe('signal inputs', () => {
|
||||
@@ -231,4 +231,43 @@ describe('signal inputs', () => {
|
||||
|
||||
expect(fixture.nativeElement.textContent).toBe('input:2');
|
||||
});
|
||||
|
||||
// TODO(crisbeto): we may not want to support this combination. Will discuss with the team.
|
||||
it('should support two-way binding to signal input and @Output decorated member', () => {
|
||||
@Directive({selector: '[dir]', standalone: true})
|
||||
class Dir {
|
||||
value = input(0);
|
||||
@Output() valueChange = new EventEmitter<number>();
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: '<div [(value)]="value" dir></div>',
|
||||
standalone: true,
|
||||
imports: [Dir],
|
||||
})
|
||||
class App {
|
||||
@ViewChild(Dir) dir!: Dir;
|
||||
value = 1;
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(App);
|
||||
const host = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
|
||||
// Initial value.
|
||||
expect(host.value).toBe(1);
|
||||
expect(host.dir.value()).toBe(1);
|
||||
|
||||
// Changing the value from within the directive.
|
||||
host.dir.valueChange.emit(2);
|
||||
fixture.detectChanges();
|
||||
expect(host.value).toBe(2);
|
||||
expect(host.dir.value()).toBe(2);
|
||||
|
||||
// Changing the value from the outside.
|
||||
host.value = 3;
|
||||
fixture.detectChanges();
|
||||
expect(host.value).toBe(3);
|
||||
expect(host.dir.value()).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user