Angular 測試實用案例大全

這篇筆記收集了 Angular 測試中的各種實用案例,涵蓋 DOM 元素檢查、表單操作、非同步處理等各個方面。每個案例都提供了實際可用的測試代碼,可作為日常開發中的參考和複製使用。

DOM 元素檢查案例

檢查元素是否存在

it('應顯示標題元素', () => {
  fixture.detectChanges();
  
  const titleElement = fixture.debugElement.query(By.css('.title'));
  expect(titleElement).toBeTruthy();
});

檢查元素不存在

it('錯誤訊息應該不顯示', () => {
  fixture.detectChanges();
  
  const errorElement = fixture.debugElement.query(By.css('.error-message'));
  expect(errorElement).toBeNull();
});

檢查文字內容

it('應顯示正確的標題文字', () => {
  component.title = '測試標題';
  fixture.detectChanges();
  
  const titleElement = fixture.debugElement.query(By.css('h1')).nativeElement;
  expect(titleElement.textContent).toContain('測試標題');
});

檢查特定 CSS 類別

it('應該有正確的 CSS 類別', () => {
  component.isActive = true;
  fixture.detectChanges();
  
  const element = fixture.debugElement.query(By.css('.container')).nativeElement;
  expect(element.classList).toContain('active');
});

檢查元素的屬性

it('圖片應該有正確的來源', () => {
  fixture.detectChanges();
  
  const imgElement = fixture.debugElement.query(By.css('img')).nativeElement;
  expect(imgElement.src).toContain('test-image.jpg');
  expect(imgElement.alt).toBe('測試圖片');
});

檢查 CSS 偽元素內容

it('必填欄位應顯示星號', () => {
  fixture.detectChanges();
  
  const requiredLabel = fixture.debugElement.query(By.css('.required-field')).nativeElement;
  const afterContent = window.getComputedStyle(requiredLabel, ':after').getPropertyValue('content');
  expect(afterContent).toBe('"*"');
});

測試條件渲染

it('當設置顯示標誌時應顯示特定元素', () => {
  // 初始狀態
  component.showDetails = false;
  fixture.detectChanges();
  let detailsElement = fixture.debugElement.query(By.css('.details'));
  expect(detailsElement).toBeNull();
  
  // 改變狀態
  component.showDetails = true;
  fixture.detectChanges();
  
  // 檢查元素現在是否存在
  detailsElement = fixture.debugElement.query(By.css('.details'));
  expect(detailsElement).toBeTruthy();
});

表單元素操作案例

設置輸入框的值

it('應正確更新輸入框的值', () => {
  const inputElement = fixture.debugElement.query(By.css('input')).nativeElement;
  
  // 設置值
  inputElement.value = '測試輸入';
  inputElement.dispatchEvent(new Event('input'));
  fixture.detectChanges();
  
  // 檢查元件屬性
  expect(component.inputValue).toBe('測試輸入');
});

操作 Reactive Forms

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

操作複選框

it('應正確切換複選框狀態', () => {
  const checkbox = fixture.debugElement.query(By.css('input[type="checkbox"]')).nativeElement;
  
  // 點擊複選框
  checkbox.click();
  fixture.detectChanges();
  
  // 檢查元件狀態
  expect(component.isChecked).toBe(true);
  // 檢查 DOM 狀態
  expect(checkbox.checked).toBe(true);
});

操作單選按鈕

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(component.selectedOption).toBe(secondRadio.value);
});

操作下拉選單

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);
});

測試表單驗證

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

測試表單提交

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

Angular 17+ 的 Input Signal 設置

it('應設置元件的 Input Signal', () => {
  // 使用 setInput 方法設置 input 值
  fixture.componentRef.setInput('title', '測試標題');
  fixture.detectChanges();
  
  // 檢查 input signal 值
  expect(component.title()).toBe('測試標題');
  
  // 檢查 DOM 是否更新
  const titleElement = fixture.nativeElement.querySelector('.title');
  expect(titleElement.textContent).toContain('測試標題');
});

測試 Angular 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');
});

測試錯誤訊息顯示

it('應根據需要顯示錯誤訊息', () => {
  // 初始檢查
  let errorElement = fixture.debugElement.query(By.css('.error-message'));
  expect(errorElement).toBeNull();
  
  // 設置錯誤訊息
  fixture.componentRef.setInput('errorMessage', '請選擇至少一個選項');
  fixture.detectChanges();
  
  // 檢查錯誤訊息是否顯示
  errorElement = fixture.debugElement.query(By.css('.error-message'));
  expect(errorElement).toBeTruthy();
  expect(errorElement.nativeElement.textContent).toContain('請選擇至少一個選項');
});

非同步操作案例

等待元素載入完成

