Skip to content

Commit 62ac20a

Browse files
dididyjongyoul
authored andcommitted
[ZEPPELIN-6358] Remove anti-patterns of E2E and tidy test suite
Applied the [`e2e-reviewer`](https://github.com/dididy/e2e-skills) skill on the existing E2E suite. The skill does static analysis — it catches tests that can never actually fail, silent skips, swallowed errors in POM methods, that kind of thing. Findings and fixes: - `home-page-enhanced-functionality.spec.ts` was mostly duplicating `home-page-elements` and `home-page-note-operations` → deleted and merged - `toBeGreaterThanOrEqual(0)` and `toBeAttached()` on static elements were always passing → replaced with assertions that can fail - `if (isVisible) { expect() }` patterns silently skip when something breaks → removed or converted to `test.skip` - Several POM methods had `.catch(() => {})` with no comment → removed; kept the intentional ones and marked with `// JUSTIFIED:` - `document.querySelector` in `page.evaluate()` → swapped for Playwright locator API - Added `aria-label` / `data-testid` to action bar HTML; a few tests were breaking on DOM structure changes - Renamed a handful of tests whose names didn't match what they actually tested; dropped the ones that only called `toBeVisible()` Improvement Refactoring ZEPPELIN-6358 * Does the license files need to update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Closes #5180 from dididy/tidy-e2e. Signed-off-by: Jongyoul Lee <jongyoul@gmail.com> (cherry picked from commit 076676a)
1 parent 6194c1f commit 62ac20a

35 files changed

Lines changed: 627 additions & 1163 deletions

zeppelin-web-angular/e2e/models/about-zeppelin-modal.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,6 @@ export class AboutZeppelinModal extends BasePage {
4343
return (await this.versionText.textContent()) || '';
4444
}
4545

46-
async isLogoVisible(): Promise<boolean> {
47-
return this.logo.isVisible();
48-
}
49-
5046
async getGetInvolvedHref(): Promise<string | null> {
5147
return this.getInvolvedLink.getAttribute('href');
5248
}

zeppelin-web-angular/e2e/models/base-page.ts

Lines changed: 7 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@ export class BasePage {
2424
readonly zeppelinHeader: Locator;
2525

2626
readonly modalTitle: Locator;
27-
readonly modalBody: Locator;
28-
readonly modalContent: Locator;
2927

3028
readonly okButton: Locator;
3129
readonly cancelButton: Locator;
@@ -41,8 +39,6 @@ export class BasePage {
4139
this.zeppelinHeader = page.locator('zeppelin-header');
4240

4341
this.modalTitle = page.locator('.ant-modal-confirm-title, .ant-modal-title');
44-
this.modalBody = page.locator('.ant-modal-confirm-content, .ant-modal-body');
45-
this.modalContent = page.locator('.ant-modal-body');
4642

4743
this.okButton = page.locator('button:has-text("OK")');
4844
this.cancelButton = page.locator('button:has-text("Cancel")');
@@ -71,11 +67,6 @@ export class BasePage {
7167
await this.navigateToRoute('/');
7268
}
7369

74-
getCurrentPath(): string {
75-
const url = new URL(this.page.url());
76-
return url.hash || url.pathname;
77-
}
78-
7970
async waitForUrlNotContaining(fragment: string): Promise<void> {
8071
await this.page.waitForURL(url => !url.toString().includes(fragment));
8172
}
@@ -85,14 +76,8 @@ export class BasePage {
8576
}
8677

8778
async waitForFormLabels(labelTexts: string[], timeout = 10000): Promise<void> {
88-
await this.page.waitForFunction(
89-
texts => {
90-
const labels = Array.from(document.querySelectorAll('nz-form-label'));
91-
return texts.some(text => labels.some(l => l.textContent?.includes(text)));
92-
},
93-
labelTexts,
94-
{ timeout }
95-
);
79+
const locators = labelTexts.map(text => this.page.locator('nz-form-label', { hasText: text }));
80+
await Promise.race(locators.map(l => l.waitFor({ state: 'attached', timeout })));
9681
}
9782

9883
async waitForElementAttribute(
@@ -109,25 +94,20 @@ export class BasePage {
10994
}
11095
}
11196

112-
async waitForRouterOutletChild(timeout = 10000): Promise<void> {
113-
await expect(this.page.locator('zeppelin-workspace router-outlet + *')).toHaveCount(1, { timeout });
114-
}
115-
11697
async fillAndVerifyInput(
11798
locator: Locator,
11899
value: string,
119100
options?: { timeout?: number; clearFirst?: boolean }
120101
): Promise<void> {
121-
const { timeout = 10000, clearFirst = true } = options || {};
102+
const { timeout = 10000 } = options || {};
122103

123104
await expect(locator).toBeVisible({ timeout });
124105
await expect(locator).toBeEnabled({ timeout: 5000 });
125106

126-
if (clearFirst) {
127-
await locator.clear();
128-
}
129-
107+
// Click first so Angular's form control is focused and its initial setValue cycle
108+
// has completed before we overwrite it. Then fill() atomically sets the value.
109+
await locator.click();
130110
await locator.fill(value);
131-
await expect(locator).toHaveValue(value);
111+
await expect(locator).toHaveValue(value, { timeout: 10000 });
132112
}
133113
}

zeppelin-web-angular/e2e/models/home-page.ts

Lines changed: 28 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@ export class HomePage extends BasePage {
1717
readonly notebookSection: Locator;
1818
readonly helpSection: Locator;
1919
readonly communitySection: Locator;
20-
readonly zeppelinLogo: Locator;
21-
readonly anonymousUserIndicator: Locator;
2220
readonly welcomeSection: Locator;
2321
readonly moreInfoGrid: Locator;
2422
readonly notebookColumn: Locator;
@@ -28,9 +26,6 @@ export class HomePage extends BasePage {
2826
readonly notebookHeading: Locator;
2927
readonly helpHeading: Locator;
3028
readonly communityHeading: Locator;
31-
readonly createNoteModal: Locator;
32-
readonly createNoteButton: Locator;
33-
readonly notebookNameInput: Locator;
3429
readonly externalLinks: {
3530
documentation: Locator;
3631
mailingList: Locator;
@@ -41,13 +36,12 @@ export class HomePage extends BasePage {
4136
createNewNoteLink: Locator;
4237
importNoteLink: Locator;
4338
filterInput: Locator;
44-
tree: Locator;
45-
noteActions: {
46-
renameNote: Locator;
47-
clearOutput: Locator;
48-
moveToTrash: Locator;
49-
};
5039
};
40+
readonly anonymousUserIndicator: Locator;
41+
private readonly zeppelinLogo: Locator;
42+
private readonly createNoteModal: Locator;
43+
private readonly createNoteButton: Locator;
44+
private readonly notebookNameInput: Locator;
5145

5246
constructor(page: Page) {
5347
super(page);
@@ -58,8 +52,8 @@ export class HomePage extends BasePage {
5852
this.anonymousUserIndicator = page.locator('text=anonymous');
5953
this.welcomeSection = page.locator('.welcome');
6054
this.moreInfoGrid = page.locator('.more-info');
61-
this.notebookColumn = page.locator('[nz-col]').first();
62-
this.helpCommunityColumn = page.locator('[nz-col]').last();
55+
this.notebookColumn = page.locator('[nz-col]').first(); // first() — left column contains the Notebook section
56+
this.helpCommunityColumn = page.locator('[nz-col]').last(); // last() — right column contains Help and Community sections
6357
this.welcomeDescription = page.locator('.welcome').getByText('Zeppelin is web-based notebook');
6458
this.refreshNoteButton = page.locator('a.refresh-note');
6559
this.notebookHeading = this.notebookColumn.locator('h3');
@@ -79,30 +73,21 @@ export class HomePage extends BasePage {
7973
this.nodeList = {
8074
createNewNoteLink: page.locator('zeppelin-node-list a').filter({ hasText: 'Create new Note' }),
8175
importNoteLink: page.locator('zeppelin-node-list a').filter({ hasText: 'Import Note' }),
82-
filterInput: page.locator('zeppelin-node-list input[placeholder*="Filter"]'),
83-
tree: page.locator('zeppelin-node-list nz-tree'),
84-
noteActions: {
85-
renameNote: page.locator('.file .operation a[nztooltiptitle*="Rename note"]'),
86-
clearOutput: page.locator('.file .operation a[nztooltiptitle*="Clear output"]'),
87-
moveToTrash: page.locator('.file .operation a[nztooltiptitle*="Move note to Trash"]')
88-
}
76+
filterInput: page.locator('zeppelin-node-list input[placeholder*="Filter"]')
8977
};
9078
}
9179

80+
async navigateToHome(): Promise<void> {
81+
await this.page.goto('/');
82+
await this.waitForPageLoad();
83+
}
84+
9285
async navigateToLogin(): Promise<void> {
9386
await this.navigateToRoute('/login');
9487
// Wait for potential redirect to complete by checking URL change
9588
await this.waitForUrlNotContaining('#/login');
9689
}
9790

98-
async isHomeContentDisplayed(): Promise<boolean> {
99-
return this.welcomeTitle.isVisible();
100-
}
101-
102-
async isAnonymousUser(): Promise<boolean> {
103-
return this.anonymousUserIndicator.isVisible();
104-
}
105-
10691
async clickZeppelinLogo(): Promise<void> {
10792
await this.zeppelinLogo.click({ timeout: 15000 });
10893
}
@@ -112,19 +97,11 @@ export class HomePage extends BasePage {
11297
return text || '';
11398
}
11499

115-
async getWelcomeDescriptionText(): Promise<string> {
116-
const text = await this.welcomeDescription.textContent();
117-
return text || '';
118-
}
119-
120100
async clickRefreshNotes(): Promise<void> {
101+
await this.refreshNoteButton.waitFor({ state: 'visible', timeout: 10000 });
121102
await this.refreshNoteButton.click({ timeout: 15000 });
122103
}
123104

124-
async isNotebookListVisible(): Promise<boolean> {
125-
return this.zeppelinNodeList.isVisible();
126-
}
127-
128105
async clickCreateNewNote(): Promise<void> {
129106
await this.nodeList.createNewNoteLink.click({ timeout: 15000 });
130107
await this.createNoteModal.waitFor({ state: 'visible' });
@@ -136,30 +113,37 @@ export class HomePage extends BasePage {
136113
// Wait for the modal form to be fully rendered with proper labels
137114
await this.page.waitForSelector('nz-form-label', { timeout: 10000 });
138115

139-
await this.waitForFormLabels(['Note Name', 'Clone Note']);
116+
await this.waitForFormLabels(['Note Name']);
140117

141118
// Fill and verify the notebook name input
142119
await this.fillAndVerifyInput(this.notebookNameInput, notebookName);
143120

144121
// Click the 'Create' button in the modal
145122
await expect(this.createNoteButton).toBeEnabled({ timeout: 5000 });
146123
await this.createNoteButton.click({ timeout: 15000 });
147-
await this.waitForPageLoad();
124+
// Wait for navigation to the notebook page — confirms the note was created server-side.
125+
// waitForPageLoad() (domcontentloaded) fires instantly on SPA routing and does not guarantee this.
126+
await this.page.waitForURL(/\/notebook\//, { timeout: 45000 });
148127
}
149128

150129
async clickImportNote(): Promise<void> {
151130
await this.nodeList.importNoteLink.click({ timeout: 15000 });
152131
}
153132

154133
async filterNotes(searchTerm: string): Promise<void> {
155-
await this.nodeList.filterInput.fill(searchTerm, { timeout: 15000 });
134+
await this.page.waitForLoadState('domcontentloaded', { timeout: 10000 });
135+
await this.nodeList.filterInput.waitFor({ state: 'visible', timeout: 5000 });
136+
// pressSequentially fires real key events so Angular's ngModel detects the change (fill() does not).
137+
// Triple-click to select all, then type to replace or Backspace to clear.
138+
await this.nodeList.filterInput.click({ clickCount: 3 });
139+
if (searchTerm) {
140+
await this.nodeList.filterInput.pressSequentially(searchTerm);
141+
} else {
142+
await this.nodeList.filterInput.press('Backspace');
143+
}
156144
}
157145

158146
async waitForRefreshToComplete(): Promise<void> {
159147
await this.waitForElementAttribute('a.refresh-note i[nz-icon]', 'nzSpin', false);
160148
}
161-
162-
async getDocumentationLinkHref(): Promise<string | null> {
163-
return this.externalLinks.documentation.getAttribute('href');
164-
}
165149
}

zeppelin-web-angular/e2e/models/node-list-page.ts

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,7 @@ export class NodeListPage extends BasePage {
4141
await this.createNewNoteButton.click();
4242
}
4343

44-
getFolderByName(folderName: string): Locator {
45-
return this.page.locator('nz-tree-node').filter({ hasText: folderName }).first();
46-
}
47-
48-
getNoteByName(noteName: string): Locator {
44+
private getNoteByName(noteName: string): Locator {
4945
return this.page.locator('nz-tree-node').filter({ hasText: noteName }).first();
5046
}
5147

@@ -56,14 +52,6 @@ export class NodeListPage extends BasePage {
5652
await noteLink.click();
5753
}
5854

59-
async isFilterInputVisible(): Promise<boolean> {
60-
return this.filterInput.isVisible();
61-
}
62-
63-
async isTrashFolderVisible(): Promise<boolean> {
64-
return this.trashFolder.isVisible();
65-
}
66-
6755
async getAllVisibleNoteNames(): Promise<string[]> {
6856
const noteElements = await this.notes.all();
6957
const names: string[] = [];

zeppelin-web-angular/e2e/models/note-create-modal.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,4 @@ export class NoteCreateModal extends BasePage {
4747
async clickCreate(): Promise<void> {
4848
await this.createButton.click();
4949
}
50-
51-
async isFolderInfoVisible(): Promise<boolean> {
52-
return this.folderInfoAlert.isVisible();
53-
}
5450
}

zeppelin-web-angular/e2e/models/note-import-modal.ts

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -59,16 +59,6 @@ export class NoteImportModal extends BasePage {
5959
await this.urlTab.click();
6060
}
6161

62-
async isJsonFileTabSelected(): Promise<boolean> {
63-
const ariaSelected = await this.jsonFileTab.getAttribute('aria-selected');
64-
return ariaSelected === 'true';
65-
}
66-
67-
async isUrlTabSelected(): Promise<boolean> {
68-
const ariaSelected = await this.urlTab.getAttribute('aria-selected');
69-
return ariaSelected === 'true';
70-
}
71-
7262
async setImportUrl(url: string): Promise<void> {
7363
await this.urlInput.fill(url);
7464
}
@@ -77,19 +67,7 @@ export class NoteImportModal extends BasePage {
7767
await this.importNoteButton.click();
7868
}
7969

80-
async isImportNoteButtonDisabled(): Promise<boolean> {
81-
return this.importNoteButton.isDisabled();
82-
}
83-
8470
async getFileSizeLimit(): Promise<string> {
8571
return (await this.fileSizeLimit.textContent()) || '';
8672
}
87-
88-
async isErrorAlertVisible(): Promise<boolean> {
89-
return this.errorAlert.isVisible();
90-
}
91-
92-
async getErrorMessage(): Promise<string> {
93-
return (await this.errorAlert.textContent()) || '';
94-
}
9573
}

zeppelin-web-angular/e2e/models/notebook-repo-item.util.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,12 @@ export class NotebookRepoItemUtil extends BasePage {
2424

2525
async verifyDisplayMode(): Promise<void> {
2626
await expect(this.repoItemPage.editButton).toBeVisible();
27-
const isEditMode = await this.repoItemPage.isEditMode();
28-
expect(isEditMode).toBe(false);
27+
await expect(this.repoItemPage.repositoryCard).not.toHaveClass(/\bedit\b/);
2928
}
3029

3130
async verifyEditMode(): Promise<void> {
3231
await expect(this.repoItemPage.saveButton).toBeVisible();
3332
await expect(this.repoItemPage.cancelButton).toBeVisible();
34-
const isEditMode = await this.repoItemPage.isEditMode();
35-
expect(isEditMode).toBe(true);
33+
await expect(this.repoItemPage.repositoryCard).toHaveClass(/\bedit\b/);
3634
}
3735
}

zeppelin-web-angular/e2e/models/notebook-repos-page.ts

Lines changed: 2 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,6 @@ export class NotebookReposPage extends BasePage {
3434
this.page.waitForSelector('zeppelin-notebook-repo-item', { state: 'visible' })
3535
]);
3636
}
37-
38-
async getRepositoryItemCount(): Promise<number> {
39-
return await this.repositoryItems.count();
40-
}
4137
}
4238

4339
export class NotebookRepoItemPage extends BasePage {
@@ -72,50 +68,22 @@ export class NotebookRepoItemPage extends BasePage {
7268
await this.cancelButton.click({ timeout: 15000 });
7369
}
7470

75-
async isEditMode(): Promise<boolean> {
76-
return await this.repositoryCard.evaluate(el => el.classList.contains('edit'));
77-
}
78-
79-
async isSaveButtonEnabled(): Promise<boolean> {
80-
return await this.saveButton.isEnabled();
81-
}
82-
83-
async getSettingValue(settingName: string): Promise<string> {
84-
const row = this.repositoryCard.locator('tbody tr').filter({ hasText: settingName });
85-
const valueCell = row.locator('td').nth(1);
86-
return (await valueCell.textContent()) || '';
87-
}
88-
8971
async fillSettingInput(settingName: string, value: string): Promise<void> {
9072
const row = this.repositoryCard.locator('tbody tr').filter({ hasText: settingName });
9173
const input = row.locator('input[nz-input]');
9274
await input.clear();
9375
await input.fill(value);
9476
}
9577

96-
async selectSettingDropdown(settingName: string, optionValue: string): Promise<void> {
97-
const row = this.repositoryCard.locator('tbody tr').filter({ hasText: settingName });
98-
const select = row.locator('nz-select');
99-
await select.click({ timeout: 15000 });
100-
await this.page.locator(`nz-option[nzvalue="${optionValue}"]`).click({ timeout: 15000 });
101-
}
102-
10378
async getSettingInputValue(settingName: string): Promise<string> {
10479
const row = this.repositoryCard.locator('tbody tr').filter({ hasText: settingName });
10580
const input = row.locator('input[nz-input]');
10681
return await input.inputValue();
10782
}
10883

109-
async isInputVisible(settingName: string): Promise<boolean> {
110-
const row = this.repositoryCard.locator('tbody tr').filter({ hasText: settingName });
111-
const input = row.locator('input[nz-input]');
112-
return await input.isVisible();
113-
}
114-
115-
async isDropdownVisible(settingName: string): Promise<boolean> {
84+
async getSettingValue(settingName: string): Promise<string> {
11685
const row = this.repositoryCard.locator('tbody tr').filter({ hasText: settingName });
117-
const select = row.locator('nz-select');
118-
return await select.isVisible();
86+
return (await row.locator('td').nth(1).textContent()) || '';
11987
}
12088

12189
async getSettingCount(): Promise<number> {

0 commit comments

Comments
 (0)