測試中的表單元素操作

在 Angular 應用的測試中,處理表單元素是一個常見的需求。我們需要模擬使用者的輸入操作、測試表單驗證邏輯、檢查表單控制狀態等。本篇筆記將詳細介紹如何在測試中操作 Angular 的表單元素。

基本表單輸入模擬

在測試中,我們經常需要模擬使用者在表單中的輸入。以下是在 Angular 測試中操作基本表單元素的方法。

為 input 元素賦值

it('應該更新輸入框的值', () => {
  // 獲取輸入元素
  const inputElement = fixture.debugElement.query(By.css('input')).nativeElement;
  
  // 設置值
  inputElement.value = '測試輸入';
  
  // 觸發 input 事件,模擬使用者輸入
  inputElement.dispatchEvent(new Event('input'));
  
  // 若使用 ngModel 或 Reactive Forms 還需要觸發變更檢測
  fixture.detectChanges();
  
  // 斷言:檢查元件屬性是否更新
  expect(component.inputValue).toBe('測試輸入');
});

使用 setValue 直接修改表單控制值

如果使用 Reactive Forms,可以直接設置 FormControl 的值:

it('應該通過 FormControl 設置值', () => {
  // 設置 FormControl 的值
  component.form.get('username').setValue('test_user');
  fixture.detectChanges();
  
  // 檢查 DOM 是否反映了變更
  const inputElement = fixture.debugElement.query(By.css('input[formControlName="username"]')).nativeElement;
  expect(inputElement.value).toBe('test_user');
});

處理不同類型的表單控制項

不同類型的表單控制項需要不同的處理方式。

文本輸入框 (text, email, password 等)

it('應該更新電子郵件輸入', () => {
  const emailInput = fixture.debugElement.query(By.css('input[type="email"]')).nativeElement;
  emailInput.value = 'test@example.com';
  emailInput.dispatchEvent(new Event('input'));
  fixture.detectChanges();
  
  expect(component.email).toBe('test@example.com');
});

複選框 (checkbox)

it('應該切換複選框狀態', () => {
  const checkbox = fixture.debugElement.query(By.css('input[type="checkbox"]')).nativeElement;
  
  // 初始狀態
  expect(checkbox.checked).toBe(false);
  
  // 點擊複選框
  checkbox.click();
  // 或者直接設置 checked 屬性
  // checkbox.checked = true;
  
  // 觸發 change 事件
  checkbox.dispatchEvent(new Event('change'));
  fixture.detectChanges();
  
  // 檢查元件狀態
  expect(component.isAgreed).toBe(true);
  // 檢查 DOM 狀態
  expect(checkbox.checked).toBe(true);
});

單選按鈕 (radio)

it('應該選擇正確的單選按鈕', () => {
  // 獲取所有單選按鈕
  const radioButtons = fixture.debugElement.queryAll(By.css('input[type="radio"]'));
  
  // 選擇第二個選項
  const secondRadio = radioButtons[1].nativeElement;
  secondRadio.checked = true;
  secondRadio.dispatchEvent(new Event('change'));
  fixture.detectChanges();
  
  // 檢查是否選中
  expect(secondRadio.checked).toBe(true);
  expect(component.selectedOption).toBe(secondRadio.value);
});

下拉選單 (select)

it('應該選擇下拉選單選項', () => {
  const select = fixture.debugElement.query(By.css('select')).nativeElement;
  
  // 選擇第二個選項
  select.value = select.options[1].value;
  select.dispatchEvent(new Event('change'));
  fixture.detectChanges();
  
  expect(component.selectedValue).toBe(select.value);
});

文本區域 (textarea)

it('應該更新文本區域', () => {
  const textarea = fixture.debugElement.query(By.css('textarea')).nativeElement;
  
  textarea.value = '這是一段測試文本';
  textarea.dispatchEvent(new Event('input'));
  fixture.detectChanges();
  
  expect(component.comments).toBe('這是一段測試文本');
});

測試 Angular 表單驗證

測試表單驗證是 Angular 測試的重要部分。

測試必填驗證

it('當必填字段為空時應顯示錯誤', () => {
  const form = component.form;
  const nameControl = form.get('name');
  
  // 設置為空並標記為 touched
  nameControl.setValue('');
  nameControl.markAsTouched();
  fixture.detectChanges();
  
  // 檢查驗證狀態
  expect(nameControl.valid).toBe(false);
  expect(nameControl.errors.required).toBeTruthy();
  
  // 檢查 DOM 中是否顯示錯誤訊息
  const errorElement = fixture.debugElement.query(By.css('.name-error'));
  expect(errorElement).toBeTruthy();
  expect(errorElement.nativeElement.textContent).toContain('此欄位必填');
});

測試自定義驗證器

it('應驗證密碼複雜度', () => {
  const form = component.form;
  const passwordControl = form.get('password');
  
  // 設置簡單密碼
  passwordControl.setValue('12345');
  fixture.detectChanges();
  
  // 檢查驗證狀態
  expect(passwordControl.valid).toBe(false);
  expect(passwordControl.errors.minlength).toBeTruthy();
  
  // 設置符合要求的密碼
  passwordControl.setValue('Complex123!');
  fixture.detectChanges();
  
  expect(passwordControl.valid).toBe(true);
  expect(passwordControl.errors).toBeNull();
});

表單事件觸發

