Skip to content

Commit c011b9f

Browse files
committed
update from e2e/notebook
1 parent 6010891 commit c011b9f

13 files changed

+164
-113
lines changed

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

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,73 @@ export const BASE_URL = 'http://localhost:4200';
1717

1818
export class BasePage {
1919
readonly page: Page;
20-
readonly e2eTestFolder: Locator;
20+
21+
// Common Zeppelin component locators
22+
readonly zeppelinNodeList: Locator;
23+
readonly zeppelinWorkspace: Locator;
24+
readonly zeppelinHeader: Locator;
25+
readonly zeppelinPageHeader: Locator;
2126

2227
constructor(page: Page) {
2328
this.page = page;
24-
this.e2eTestFolder = page.locator(`[data-testid="folder-${E2E_TEST_FOLDER}"]`);
29+
this.zeppelinNodeList = page.locator('zeppelin-node-list');
30+
this.zeppelinWorkspace = page.locator('zeppelin-workspace');
31+
this.zeppelinHeader = page.locator('zeppelin-header');
32+
this.zeppelinPageHeader = page.locator('zeppelin-page-header');
2533
}
2634

2735
async waitForPageLoad(): Promise<void> {
2836
await this.page.waitForLoadState('domcontentloaded', { timeout: 15000 });
2937
}
38+
39+
// Common navigation patterns
40+
async navigateToRoute(
41+
route: string,
42+
options?: { timeout?: number; waitUntil?: 'load' | 'domcontentloaded' | 'networkidle' }
43+
): Promise<void> {
44+
await this.page.goto(`/#${route}`, {
45+
waitUntil: 'domcontentloaded',
46+
timeout: 60000,
47+
...options
48+
});
49+
await this.waitForPageLoad();
50+
}
51+
52+
async navigateToHome(): Promise<void> {
53+
await this.navigateToRoute('/');
54+
}
55+
56+
getCurrentPath(): string {
57+
const url = new URL(this.page.url());
58+
return url.hash || url.pathname;
59+
}
60+
61+
async waitForUrlNotContaining(fragment: string): Promise<void> {
62+
await this.page.waitForURL(url => !url.toString().includes(fragment));
63+
}
64+
65+
// Common form interaction patterns
66+
async fillInput(locator: Locator, value: string, options?: { timeout?: number; force?: boolean }): Promise<void> {
67+
await locator.fill(value, { timeout: 15000, ...options });
68+
}
69+
70+
async clickElement(locator: Locator, options?: { timeout?: number; force?: boolean }): Promise<void> {
71+
await locator.click({ timeout: 15000, ...options });
72+
}
73+
74+
async getInputValue(locator: Locator): Promise<string> {
75+
return await locator.inputValue();
76+
}
77+
78+
async isElementVisible(locator: Locator): Promise<boolean> {
79+
return await locator.isVisible();
80+
}
81+
82+
async isElementEnabled(locator: Locator): Promise<boolean> {
83+
return await locator.isEnabled();
84+
}
85+
86+
async getElementText(locator: Locator): Promise<string> {
87+
return (await locator.textContent()) || '';
88+
}
3089
}

zeppelin-web-angular/e2e/models/folder-rename-page.util.ts

Lines changed: 18 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,29 +10,20 @@
1010
* limitations under the License.
1111
*/
1212

13-
import { expect, Page } from '@playwright/test';
13+
import { expect } from '@playwright/test';
1414
import { FolderRenamePage } from './folder-rename-page';
1515

