使用 Protractor 控制 Select

HTML 的 <select> 是常見的控制項,該如何使用 Protractor 對 select 寫驗收測試呢 ?

Version


Protractor 5.1.2

Requirement


select000

畫面只有 1 個 <select>,下方會顯示目前所選擇的 value。

select001

<select> 下共有 3 個 option,各為 AWSAzureGCP

select000

當選擇 AWS 時,下方顯示 0

select002

當選擇 Azure 時,下方顯示 1

select003

當選擇 GCP,下方顯示 2

Acceptance Test (紅燈)


測試案例 :

  1. 應該有 1<select>
  2. 應該有 3<option>
  3. 當選擇 AWS,下方應該出現 0
  4. 當選擇 Azure,下方應該出現 1
  5. 當選擇 GCP,下方應該出現 2

src/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
import { AppPage } from './app.po';

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

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

it(`should have '1' select`, () => {
expect(page.getSelect().isPresent()).toBe(true);
});

it(`should have '3' options in select`, () => {
expect(page.getSelectCount()).toBe(3);
});

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

it(`should show '1' when selecting 'Azure'`, () => {
page.selectCloudByText('Azure');
expect(page.getSelectedId()).toBe('1');
});

it(`should show '2' when selecting 'GCP'`, () => {
page.selectCloudByText('GCP');
expect(page.getSelectedId()).toBe('2');
});
});

11 行

1
2
3
it(`should have '1' select`, () => {
expect(page.getSelect().isPresent()).toBe(true);
});

測試案例 : 應該有 1<select>

<select> 存在,則 isPresent() 回傳 true,否則回傳 false

page.getSelect() 將由 page object 傳回 ElementFinder 型別且符合條件的 <select>

15 行

1
2
3
it(`should have '3' options in select`, () => {
expect(page.getSelectCount()).toBe(3);
});

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

測試 <select> 是否有 AWSAzureGCP 3 個 <option>,實務上這些資料會從後端 API 來,可測試 API 是否正常,也可測試 API 的 SQL 或 ORM 是否正確。

若要測試資料完全相同比較困難時,最少可測試資料的筆數是否正確。

page.getSelectCount() 將由 page object 傳回 <option> 的個數。

19 行

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

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

要模擬 user 選擇 <select>,有 2 種方式,一種是使用 index 選擇,一種是由文字選擇。

page.selectCloudByIndex() 將使用 index 方式選擇 <select>

24 行

1
2
3
4
it(`should show '1' when selecting 'Azure'`, () => {
page.selectCloudByText('Azure');
expect(page.getSelectedId()).toBe('1');
});

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

page.selectCloudByText() 將使用文字方式選擇 <select>

src/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
34
35
36
37
38
import { browser, by, element } from 'protractor';

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

getSelect(): ElementFinder {
return element(by.id('TDDSelect'));
}

getSelectCount(): any {
return element(by.id('TDDSelect'))
.all(by.tagName('option'))
.count();
}

selectCloudByIndex(index: number): AppPage {
element(by.id('TDDSelect'))
.all(by.tagName('option'))
.get(index)
.click();

return this;
}

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

return this;
}

getSelectedId(): 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
getSelect(): ElementFinder {
return element(by.id('TDDSelect'));
}

根據 id 為 TDDSelect 回傳 ElementFinder 物件,主要是為了 isPresent() 判斷 <select> 是否存在。

12 行

1
2
3
4
5
getSelectCount(): any {
return element(by.id('TDDSelect'))
.all(by.tagName('option'))
.count();
}

回傳 <select> 下共有幾個 <option>

若要在 HTML 的階層下找單一 element,可用 element(locator).element(locator)

若要在 HTML 的階層下找多個 element,可用 element(locator).all(locator)

因為 all() 是陣列,可用 count() 得知陣列筆數。

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

18 行

1
2
3
4
5
6
7
8
selectCloudByIndex(index: number): AppPage {
element(by.id('TDDSelect'))
.all(by.tagName('option'))
.get(index)
.click();

return this;
}

使用 index 選擇 <select>

先用 element(by.id('TDDSelect')) 找到 <select>,由 all(by.tagName('option')) 取得 <select> 下所有 <option> 的陣列,再透過 get(index) 選擇 <select><option>,最後 click()

27 行

1
2
3
4
5
6
7
selectCloudByText(text: string): AppPage {
element(by.id('TDDSelect'))
.element(by.cssContainingText('option', text))
.click();

return this;
}

使用文字選擇 <select>

先用 element(by.id('TDDSelect')) 找到 <select>,因為要直接由文字得到單一 <option>,就不再使用 all(),直接使用 element()

透過 by.cssContainingText('option', text) 取得 <option>,最後 click()

select004

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

Acceptance Test (綠燈)


測試案例 :

  1. 應該有 1<select>
  2. 應該有 3<option>
  3. 當選擇 AWS,下方應該出現 0
  4. 當選擇 Azure,下方應該出現 1
  5. 當選擇 GCP,下方應該出現 2

src/app/app.component.html

1
2
3
4
<select id="TDDSelect" (change)="onChange(mySelect)" #mySelect>
<option *ngFor="let cloud of clouds" [value]="cloud.id">{{ cloud.name }}</option>
</select>
<p>{{ selectedId }}</p>
1
<select id="TDDSelect" (change)="onChange(mySelect)" #mySelect>

change event 綁定到 onChange(),並將 #mySelect 傳入 onChange()

1
<option *ngFor="let cloud of clouds" [value]="cloud.id">{{ cloud.name }}</option>

使用 *ngFor 根據 clouds field 產生 <option>

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 {
selectedId = '0';
clouds: Cloud[] = [
{id: '0', name: 'AWS'},
{id: '1', name: 'Azure'},
{id: '2', name: 'GCP'}
];

onChange(element: HTMLSelectElement) {
this.selectedId = element.value;
}
}

第 10 行

1
selectedId = '0';

selectedId field 預設為 0

11 行

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

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

17 行

1
2
3
onChange(element: HTMLSelectElement) {
this.selectedId = element.value;
}

HTMLSelectElement.value 值指定到 selectedId field。

select005

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

Conclusion


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

Sample Code


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

2017-08-21