Skip to content

Commit 1662cbd

Browse files
authored
[ZEPPELIN-6358] simplify utils, promote POM usage, and consolidate base logic from #5101
### What is this PR for? ### PR Description This PR improves the readability and maintainability of the E2E notebook tests. - Removed over-abstracted util and wrapper methods - Moved test logic from util files into the test cases - Simplified page objects to focus on direct UI interactions - Consolidated shared logic into a base page class As a result, the tests are clearer, flatter, and easier to maintain. ### What type of PR is it? Refactoring ### Todos ### What is the Jira issue? ZEPPELIN-6358 ### How should this be tested? ### Screenshots (if appropriate) ### Questions: * Does the license files need to update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Closes #5131 from dididy/e2e/notebook-edited. Signed-off-by: ChanHo Lee <chanholee@apache.org>
1 parent 8251dc4 commit 1662cbd

31 files changed

Lines changed: 707 additions & 1219 deletions

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

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* limitations under the License.
1111
*/
1212

13-
import { Locator, Page } from '@playwright/test';
13+
import { expect, Locator, Page } from '@playwright/test';
1414

1515
export const E2E_TEST_FOLDER = 'E2E_TEST_FOLDER';
1616
export const BASE_URL = 'http://localhost:4200';
@@ -23,12 +23,32 @@ export class BasePage {
2323
readonly zeppelinPageHeader: Locator;
2424
readonly zeppelinHeader: Locator;
2525

26+
readonly modalTitle: Locator;
27+
readonly modalBody: Locator;
28+
readonly modalContent: Locator;
29+
30+
readonly okButton: Locator;
31+
readonly cancelButton: Locator;
32+
readonly runButton: Locator;
33+
34+
readonly welcomeTitle: Locator;
35+
2636
constructor(page: Page) {
2737
this.page = page;
2838
this.zeppelinNodeList = page.locator('zeppelin-node-list');
2939
this.zeppelinWorkspace = page.locator('zeppelin-workspace');
3040
this.zeppelinPageHeader = page.locator('zeppelin-page-header');
3141
this.zeppelinHeader = page.locator('zeppelin-header');
42+
43+
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');
46+
47+
this.okButton = page.locator('button:has-text("OK")');
48+
this.cancelButton = page.locator('button:has-text("Cancel")');
49+
this.runButton = page.locator('button:has-text("Run")');
50+
51+
this.welcomeTitle = page.getByRole('heading', { name: 'Welcome to Zeppelin!' });
3252
}
3353

3454
async waitForPageLoad(): Promise<void> {
@@ -63,4 +83,51 @@ export class BasePage {
6383
async getElementText(locator: Locator): Promise<string> {
6484
return (await locator.textContent()) || '';
6585
}
86+
87+
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+
);
96+
}
97+
98+
async waitForElementAttribute(
99+
selector: string,
100+
attribute: string,
101+
exists: boolean = true,
102+
timeout = 10000
103+
): Promise<void> {
104+
const locator = this.page.locator(selector);
105+
if (exists) {
106+
await expect(locator).toHaveAttribute(attribute, { timeout });
107+
} else {
108+
await expect(locator).not.toHaveAttribute(attribute, { timeout });
109+
}
110+
}
111+
112+
async waitForRouterOutletChild(timeout = 10000): Promise<void> {
113+
await expect(this.page.locator('zeppelin-workspace router-outlet + *')).toHaveCount(1, { timeout });
114+
}
115+
116+
async fillAndVerifyInput(
117+
locator: Locator,
118+
value: string,
119+
options?: { timeout?: number; clearFirst?: boolean }
120+
): Promise<void> {
121+
const { timeout = 10000, clearFirst = true } = options || {};
122+
123+
await expect(locator).toBeVisible({ timeout });
124+
await expect(locator).toBeEnabled({ timeout: 5000 });
125+
126+
if (clearFirst) {
127+
await locator.clear();
128+
}
129+
130+
await locator.fill(value);
131+
await expect(locator).toHaveValue(value);
132+
}
66133
}

zeppelin-web-angular/e2e/models/theme.page.ts renamed to zeppelin-web-angular/e2e/models/dark-mode-page.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,36 +11,36 @@
1111
*/
1212

