【问题标题】:How to change value of a select box in angular2 unit test?如何在angular2单元测试中更改选择框的值?
【发布时间】:2017-02-09 03:26:13
【问题描述】:

我有一个 Angular2 组件,其中包含一个看起来像的选择框

<select [(ngModel)]="envFilter" class="form-control" name="envSelector" (ngModelChange)="onChangeFilter($event)">
    <option *ngFor="let env of envs" [ngValue]="env">{{env}}</option>
</select>

我正在尝试为 ngModelChange 事件编写单元测试。这是我最近一次失败的尝试

it("should filter and show correct items", async(() => {
    fixture.detectChanges();
    fixture.whenStable().then(() => {
        el = fixture.debugElement.query(By.name("envSelector"));
        fixture.detectChanges();
        makeResponse([hist2, longhist]);
        comp.envFilter = 'env3';
        el.triggerEventHandler('change', {});
        fixture.whenStable().then(() => {
            fixture.detectChanges();
            expect(comp.displayedHistory).toEqual(longhist);
        });
    });

我遇到的问题是更改底层模型comp.envFilter = 'env3'; 的值不会触发更改方法。我添加了el.triggerEventHandler('change', {});,但这会引发Failed: Uncaught (in promise): ReferenceError: By is not defined。我在文档中找不到任何提示...有什么想法吗?

【问题讨论】:

    标签: unit-testing angular typescript karma-jasmine angular2-testing


    【解决方案1】:

    就错误而言。看来您只需要导入By。这不是全球性的。它应该从以下模块导入

    import { By } from '@angular/platform-browser';
    

    就测试部分而言,这是我能够弄清楚的。当您更改组件中的值时,您需要触发更改检测以更新视图。您可以使用fixture.detectChanges() 执行此操作。完成此操作后,通常应该使用该值更新视图。

    从测试类似于您的示例的内容来看,情况似乎并非如此。在更改检测之后似乎仍然有一些异步任务正在进行。假设我们有以下

    const comp = fixture.componentInstance;
    const select = fixture.debugElement.query(By.css('select'));
    
    comp.selectedValue = 'a value';
    fixture.DetectChanges();
    expect(select.nativeElement.value).toEqual('1: a value');
    

    这似乎不起作用。似乎有一些异步正在进行,导致尚未设置该值。所以我们需要通过调用fixture.whenStable来等待异步任务

    comp.selectedValue = 'a value';
    fixture.DetectChanges();
    fixture.whenStable().then(() => {
      expect(select.nativeElement.value).toEqual('1: a value');
    });
    

    以上方法可行。但是现在我们需要触发 change 事件,因为这不会自动发生。

    fixture.whenStable().then(() => {
      expect(select.nativeElement.value).toEqual('1: a value');
    
      dispatchEvent(select.nativeElement, 'change');
      fixture.detectChanges();
      fixture.whenStable().then(() => {
        // component expectations here
      });
    });
    

    现在我们有另一个来自事件的异步任务。所以我们需要再次稳定它

    以下是我测试过的完整测试。这是source code integration tests 中示例的重构。他们使用了fakeAsynctick,这类似于使用asyncwhenStable。但是对于fakeAsync,你不能使用templateUrl,所以我认为最好将它重构为使用async

    源代码测试也是一种双单向测试,首先测试模型以查看,然后查看模型。虽然看起来你的测试试图做一种双向测试,从模型到模型。因此,我对其进行了一些重构,以更好地适应您的示例。

    import { Component } from '@angular/core';
    import { TestBed, getTestBed, async } from '@angular/core/testing';
    import { FormsModule } from '@angular/forms';
    import { By } from '@angular/platform-browser';
    import { dispatchEvent } from '@angular/platform-browser/testing/browser_util';
    
    @Component({
      selector: 'ng-model-select-form',
      template: `
        <select [(ngModel)]="selectedCity" (ngModelChange)="onSelected($event)">
          <option *ngFor="let c of cities" [ngValue]="c"> {{c.name}} </option>
        </select>
      `
    })
    class NgModelSelectForm {
      selectedCity: {[k: string]: string} = {};
      cities: any[] = [];
    
      onSelected(value) {
      }
    }
    
    describe('component: NgModelSelectForm', () => {
      beforeEach(() => {
        TestBed.configureTestingModule({
          imports: [ FormsModule ],
          declarations: [ NgModelSelectForm ]
        });
      });
    
      it('should go from model to change event', async(() => {
        const fixture = TestBed.createComponent(NgModelSelectForm);
        const comp = fixture.componentInstance;
        spyOn(comp, 'onSelected');
        comp.cities = [{'name': 'SF'}, {'name': 'NYC'}, {'name': 'Buffalo'}];
        comp.selectedCity = comp.cities[1];
        fixture.detectChanges();
        const select = fixture.debugElement.query(By.css('select'));
    
        fixture.whenStable().then(() => {
          dispatchEvent(select.nativeElement, 'change');
          fixture.detectChanges();
          fixture.whenStable().then(() => {
            expect(comp.onSelected).toHaveBeenCalledWith({name : 'NYC'});
            console.log('after expect NYC');
          });
        });
      }));
    });
    

    【讨论】:

    【解决方案2】:

    我发现 peeskillet 的答案非常有用,但遗憾的是它有点过时了,因为调度事件的方式已经改变。我还发现对 whenStable() 的调用是不必要的。所以这是一个使用 peeskillet 设置的更新测试:

        it('should go from model to change event', async(() => {
            const fixture = TestBed.createComponent(NgModelSelectForm);
            const comp = fixture.componentInstance;
            spyOn(comp, 'onSelected');
            comp.cities = [{'name': 'SF'}, {'name': 'NYC'}, {'name': 'Buffalo'}];
            comp.selectedCity = comp.cities[1];
            fixture.detectChanges();
            const select = fixture.debugElement.query(By.css('select'));
    
            fixture.whenStable().then(() => {
                select.nativeElement.dispatchEvent(new Event('change'));
                fixture.detectChanges();
                expect(comp.onSelected).toHaveBeenCalledWith({name : 'NYC'});
                console.log('after expect NYC');
            });
        }));
    

    【讨论】:

    • 感谢 Iain:在设置 select.nativeElement.selectedIndex 之后点亮了我的单元测试:“select.nativeElement.dispatchEvent(new Event('change'));” :-)
    【解决方案3】:

    看这个例子,来自角度源 (template_integration_spec.ts)

    @Component({
      selector: 'ng-model-select-form',
      template: `
        <select [(ngModel)]="selectedCity">
          <option *ngFor="let c of cities" [ngValue]="c"> {{c.name}} </option>
        </select>
      `
    })
    class NgModelSelectForm {
      selectedCity: {[k: string]: string} = {};
      cities: any[] = [];
    }
    
    
    
      it('with option values that are objects', fakeAsync(() => {
           const fixture = TestBed.createComponent(NgModelSelectForm);
           const comp = fixture.componentInstance;
           comp.cities = [{'name': 'SF'}, {'name': 'NYC'}, {'name': 'Buffalo'}];
           comp.selectedCity = comp.cities[1];
           fixture.detectChanges();
           tick();
    
           const select = fixture.debugElement.query(By.css('select'));
           const nycOption = fixture.debugElement.queryAll(By.css('option'))[1];
    
           // model -> view
           expect(select.nativeElement.value).toEqual('1: Object');
           expect(nycOption.nativeElement.selected).toBe(true);
    
           select.nativeElement.value = '2: Object';
           dispatchEvent(select.nativeElement, 'change');
           fixture.detectChanges();
           tick();
    
           // view -> model
           expect(comp.selectedCity['name']).toEqual('Buffalo');
         }));
    

    【讨论】:

    【解决方案4】:

    希望这会对某人有所帮助。与 OP 提出的问题相同,但代码略有不同。

    适用于 Angular 7。

    HTML:

    <select id="dashboard-filter" class="form-control" name="dashboard-filter" [ngModel]="dashboardFilterValue" (ngModelChange)="onFilterChange($event)"
                  [disabled]="disabled">
      <option *ngFor="let filter of dashboardFilters" [ngValue]="filter.value">{{ filter.name }}</option>
    </select>
    

    单元测试:

    it('onFilterChange', () => {
    
      // ensure dropdown is enabled
      expect(component.disabled).toBe(false)
    
      // spies
      spyOn(component, 'onFilterChange').and.callThrough()
      spyOn(component.filterChange, 'emit')
    
      // initially the 3rd item in the dropdown is selected
      const INITIAL_FILTER_INDEX = 2
      // we want to select the 5th item in the dropdown
      const FILTER_INDEX = 4
      // the expected filter value is the value of the 5th dashboard filter (as used to populate the dropdown)
      const EXPECTED_FILTER_VALUE = getDashboardFiltersData.dashboardFilters[FILTER_INDEX].value
    
      // handle on the dropdown
      const filterDropdown = fixture.debugElement.query(By.css('select')).nativeElement
    
      // let bindings complete
      fixture.whenStable().then(() => {
    
        // ensure filterDropdown.value is stable
        expect(filterDropdown.value).toContain(getDashboardFiltersData.dashboardFilters[INITIAL_FILTER_INDEX].value)
    
        // update filterDropdown.value and dispatch change event
        filterDropdown.value = filterDropdown.options[FILTER_INDEX].value
        filterDropdown.dispatchEvent(new Event('change'))
    
        // check component data
        expect(component.dashboardFilterValue).toBe(EXPECTED_FILTER_VALUE)
        expect(component.dashboardFilterChangeInProgress).toBe(false)
    
        // check spies
        expect(component.onFilterChange).toHaveBeenCalledWith(EXPECTED_FILTER_VALUE)
        expect(setDashboardFilterSpy).toHaveBeenCalledWith(EXPECTED_FILTER_VALUE)
        expect(component.filterChange.emit).toHaveBeenCalledWith(true)
      })
    })
    

    【讨论】:

    • 不得不手动操作.dispatchEvent(new Event('change')) 真是太棒了。不过,对这个答案没有批评。很高兴我找到了它。
    猜你喜欢
    • 2017-11-06
    • 2017-05-13
    • 1970-01-01
    • 2018-03-04
    • 2021-11-27
    • 2016-11-26
    • 2018-01-08
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多