除了基本的值設置外,有時我們還需要測試表單事件。

提交表單

it('應處理表單提交', () => {
  // 設置表單值
  component.form.get('username').setValue('testuser');
  component.form.get('password').setValue('password123');
  
  // 監聽 submit 方法
  spyOn(component, 'onSubmit').and.callThrough();
  
  // 觸發表單提交
  const form = fixture.debugElement.query(By.css('form'));
  form.triggerEventHandler('submit', {});
  
  // 檢查 submit 方法是否被調用
  expect(component.onSubmit).toHaveBeenCalled();
});

測試按鍵事件

it('按下 Enter 鍵應提交表單', () => {
  // 監聽提交方法
  spyOn(component, 'onSubmit');
  
  // 獲取輸入元素
  const inputElement = fixture.debugElement.query(By.css('input')).nativeElement;
  
  // 模擬按鍵事件
  const enterEvent = new KeyboardEvent('keyup', {
    key: 'Enter',
    bubbles: true
  });
  
  inputElement.dispatchEvent(enterEvent);
  
  // 檢查是否調用了提交方法
  expect(component.onSubmit).toHaveBeenCalled();
});

測試 Angular Material 表單元件

Angular Material 元件通常需要特殊的測試方法。

Material Input

it('應該設置 Material 輸入框的值', () => {
  // 獲取 MatInput 元素
  const matInput = fixture.debugElement.query(By.css('input.mat-input-element')).nativeElement;
  
  // 設置值
  matInput.value = '測試值';
  matInput.dispatchEvent(new Event('input'));
  fixture.detectChanges();
  
  expect(component.inputValue).toBe('測試值');
});

Material Select

it('應該選擇 Material Select 選項', async () => {
  // 打開選擇器
  const matSelect = fixture.debugElement.query(By.css('mat-select')).nativeElement;
  matSelect.click();
  fixture.detectChanges();
  
  // 等待選項面板渲染
  await fixture.whenStable();
  
  // 選擇選項
  const options = document.querySelectorAll('mat-option');
  options[1].click();  // 選擇第二個選項
  fixture.detectChanges();
  
  // 檢查選擇結果
  expect(component.selectedValue).toBe('option2');
});

Material Checkbox

it('應該切換 Material Checkbox', () => {
  const matCheckbox = fixture.debugElement.query(By.css('mat-checkbox')).nativeElement;
  matCheckbox.click();
  fixture.detectChanges();
  
  expect(component.isChecked).toBe(true);
});

Material Radio Button

it('應該選擇 Material Radio 按鈕', () => {
  const radioButtons = fixture.debugElement.queryAll(By.css('mat-radio-button'));
  radioButtons[1].nativeElement.click();
  fixture.detectChanges();
  
  expect(component.selectedOption).toBe('option2');
});

Angular 17+ 的 Input Signal 值設置

從 Angular 17 開始,引入了新的 input() 函數語法來定義輸入屬性。在測試中設置這些 input 值需要特殊的方法。

使用 setInput 方法

當測試 Angular 17+ 具有 input() 函數定義的元件時,可以使用 setInput 方法:

it('應該設置 title 輸入', () => {
  // 使用 setInput 方法設置 input 值
  fixture.componentRef.setInput('title', '測試標題');
  fixture.detectChanges();

  // 注意:input() 返回的是一個函數,需要調用它獲取值
  expect(component.title()).toBe('測試標題');
  
  // 檢查 DOM 是否反映了變更
  const titleElement = fixture.nativeElement.querySelector('.title');
  expect(titleElement.textContent).toContain('測試標題');
});

在元件創建時提供輸入值

也可以在創建元件時直接提供初始的輸入值:

beforeEach(() => {
  TestBed.configureTestingModule({
    imports: [MyComponent]
  });
  
  // 創建元件時提供輸入值
  fixture = TestBed.createComponent(MyComponent, {
    componentInputs: {
      title: '預設標題',
      options: [{ id: 1, name: '選項1' }],
      disabled: false
    }
  });
  
  component = fixture.componentInstance;
  fixture.detectChanges();
});

it('應該有正確的初始輸入值', () => {
  expect(component.title()).toBe('預設標題');
  expect(component.options().length).toBe(1);
  expect(component.disabled()).toBe(false);
});

測試預設值

測試當沒有提供輸入值時組件使用的預設值:

it('title 沒有提供時應使用預設值', () => {
  // 創建元件時不提供 title 輸入
  const defaultFixture = TestBed.createComponent(MyComponent);
  const defaultComponent = defaultFixture.componentInstance;
  defaultFixture.detectChanges();
  
  // 檢查是否使用了預設值
  expect(defaultComponent.title()).toBe(''); // 假設預設值是空字串
});

動態更改輸入值

測試更新輸入值時組件的響應:

it('更新輸入值後應反映變更', () => {
  // 初始設置
  fixture.componentRef.setInput('count', 5);
  fixture.detectChanges();
  expect(component.count()).toBe(5);
  
  // 更新值
  fixture.componentRef.setInput('count', 10);
  fixture.detectChanges();
  
  // 檢查更新後的值
  expect(component.count()).toBe(10);
  
  // 檢查 DOM 是否反映了變更
  const countElement = fixture.nativeElement.querySelector('.count-display');
  expect(countElement.textContent).toContain('10');
});

Last updated