1313
import { expect, Locator, Page } from '@playwright/test';
14+
import { BasePage } from './base-page';
1415

15-
export class ThemePage {
16-
readonly page: Page;
16+
export class DarkModePage extends BasePage {
1717
readonly themeToggleButton: Locator;
1818
readonly rootElement: Locator;
1919

2020
constructor(page: Page) {
21-
this.page = page;
21+
super(page);
2222
this.themeToggleButton = page.locator('zeppelin-theme-toggle button');
2323
this.rootElement = page.locator('html');
2424
}
2525

2626
async toggleTheme() {
27-
await this.themeToggleButton.click();
27+
await this.themeToggleButton.click({ timeout: 15000 });
2828
}
2929

3030
async assertDarkTheme() {
31-
await expect(this.rootElement).toHaveClass(/dark/);
31+
await expect(this.rootElement).toHaveClass(/dark/, { timeout: 10000 });
3232
await expect(this.rootElement).toHaveAttribute('data-theme', 'dark');
3333
await expect(this.themeToggleButton).toHaveText('dark_mode');
3434
}
3535

3636
async assertLightTheme() {
37-
await expect(this.rootElement).toHaveClass(/light/);
37+
await expect(this.rootElement).toHaveClass(/light/, { timeout: 10000 });
3838
await expect(this.rootElement).toHaveAttribute('data-theme', 'light');
3939
await expect(this.themeToggleButton).toHaveText('light_mode');
4040
}
4141

4242
async assertSystemTheme() {
43-
await expect(this.themeToggleButton).toHaveText('smart_toy');
43+
await expect(this.themeToggleButton).toHaveText('smart_toy', { timeout: 60000 });
4444
}
4545

4646
async setThemeInLocalStorage(theme: 'light' | 'dark' | 'system') {

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

Lines changed: 36 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,12 @@
1111
*/
1212

1313
import { expect, Locator, Page } from '@playwright/test';
14-
import { getCurrentPath, waitForUrlNotContaining } from '../utils';
1514
import { BasePage } from './base-page';
1615

1716
export class HomePage extends BasePage {
18-
readonly welcomeHeading: Locator;
1917
readonly notebookSection: Locator;
2018
readonly helpSection: Locator;
2119
readonly communitySection: Locator;
22-
readonly createNewNoteButton: Locator;
23-
readonly importNoteButton: Locator;
24-
readonly searchInput: Locator;
25-
readonly filterInput: Locator;
2620
readonly zeppelinLogo: Locator;
2721
readonly anonymousUserIndicator: Locator;
2822
readonly welcomeSection: Locator;
@@ -31,11 +25,12 @@ export class HomePage extends BasePage {
3125
readonly helpCommunityColumn: Locator;
3226
readonly welcomeDescription: Locator;
3327
readonly refreshNoteButton: Locator;
34-
readonly refreshIcon: Locator;
35-
readonly notebookList: Locator;
3628
readonly notebookHeading: Locator;
3729
readonly helpHeading: Locator;
3830
readonly communityHeading: Locator;
31+
readonly createNoteModal: Locator;
32+
readonly createNoteButton: Locator;
33+
readonly notebookNameInput: Locator;
3934
readonly externalLinks: {
4035
documentation: Locator;
4136
mailingList: Locator;
@@ -52,27 +47,13 @@ export class HomePage extends BasePage {
5247
clearOutput: Locator;
5348
moveToTrash: Locator;
5449
};
55-
folderActions: {
56-
createNote: Locator;
57-
renameFolder: Locator;
58-
moveToTrash: Locator;
59-
};
60-
trashActions: {
61-
restoreAll: Locator;
62-
emptyAll: Locator;
63-
};
6450
};
6551

6652
constructor(page: Page) {
6753
super(page);
68-
this.welcomeHeading = page.locator('h1', { hasText: 'Welcome to Zeppelin!' });
6954
this.notebookSection = page.locator('text=Notebook').first();
7055
this.helpSection = page.locator('text=Help').first();
7156
this.communitySection = page.locator('text=Community').first();
72-
this.createNewNoteButton = page.locator('text=Create new Note');
73-
this.importNoteButton = page.locator('text=Import Note');
74-
this.searchInput = page.locator('textbox', { hasText: 'Search' });
75-
this.filterInput = page.locator('input[placeholder*="Filter"]');
7657
this.zeppelinLogo = page.locator('text=Zeppelin').first();
7758
this.anonymousUserIndicator = page.locator('text=anonymous');
7859
this.welcomeSection = page.locator('.welcome');
@@ -81,11 +62,12 @@ export class HomePage extends BasePage {
8162
this.helpCommunityColumn = page.locator('[nz-col]').last();
8263
this.welcomeDescription = page.locator('.welcome').getByText('Zeppelin is web-based notebook');
8364
this.refreshNoteButton = page.locator('a.refresh-note');
84-
this.refreshIcon = page.locator('a.refresh-note i[nz-icon]');
85-
this.notebookList = page.locator('zeppelin-node-list');
8665
this.notebookHeading = this.notebookColumn.locator('h3');
8766
this.helpHeading = page.locator('h3').filter({ hasText: 'Help' });
8867
this.communityHeading = page.locator('h3').filter({ hasText: 'Community' });
68+
this.createNoteModal = page.locator('div.ant-modal-content');
69+
this.createNoteButton = this.createNoteModal.locator('button', { hasText: 'Create' });
70+
this.notebookNameInput = this.createNoteModal.locator('input[name="noteName"]');
8971

9072
this.externalLinks = {
9173
documentation: page.locator('a[href*="zeppelin.apache.org/docs"]'),
@@ -103,67 +85,30 @@ export class HomePage extends BasePage {
10385
renameNote: page.locator('.file .operation a[nztooltiptitle*="Rename note"]'),
10486
clearOutput: page.locator('.file .operation a[nztooltiptitle*="Clear output"]'),
10587
moveToTrash: page.locator('.file .operation a[nztooltiptitle*="Move note to Trash"]')
106-
},
107-
folderActions: {
108-
createNote: page.locator('.folder .operation a[nztooltiptitle*="Create new note"]'),
109-
renameFolder: page.locator('.folder .operation a[nztooltiptitle*="Rename folder"]'),
110-
moveToTrash: page.locator('.folder .operation a[nztooltiptitle*="Move folder to Trash"]')
111-
},
112-
trashActions: {
113-
restoreAll: page.locator('.folder .operation a[nztooltiptitle*="Restore all"]'),
114-
emptyAll: page.locator('.folder .operation a[nztooltiptitle*="Empty all"]')
11588
}
11689
};
11790
}
11891

119-
async navigateToHome(): Promise<void> {
120-
await this.page.goto('/', { waitUntil: 'load' });
121-
await this.waitForPageLoad();
122-
}
123-
12492
async navigateToLogin(): Promise<void> {
125-
await this.page.goto('/#/login', { waitUntil: 'load' });
126-
await this.waitForPageLoad();
93+
await this.navigateToRoute('/login');
12794
// Wait for potential redirect to complete by checking URL change
128-
await waitForUrlNotContaining(this.page, '#/login');
95+
await this.waitForUrlNotContaining('#/login');
12996
}
13097

13198
async isHomeContentDisplayed(): Promise<boolean> {
132-
try {
133-
await expect(this.welcomeHeading).toBeVisible();
134-
return true;
135-
} catch {
136-
return false;
137-
}
99+
return this.welcomeTitle.isVisible();
138100
}
139101

140102
async isAnonymousUser(): Promise<boolean> {
141-
try {
142-
await expect(this.anonymousUserIndicator).toBeVisible();
143-
return true;
144-
} catch {
145-
return false;
146-
}
103+
return this.anonymousUserIndicator.isVisible();
147104
}
148105

149106
async clickZeppelinLogo(): Promise<void> {
150-
await this.zeppelinLogo.click();
151-
}
152-
153-
async getCurrentURL(): Promise<string> {
154-
return this.page.url();
155-
}
156-
157-
getCurrentPath(): string {
158-
return getCurrentPath(this.page);
159-
}
160-
161-
async getPageTitle(): Promise<string> {
162-
return this.page.title();
107+
await this.zeppelinLogo.click({ timeout: 15000 });
163108
}
164109

165110
async getWelcomeHeadingText(): Promise<string> {
166-
const text = await this.welcomeHeading.textContent();
111+
const text = await this.welcomeTitle.textContent();
167112
return text || '';
168113
}
169114

@@ -173,65 +118,48 @@ export class HomePage extends BasePage {
173118
}
174119

175120
async clickRefreshNotes(): Promise<void> {
176-
await this.refreshNoteButton.click();
121+
await this.refreshNoteButton.click({ timeout: 15000 });
177122
}
178123

179124
async isNotebookListVisible(): Promise<boolean> {
180-
return this.notebookList.isVisible();
125+
return this.zeppelinNodeList.isVisible();
181126
}
182127

183128
async clickCreateNewNote(): Promise<void> {
184-
await this.nodeList.createNewNoteLink.click();
129+
await this.nodeList.createNewNoteLink.click({ timeout: 15000 });
130+
await this.createNoteModal.waitFor({ state: 'visible' });
185131
}
186132

187-
async clickImportNote(): Promise<void> {
188-
await this.nodeList.importNoteLink.click();
189-
}
133+
async createNote(notebookName: string): Promise<void> {
134+
await this.clickCreateNewNote();
190135

191-
async filterNotes(searchTerm: string): Promise<void> {
192-
await this.nodeList.filterInput.fill(searchTerm);
193-
}
136+
// Wait for the modal form to be fully rendered with proper labels
137+
await this.page.waitForSelector('nz-form-label', { timeout: 10000 });
194138

195-
async isRefreshIconSpinning(): Promise<boolean> {
196-
const spinAttribute = await this.refreshIcon.getAttribute('nzSpin');
197-
return spinAttribute === 'true' || spinAttribute === '';
198-
}
139+
await this.waitForFormLabels(['Note Name', 'Clone Note']);
199140

200-
async waitForRefreshToComplete(): Promise<void> {
201-
await this.page.waitForFunction(
202-
() => {
203-
const icon = document.querySelector('a.refresh-note i[nz-icon]');
204-
return icon && !icon.hasAttribute('nzSpin');
205-
},
206-
{ timeout: 10000 }
207-
);
141+
// Fill and verify the notebook name input
142+
await this.fillAndVerifyInput(this.notebookNameInput, notebookName);
143+
144+
// Click the 'Create' button in the modal
145+
await expect(this.createNoteButton).toBeEnabled({ timeout: 5000 });
146+
await this.createNoteButton.click({ timeout: 15000 });
147+
await this.waitForPageLoad();
208148
}
209149

210-
async getDocumentationLinkHref(): Promise<string | null> {
211-
return this.externalLinks.documentation.getAttribute('href');
150+
async clickImportNote(): Promise<void> {
151+
await this.nodeList.importNoteLink.click({ timeout: 15000 });
212152
}
213153

214-
async areExternalLinksVisible(): Promise<boolean> {
215-
const links = [
216-
this.externalLinks.documentation,
217-
this.externalLinks.mailingList,
218-
this.externalLinks.issuesTracking,
219-
this.externalLinks.github
220-
];
221-
222-
for (const link of links) {
223-
if (!(await link.isVisible())) {
224-
return false;
225-
}
226-
}
227-
return true;
154+
async filterNotes(searchTerm: string): Promise<void> {
155+
await this.nodeList.filterInput.fill(searchTerm, { timeout: 15000 });
228156
}
229157

230-
async isWelcomeSectionVisible(): Promise<boolean> {
231-
return this.welcomeSection.isVisible();
158+
async waitForRefreshToComplete(): Promise<void> {
159+
await this.waitForElementAttribute('a.refresh-note i[nz-icon]', 'nzSpin', false);
232160
}
233161

234-
async isMoreInfoGridVisible(): Promise<boolean> {
235-
return this.moreInfoGrid.isVisible();
162+
async getDocumentationLinkHref(): Promise<string | null> {
163+
return this.externalLinks.documentation.getAttribute('href');
236164
}
237165
}

0 commit comments

Comments
 (0)