使用 Protractor 控制 Checkbox

HTML 的 <input type="checkbox"> 是常見的控制項,該如何使用 Protractor 對 checkbox 寫驗收測試 ?

Version


Protractor 5.1.2

Requirement


checkbox000

畫面上共有 3 個 checkbox,各為 AWSAzureGCP

下方會顯示目前所選擇的 checkbox 數量。

下方一開始數量為 0

checkbox001

當選擇 AWS 時,下方顯示為 1

checkbox002

當選擇 AWSAzure 時,下方顯示為 2

checkbox003

當選擇 AWSAzureGCP 時,下方顯示為 3

Acceptance Test (紅燈)


測試案例 :

  1. 應該有 3<label>
  2. 應該有 3<input type="checkbox">
  3. 一開始下方應該出現 0
  4. 當選擇 AWS,下方應該出現 1
  5. 當選擇 AWSAzure ,下方應該出現 2
  6. 當選擇 AWSAzureGCP ,下方應該出現 3

e2e/app.e2e-spec.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import { AppPage } from './app.po';

describe('protractor512-checkbox App', () => {
let page: AppPage;

beforeEach(() => {
page = new AppPage();
page.navigateTo();
});

it(`should have '3' labels`, () => {
expect(page.getLabelCount()).toBe(3);
});

it(`should have '3' checkboxes`, () => {
expect(page.getCheckboxCount()).toBe(3);
});

it(`should have '0' checkbox selected by default`, () => {
expect(page.getSelectedCloudCount()).toBe('0');
});

it(`should show '1' when 'AWS' selected`, () => {
page.selectCloudByIndex(0);
expect(page.getSelectedCloudCount()).toBe('1');
});

it(`should have '2' when 'AWS' and 'Azure' selected`, () => {
page
.selectCloudByText('AWS')
.selectCloudByText('Azure');
expect(page.getSelectedCloudCount()).toBe('2');
});

it(`should '3' when all checkbox selected`, () => {
page
.selectCloudByText('AWS')
.selectCloudByText('Azure')
.selectCloudByText('GCP');
expect(page.getSelectedCloudCount()).toBe('3');
});
});

11 行

1
2
3
it(`should have '3' labels`, () => {
expect(page.getLabelCount()).toBe(3);
});

測試案例 : 應該有 3<label>

page.getLabelCount() 將由 page object 傳回 <label> 的個數。

15 行

1
2
3
it(`should have '3' checkboxes`, () => {
expect(page.getCheckboxCount()).toBe(3);
});

測試案例 : 應該有 3<input type="checkbox">

page.getCheckboxCount() 將由 page object 傳回 checkbox 的個數。

19 行

1
2
3
it(`should have '0' checkbox selected by default`, () => {
expect(page.getSelectedCloudCount()).toBe('0');
});

測試案例 : 一開始下方應該出現 0

page.getSelectedCloudCount() 將由 page object 傳回下方所顯示的選擇個數。

23 行

1
2
3
4
it(`should show '1' when 'AWS' selected`, () => {
page.selectCloudByIndex(0);
expect(page.getSelectedCloudCount()).toBe('1');
});

測試案例 : 當選擇 AWS,下方應該出現 1

要模擬 user 選擇 checkbox,有兩種方式,一種是使用 index 選擇,一種是使用文字選擇。

page.selectCloudByIndex() 將由 index 方式選擇 checkbox。

28 行

1
2
3
4
5
6
it(`should have '2' when 'AWS' and 'Azure' selected`, () => {
page
.selectCloudByText('AWS')
.selectCloudByText('Azure');
expect(page.getSelectedCloudCount()).toBe('2');
});

測試案例 : 當選擇 AWSAzure ,下方應該出現 2

page.selectCloudByText() 將使用文字方式選擇 checkbox。

e2e/app.po.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import { browser, by, element } from 'protractor';

export class AppPage {
navigateTo() {
return browser.get('/');
}

getLabelCount(): any {
return element.all(by.tagName('label')).count();
}

getCheckboxCount(): any {
return element.all(by.css('input[type="checkbox"]')).count();
}

selectCloudByIndex(index: number): AppPage {
element.all(by.css('label[name="cloud"]'))
.get(index)
.click();

return this;
}

selectCloudByText(text: string): AppPage {
element(by.cssContainingText('label', text)).click();

return this;
}

getSelectedCloudCount(): any {
return element(by.css('p')).getText();
}
}

*.e2e-spec.ts 負責描述測試案例,不包含 HTML 與 CSS 部分。

*.po.ts 則負責描述 HTML 與 CSS 部分。

Page object 主要目的在於讓測試與 HTML/CSS 解耦合,不要 designer 若變動了 HTML 或 CSS,則所有驗收測試都要修改,只要修改 page object 即可。

驗收測試應該只根據需求變動而修改,不應該因為 HTML/CSS 變動而修改。

第 8 行

