angularangular-testangular-unit-test

Angular test .spec.ts How to select multiple options


I have a multiple Select list that I want to test, basically when you select n items you then click a button that will copy the items from one list an another. I am having problems trying to select more than one value. This is a multi select so I want to be able to select n items.

I can select one item with this line, but how do I select multiple items in the test?

select.value = select.options[0].value;

here is the test selector.component.spec.ts

 it('When assign button is clicked expect to be displayed in assigned list', () => {

    let unassignedItems = [
      { id:1, name: 'Rob' },
      { id:2, name: 'Dave' },
      { id:3, name: 'Steve' }      
    ] as Person[];
    component.unassignedItems = unassignedItems;
    fixture.detectChanges();

    let select: HTMLSelectElement = unassignedItemsSelect.nativeElement;    
    select.value = select.options[0].value;
    select.dispatchEvent(new Event('change'));
    fixture.detectChanges();

    assignedItemButton.triggerEventHandler('click', null);

    expect(component.assignedItems[0].id).toBe(1)
    
  })

The component looks like this selector.component.ts

import { Component, Input, OnInit} from '@angular/core';
import { Person } from '../Person';

@Component({
  selector: 'app-selector',
  templateUrl: './selector.component.html',
  styleUrls: ['./selector.component.scss']
})
export class SelectorComponent implements OnInit {

  constructor() { }

  unassignedSelectedItems: Person[] = [];
  assignedSelectedItems: Person[] = [];

  @Input() unassignedItems : Person[] = [];
  @Input() assignedItems: Person[] = [];

  ngOnInit(): void {
  }

  assignSelections(): void {
    this.assignedItems.push(...this.unassignedSelectedItems);
    // remove the item from the unassigned list
    this.unassignedSelectedItems.forEach(selectedItem => {
      let index = this.unassignedItems.findIndex(x => x.id == selectedItem.id);
      this.unassignedItems.splice(index, 1); // removes the item at that index  
    });
    // clear the selected item for the next ones
    this.unassignedSelectedItems = [];
  }

  unassignSelections(): void {
    this.unassignedItems.push(...this.assignedSelectedItems);
    // remove the item from the assigned list
    this.assignedSelectedItems.forEach(selectedItem => {
      let index = this.assignedItems.findIndex(x => x.id == selectedItem.id);
      this.assignedItems.splice(index, 1); // removes the item at that index
    });
    // clear the selected item for the next ones
    this.assignedSelectedItems = [];
  }

}

The template looks like this selector.component.html

<div class="container">
    <div class="row">

        <div class="col-3">
            <select id="unassignedItems" class="form-select" multiple [(ngModel)]="unassignedSelectedItems">
                <option *ngFor="let item of unassignedItems" [ngValue]="item">{{item.name}}</option>
            </select>
        </div>

        <div class="col-1 align-middle" >
            <button id="assignedItemButton" [disabled]="unassignedItems.length==0" class="btn btn-primary m-1" type="button" (click)="assignSelections()">&gt;</button>
            <button id="unassignedItemButton" [disabled]="assignedItems.length==0" class="btn btn-primary m-1" type="button" (click)="unassignSelections()">&lt;</button>
        </div>

        <div class="col-3">
            <select id="assignedItems" class="form-select" multiple [(ngModel)]="assignedSelectedItems">
                <option *ngFor="let item of assignedItems" [ngValue]="item">{{item.name}}</option>
            </select>
        </div>

    </div>
</div>

Solution

  • Tricky one, testing a native multi select. The problem is, that <select> doesn't reflect all selected values in it's value, only one of them. So setting it to multiple values won't have any effect. Given the awkward way to extract multiple selected values from a select, and the fact that testing native behavior and ngModel probably isn't (and shouldn't be) your goal, I'd settle for setting the unassignedItems and assignedItems manually.

    But if you really want to simulate this in your test the "native" way, set the options you want to select like

    options[0].selected = true;
    

    and then emit the change event. This will update <select>'s selectedOptions property, which is used internally by angular's SelectMultipleControlValueAccessor. Check its code if you find it interesting. That code will execute upon change event, grab the selectedOptions' values, and propagate them into the variable associated with [(ngModel)].