it('應在元素載入後顯示數據', async () => {
  // 觸發數據載入
  component.loadData();
  fixture.detectChanges();
  
  // 等待載入完成
  await fixture.whenStable();
  
  // 檢查數據是否顯示
  const dataElements = fixture.debugElement.queryAll(By.css('.data-item'));
  expect(dataElements.length).toBeGreaterThan(0);
});

測試 API 調用

it('應呼叫API並處理回應', async () => {
  // 模擬服務響應
  const testData = [{ id: 1, name: '測試項目' }];
  spyOn(dataService, 'getData').and.returnValue(of(testData));
  
  // 觸發數據載入
  component.loadData();
  fixture.detectChanges();
  
  // 檢查服務是否被調用
  expect(dataService.getData).toHaveBeenCalled();
  
  // 檢查元件數據是否更新
  expect(component.items).toEqual(testData);
  
  // 檢查 DOM 是否更新
  const itemElements = fixture.debugElement.queryAll(By.css('.item'));
  expect(itemElements.length).toBe(1);
  expect(itemElements[0].nativeElement.textContent).toContain('測試項目');
});

測試動態下拉選單

it('點擊 mat-select 後應顯示選項', async () => {
  // 初始檢查
  let optionsPanel = document.querySelector('.mat-select-panel');
  expect(optionsPanel).toBeNull();
  
  // 點擊 select 以展開選項
  const select = fixture.debugElement.query(By.css('mat-select')).nativeElement;
  select.click();
  fixture.detectChanges();
  
  // 等待面板渲染
  await fixture.whenStable();
  
  // 檢查選項面板是否存在
  optionsPanel = document.querySelector('.mat-select-panel');
  expect(optionsPanel).toBeTruthy();
  
  // 檢查選項數量
  const options = document.querySelectorAll('.mat-option');
  expect(options.length).toBe(3); // 假設有 3 個選項
});

測試動態加載的搜尋框

it('searchable 設置為 true 應顯示搜尋框', async () => {
  // 設置輸入參數
  fixture.componentRef.setInput('searchable', true);
  fixture.detectChanges();

  // 點擊 select 以顯示下拉選單
  const selectElement = fixture.elementRef.nativeElement.querySelector('mat-select');
  selectElement.click();
  fixture.detectChanges();

  // 等待面板渲染
  await fixture.whenStable();

  // 檢查搜尋框是否存在
  const searchElement = document.querySelector('.mat-select-search-input');
  expect(component.searchable()).toBe(true);
  expect(searchElement).toBeTruthy();
});

使用 fakeAsync 和 tick

it('應在延遲後顯示訊息', fakeAsync(() => {
  component.showMessageAfterDelay();
  
  // 推進時間
  tick(1000);
  fixture.detectChanges();
  
  // 檢查訊息是否顯示
  const message = fixture.nativeElement.querySelector('.message');
  expect(message.textContent).toContain('延遲訊息');
}));

事件處理案例

測試點擊事件

it('點擊按鈕應觸發事件處理', () => {
  // 監聽事件處理方法
  spyOn(component, 'handleClick');
  
  // 點擊按鈕
  const button = fixture.debugElement.query(By.css('button'));
  button.nativeElement.click();
  
  // 檢查事件處理方法是否被調用
  expect(component.handleClick).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();
});

測試自訂事件

it('自訂事件應正確觸發', () => {
  // 創建事件監聽 spy
  const eventSpy = spyOn(component.itemSelected, 'emit');
  
  // 觸發事件
  component.selectItem(123);
  
  // 檢查事件是否被觸發並傳遞正確的值
  expect(eventSpy).toHaveBeenCalledWith(123);
});

測試拖放事件

it('應處理拖放事件', () => {
  // 監聽拖放處理方法
  spyOn(component, 'onDrop').and.callThrough();
  
  // 創建拖放事件
  const dropEvent = {
    preventDefault: jasmine.createSpy('preventDefault'),
    dataTransfer: {
      getData: () => '123'
    }
  };
  
  // 獲取拖放區域
  const dropZone = fixture.debugElement.query(By.css('.drop-zone'));
  
  // 觸發拖放事件
  dropZone.triggerEventHandler('drop', dropEvent);
  
  // 檢查處理方法是否被調用
  expect(component.onDrop).toHaveBeenCalled();
  expect(dropEvent.preventDefault).toHaveBeenCalled();
  
  // 檢查拖放項目是否正確處理
  expect(component.droppedItemId).toBe('123');
});

複雜用例案例

完整測試登入表單

