本文包含了常見的組件通訊場景,也就是讓兩個(gè)或多個(gè)組件之間共享信息的方法。
參閱現(xiàn)場演練 / 下載范例。
?HeroChildComponent
?有兩個(gè)輸入型屬性,它們通常帶?@Input
? 裝飾器。
import { Component, Input } from '@angular/core';
import { Hero } from './hero';
@Component({
selector: 'app-hero-child',
template: `
<h3>{{hero.name}} says:</h3>
<p>I, {{hero.name}}, am at your service, {{masterName}}.</p>
`
})
export class HeroChildComponent {
@Input() hero!: Hero;
@Input('master') masterName = '';
}
第二個(gè) ?@Input
? 為子組件的屬性名 ?masterName
?指定一個(gè)別名 ?master
?(譯者注:不推薦為起別名,請(qǐng)參閱風(fēng)格指南).
父組件 ?HeroParentComponent
?把子組件的 ?HeroChildComponent
?放到 ?*ngFor
? 循環(huán)器中,把自己的 ?master
?字符串屬性綁定到子組件的 ?master
?別名上,并把每個(gè)循環(huán)的 ?hero
?實(shí)例綁定到子組件的 ?hero
?屬性。
import { Component } from '@angular/core';
import { HEROES } from './hero';
@Component({
selector: 'app-hero-parent',
template: `
<h2>{{master}} controls {{heroes.length}} heroes</h2>
<app-hero-child
*ngFor="let hero of heroes"
[hero]="hero"
[master]="master">
</app-hero-child>
`
})
export class HeroParentComponent {
heroes = HEROES;
master = 'Master';
}
運(yùn)行應(yīng)用程序會(huì)顯示三個(gè)英雄:
端到端測試,用于確保所有的子組件都如預(yù)期般初始化并顯示出來:
// ...
const heroNames = ['Dr IQ', 'Magneta', 'Bombasto'];
const masterName = 'Master';
it('should pass properties to children properly', async () => {
const parent = element(by.tagName('app-hero-parent'));
const heroes = parent.all(by.tagName('app-hero-child'));
for (let i = 0; i < heroNames.length; i++) {
const childTitle = await heroes.get(i).element(by.tagName('h3')).getText();
const childDetail = await heroes.get(i).element(by.tagName('p')).getText();
expect(childTitle).toEqual(heroNames[i] + ' says:');
expect(childDetail).toContain(masterName);
}
});
// ...
使用一個(gè)輸入屬性的 setter,以攔截父組件中值的變化,并采取行動(dòng)。
子組件 ?NameChildComponent
?的輸入屬性 ?name
?上的這個(gè) setter,會(huì) trim 掉名字里的空格,并把空值替換成默認(rèn)字符串。
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-name-child',
template: '<h3>"{{name}}"</h3>'
})
export class NameChildComponent {
@Input()
get name(): string { return this._name; }
set name(name: string) {
this._name = (name && name.trim()) || '<no name set>';
}
private _name = '';
}
下面的 ?NameParentComponent
?展示了各種名字的處理方式,包括一個(gè)全是空格的名字。
import { Component } from '@angular/core';
@Component({
selector: 'app-name-parent',
template: `
<h2>Master controls {{names.length}} names</h2>
<app-name-child *ngFor="let name of names" [name]="name"></app-name-child>
`
})
export class NameParentComponent {
// Displays 'Dr IQ', '<no name set>', 'Bombasto'
names = ['Dr IQ', ' ', ' Bombasto '];
}
端到端測試:輸入屬性的 setter,分別使用空名字和非空名字。
// ...
it('should display trimmed, non-empty names', async () => {
const nonEmptyNameIndex = 0;
const nonEmptyName = '"Dr IQ"';
const parent = element(by.tagName('app-name-parent'));
const hero = parent.all(by.tagName('app-name-child')).get(nonEmptyNameIndex);
const displayName = await hero.element(by.tagName('h3')).getText();
expect(displayName).toEqual(nonEmptyName);
});
it('should replace empty name with default name', async () => {
const emptyNameIndex = 1;
const defaultName = '"<no name set>"';
const parent = element(by.tagName('app-name-parent'));
const hero = parent.all(by.tagName('app-name-child')).get(emptyNameIndex);
const displayName = await hero.element(by.tagName('h3')).getText();
expect(displayName).toEqual(defaultName);
});
// ...
使用 ?OnChanges
?生命周期鉤子接口的 ?ngOnChanges()
? 方法來監(jiān)測輸入屬性值的變化并做出回應(yīng)。
當(dāng)需要監(jiān)視多個(gè)、交互式輸入屬性的時(shí)候,本方法比用屬性的 ?setter ?更合適。
這個(gè) ?VersionChildComponent
?會(huì)監(jiān)測輸入屬性 ?major
?和 ?minor
?的變化,并把這些變化編寫成日志以報(bào)告這些變化。
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
@Component({
selector: 'app-version-child',
template: `
<h3>Version {{major}}.{{minor}}</h3>
<h4>Change log:</h4>
<ul>
<li *ngFor="let change of changeLog">{{change}}</li>
</ul>
`
})
export class VersionChildComponent implements OnChanges {
@Input() major = 0;
@Input() minor = 0;
changeLog: string[] = [];
ngOnChanges(changes: SimpleChanges) {
const log: string[] = [];
for (const propName in changes) {
const changedProp = changes[propName];
const to = JSON.stringify(changedProp.currentValue);
if (changedProp.isFirstChange()) {
log.push(`Initial value of ${propName} set to ${to}`);
} else {
const from = JSON.stringify(changedProp.previousValue);
log.push(`${propName} changed from ${from} to ${to}`);
}
}
this.changeLog.push(log.join(', '));
}
}
?VersionParentComponent
?提供 ?minor
?和 ?major
?值,把修改它們值的方法綁定到按鈕上。
import { Component } from '@angular/core';
@Component({
selector: 'app-version-parent',
template: `
<h2>Source code version</h2>
<button (click)="newMinor()">New minor version</button>
<button (click)="newMajor()">New major version</button>
<app-version-child [major]="major" [minor]="minor"></app-version-child>
`
})
export class VersionParentComponent {
major = 1;
minor = 23;
newMinor() {
this.minor++;
}
newMajor() {
this.major++;
this.minor = 0;
}
}
下面是點(diǎn)擊按鈕的結(jié)果。
測試確保這兩個(gè)輸入屬性值都被初始化了,當(dāng)點(diǎn)擊按鈕后,?ngOnChanges
?應(yīng)該被調(diào)用,屬性的值也符合預(yù)期。
// ...
// Test must all execute in this exact order
it('should set expected initial values', async () => {
const actual = await getActual();
const initialLabel = 'Version 1.23';
const initialLog = 'Initial value of major set to 1, Initial value of minor set to 23';
expect(actual.label).toBe(initialLabel);
expect(actual.count).toBe(1);
expect(await actual.logs.get(0).getText()).toBe(initialLog);
});
it("should set expected values after clicking 'Minor' twice", async () => {
const repoTag = element(by.tagName('app-version-parent'));
const newMinorButton = repoTag.all(by.tagName('button')).get(0);
await newMinorButton.click();
await newMinorButton.click();
const actual = await getActual();
const labelAfter2Minor = 'Version 1.25';
const logAfter2Minor = 'minor changed from 24 to 25';
expect(actual.label).toBe(labelAfter2Minor);
expect(actual.count).toBe(3);
expect(await actual.logs.get(2).getText()).toBe(logAfter2Minor);
});
it("should set expected values after clicking 'Major' once", async () => {
const repoTag = element(by.tagName('app-version-parent'));
const newMajorButton = repoTag.all(by.tagName('button')).get(1);
await newMajorButton.click();
const actual = await getActual();
const labelAfterMajor = 'Version 2.0';
const logAfterMajor = 'major changed from 1 to 2, minor changed from 23 to 0';
expect(actual.label).toBe(labelAfterMajor);
expect(actual.count).toBe(2);
expect(await actual.logs.get(1).getText()).toBe(logAfterMajor);
});
async function getActual() {
const versionTag = element(by.tagName('app-version-child'));
const label = await versionTag.element(by.tagName('h3')).getText();
const ul = versionTag.element((by.tagName('ul')));
const logs = ul.all(by.tagName('li'));
return {
label,
logs,
count: await logs.count(),
};
}
// ...
子組件暴露一個(gè) ?EventEmitter
?屬性,當(dāng)事件發(fā)生時(shí),子組件利用該屬性 ?emits
?(向上彈射)事件。父組件綁定到這個(gè)事件屬性,并在事件發(fā)生時(shí)作出回應(yīng)。
子組件的 ?EventEmitter
?屬性是一個(gè)輸出屬性,通常帶有?@Output
? 裝飾器,就像在 ?VoterComponent
?中看到的。
import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({
selector: 'app-voter',
template: `
<h4>{{name}}</h4>
<button (click)="vote(true)" [disabled]="didVote">Agree</button>
<button (click)="vote(false)" [disabled]="didVote">Disagree</button>
`
})
export class VoterComponent {
@Input() name = '';
@Output() voted = new EventEmitter<boolean>();
didVote = false;
vote(agreed: boolean) {
this.voted.emit(agreed);
this.didVote = true;
}
}
點(diǎn)擊按鈕會(huì)觸發(fā) ?true
?或 ?false
?(布爾型有效載荷)的事件。
父組件 ?VoteTakerComponent
?綁定了一個(gè)事件處理器(?onVoted()
?),用來響應(yīng)子組件的事件(?$event
?)并更新一個(gè)計(jì)數(shù)器。
import { Component } from '@angular/core';
@Component({
selector: 'app-vote-taker',
template: `
<h2>Should mankind colonize the Universe?</h2>
<h3>Agree: {{agreed}}, Disagree: {{disagreed}}</h3>
<app-voter
*ngFor="let voter of voters"
[name]="voter"
(voted)="onVoted($event)">
</app-voter>
`
})
export class VoteTakerComponent {
agreed = 0;
disagreed = 0;
voters = ['Narco', 'Celeritas', 'Bombasto'];
onVoted(agreed: boolean) {
if (agreed) {
this.agreed++;
} else {
this.disagreed++;
}
}
}
本框架把事件參數(shù)(用 ?$event
? 表示)傳給事件處理方法,該方法會(huì)處理它:
測試確保點(diǎn)擊 Agree 和 Disagree 按鈕時(shí),計(jì)數(shù)器被正確更新。
// ...
it('should not emit the event initially', async () => {
const voteLabel = element(by.tagName('app-vote-taker')).element(by.tagName('h3'));
expect(await voteLabel.getText()).toBe('Agree: 0, Disagree: 0');
});
it('should process Agree vote', async () => {
const voteLabel = element(by.tagName('app-vote-taker')).element(by.tagName('h3'));
const agreeButton1 = element.all(by.tagName('app-voter')).get(0)
.all(by.tagName('button')).get(0);
await agreeButton1.click();
expect(await voteLabel.getText()).toBe('Agree: 1, Disagree: 0');
});
it('should process Disagree vote', async () => {
const voteLabel = element(by.tagName('app-vote-taker')).element(by.tagName('h3'));
const agreeButton1 = element.all(by.tagName('app-voter')).get(1)
.all(by.tagName('button')).get(1);
await agreeButton1.click();
expect(await voteLabel.getText()).toBe('Agree: 0, Disagree: 1');
});
// ...
父組件不能使用數(shù)據(jù)綁定來讀取子組件的屬性或調(diào)用子組件的方法。但可以在父組件模板里,新建一個(gè)本地變量來代表子組件,然后利用這個(gè)變量來讀取子組件的屬性和調(diào)用子組件的方法,如下例所示。
子組件 ?CountdownTimerComponent
?進(jìn)行倒計(jì)時(shí),歸零時(shí)發(fā)射一個(gè)導(dǎo)彈。?start
?和 ?stop
?方法負(fù)責(zé)控制時(shí)鐘并在模板里顯示倒計(jì)時(shí)的狀態(tài)信息。
import { Component, OnDestroy } from '@angular/core';
@Component({
selector: 'app-countdown-timer',
template: '<p>{{message}}</p>'
})
export class CountdownTimerComponent implements OnDestroy {
intervalId = 0;
message = '';
seconds = 11;
ngOnDestroy() { this.clearTimer(); }
start() { this.countDown(); }
stop() {
this.clearTimer();
this.message = `Holding at T-${this.seconds} seconds`;
}
private clearTimer() { clearInterval(this.intervalId); }
private countDown() {
this.clearTimer();
this.intervalId = window.setInterval(() => {
this.seconds -= 1;
if (this.seconds === 0) {
this.message = 'Blast off!';
} else {
if (this.seconds < 0) { this.seconds = 10; } // reset
this.message = `T-${this.seconds} seconds and counting`;
}
}, 1000);
}
}
計(jì)時(shí)器組件的宿主組件 ?CountdownLocalVarParentComponent
?如下:
import { Component } from '@angular/core';
import { CountdownTimerComponent } from './countdown-timer.component';
@Component({
selector: 'app-countdown-parent-lv',
template: `
<h3>Countdown to Liftoff (via local variable)</h3>
<button (click)="timer.start()">Start</button>
<button (click)="timer.stop()">Stop</button>
<div class="seconds">{{timer.seconds}}</div>
<app-countdown-timer #timer></app-countdown-timer>
`,
styleUrls: ['../assets/demo.css']
})
export class CountdownLocalVarParentComponent { }
父組件不能通過數(shù)據(jù)綁定使用子組件的 ?start
?和 ?stop
?方法,也不能訪問子組件的 ?seconds
?屬性。
把本地變量(?#timer
?)放到(?<countdown-timer>
?)標(biāo)簽中,用來代表子組件。這樣父組件的模板就得到了子組件的引用,于是可以在父組件的模板中訪問子組件的所有屬性和方法。
這個(gè)例子把父組件的按鈕綁定到子組件的 ?start
?和 ?stop
?方法,并用插值來顯示子組件的 ?seconds
?屬性。
下面是父組件和子組件一起工作時(shí)的效果。
測試確保在父組件模板中顯示的秒數(shù)和子組件狀態(tài)信息里的秒數(shù)同步。它還會(huì)點(diǎn)擊 Stop 按鈕來停止倒計(jì)時(shí):
// ...
// The tests trigger periodic asynchronous operations (via `setInterval()`), which will prevent
// the app from stabilizing. See https://angular.io/api/core/ApplicationRef#is-stable-examples
// for more details.
// To allow the tests to complete, we will disable automatically waiting for the Angular app to
// stabilize.
beforeEach(() => browser.waitForAngularEnabled(false));
afterEach(() => browser.waitForAngularEnabled(true));
it('timer and parent seconds should match', async () => {
const parent = element(by.tagName(parentTag));
const startButton = parent.element(by.buttonText('Start'));
const seconds = parent.element(by.className('seconds'));
const timer = parent.element(by.tagName('app-countdown-timer'));
await startButton.click();
// Wait for `<app-countdown-timer>` to be populated with any text.
await browser.wait(() => timer.getText(), 2000);
expect(await timer.getText()).toContain(await seconds.getText());
});
it('should stop the countdown', async () => {
const parent = element(by.tagName(parentTag));
const startButton = parent.element(by.buttonText('Start'));
const stopButton = parent.element(by.buttonText('Stop'));
const timer = parent.element(by.tagName('app-countdown-timer'));
await startButton.click();
expect(await timer.getText()).not.toContain('Holding');
await stopButton.click();
expect(await timer.getText()).toContain('Holding');
});
// ...
這個(gè)本地變量方法是個(gè)簡單明了的方法。但是它也有局限性,因?yàn)楦附M件-子組件的連接必須全部在父組件的模板中進(jìn)行。父組件本身的代碼對(duì)子組件沒有訪問權(quán)。
如果父組件的類需要依賴于子組件類,就不能使用本地變量方法。組件之間的父子關(guān)系 組件的父子關(guān)系不能通過在每個(gè)組件的類中各自定義本地變量來建立。這是因?yàn)檫@兩個(gè)類的實(shí)例互相不知道,因此父類也就不能訪問子類中的屬性和方法。
當(dāng)父組件類需要這種訪問時(shí),可以把子組件作為 ViewChild,注入到父組件里面。
下面的例子用與倒計(jì)時(shí)相同的范例來解釋這種技術(shù)。 它的外觀或行為沒有變化。子組件?CountdownTimerComponent
?也和原來一樣。
由本地變量切換到 ViewChild 技術(shù)的唯一目的就是做示范。
下面是父組件 ?CountdownViewChildParentComponent
?:
import { AfterViewInit, ViewChild } from '@angular/core';
import { Component } from '@angular/core';
import { CountdownTimerComponent } from './countdown-timer.component';
@Component({
selector: 'app-countdown-parent-vc',
template: `
<h3>Countdown to Liftoff (via ViewChild)</h3>
<button (click)="start()">Start</button>
<button (click)="stop()">Stop</button>
<div class="seconds">{{ seconds() }}</div>
<app-countdown-timer></app-countdown-timer>
`,
styleUrls: ['../assets/demo.css']
})
export class CountdownViewChildParentComponent implements AfterViewInit {
@ViewChild(CountdownTimerComponent)
private timerComponent!: CountdownTimerComponent;
seconds() { return 0; }
ngAfterViewInit() {
// Redefine `seconds()` to get from the `CountdownTimerComponent.seconds` ...
// but wait a tick first to avoid one-time devMode
// unidirectional-data-flow-violation error
setTimeout(() => this.seconds = () => this.timerComponent.seconds, 0);
}
start() { this.timerComponent.start(); }
stop() { this.timerComponent.stop(); }
}
把子組件的視圖插入到父組件類需要做一點(diǎn)額外的工作。
首先,你必須導(dǎo)入對(duì)裝飾器 ?ViewChild
?以及生命周期鉤子 ?AfterViewInit
?的引用。
接著,通過 ?@ViewChild
? 屬性裝飾器,將子組件 ?CountdownTimerComponent
?注入到私有屬性 ?timerComponent
?里面。
組件元數(shù)據(jù)里就不再需要 ?#timer
? 本地變量了。而是把按鈕綁定到父組件自己的 ?start
?和 ?stop
?方法,使用父組件的 ?seconds
?方法的插值來展示秒數(shù)變化。
這些方法可以直接訪問被注入的計(jì)時(shí)器組件。
?ngAfterViewInit()
? 生命周期鉤子是非常重要的一步。被注入的計(jì)時(shí)器組件只有在 Angular 顯示了父組件視圖之后才能訪問,所以它先把秒數(shù)顯示為 0.
然后 Angular 會(huì)調(diào)用 ?ngAfterViewInit
?生命周期鉤子,但這時(shí)候再更新父組件視圖的倒計(jì)時(shí)就已經(jīng)太晚了。Angular 的單向數(shù)據(jù)流規(guī)則會(huì)阻止在同一個(gè)周期內(nèi)更新父組件視圖。應(yīng)用在顯示秒數(shù)之前會(huì)被迫再等一輪。
使用 ?setTimeout()
? 來等下一輪,然后改寫 ?seconds()
? 方法,這樣它接下來就會(huì)從注入的這個(gè)計(jì)時(shí)器組件里獲取秒數(shù)的值。
父組件和它的子組件共享同一個(gè)服務(wù),利用該服務(wù)在組件家族內(nèi)部實(shí)現(xiàn)雙向通訊。
該服務(wù)實(shí)例的作用域被限制在父組件和其子組件內(nèi)。這個(gè)組件子樹之外的組件將無法訪問該服務(wù)或者與它們通訊。
這個(gè) ?MissionService
?把 ?MissionControlComponent
?和多個(gè) ?AstronautComponent
?子組件連接起來。
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';
@Injectable()
export class MissionService {
// Observable string sources
private missionAnnouncedSource = new Subject<string>();
private missionConfirmedSource = new Subject<string>();
// Observable string streams
missionAnnounced$ = this.missionAnnouncedSource.asObservable();
missionConfirmed$ = this.missionConfirmedSource.asObservable();
// Service message commands
announceMission(mission: string) {
this.missionAnnouncedSource.next(mission);
}
confirmMission(astronaut: string) {
this.missionConfirmedSource.next(astronaut);
}
}
?MissionControlComponent
?提供服務(wù)的實(shí)例,并將其共享給它的子組件(通過 ?providers
?元數(shù)據(jù)數(shù)組),子組件可以通過構(gòu)造函數(shù)將該實(shí)例注入到自身。
import { Component } from '@angular/core';
import { MissionService } from './mission.service';
@Component({
selector: 'app-mission-control',
template: `
<h2>Mission Control</h2>
<button (click)="announce()">Announce mission</button>
<app-astronaut
*ngFor="let astronaut of astronauts"
[astronaut]="astronaut">
</app-astronaut>
<h3>History</h3>
<ul>
<li *ngFor="let event of history">{{event}}</li>
</ul>
`,
providers: [MissionService]
})
export class MissionControlComponent {
astronauts = ['Lovell', 'Swigert', 'Haise'];
history: string[] = [];
missions = ['Fly to the moon!',
'Fly to mars!',
'Fly to Vegas!'];
nextMission = 0;
constructor(private missionService: MissionService) {
missionService.missionConfirmed$.subscribe(
astronaut => {
this.history.push(`${astronaut} confirmed the mission`);
});
}
announce() {
const mission = this.missions[this.nextMission++];
this.missionService.announceMission(mission);
this.history.push(`Mission "${mission}" announced`);
if (this.nextMission >= this.missions.length) { this.nextMission = 0; }
}
}
?AstronautComponent
?也通過自己的構(gòu)造函數(shù)注入該服務(wù)。由于每個(gè) ?AstronautComponent
?都是 ?MissionControlComponent
?的子組件,所以它們獲取到的也是父組件的這個(gè)服務(wù)實(shí)例。
import { Component, Input, OnDestroy } from '@angular/core';
import { MissionService } from './mission.service';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-astronaut',
template: `
<p>
{{astronaut}}: <strong>{{mission}}</strong>
<button
(click)="confirm()"
[disabled]="!announced || confirmed">
Confirm
</button>
</p>
`
})
export class AstronautComponent implements OnDestroy {
@Input() astronaut = '';
mission = '<no mission announced>';
confirmed = false;
announced = false;
subscription: Subscription;
constructor(private missionService: MissionService) {
this.subscription = missionService.missionAnnounced$.subscribe(
mission => {
this.mission = mission;
this.announced = true;
this.confirmed = false;
});
}
confirm() {
this.confirmed = true;
this.missionService.confirmMission(this.astronaut);
}
ngOnDestroy() {
// prevent memory leak when component destroyed
this.subscription.unsubscribe();
}
}
注意,這個(gè)例子保存了 ?subscription
?變量,并在 ?AstronautComponent
?被銷毀時(shí)調(diào)用 ?unsubscribe()
? 退訂。 這是一個(gè)用于防止內(nèi)存泄漏的保護(hù)措施。實(shí)際上,在這個(gè)應(yīng)用程序中并沒有這個(gè)風(fēng)險(xiǎn),因?yàn)?nbsp;?AstronautComponent
?的生命期和應(yīng)用程序的生命期一樣長。但在更復(fù)雜的應(yīng)用程序環(huán)境中就不一定了。
不需要在 ?MissionControlComponent
?中添加這個(gè)保護(hù)措施,因?yàn)樗鳛楦附M件,控制著 ?MissionService
?的生命期。
History 日志證明了:在父組件 ?MissionControlComponent
?和子組件 ?AstronautComponent
?之間,信息通過該服務(wù)實(shí)現(xiàn)了雙向傳遞。
測試確保點(diǎn)擊父組件 ?MissionControlComponent
?和子組件 ?AstronautComponent
?兩個(gè)的組件的按鈕時(shí),History 日志和預(yù)期的一樣。
// ...
it('should announce a mission', async () => {
const missionControl = element(by.tagName('app-mission-control'));
const announceButton = missionControl.all(by.tagName('button')).get(0);
const history = missionControl.all(by.tagName('li'));
await announceButton.click();
expect(await history.count()).toBe(1);
expect(await history.get(0).getText()).toMatch(/Mission.* announced/);
});
it('should confirm the mission by Lovell', async () => {
await testConfirmMission(1, 'Lovell');
});
it('should confirm the mission by Haise', async () => {
await testConfirmMission(3, 'Haise');
});
it('should confirm the mission by Swigert', async () => {
await testConfirmMission(2, 'Swigert');
});
async function testConfirmMission(buttonIndex: number, astronaut: string) {
const missionControl = element(by.tagName('app-mission-control'));
const announceButton = missionControl.all(by.tagName('button')).get(0);
const confirmButton = missionControl.all(by.tagName('button')).get(buttonIndex);
const history = missionControl.all(by.tagName('li'));
await announceButton.click();
await confirmButton.click();
expect(await history.count()).toBe(2);
expect(await history.get(1).getText()).toBe(`${astronaut} confirmed the mission`);
}
// ...
更多建議: