Skip to content

Commit 835d6fa

Browse files
authored
Merge pull request #3028 from IntersectMBO/fix/flaky-timeout-test
Fix: flaky/timeout issue of report-321
2 parents 81de77d + 8a89f0d commit 835d6fa

22 files changed

+334
-109
lines changed

tests/govtool-frontend/playwright/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ allure-report/
1616
lib/_mock/registerDRepWallets.json
1717
lib/_mock/registeredDRepWallets.json
1818
lib/_mock/proposalSubmissionWallets.json
19+
lib/_mock/proposalSubmissionCopyWallets.json
1920
lib/_mock/registerDRepCopyWallets.json
2021
lib/_mock/registeredDRepCopyWallets.json
2122
lib/_mock/wallets.json

tests/govtool-frontend/playwright/lib/helpers/exceptionHandler.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { Expect, ExpectMatcherUtils } from "@playwright/test";
1+
import { Page } from "@playwright/test";
2+
import { Logger } from "./logger";
23

34
export async function expectWithInfo(
45
expectation: () => Promise<void>,
@@ -10,3 +11,20 @@ export async function expectWithInfo(
1011
throw new Error(errorMessage);
1112
}
1213
}
14+
15+
export async function failureWithConsoleMessages(page: Page, fn: Function) {
16+
const consoleMessages: string[] = [];
17+
page.on("console", (msg) => {
18+
if (msg.type() === "error") {
19+
consoleMessages.push(msg.text());
20+
}
21+
});
22+
try {
23+
await fn();
24+
} catch (error) {
25+
Logger.fail(
26+
`Failed: ${error.message}\nConsole messages: ${consoleMessages.join("\n")}`
27+
);
28+
throw error;
29+
}
30+
}

tests/govtool-frontend/playwright/lib/helpers/shellyWallet.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,11 @@ export default function extractDRepFromWallet(wallet: ShelleyWallet) {
1212
const dRepIdBech32 = bech32.encode("drep", words);
1313
return dRepIdBech32;
1414
}
15+
16+
export async function generateWallets(num: number) {
17+
return await Promise.all(
18+
Array.from({ length: num }, () =>
19+
ShelleyWallet.generate().then((wallet) => wallet.json())
20+
)
21+
);
22+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
export async function waitedLoop(
2+
conditionFn,
3+
timeout = 60000,
4+
interval = 2000
5+
) {
6+
const startTime = Date.now();
7+
while (Date.now() - startTime < timeout) {
8+
if (await conditionFn()) return true;
9+
await new Promise((resolve) => setTimeout(resolve, interval));
10+
}
11+
return false;
12+
}
13+
14+
export async function functionWaitedAssert(
15+
fn,
16+
options: { timeout?: number; interval?: number; message?: string } = {
17+
timeout: 6000,
18+
interval: 2000,
19+
message: null,
20+
}
21+
) {
22+
const { timeout, interval, message } = options;
23+
const startTime = Date.now();
24+
25+
while (true) {
26+
try {
27+
await fn();
28+
return true;
29+
} catch (error) {
30+
if (Date.now() - startTime >= timeout) {
31+
const errorMessage = message || error.message;
32+
throw new Error(errorMessage);
33+
}
34+
await new Promise((resolve) => setTimeout(resolve, interval));
35+
}
36+
}
37+
}

tests/govtool-frontend/playwright/lib/pages/dRepDirectoryPage.ts

Lines changed: 46 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { convertDRepToCIP129 } from "@helpers/dRep";
2+
import { functionWaitedAssert, waitedLoop } from "@helpers/waitedLoop";
23
import { Locator, Page, expect } from "@playwright/test";
34
import { IDRep } from "@types";
45
import environments from "lib/constants/environments";
@@ -40,6 +41,8 @@ export default class DRepDirectoryPage {
4041
'[data-testid$="-delegate-button"]'
4142
);
4243

44+
readonly NoDRepText = this.page.getByText("No DReps found");
45+
4346
constructor(private readonly page: Page) {}
4447

4548
async goto() {
@@ -75,22 +78,35 @@ export default class DRepDirectoryPage {
7578
}
7679

7780
async validateFilters(filters: string[], filterOptions: string[]) {
78-
const excludedFilters = filterOptions.filter(
79-
(filter) => !filters.includes(filter)
81+
let errorMessage = "";
82+
await functionWaitedAssert(
83+
async () => {
84+
const excludedFilters = filterOptions.filter(
85+
(filter) => !filters.includes(filter)
86+
);
87+
88+
const dRepList = await this.getAllListedDReps();
89+
90+
for (const filter of excludedFilters) {
91+
await expect(this.page.getByTestId(`${filter}-checkbox`)).toHaveCount(
92+
1
93+
);
94+
}
95+
96+
for (const dRep of dRepList) {
97+
const hasFilter = await this._validateTypeFiltersInDRep(
98+
dRep,
99+
filters
100+
);
101+
const actualFilter = await dRep
102+
.locator('[data-testid$="-pill"]')
103+
.textContent();
104+
errorMessage = `${actualFilter} does not match any of the ${filters}`;
105+
expect(hasFilter).toBe(true);
106+
}
107+
},
108+
{ message: errorMessage }
80109
);
81-
82-
await this.page.waitForTimeout(4_000); // wait for the dRep list to render properly
83-
84-
const dRepList = await this.getAllListedDReps();
85-
86-
for (const filter of excludedFilters) {
87-
await expect(this.page.getByTestId(`${filter}-checkbox`)).toHaveCount(1);
88-
}
89-
90-
for (const dRep of dRepList) {
91-
const hasFilter = await this._validateTypeFiltersInDRep(dRep, filters);
92-
expect(hasFilter).toBe(true);
93-
}
94110
}
95111

96112
async _validateTypeFiltersInDRep(
@@ -128,7 +144,6 @@ export default class DRepDirectoryPage {
128144
const isValid = validationFn(dRepList[i], dRepList[i + 1]);
129145
expect(isValid).toBe(true);
130146
}
131-
132147
// Frontend validation
133148
const cip105DRepListFE = await this.getAllListedCIP105DRepIds();
134149
const cip129DRepListFE = await this.getAllListedCIP129DRepIds();
@@ -138,9 +153,9 @@ export default class DRepDirectoryPage {
138153
);
139154

140155
for (let i = 0; i <= cip105DRepListFE.length - 1; i++) {
141-
await expect(cip105DRepListFE[i]).toHaveText(dRepList[i].view);
142-
await expect(cip129DRepListFE[i]).toHaveText(
143-
`(CIP-129) ${cip129DRepListApi[i]}`
156+
await expect(cip129DRepListFE[i]).toHaveText(cip129DRepListApi[i]);
157+
await expect(cip105DRepListFE[i]).toHaveText(
158+
`(CIP-105) ${dRepList[i].view}`
144159
);
145160
}
146161
}
@@ -149,46 +164,40 @@ export default class DRepDirectoryPage {
149164
}
150165

151166
async getAllListedCIP105DRepIds() {
152-
await this.page.waitForTimeout(2_000);
153-
154167
const dRepCards = await this.getAllListedDReps();
155168

156169
return dRepCards.map((dRep) =>
157-
dRep.locator('[data-testid$="-copy-id-button"]').first()
170+
dRep.locator('[data-testid$="-copy-id-button"]').last()
158171
);
159172
}
160173

161174
async getAllListedCIP129DRepIds() {
162-
await this.page.waitForTimeout(2_000);
163-
164175
const dRepCards = await this.getAllListedDReps();
165176

166177
return dRepCards.map((dRep) =>
167-
dRep.locator('[data-testid$="-copy-id-button"]').last()
178+
dRep.locator('[data-testid$="-copy-id-button"]').first()
168179
);
169180
}
170181

171182
async getAllListedDReps() {
172-
// wait for the dRep list to render properly
173-
await this.page.waitForTimeout(3_000);
174-
// add assertion to wait until the search input is visible
175183
await expect(this.searchInput).toBeVisible({ timeout: 10_000 });
176184

177-
return await this.page
178-
.getByRole("list")
179-
.locator('[data-testid$="-drep-card"]')
180-
.all();
185+
await waitedLoop(async () => {
186+
return (
187+
(await this.page.locator('[data-testid$="-drep-card"]').count()) > 0
188+
);
189+
});
190+
191+
return this.page.locator('[data-testid$="-drep-card"]').all();
181192
}
182193

183194
async verifyDRepInList(dRepId: string) {
184195
await this.goto();
185196

186197
await this.searchInput.fill(dRepId);
187198

188-
await this.page.waitForTimeout(5_000); // wait until the dRep list render properly
189-
190-
await expect(
191-
this.page.getByTestId(`${dRepId}-drep-card`)
192-
).not.toBeVisible();
199+
await expect(this.page.getByText("No DReps found")).toBeVisible({
200+
timeout: 20_000,
201+
});
193202
}
194203
}

tests/govtool-frontend/playwright/lib/pages/governanceActionsPage.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { GovernanceActionType, IProposal } from "@types";
44
import environments from "lib/constants/environments";
55
import GovernanceActionDetailsPage from "./governanceActionDetailsPage";
66
import { getEnumKeyByValue } from "@helpers/enum";
7+
import { waitedLoop } from "@helpers/waitedLoop";
78

89
const MAX_SLIDES_DISPLAY_PER_TYPE = 6;
910

@@ -94,8 +95,13 @@ export default class GovernanceActionsPage {
9495
}
9596
}
9697

97-
async getAllProposals() {
98-
await this.page.waitForTimeout(4_000); // waits for proposals to render
98+
async getAllProposals(): Promise<Locator[]> {
99+
await waitedLoop(async () => {
100+
return (
101+
(await this.page.locator('[data-testid$="-card"]').count()) > 0 ||
102+
this.page.getByText("No results for the search.")
103+
);
104+
});
99105
return this.page.locator('[data-testid$="-card"]').all();
100106
}
101107

tests/govtool-frontend/playwright/lib/pages/proposalDiscussionPage.ts

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { expect, Locator, Page } from "@playwright/test";
22
import { ProposalCreateRequest, ProposalType, ProposedGovAction } from "@types";
33
import environments from "lib/constants/environments";
44
import ProposalDiscussionDetailsPage from "./proposalDiscussionDetailsPage";
5+
import { functionWaitedAssert, waitedLoop } from "@helpers/waitedLoop";
56

67
export default class ProposalDiscussionPage {
78
// Buttons
@@ -31,15 +32,6 @@ export default class ProposalDiscussionPage {
3132
await this.page.waitForTimeout(2_000);
3233
}
3334

34-
async closeUsernamePrompt() {
35-
await this.page.waitForTimeout(5_000);
36-
await this.page
37-
.locator("div")
38-
.filter({ hasText: /^Hey, setup your username$/ })
39-
.getByRole("button")
40-
.click();
41-
}
42-
4335
async viewFirstProposal(): Promise<ProposalDiscussionDetailsPage> {
4436
await this.page
4537
.locator('[data-testid^="proposal-"][data-testid$="-view-details"]')
@@ -49,7 +41,13 @@ export default class ProposalDiscussionPage {
4941
}
5042

5143
async getAllProposals() {
52-
await this.page.waitForTimeout(4_000); // waits for proposals to render
44+
await waitedLoop(async () => {
45+
return (
46+
(await this.page
47+
.locator('[data-testid^="proposal-"][data-testid$="-card"]')
48+
.count()) > 0
49+
);
50+
});
5351

5452
return this.page
5553
.locator('[data-testid^="proposal-"][data-testid$="-card"]')
@@ -129,12 +127,22 @@ export default class ProposalDiscussionPage {
129127
filters: string[],
130128
validateFunction: (proposalCard: any, filters: string[]) => Promise<boolean>
131129
) {
132-
const proposalCards = await this.getAllProposals();
133-
134-
for (const proposalCard of proposalCards) {
135-
const hasFilter = await validateFunction(proposalCard, filters);
136-
expect(hasFilter).toBe(true);
137-
}
130+
let errorMessage = "";
131+
await functionWaitedAssert(
132+
async () => {
133+
const proposalCards = await this.getAllProposals();
134+
135+
for (const proposalCard of proposalCards) {
136+
const type = await proposalCard
137+
.getByTestId("governance-action-type")
138+
.textContent();
139+
const hasFilter = await validateFunction(proposalCard, filters);
140+
errorMessage = `A governance action type ${type} does not contain on ${filters}`;
141+
expect(hasFilter).toBe(true);
142+
}
143+
},
144+
{ message: errorMessage }
145+
);
138146
}
139147

140148
async sortAndValidate(

tests/govtool-frontend/playwright/lib/pages/proposalSubmissionPage.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,6 @@ export default class ProposalSubmissionPage {
129129
async goto() {
130130
await this.page.goto(`${environments.frontendUrl}/proposal_discussion`);
131131

132-
await this.page.waitForTimeout(2_000); // wait until page load properly
133-
134132
await this.verifyIdentityBtn.click();
135133
await this.proposalCreateBtn.click();
136134

@@ -482,9 +480,7 @@ export default class ProposalSubmissionPage {
482480
await this.continueBtn.click();
483481
await this.submitBtn.click();
484482

485-
// Wait for redirection to `proposal-discussion-details` page
486-
await this.page.waitForTimeout(2_000);
487-
483+
await expect(this.page.getByTestId("submit-as-GA-button")).toBeVisible();
488484
const currentPageUrl = this.page.url();
489485
return extractProposalIdFromUrl(currentPageUrl);
490486
}

tests/govtool-frontend/playwright/lib/services/kuberService.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,12 @@ const kuberService = {
137137
});
138138
},
139139
// register stake and outputs 20A
140-
initializeWallets: (wallets: StaticWallet[]) => {
141-
const kuber = new Kuber(faucetWallet.address, faucetWallet.payment.private);
140+
initializeWallets: (
141+
wallets: StaticWallet[],
142+
faucetAddress: string = faucetWallet.address,
143+
faucetStakeKey: string = faucetWallet.payment.private
144+
) => {
145+
const kuber = new Kuber(faucetAddress, faucetStakeKey);
142146
const outputs = [];
143147
const stakes = [];
144148
const certificates = [];
@@ -195,9 +199,11 @@ const kuberService = {
195199
},
196200

197201
multipleTransferADA: (
198-
outputs: { address: string; value: string | number }[]
202+
outputs: { address: string; value: string | number }[],
203+
addr = faucetWallet.address,
204+
signingKey = faucetWallet.payment.private
199205
) => {
200-
const kuber = new Kuber(faucetWallet.address, faucetWallet.payment.private);
206+
const kuber = new Kuber(addr, signingKey);
201207
const req = {
202208
outputs,
203209
};

tests/govtool-frontend/playwright/lib/walletManager.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export type Purpose =
99
| "registerDRep"
1010
| "registeredDRep"
1111
| "proposalSubmission"
12+
| "proposalSubmissionCopy"
1213
| "registerDRepCopy"
1314
| "registeredDRepCopy";
1415

0 commit comments

Comments
 (0)