describe('登入表單', () => {
  it('應驗證表單並提交', () => {
    // 獲取表單控制項
    const emailInput = fixture.debugElement.query(By.css('input[formControlName="email"]')).nativeElement;
    const passwordInput = fixture.debugElement.query(By.css('input[formControlName="password"]')).nativeElement;
    
    // 輸入無效的電子郵件
    emailInput.value = 'invalid-email';
    emailInput.dispatchEvent(new Event('input'));
    
    // 輸入有效的密碼
    passwordInput.value = 'valid-password';
    passwordInput.dispatchEvent(new Event('input'));
    
    fixture.detectChanges();
    
    // 檢查表單驗證
    expect(component.loginForm.get('email').valid).toBe(false);
    expect(component.loginForm.valid).toBe(false);
    
    // 修正電子郵件
    emailInput.value = 'valid@example.com';
    emailInput.dispatchEvent(new Event('input'));
    fixture.detectChanges();
    
    // 現在表單應該有效
    expect(component.loginForm.valid).toBe(true);
    
    // 監聽登入方法
    spyOn(component, 'login');
    
    // 提交表單
    const form = fixture.debugElement.query(By.css('form'));
    form.triggerEventHandler('submit', {});
    
    // 檢查是否調用了登入方法
    expect(component.login).toHaveBeenCalled();
  });
});

測試動態表單控制項

describe('動態表單控制項', () => {
  it('應動態添加表單控制項', () => {
    // 初始檢查
    expect(component.dynamicForm.get('items').length).toBe(1);
    
    // 點擊「添加項目」按鈕
    const addButton = fixture.debugElement.query(By.css('.add-item-btn'));
    addButton.nativeElement.click();
    fixture.detectChanges();
    
    // 檢查是否添加了新的控制項
    expect(component.dynamicForm.get('items').length).toBe(2);
    
    // 設置第二個控制項的值
    const formArray = component.dynamicForm.get('items') as FormArray;
    formArray.at(1).get('name').setValue('第二項');
    fixture.detectChanges();
    
    // 檢查 DOM 是否反映了變更
    const inputElements = fixture.debugElement.queryAll(By.css('input[formControlName="name"]'));
    expect(inputElements.length).toBe(2);
    expect(inputElements[1].nativeElement.value).toBe('第二項');
  });
});

測試 Material Search 元件

describe('Material 搜尋元件', () => {
  it('應在輸入後過濾選項', async () => {
    // 打開選擇器
    const matSelect = fixture.debugElement.query(By.css('mat-select')).nativeElement;
    matSelect.click();
    fixture.detectChanges();
    await fixture.whenStable();
    
    // 查找搜尋輸入框
    const searchInput = document.querySelector('input.mat-select-search-input');
    expect(searchInput).toBeTruthy();
    
    // 輸入搜尋文字
    searchInput.value = '選項1';
    searchInput.dispatchEvent(new Event('input'));
    fixture.detectChanges();
    await fixture.whenStable();
    
    // 檢查過濾後的選項
    const visibleOptions = document.querySelectorAll('mat-option:not(.mat-select-search-no-entries)');
    expect(visibleOptions.length).toBe(1);
    expect(visibleOptions[0].textContent).toContain('選項1');
  });
});

測試表單錯誤訊息和樣式

describe('表單錯誤訊息', () => {
  it('應顯示錯誤訊息並應用適當樣式', () => {
    // 初始檢查
    let errorElement = fixture.debugElement.query(By.css('.error-message'));
    expect(errorElement).toBeNull();
    
    // 設置錯誤訊息
    fixture.componentRef.setInput('errorMessage', '請選擇至少一個選項');
    fixture.detectChanges();
    
    // 檢查錯誤訊息和樣式
    errorElement = fixture.debugElement.query(By.css('.error-message'));
    expect(errorElement).toBeTruthy();
    expect(errorElement.nativeElement.textContent).toContain('請選擇至少一個選項');
    
    const selectElement = fixture.debugElement.query(By.css('.select-container')).nativeElement;
    expect(selectElement.classList).toContain('has-error');
  });
});

測試自動完成功能

describe('自動完成輸入框', () => {
  it('應顯示和篩選自動完成選項', async () => {
    // 獲取自動完成輸入框
    const autocompleteInput = fixture.debugElement.query(
      By.css('input[formControlName="city"]')
    ).nativeElement;
    
    // 聚焦並開始輸入
    autocompleteInput.focus();
    autocompleteInput.value = '台';
    autocompleteInput.dispatchEvent(new Event('input'));
    fixture.detectChanges();
    
    // 等待自動完成選項出現
    await fixture.whenStable();
    
    // 檢查自動完成面板是否顯示
    const matOptions = document.querySelectorAll('mat-option');
    expect(matOptions.length).toBeGreaterThan(0);
    
    // 驗證選項已經過濾
    expect(Array.from(matOptions).some(option => 
      option.textContent.includes('台北')
    )).toBeTrue();
    
    // 選擇一個選項
    matOptions[0].click();
    fixture.detectChanges();
    
    // 檢查選擇的值是否正確設置
    expect(component.form.get('city').value).toBe(matOptions[0].getAttribute('ng-reflect-value'));
  });
});

結語

這些案例涵蓋了 Angular 測試中常見的各種情境,可以根據需要複製和調整來測試你的應用程式。

Last updated