1
2
3
getLabelCount(): any {
return element.all(by.tagName('label')).count();
}

回傳所有 <label> 的個數。

count() 的型別不是 number,而是 wdpromise.Promise<number>,因為型別比較複雜,所以迴船型別使用 any 代替。

12 行

1
2
3
getCheckboxCount(): any {
return element.all(by.css('input[type="checkbox"]')).count();
}

使用 CSS selector 方式找到所有 checkbox。all() 回傳為陣列,加上 count() 可獲得陣列的筆數。

16 行

1
2
3
4
5
6
7
selectCloudByIndex(index: number): AppPage {
element.all(by.css('label[name="cloud"]'))
.get(index)
.click();

return this;
}

使用 index 選擇 checkbox。

因為 checkbox 已經被包在 <label> 內,所以 click <label>,就相當於 click checkbox。

實務上 <label> 會使用 name,表示同一組 checkbox ,因此可用 by.css('label[name="cloud"]') 找到所有 <label> 的陣列,再透過 get(index) 選擇 <label> ,最後 click()

24 行

1
2
3
4
5
selectCloudByText(text: string): AppPage {
element(by.cssContainingText('label', text)).click();

return this;
}

使用文字選擇 checkbox。

因為 checkbox 已經被包在 <label> 內,所以 click <label>,就相當於 click checkbox。

因為文字是屬於 <label>,而不是屬於 checkbox,所以使用 by.cssContainingText('label', cloud) 直接找到符合條件的 <label>,然後 click()

checkbox004

因為我們還沒實作任何功能,得到預期的驗收測試 紅燈

Acceptance Test (綠燈)


測試案例 :

  1. 應該有 3<label>
  2. 應該有 3<input type="checkbox">
  3. 一開始下方應該出現 0
  4. 當選擇 AWS,下方應該出現 1
  5. 當選擇 AWSAzure ,下方應該出現 2
  6. 當選擇 AWSAzureGCP ,下方應該出現 3

src/app/app.component.html

1
2
3
4
5
<label *ngFor="let cloud of clouds" name="cloud" [for]="cloud.name|lowercase">
<input type="checkbox" [id]="cloud.name|lowercase" [name]="cloud" [checked]="cloud.checked" (change)="onChange(myCheckbox)" #myCheckbox>
{{ cloud.name }}
</label>
<p>{{ selectedCount }}</p>
1
<label *ngFor="let cloud of clouds" name="cloud" [for]="cloud.name|lowercase">{{ cloud.name }}</label>

<label><input type="checkbox"> 會依賴後端的資料顯示,故適合使用 *ngFor 產生。

for 會與 checkbox 的 id 先對應,使用 lowercase pipe 將 cloud.name 轉成小寫。

<label> 所顯示的值則使用 cloud.name 直接 interpolation binding。

1
<input type="checkbox" [id]="cloud.name|lowercase" [name]="cloud" [checked]="cloud.checked" (change)="onChange(myCheckbox)" #myCheckbox>

為了讓 checkbox 同一組,故 name 都使用 cloud

id 使用 lowercase pipe 將 cloud.name 轉成小寫。

checked 根據 cloud.checked 決定 radio 一開始是否選取。

change event 綁訂到 onChange(),並將 #myCheckbox 傳入 onChange()

src/app/app.component.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { Component } from '@angular/core';
import { Cloud } from './cloud';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
selectedCount = 0;
clouds: Cloud[] = [
{id: '0', name: 'AWS', checked: false},
{id: '1', name: 'Azure', checked: false},
{id: '2', name: 'GCP', checked: false}
];

onChange(element: HTMLInputElement) {
(element.checked) ? this.selectedCount++ : this.selectedCount--;
}
}

第 10 行

1
selectedCount = 0;

selectedCount field 預設為 0

11 行

1
2
3
4
5
clouds: Cloud[] = [
{id: '0', name: 'AWS', checked: false},
{id: '1', name: 'Azure', checked: false},
{id: '2', name: 'GCP', checked: false}
];

建立 cloud field 與設定初始陣列。

17 行

1
2
3
onChange(element: HTMLInputElement) {
(element.checked) ? this.selectedCount++ : this.selectedCount--;
}

element.checkedtrue,則 selectedCount + 1,否則 selectedCount - 1 。

checkbox005

功能都實作出來了,重新跑一次驗收測試確認都為 綠燈

Conclusion


  • 實務上整個 ATDD 循環應該是驗收測試 (紅燈) -> 整合測試 (紅燈) -> 單元測試 (紅燈) -> 單元測試 (綠燈) -> 整合測試 (綠燈) -> 驗收測試 (綠燈),因為本文重點在於 Protract 驗收測試的 radio 寫法,所以省略了整合測試與單元測試部分。
  • 本文特別針對 checkbox 展示了 selectCloudByIndex()selectCloudByText() 兩種寫法。

Sample Code


完整的範例可以在我的 GitHub 找到。

2017-08-23