1616
export class FolderRenamePageUtil {
17-
constructor(
18-
private readonly page: Page,
19-
private readonly folderRenamePage: FolderRenamePage
20-
) {}
17+
private folderRenamePage: FolderRenamePage;
2118

22-
private getFolderNode(folderName: string) {
23-
return this.page
24-
.locator('.node')
25-
.filter({
26-
has: this.page.locator('.folder .name', { hasText: folderName })
27-
})
28-
.first();
19+
constructor(folderRenamePage: FolderRenamePage) {
20+
this.folderRenamePage = folderRenamePage;
2921
}
3022

3123
async verifyRenameButtonIsVisible(folderName: string): Promise<void> {
3224
await this.folderRenamePage.hoverOverFolder(folderName);
3325
const folderNode = this.getFolderNode(folderName);
3426
const renameButton = folderNode.locator('.folder .operation a[nz-tooltip][nztooltiptitle="Rename folder"]');
35-
// Just verify the element exists in DOM, not visibility(for Webkit & Edge)
3627
await expect(renameButton).toHaveCount(1);
3728
}
3829

@@ -53,8 +44,6 @@ export class FolderRenamePageUtil {
5344

5445
async verifyRenameModalOpens(folderName: string): Promise<void> {
5546
await this.folderRenamePage.clickRenameMenuItem(folderName);
56-
57-
// Wait for modal to appear with extended timeout
5847
await expect(this.folderRenamePage.renameModal).toBeVisible({ timeout: 10000 });
5948
}
6049

@@ -71,21 +60,17 @@ export class FolderRenamePageUtil {
7160

7261
await this.folderRenamePage.clickConfirm();
7362

74-
// Wait for the modal to disappear
7563
await expect(this.folderRenamePage.renameModal).not.toBeVisible({ timeout: 10000 });
7664

77-
// Wait for the UI to update before reloading for the old name to disappear
78-
const oldFolder = this.page.locator('.folder .name', { hasText: oldName });
65+
const oldFolder = this.folderRenamePage.page.locator('.folder .name', { hasText: oldName });
7966
await expect(oldFolder).not.toBeVisible({ timeout: 10000 });
8067

81-
// Optional: Keep the reload as a final sanity check against the backend state
82-
await this.page.reload();
83-
await this.page.waitForLoadState('networkidle', { timeout: 15000 });
68+
await this.folderRenamePage.page.reload();
69+
await this.folderRenamePage.waitForPageLoad();
8470

8571
const baseNewName = newName.split('/').pop();
8672

87-
// Ensure the folder list is stable and contains the new folder after reload
88-
await this.page.waitForFunction(
73+
await this.folderRenamePage.page.waitForFunction(
8974
([expectedBaseName]) => {
9075
if (!expectedBaseName) {
9176
throw Error('Renamed Folder name is not exist.');
@@ -103,15 +88,12 @@ export class FolderRenamePageUtil {
10388
await this.folderRenamePage.clickRenameMenuItem(folderName);
10489
await this.folderRenamePage.clearNewName();
10590

106-
// NEW ASSERTION: The confirm button should be disabled when the input is empty.
10791
await expect(this.folderRenamePage.confirmButton).toBeDisabled({ timeout: 5000 });
10892

109-
// Clean up: Click cancel to close the modal after verifying validation.
11093
await this.folderRenamePage.clickCancel();
11194
await expect(this.folderRenamePage.renameModal).not.toBeVisible({ timeout: 5000 });
11295

113-
// Verify the original folder still exists and was not renamed or deleted.
114-
const originalFolderLocator = this.page.locator('.folder .name', { hasText: folderName });
96+
const originalFolderLocator = this.folderRenamePage.page.locator('.folder .name', { hasText: folderName });
11597
await expect(originalFolderLocator).toBeVisible({ timeout: 5000 });
11698
}
11799

@@ -127,4 +109,13 @@ export class FolderRenamePageUtil {
127109
await this.verifyRenameButtonIsVisible(folderName);
128110
await this.verifyDeleteButtonIsVisible(folderName);
129111
}
112+
113+
private getFolderNode(folderName: string) {
114+
return this.folderRenamePage.page
115+
.locator('.node')
116+
.filter({
117+
has: this.folderRenamePage.page.locator('.folder .name', { hasText: folderName })
118+
})
119+
.first();
120+
}
130121
}

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ export class HomePage extends BasePage {
3333
readonly notebookHeading: Locator;
3434
readonly helpHeading: Locator;
3535
readonly communityHeading: Locator;
36+
readonly createNoteModal: Locator;
37+
readonly createNoteButton: Locator;
38+
readonly notebookNameInput: Locator;
3639
readonly externalLinks: {
3740
documentation: Locator;
3841
mailingList: Locator;
@@ -80,6 +83,9 @@ export class HomePage extends BasePage {
8083
this.notebookHeading = this.notebookColumn.locator('h3');
8184
this.helpHeading = page.locator('h3').filter({ hasText: 'Help' });
8285
this.communityHeading = page.locator('h3').filter({ hasText: 'Community' });
86+
this.createNoteModal = page.locator('div.ant-modal-content');
87+
this.createNoteButton = this.createNoteModal.locator('button', { hasText: 'Create' });
88+
this.notebookNameInput = this.createNoteModal.locator('input[name="noteName"]');
8389

8490
this.externalLinks = {
8591
documentation: page.locator('a[href*="zeppelin.apache.org/docs"]'),
@@ -171,6 +177,25 @@ export class HomePage extends BasePage {
171177

172178
async clickCreateNewNote(): Promise<void> {
173179
await this.nodeList.createNewNoteLink.click();
180+
await this.createNoteModal.waitFor({ state: 'visible' });
181+
}
182+
183+
async createNote(notebookName: string): Promise<void> {
184+
await this.clickCreateNewNote();
185+
await this.fillInput(this.notebookNameInput, notebookName, { force: true });
186+
187+
// Wait for input value to be set (especially important for Webkit)
188+
await this.page.waitForFunction(
189+
name => {
190+
const input = document.querySelector('input[name="noteName"]') as HTMLInputElement;
191+
return input && input.value === name;
192+
},
193+
notebookName,
194+
{ timeout: 5000 }
195+
);
196+
197+
await this.clickElement(this.createNoteButton);
198+
await this.waitForPageLoad();
174199
}
175200

176201
async clickImportNote(): Promise<void> {

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

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@
1313
import { expect, Page } from '@playwright/test';
1414
import { getBasicPageMetadata } from '../utils';
1515
import { HomePage } from './home-page';
16+
import { BasePage } from './base-page';
1617

17-
export class HomePageUtil {
18+
export class HomePageUtil extends BasePage {
1819
private homePage: HomePage;
19-
private page: Page;
2020

2121
constructor(page: Page) {
22-
this.page = page;
22+
super(page);
2323
this.homePage = new HomePage(page);
2424
}
2525

@@ -31,7 +31,7 @@ export class HomePageUtil {
3131
}> {
3232
await this.homePage.navigateToLogin();
3333

34-
const currentPath = this.homePage.getCurrentPath();
34+
const currentPath = this.getCurrentPath();
3535
const isLoginUrlMaintained = currentPath.includes('#/login');
3636
const isHomeContentDisplayed = await this.homePage.isHomeContentDisplayed();
3737
const isAnonymousUser = await this.homePage.isAnonymousUser();
@@ -63,12 +63,12 @@ export class HomePageUtil {
6363
pathAfterClick: string;
6464
homeContentMaintained: boolean;
6565
}> {
66-
const pathBeforeClick = this.homePage.getCurrentPath();
66+
const pathBeforeClick = this.getCurrentPath();
6767

6868
await this.homePage.clickZeppelinLogo();
69-
await this.homePage.waitForPageLoad();
69+
await this.waitForPageLoad();
7070

71-
const pathAfterClick = this.homePage.getCurrentPath();
71+
const pathAfterClick = this.getCurrentPath();
7272
const homeContentMaintained = await this.homePage.isHomeContentDisplayed();
7373

7474
return {
@@ -99,7 +99,7 @@ export class HomePageUtil {
9999
const headingText = await this.homePage.getWelcomeHeadingText();
100100
expect(headingText.trim()).toBe('Welcome to Zeppelin!');
101101

102-
const welcomeText = await this.homePage.welcomeDescription.textContent();
102+
const welcomeText = await this.getElementText(this.homePage.welcomeDescription);
103103
expect(welcomeText).toContain('web-based notebook');
104104
expect(welcomeText).toContain('interactive data analytics');
105105
}
@@ -109,20 +109,17 @@ export class HomePageUtil {
109109
await expect(this.homePage.notebookHeading).toBeVisible();
110110
await expect(this.homePage.refreshNoteButton).toBeVisible();
111111

112-
// Wait for notebook list to load with timeout
113112
await this.page.waitForSelector('zeppelin-node-list', { timeout: 10000 });
114-
await expect(this.homePage.notebookList).toBeVisible();
113+
await expect(this.zeppelinNodeList).toBeVisible();
115114
}
116115

117116
async verifyNotebookRefreshFunctionality(): Promise<void> {
118117
await this.homePage.clickRefreshNotes();
119118

120-
// Wait for refresh operation to complete
121119
await this.homePage.waitForRefreshToComplete();
122120

123-
// Ensure the notebook list is still visible after refresh
124-
await expect(this.homePage.notebookList).toBeVisible();
125-
const isStillVisible = await this.homePage.isNotebookListVisible();
121+
await expect(this.zeppelinNodeList).toBeVisible();
122+
const isStillVisible = await this.isElementVisible(this.zeppelinNodeList);
126123
expect(isStillVisible).toBe(true);
127124
}
128125

@@ -142,7 +139,6 @@ export class HomePageUtil {
142139
issuesTrackingHref: string | null;
143140
githubHref: string | null;
144141
}> {
145-
// Get the parent links that contain the text
146142
const docLink = this.page.locator('a').filter({ hasText: 'Zeppelin documentation' });
147143
const mailLink = this.page.locator('a').filter({ hasText: 'Mailing list' });
148144
const issuesLink = this.page.locator('a').filter({ hasText: 'Issues tracking' });

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

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export class LoginPage extends BasePage {
1919
readonly loginButton: Locator;
2020
readonly welcomeTitle: Locator;
2121
readonly formContainer: Locator;
22+
readonly errorMessage: Locator;
2223

2324
constructor(page: Page) {
2425
super(page);
@@ -27,27 +28,24 @@ export class LoginPage extends BasePage {
2728
this.loginButton = page.getByRole('button', { name: 'Login' });
2829
this.welcomeTitle = page.getByRole('heading', { name: 'Welcome to Zeppelin!' });
2930
this.formContainer = page.locator('form[nz-form]');
31+
this.errorMessage = page.locator("text=The username and password that you entered don't match.").first();
3032
}
3133

3234
async navigate(): Promise<void> {
33-
await this.page.goto('/#/login');
34-
await this.waitForPageLoad();
35+
await this.navigateToRoute('/login');
3536
}
3637

3738
async login(username: string, password: string): Promise<void> {
38-
await this.userNameInput.fill(username);
39-
await this.passwordInput.fill(password);
40-
await this.loginButton.click();
39+
await this.fillInput(this.userNameInput, username);
40+
await this.fillInput(this.passwordInput, password);
41+
await this.clickElement(this.loginButton);
4142
}
4243

4344
async waitForErrorMessage(): Promise<void> {
44-
await this.page.waitForSelector("text=The username and password that you entered don't match.", { timeout: 5000 });
45+
await this.errorMessage.waitFor({ state: 'visible', timeout: 5000 });
4546
}
4647

4748
async getErrorMessageText(): Promise<string> {
48-
return (
49-
(await this.page.locator("text=The username and password that you entered don't match.").first().textContent()) ||
50-
''
51-
);
49+
return this.getElementText(this.errorMessage);
5250
}
5351
}

0 commit comments

Comments
 (0)