This is an automated email from the ASF dual-hosted git repository.
chanholee pushed a commit to branch branch-0.12
in repository https://gitbox.apache.org/repos/asf/zeppelin.git
The following commit(s) were added to refs/heads/branch-0.12 by this push:
new b776fd67b2 [ZEPPELIN-6359] Add E2E tests about Home/Dashboard Page for
New UI
b776fd67b2 is described below
commit b776fd67b206d0ae454e70cfc225c97b12a6361f
Author: YONGJAE LEE(이용재) <[email protected]>
AuthorDate: Sun Oct 26 17:16:13 2025 +0900
[ZEPPELIN-6359] Add E2E tests about Home/Dashboard Page for New UI
### What is this PR for?
Addition and improvement of Home/Dashboard-related E2E tests for New UI
---
PAGES.WORKSPACE.HOME
→ src/app/pages/workspace/home/home.component
PAGES.WORKSPACE.MAIN
→ src/app/pages/workspace/workspace.component
### What type of PR is it?
Improvement
### Todos
### What is the Jira issue?
ZEPPELIN-6359
### 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 #5102 from dididy/e2e/home.
Signed-off-by: ChanHo Lee <[email protected]>
(cherry picked from commit 8432cf66e73b39d003b169cd86d8604e871349e6)
Signed-off-by: ChanHo Lee <[email protected]>
---
zeppelin-web-angular/e2e/models/home-page.ts | 155 ++++++++++--
zeppelin-web-angular/e2e/models/home-page.util.ts | 184 +++++++++++----
zeppelin-web-angular/e2e/models/workspace-page.ts | 32 +++
.../e2e/models/workspace-page.util.ts | 74 ++++++
.../anonymous-login-redirect.spec.ts | 8 +-
.../e2e/tests/home/home-page-elements.spec.ts | 172 ++++++++++++++
.../home/home-page-enhanced-functionality.spec.ts | 64 +++++
.../tests/home/home-page-external-links.spec.ts | 169 ++++++++++++++
.../e2e/tests/home/home-page-layout.spec.ts | 139 +++++++++++
.../tests/home/home-page-note-operations.spec.ts | 260 +++++++++++++++++++++
.../tests/home/home-page-notebook-actions.spec.ts | 68 ++++++
.../e2e/tests/workspace/workspace-main.spec.ts | 69 ++++++
12 files changed, 1333 insertions(+), 61 deletions(-)
diff --git a/zeppelin-web-angular/e2e/models/home-page.ts
b/zeppelin-web-angular/e2e/models/home-page.ts
index 7d24fdf3ed..872784dfa0 100644
--- a/zeppelin-web-angular/e2e/models/home-page.ts
+++ b/zeppelin-web-angular/e2e/models/home-page.ts
@@ -10,9 +10,9 @@
* limitations under the License.
*/
-import { Locator, Page, expect } from '@playwright/test';
-import { BasePage } from './base-page';
+import { expect, Locator, Page } from '@playwright/test';
import { getCurrentPath, waitForUrlNotContaining } from '../utils';
+import { BasePage } from './base-page';
export class HomePage extends BasePage {
readonly welcomeHeading: Locator;
@@ -25,19 +25,43 @@ export class HomePage extends BasePage {
readonly filterInput: Locator;
readonly zeppelinLogo: Locator;
readonly anonymousUserIndicator: Locator;
- readonly tutorialNotebooks: {
- flinkTutorial: Locator;
- pythonTutorial: Locator;
- sparkTutorial: Locator;
- rTutorial: Locator;
- miscellaneousTutorial: Locator;
- };
+ readonly welcomeSection: Locator;
+ readonly moreInfoGrid: Locator;
+ readonly notebookColumn: Locator;
+ readonly helpCommunityColumn: Locator;
+ readonly welcomeDescription: Locator;
+ readonly refreshNoteButton: Locator;
+ readonly refreshIcon: Locator;
+ readonly notebookList: Locator;
+ readonly notebookHeading: Locator;
+ readonly helpHeading: Locator;
+ readonly communityHeading: Locator;
readonly externalLinks: {
documentation: Locator;
mailingList: Locator;
issuesTracking: Locator;
github: Locator;
};
+ readonly nodeList: {
+ createNewNoteLink: Locator;
+ importNoteLink: Locator;
+ filterInput: Locator;
+ tree: Locator;
+ noteActions: {
+ renameNote: Locator;
+ clearOutput: Locator;
+ moveToTrash: Locator;
+ };
+ folderActions: {
+ createNote: Locator;
+ renameFolder: Locator;
+ moveToTrash: Locator;
+ };
+ trashActions: {
+ restoreAll: Locator;
+ emptyAll: Locator;
+ };
+ };
constructor(page: Page) {
super(page);
@@ -51,14 +75,17 @@ export class HomePage extends BasePage {
this.filterInput = page.locator('input[placeholder*="Filter"]');
this.zeppelinLogo = page.locator('text=Zeppelin').first();
this.anonymousUserIndicator = page.locator('text=anonymous');
-
- this.tutorialNotebooks = {
- flinkTutorial: page.locator('text=Flink Tutorial'),
- pythonTutorial: page.locator('text=Python Tutorial'),
- sparkTutorial: page.locator('text=Spark Tutorial'),
- rTutorial: page.locator('text=R Tutorial'),
- miscellaneousTutorial: page.locator('text=Miscellaneous Tutorial')
- };
+ this.welcomeSection = page.locator('.welcome');
+ this.moreInfoGrid = page.locator('.more-info');
+ this.notebookColumn = page.locator('[nz-col]').first();
+ this.helpCommunityColumn = page.locator('[nz-col]').last();
+ this.welcomeDescription = page.locator('.welcome').getByText('Zeppelin is
web-based notebook');
+ this.refreshNoteButton = page.locator('a.refresh-note');
+ this.refreshIcon = page.locator('a.refresh-note i[nz-icon]');
+ this.notebookList = page.locator('zeppelin-node-list');
+ this.notebookHeading = this.notebookColumn.locator('h3');
+ this.helpHeading = page.locator('h3').filter({ hasText: 'Help' });
+ this.communityHeading = page.locator('h3').filter({ hasText: 'Community'
});
this.externalLinks = {
documentation: page.locator('a[href*="zeppelin.apache.org/docs"]'),
@@ -66,6 +93,27 @@ export class HomePage extends BasePage {
issuesTracking: page.locator('a[href*="issues.apache.org"]'),
github: page.locator('a[href*="github.com/apache/zeppelin"]')
};
+
+ this.nodeList = {
+ createNewNoteLink: page.locator('zeppelin-node-list a').filter({
hasText: 'Create new Note' }),
+ importNoteLink: page.locator('zeppelin-node-list a').filter({ hasText:
'Import Note' }),
+ filterInput: page.locator('zeppelin-node-list
input[placeholder*="Filter"]'),
+ tree: page.locator('zeppelin-node-list nz-tree'),
+ noteActions: {
+ renameNote: page.locator('.file .operation a[nztooltiptitle*="Rename
note"]'),
+ clearOutput: page.locator('.file .operation a[nztooltiptitle*="Clear
output"]'),
+ moveToTrash: page.locator('.file .operation a[nztooltiptitle*="Move
note to Trash"]')
+ },
+ folderActions: {
+ createNote: page.locator('.folder .operation a[nztooltiptitle*="Create
new note"]'),
+ renameFolder: page.locator('.folder .operation
a[nztooltiptitle*="Rename folder"]'),
+ moveToTrash: page.locator('.folder .operation a[nztooltiptitle*="Move
folder to Trash"]')
+ },
+ trashActions: {
+ restoreAll: page.locator('.folder .operation
a[nztooltiptitle*="Restore all"]'),
+ emptyAll: page.locator('.folder .operation a[nztooltiptitle*="Empty
all"]')
+ }
+ };
}
async navigateToHome(): Promise<void> {
@@ -113,4 +161,77 @@ export class HomePage extends BasePage {
async getPageTitle(): Promise<string> {
return this.page.title();
}
+
+ async getWelcomeHeadingText(): Promise<string> {
+ const text = await this.welcomeHeading.textContent();
+ return text || '';
+ }
+
+ async getWelcomeDescriptionText(): Promise<string> {
+ const text = await this.welcomeDescription.textContent();
+ return text || '';
+ }
+
+ async clickRefreshNotes(): Promise<void> {
+ await this.refreshNoteButton.click();
+ }
+
+ async isNotebookListVisible(): Promise<boolean> {
+ return this.notebookList.isVisible();
+ }
+
+ async clickCreateNewNote(): Promise<void> {
+ await this.nodeList.createNewNoteLink.click();
+ }
+
+ async clickImportNote(): Promise<void> {
+ await this.nodeList.importNoteLink.click();
+ }
+
+ async filterNotes(searchTerm: string): Promise<void> {
+ await this.nodeList.filterInput.fill(searchTerm);
+ }
+
+ async isRefreshIconSpinning(): Promise<boolean> {
+ const spinAttribute = await this.refreshIcon.getAttribute('nzSpin');
+ return spinAttribute === 'true' || spinAttribute === '';
+ }
+
+ async waitForRefreshToComplete(): Promise<void> {
+ await this.page.waitForFunction(
+ () => {
+ const icon = document.querySelector('a.refresh-note i[nz-icon]');
+ return icon && !icon.hasAttribute('nzSpin');
+ },
+ { timeout: 10000 }
+ );
+ }
+
+ async getDocumentationLinkHref(): Promise<string | null> {
+ return this.externalLinks.documentation.getAttribute('href');
+ }
+
+ async areExternalLinksVisible(): Promise<boolean> {
+ const links = [
+ this.externalLinks.documentation,
+ this.externalLinks.mailingList,
+ this.externalLinks.issuesTracking,
+ this.externalLinks.github
+ ];
+
+ for (const link of links) {
+ if (!(await link.isVisible())) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ async isWelcomeSectionVisible(): Promise<boolean> {
+ return this.welcomeSection.isVisible();
+ }
+
+ async isMoreInfoGridVisible(): Promise<boolean> {
+ return this.moreInfoGrid.isVisible();
+ }
}
diff --git a/zeppelin-web-angular/e2e/models/home-page.util.ts
b/zeppelin-web-angular/e2e/models/home-page.util.ts
index 4211a722c0..5a5a6ff210 100644
--- a/zeppelin-web-angular/e2e/models/home-page.util.ts
+++ b/zeppelin-web-angular/e2e/models/home-page.util.ts
@@ -10,9 +10,9 @@
* limitations under the License.
*/
-import { Page, expect } from '@playwright/test';
+import { expect, Page } from '@playwright/test';
+import { getBasicPageMetadata } from '../utils';
import { HomePage } from './home-page';
-import { getBasicPageMetadata, waitForUrlNotContaining } from '../utils';
export class HomePageUtil {
private homePage: HomePage;
@@ -44,13 +44,6 @@ export class HomePageUtil {
};
}
- async verifyHomePageIntegrity(): Promise<void> {
- await this.verifyHomePageElements();
- await this.verifyNotebookFunctionalities();
- await this.verifyTutorialNotebooks();
- await this.verifyExternalLinks();
- }
-
async verifyHomePageElements(): Promise<void> {
await expect(this.homePage.welcomeHeading).toBeVisible();
await expect(this.homePage.notebookSection).toBeVisible();
@@ -58,34 +51,11 @@ export class HomePageUtil {
await expect(this.homePage.communitySection).toBeVisible();
}
- async verifyNotebookFunctionalities(): Promise<void> {
- await expect(this.homePage.createNewNoteButton).toBeVisible();
- await expect(this.homePage.importNoteButton).toBeVisible();
-
- const filterInputCount = await this.homePage.filterInput.count();
- if (filterInputCount > 0) {
- await expect(this.homePage.filterInput).toBeVisible();
- }
- }
-
- async verifyTutorialNotebooks(): Promise<void> {
- await expect(this.homePage.tutorialNotebooks.flinkTutorial).toBeVisible();
- await expect(this.homePage.tutorialNotebooks.pythonTutorial).toBeVisible();
- await expect(this.homePage.tutorialNotebooks.sparkTutorial).toBeVisible();
- await expect(this.homePage.tutorialNotebooks.rTutorial).toBeVisible();
- await
expect(this.homePage.tutorialNotebooks.miscellaneousTutorial).toBeVisible();
- }
-
async verifyExternalLinks(): Promise<void> {
- const docCount = await this.homePage.externalLinks.documentation.count();
- const mailCount = await this.homePage.externalLinks.mailingList.count();
- const issuesCount = await
this.homePage.externalLinks.issuesTracking.count();
- const githubCount = await this.homePage.externalLinks.github.count();
-
- if (docCount > 0) await
expect(this.homePage.externalLinks.documentation).toBeVisible();
- if (mailCount > 0) await
expect(this.homePage.externalLinks.mailingList).toBeVisible();
- if (issuesCount > 0) await
expect(this.homePage.externalLinks.issuesTracking).toBeVisible();
- if (githubCount > 0) await
expect(this.homePage.externalLinks.github).toBeVisible();
+ await expect(this.homePage.externalLinks.documentation).toBeVisible();
+ await expect(this.homePage.externalLinks.mailingList).toBeVisible();
+ await expect(this.homePage.externalLinks.issuesTracking).toBeVisible();
+ await expect(this.homePage.externalLinks.github).toBeVisible();
}
async testNavigationConsistency(): Promise<{
@@ -108,7 +78,7 @@ export class HomePageUtil {
};
}
- async getPageMetadata(): Promise<{
+ async getHomePageMetadata(): Promise<{
title: string;
path: string;
isAnonymous: boolean;
@@ -122,8 +92,142 @@ export class HomePageUtil {
};
}
- async navigateToLoginAndWaitForRedirect(): Promise<void> {
- await this.page.goto('/#/login', { waitUntil: 'load' });
- await waitForUrlNotContaining(this.page, '#/login');
+ async verifyWelcomeSection(): Promise<void> {
+ await expect(this.homePage.welcomeSection).toBeVisible();
+ await expect(this.homePage.welcomeHeading).toBeVisible();
+
+ const headingText = await this.homePage.getWelcomeHeadingText();
+ expect(headingText.trim()).toBe('Welcome to Zeppelin!');
+
+ const welcomeText = await this.homePage.welcomeDescription.textContent();
+ expect(welcomeText).toContain('web-based notebook');
+ expect(welcomeText).toContain('interactive data analytics');
+ }
+
+ async verifyNotebookSection(): Promise<void> {
+ await expect(this.homePage.notebookSection).toBeVisible();
+ await expect(this.homePage.notebookHeading).toBeVisible();
+ await expect(this.homePage.refreshNoteButton).toBeVisible();
+
+ // Wait for notebook list to load with timeout
+ await this.page.waitForSelector('zeppelin-node-list', { timeout: 10000 });
+ await expect(this.homePage.notebookList).toBeVisible();
+
+ // Additional wait for content to load
+ await this.page.waitForTimeout(1000);
+ }
+
+ async verifyNotebookRefreshFunctionality(): Promise<void> {
+ await this.homePage.clickRefreshNotes();
+
+ // Wait for refresh operation to complete
+ await this.page.waitForTimeout(2000);
+
+ // Ensure the notebook list is still visible after refresh
+ await expect(this.homePage.notebookList).toBeVisible();
+ const isStillVisible = await this.homePage.isNotebookListVisible();
+ expect(isStillVisible).toBe(true);
+ }
+
+ async verifyHelpSection(): Promise<void> {
+ await expect(this.homePage.helpSection).toBeVisible();
+ await expect(this.homePage.helpHeading).toBeVisible();
+ }
+
+ async verifyCommunitySection(): Promise<void> {
+ await expect(this.homePage.communitySection).toBeVisible();
+ await expect(this.homePage.communityHeading).toBeVisible();
+ }
+
+ async testExternalLinkTargets(): Promise<{
+ documentationHref: string | null;
+ mailingListHref: string | null;
+ issuesTrackingHref: string | null;
+ githubHref: string | null;
+ }> {
+ // Get the parent links that contain the text
+ const docLink = this.page.locator('a').filter({ hasText: 'Zeppelin
documentation' });
+ const mailLink = this.page.locator('a').filter({ hasText: 'Mailing list'
});
+ const issuesLink = this.page.locator('a').filter({ hasText: 'Issues
tracking' });
+ const githubLink = this.page.locator('a').filter({ hasText: 'Github' });
+
+ return {
+ documentationHref: await docLink.getAttribute('href'),
+ mailingListHref: await mailLink.getAttribute('href'),
+ issuesTrackingHref: await issuesLink.getAttribute('href'),
+ githubHref: await githubLink.getAttribute('href')
+ };
+ }
+
+ async verifyNotebookActions(): Promise<void> {
+ await expect(this.homePage.nodeList.createNewNoteLink).toBeVisible();
+ await expect(this.homePage.nodeList.importNoteLink).toBeVisible();
+ await expect(this.homePage.nodeList.filterInput).toBeVisible();
+ await expect(this.homePage.nodeList.tree).toBeVisible();
+ }
+
+ async testNotebookRefreshLoadingState(): Promise<void> {
+ const refreshButton = this.page.locator('a.refresh-note');
+ const refreshIcon = this.page.locator('a.refresh-note i[nz-icon]');
+
+ await expect(refreshButton).toBeVisible();
+ await expect(refreshIcon).toBeVisible();
+
+ await this.homePage.clickRefreshNotes();
+
+ await this.page.waitForTimeout(500);
+
+ await expect(refreshIcon).toBeVisible();
+ }
+
+ async verifyCreateNewNoteWorkflow(): Promise<void> {
+ await this.homePage.clickCreateNewNote();
+
+ await this.page.waitForFunction(
+ () => {
+ return document.querySelector('zeppelin-note-create') !== null;
+ },
+ { timeout: 10000 }
+ );
+ }
+
+ async verifyImportNoteWorkflow(): Promise<void> {
+ await this.homePage.clickImportNote();
+
+ await this.page.waitForFunction(
+ () => {
+ return document.querySelector('zeppelin-note-import') !== null;
+ },
+ { timeout: 10000 }
+ );
+ }
+
+ async testFilterFunctionality(filterTerm: string): Promise<void> {
+ await this.homePage.filterNotes(filterTerm);
+
+ await this.page.waitForTimeout(1000);
+
+ const filteredResults = await this.page.locator('nz-tree .node').count();
+ expect(filteredResults).toBeGreaterThanOrEqual(0);
+ }
+
+ async verifyDocumentationVersionLink(): Promise<void> {
+ const href = await this.homePage.getDocumentationLinkHref();
+ expect(href).toContain('zeppelin.apache.org/docs');
+ expect(href).toMatch(/\/docs\/\d+\.\d+\.\d+(-SNAPSHOT)?\//);
+ }
+
+ async verifyAllExternalLinksTargetBlank(): Promise<void> {
+ const links = [
+ this.homePage.externalLinks.documentation,
+ this.homePage.externalLinks.mailingList,
+ this.homePage.externalLinks.issuesTracking,
+ this.homePage.externalLinks.github
+ ];
+
+ for (const link of links) {
+ const target = await link.getAttribute('target');
+ expect(target).toBe('_blank');
+ }
}
}
diff --git a/zeppelin-web-angular/e2e/models/workspace-page.ts
b/zeppelin-web-angular/e2e/models/workspace-page.ts
new file mode 100644
index 0000000000..57c0da8796
--- /dev/null
+++ b/zeppelin-web-angular/e2e/models/workspace-page.ts
@@ -0,0 +1,32 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Locator, Page } from '@playwright/test';
+import { BasePage } from './base-page';
+
+export class WorkspacePage extends BasePage {
+ readonly workspaceComponent: Locator;
+ readonly header: Locator;
+ readonly routerOutlet: Locator;
+
+ constructor(page: Page) {
+ super(page);
+ this.workspaceComponent = page.locator('zeppelin-workspace');
+ this.header = page.locator('zeppelin-header');
+ this.routerOutlet = page.locator('zeppelin-workspace router-outlet');
+ }
+
+ async navigateToWorkspace(): Promise<void> {
+ await this.page.goto('/', { waitUntil: 'load' });
+ await this.waitForPageLoad();
+ }
+}
diff --git a/zeppelin-web-angular/e2e/models/workspace-page.util.ts
b/zeppelin-web-angular/e2e/models/workspace-page.util.ts
new file mode 100644
index 0000000000..7ff706f93a
--- /dev/null
+++ b/zeppelin-web-angular/e2e/models/workspace-page.util.ts
@@ -0,0 +1,74 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { expect, Page } from '@playwright/test';
+import { performLoginIfRequired, waitForZeppelinReady } from '../utils';
+import { WorkspacePage } from './workspace-page';
+
+export class WorkspaceTestUtil {
+ private page: Page;
+ private workspacePage: WorkspacePage;
+
+ constructor(page: Page) {
+ this.page = page;
+ this.workspacePage = new WorkspacePage(page);
+ }
+
+ async navigateAndWaitForLoad(): Promise<void> {
+ await this.workspacePage.navigateToWorkspace();
+ await waitForZeppelinReady(this.page);
+ await performLoginIfRequired(this.page);
+ }
+
+ async verifyWorkspaceLayout(): Promise<void> {
+ await expect(this.workspacePage.workspaceComponent).toBeVisible();
+ await expect(this.workspacePage.routerOutlet).toBeAttached();
+ }
+
+ async verifyHeaderVisibility(shouldBeVisible: boolean): Promise<void> {
+ if (shouldBeVisible) {
+ await expect(this.workspacePage.header).toBeVisible();
+ } else {
+ await expect(this.workspacePage.header).toBeHidden();
+ }
+ }
+
+ async verifyWorkspaceContainer(): Promise<void> {
+ await expect(this.workspacePage.workspaceComponent).toBeVisible();
+ const contentElements = await this.page.locator('.content').count();
+ expect(contentElements).toBeGreaterThan(0);
+ }
+
+ async verifyRouterOutletActivation(): Promise<void> {
+ await expect(this.workspacePage.routerOutlet).toBeAttached();
+
+ await this.page.waitForFunction(
+ () => {
+ const workspace = document.querySelector('zeppelin-workspace');
+ const outlet = workspace?.querySelector('router-outlet');
+ return outlet && outlet.nextElementSibling !== null;
+ },
+ { timeout: 10000 }
+ );
+ }
+
+ async waitForComponentActivation(): Promise<void> {
+ await this.page.waitForFunction(
+ () => {
+ const workspace = document.querySelector('zeppelin-workspace');
+ const content = workspace?.querySelector('.content');
+ return content && content.children.length > 1;
+ },
+ { timeout: 15000 }
+ );
+ }
+}
diff --git
a/zeppelin-web-angular/e2e/tests/authentication/anonymous-login-redirect.spec.ts
b/zeppelin-web-angular/e2e/tests/authentication/anonymous-login-redirect.spec.ts
index ce66ce2f74..c123a48fb9 100644
---
a/zeppelin-web-angular/e2e/tests/authentication/anonymous-login-redirect.spec.ts
+++
b/zeppelin-web-angular/e2e/tests/authentication/anonymous-login-redirect.spec.ts
@@ -62,7 +62,7 @@ test.describe('Anonymous User Login Redirect', () => {
await waitForZeppelinReady(page);
await page.waitForURL(url => !url.toString().includes('#/login'));
- await homePageUtil.verifyHomePageIntegrity();
+ await homePageUtil.verifyHomePageElements();
});
test('When clicking Zeppelin logo after redirect, Then should maintain
home URL and content', async ({ page }) => {
@@ -83,7 +83,7 @@ test.describe('Anonymous User Login Redirect', () => {
await waitForZeppelinReady(page);
await page.waitForURL(url => !url.toString().includes('#/login'));
- const metadata = await homePageUtil.getPageMetadata();
+ const metadata = await homePageUtil.getHomePageMetadata();
expect(metadata.title).toContain('Zeppelin');
expect(metadata.path).toContain('#/');
@@ -148,7 +148,7 @@ test.describe('Anonymous User Login Redirect', () => {
await page.goto('/', { waitUntil: 'load' });
await waitForZeppelinReady(page);
- const homeMetadata = await homePageUtil.getPageMetadata();
+ const homeMetadata = await homePageUtil.getHomePageMetadata();
expect(homeMetadata.path).toContain('#/');
expect(homeMetadata.isAnonymous).toBe(true);
@@ -156,7 +156,7 @@ test.describe('Anonymous User Login Redirect', () => {
await waitForZeppelinReady(page);
await page.waitForURL(url => !url.toString().includes('#/login'));
- const loginMetadata = await homePageUtil.getPageMetadata();
+ const loginMetadata = await homePageUtil.getHomePageMetadata();
expect(loginMetadata.path).toContain('#/');
expect(loginMetadata.path).not.toContain('#/login');
expect(loginMetadata.isAnonymous).toBe(true);
diff --git a/zeppelin-web-angular/e2e/tests/home/home-page-elements.spec.ts
b/zeppelin-web-angular/e2e/tests/home/home-page-elements.spec.ts
new file mode 100644
index 0000000000..f9f27d59e5
--- /dev/null
+++ b/zeppelin-web-angular/e2e/tests/home/home-page-elements.spec.ts
@@ -0,0 +1,172 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { expect, test } from '@playwright/test';
+import { HomePage } from '../../models/home-page';
+import { HomePageUtil } from '../../models/home-page.util';
+import { addPageAnnotationBeforeEach, performLoginIfRequired,
waitForZeppelinReady, PAGES } from '../../utils';
+
+test.describe('Home Page - Core Elements', () => {
+ addPageAnnotationBeforeEach(PAGES.WORKSPACE.HOME);
+
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/#/');
+ await waitForZeppelinReady(page);
+ await performLoginIfRequired(page);
+ });
+
+ test.describe('Welcome Section', () => {
+ test('should display welcome section with correct content', async ({ page
}) => {
+ const homePageUtil = new HomePageUtil(page);
+
+ await test.step('Given I am on the home page', async () => {
+ const homePage = new HomePage(page);
+ await homePage.navigateToHome();
+ });
+
+ await test.step('When the page loads', async () => {
+ await waitForZeppelinReady(page);
+ });
+
+ await test.step('Then I should see the welcome section with correct
content', async () => {
+ await homePageUtil.verifyWelcomeSection();
+ });
+ });
+
+ test('should have proper welcome message structure', async ({ page }) => {
+ const homePage = new HomePage(page);
+
+ await test.step('Given I am on the home page', async () => {
+ await homePage.navigateToHome();
+ });
+
+ await test.step('When I examine the welcome section', async () => {
+ await expect(homePage.welcomeSection).toBeVisible();
+ });
+
+ await test.step('Then I should see the welcome heading', async () => {
+ await expect(homePage.welcomeHeading).toBeVisible();
+ const headingText = await homePage.getWelcomeHeadingText();
+ expect(headingText.trim()).toBe('Welcome to Zeppelin!');
+ });
+
+ await test.step('And I should see the welcome description', async () => {
+ await expect(homePage.welcomeDescription).toBeVisible();
+ const descriptionText = await homePage.getWelcomeDescriptionText();
+ expect(descriptionText).toContain('web-based notebook');
+ expect(descriptionText).toContain('interactive data analytics');
+ });
+ });
+ });
+
+ test.describe('Notebook Section', () => {
+ test('should display notebook section with all components', async ({ page
}) => {
+ const homePageUtil = new HomePageUtil(page);
+
+ await test.step('Given I am on the home page', async () => {
+ const homePage = new HomePage(page);
+ await homePage.navigateToHome();
+ });
+
+ await test.step('When I look for the notebook section', async () => {
+ await waitForZeppelinReady(page);
+ });
+
+ await test.step('Then I should see all notebook section components',
async () => {
+ await homePageUtil.verifyNotebookSection();
+ });
+ });
+
+ test('should have functional refresh notes button', async ({ page }) => {
+ const homePage = new HomePage(page);
+ const homePageUtil = new HomePageUtil(page);
+
+ await test.step('Given I am on the home page with notebook section
visible', async () => {
+ await homePage.navigateToHome();
+ await expect(homePage.refreshNoteButton).toBeVisible();
+ });
+
+ await test.step('When I click the refresh notes button', async () => {
+ await homePage.clickRefreshNotes();
+ });
+
+ await test.step('Then the notebook list should still be visible', async
() => {
+ await homePageUtil.verifyNotebookRefreshFunctionality();
+ });
+ });
+
+ test('should display notebook list component', async ({ page }) => {
+ const homePage = new HomePage(page);
+
+ await test.step('Given I am on the home page', async () => {
+ await homePage.navigateToHome();
+ });
+
+ await test.step('When I look for the notebook list', async () => {
+ await waitForZeppelinReady(page);
+ });
+
+ await test.step('Then I should see the notebook list component', async
() => {
+ await expect(homePage.notebookList).toBeVisible();
+ const isVisible = await homePage.isNotebookListVisible();
+ expect(isVisible).toBe(true);
+ });
+ });
+ });
+
+ test.describe('Help Section', () => {
+ test('should display help section with documentation link', async ({ page
}) => {
+ const homePageUtil = new HomePageUtil(page);
+
+ await test.step('Given I am on the home page', async () => {
+ const homePage = new HomePage(page);
+ await homePage.navigateToHome();
+ });
+
+ await test.step('When I look for the help section', async () => {
+ await waitForZeppelinReady(page);
+ });
+
+ await test.step('Then I should see the help section', async () => {
+ await homePageUtil.verifyHelpSection();
+ });
+
+ await test.step('And I should see the documentation link', async () => {
+ const homePage = new HomePage(page);
+ await expect(homePage.externalLinks.documentation).toBeVisible();
+ });
+ });
+ });
+
+ test.describe('Community Section', () => {
+ test('should display community section with all links', async ({ page })
=> {
+ const homePageUtil = new HomePageUtil(page);
+
+ await test.step('Given I am on the home page', async () => {
+ const homePage = new HomePage(page);
+ await homePage.navigateToHome();
+ });
+
+ await test.step('When I look for the community section', async () => {
+ await waitForZeppelinReady(page);
+ });
+
+ await test.step('Then I should see the community section', async () => {
+ await homePageUtil.verifyCommunitySection();
+ });
+
+ await test.step('And I should see all community links', async () => {
+ await homePageUtil.verifyExternalLinks();
+ });
+ });
+ });
+});
diff --git
a/zeppelin-web-angular/e2e/tests/home/home-page-enhanced-functionality.spec.ts
b/zeppelin-web-angular/e2e/tests/home/home-page-enhanced-functionality.spec.ts
new file mode 100644
index 0000000000..f6a93c725d
--- /dev/null
+++
b/zeppelin-web-angular/e2e/tests/home/home-page-enhanced-functionality.spec.ts
@@ -0,0 +1,64 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { expect, test } from '@playwright/test';
+import { HomePageUtil } from '../../models/home-page.util';
+import { addPageAnnotationBeforeEach, performLoginIfRequired,
waitForZeppelinReady, PAGES } from '../../utils';
+
+addPageAnnotationBeforeEach(PAGES.WORKSPACE.HOME);
+
+test.describe('Home Page Enhanced Functionality', () => {
+ let homeUtil: HomePageUtil;
+
+ test.beforeEach(async ({ page }) => {
+ homeUtil = new HomePageUtil(page);
+ await page.goto('/');
+ await waitForZeppelinReady(page);
+ await performLoginIfRequired(page);
+ });
+
+ test.describe('Given documentation links are displayed', () => {
+ test('When documentation link is checked Then should have correct version
in URL', async ({ page }) => {
+ await homeUtil.verifyDocumentationVersionLink();
+ });
+
+ test('When external links are checked Then should all open in new tab',
async ({ page }) => {
+ await homeUtil.verifyAllExternalLinksTargetBlank();
+ });
+ });
+
+ test.describe('Given welcome section display', () => {
+ test('When page loads Then should show welcome content with proper text',
async ({ page }) => {
+ await homeUtil.verifyWelcomeSection();
+ });
+
+ test('When welcome section is displayed Then should contain interactive
elements', async ({ page }) => {
+ await homeUtil.verifyNotebookSection();
+ });
+ });
+
+ test.describe('Given community section content', () => {
+ test('When community section loads Then should display help and community
headings', async ({ page }) => {
+ await homeUtil.verifyHelpSection();
+ await homeUtil.verifyCommunitySection();
+ });
+
+ test('When external links are displayed Then should show correct targets',
async ({ page }) => {
+ const linkTargets = await homeUtil.testExternalLinkTargets();
+
+
expect(linkTargets.documentationHref).toContain('zeppelin.apache.org/docs');
+ expect(linkTargets.mailingListHref).toContain('community.html');
+ expect(linkTargets.issuesTrackingHref).toContain('issues.apache.org');
+ expect(linkTargets.githubHref).toContain('github.com/apache/zeppelin');
+ });
+ });
+});
diff --git
a/zeppelin-web-angular/e2e/tests/home/home-page-external-links.spec.ts
b/zeppelin-web-angular/e2e/tests/home/home-page-external-links.spec.ts
new file mode 100644
index 0000000000..34e7e27de0
--- /dev/null
+++ b/zeppelin-web-angular/e2e/tests/home/home-page-external-links.spec.ts
@@ -0,0 +1,169 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { expect, test } from '@playwright/test';
+import { HomePage } from '../../models/home-page';
+import { HomePageUtil } from '../../models/home-page.util';
+import { addPageAnnotationBeforeEach, performLoginIfRequired,
waitForZeppelinReady, PAGES } from '../../utils';
+
+test.describe('Home Page - External Links', () => {
+ addPageAnnotationBeforeEach(PAGES.WORKSPACE.HOME);
+
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/#/');
+ await waitForZeppelinReady(page);
+ await performLoginIfRequired(page);
+ });
+
+ test.describe('Documentation Link', () => {
+ test('should have correct documentation link with dynamic version', async
({ page }) => {
+ const homePage = new HomePage(page);
+ const homePageUtil = new HomePageUtil(page);
+
+ await test.step('Given I am on the home page', async () => {
+ await homePage.navigateToHome();
+ });
+
+ await test.step('When I examine the documentation link', async () => {
+ await expect(homePage.externalLinks.documentation).toBeVisible();
+ });
+
+ await test.step('Then it should have the correct href pattern', async ()
=> {
+ const linkTargets = await homePageUtil.testExternalLinkTargets();
+
expect(linkTargets.documentationHref).toContain('zeppelin.apache.org/docs');
+ expect(linkTargets.documentationHref).toContain('index.html');
+ });
+
+ await test.step('And it should open in a new tab', async () => {
+ const target = await
homePage.externalLinks.documentation.getAttribute('target');
+ expect(target).toBe('_blank');
+ });
+ });
+ });
+
+ test.describe('Community Links', () => {
+ test('should have correct mailing list link', async ({ page }) => {
+ const homePage = new HomePage(page);
+ const homePageUtil = new HomePageUtil(page);
+
+ await test.step('Given I am on the home page', async () => {
+ await homePage.navigateToHome();
+ });
+
+ await test.step('When I examine the mailing list link', async () => {
+ await expect(homePage.externalLinks.mailingList).toBeVisible();
+ });
+
+ await test.step('Then it should have the correct href', async () => {
+ const linkTargets = await homePageUtil.testExternalLinkTargets();
+
expect(linkTargets.mailingListHref).toBe('http://zeppelin.apache.org/community.html');
+ });
+
+ await test.step('And it should open in a new tab', async () => {
+ const target = await
homePage.externalLinks.mailingList.getAttribute('target');
+ expect(target).toBe('_blank');
+ });
+
+ await test.step('And it should have the mail icon', async () => {
+ const mailIcon =
homePage.externalLinks.mailingList.locator('i[nz-icon][nzType="mail"]');
+ await expect(mailIcon).toBeVisible();
+ });
+ });
+
+ test('should have correct issues tracking link', async ({ page }) => {
+ const homePage = new HomePage(page);
+ const homePageUtil = new HomePageUtil(page);
+
+ await test.step('Given I am on the home page', async () => {
+ await homePage.navigateToHome();
+ });
+
+ await test.step('When I examine the issues tracking link', async () => {
+ await expect(homePage.externalLinks.issuesTracking).toBeVisible();
+ });
+
+ await test.step('Then it should have the correct href', async () => {
+ const linkTargets = await homePageUtil.testExternalLinkTargets();
+ expect(linkTargets.issuesTrackingHref).toBe(
+
'https://issues.apache.org/jira/projects/ZEPPELIN/issues/filter=allopenissues'
+ );
+ });
+
+ await test.step('And it should open in a new tab', async () => {
+ const target = await
homePage.externalLinks.issuesTracking.getAttribute('target');
+ expect(target).toBe('_blank');
+ });
+
+ await test.step('And it should have the exception icon', async () => {
+ const exceptionIcon =
homePage.externalLinks.issuesTracking.locator('i[nz-icon][nzType="exception"]');
+ await expect(exceptionIcon).toBeVisible();
+ });
+ });
+
+ test('should have correct GitHub link', async ({ page }) => {
+ const homePage = new HomePage(page);
+ const homePageUtil = new HomePageUtil(page);
+
+ await test.step('Given I am on the home page', async () => {
+ await homePage.navigateToHome();
+ });
+
+ await test.step('When I examine the GitHub link', async () => {
+ await expect(homePage.externalLinks.github).toBeVisible();
+ });
+
+ await test.step('Then it should have the correct href', async () => {
+ const linkTargets = await homePageUtil.testExternalLinkTargets();
+
expect(linkTargets.githubHref).toBe('https://github.com/apache/zeppelin');
+ });
+
+ await test.step('And it should open in a new tab', async () => {
+ const target = await
homePage.externalLinks.github.getAttribute('target');
+ expect(target).toBe('_blank');
+ });
+
+ await test.step('And it should have the GitHub icon', async () => {
+ const githubIcon =
homePage.externalLinks.github.locator('i[nz-icon][nzType="github"]');
+ await expect(githubIcon).toBeVisible();
+ });
+ });
+ });
+
+ test.describe('Link Verification', () => {
+ test('should have all external links with proper attributes', async ({
page }) => {
+ const homePage = new HomePage(page);
+
+ await test.step('Given I am on the home page', async () => {
+ await homePage.navigateToHome();
+ });
+
+ await test.step('When I examine all external links', async () => {
+ await expect(homePage.externalLinks.documentation).toBeVisible();
+ await expect(homePage.externalLinks.mailingList).toBeVisible();
+ await expect(homePage.externalLinks.issuesTracking).toBeVisible();
+ await expect(homePage.externalLinks.github).toBeVisible();
+ });
+
+ await test.step('Then all links should open in new tabs', async () => {
+ const docTarget = await
homePage.externalLinks.documentation.getAttribute('target');
+ const mailTarget = await
homePage.externalLinks.mailingList.getAttribute('target');
+ const issuesTarget = await
homePage.externalLinks.issuesTracking.getAttribute('target');
+ const githubTarget = await
homePage.externalLinks.github.getAttribute('target');
+
+ expect(docTarget).toBe('_blank');
+ expect(mailTarget).toBe('_blank');
+ expect(issuesTarget).toBe('_blank');
+ expect(githubTarget).toBe('_blank');
+ });
+ });
+ });
+});
diff --git a/zeppelin-web-angular/e2e/tests/home/home-page-layout.spec.ts
b/zeppelin-web-angular/e2e/tests/home/home-page-layout.spec.ts
new file mode 100644
index 0000000000..ef2698750c
--- /dev/null
+++ b/zeppelin-web-angular/e2e/tests/home/home-page-layout.spec.ts
@@ -0,0 +1,139 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { expect, test } from '@playwright/test';
+import { HomePage } from '../../models/home-page';
+import { HomePageUtil } from '../../models/home-page.util';
+import { addPageAnnotationBeforeEach, performLoginIfRequired,
waitForZeppelinReady, PAGES } from '../../utils';
+
+test.describe('Home Page - Layout and Grid', () => {
+ addPageAnnotationBeforeEach(PAGES.WORKSPACE.HOME);
+
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/#/');
+ await waitForZeppelinReady(page);
+ await performLoginIfRequired(page);
+ });
+
+ test.describe('Responsive Grid Layout', () => {
+ test('should display responsive grid structure', async ({ page }) => {
+ const homePageUtil = new HomePageUtil(page);
+
+ await test.step('Given I am on the home page', async () => {
+ const homePage = new HomePage(page);
+ await homePage.navigateToHome();
+ });
+
+ await test.step('When the page loads', async () => {
+ await waitForZeppelinReady(page);
+ });
+ });
+
+ test('should have proper column distribution', async ({ page }) => {
+ const homePage = new HomePage(page);
+
+ await test.step('Given I am on the home page', async () => {
+ await homePage.navigateToHome();
+ });
+
+ await test.step('When I examine the grid columns', async () => {
+ await expect(homePage.moreInfoGrid).toBeVisible();
+ });
+
+ await test.step('Then I should see the notebook column with proper
sizing', async () => {
+ await expect(homePage.notebookColumn).toBeVisible();
+ // Check that the column contains notebook content
+ const notebookHeading = homePage.notebookColumn.locator('h3');
+ await expect(notebookHeading).toBeVisible();
+ const headingText = await notebookHeading.textContent();
+ expect(headingText).toContain('Notebook');
+ });
+
+ await test.step('And I should see the help/community column with proper
sizing', async () => {
+ await expect(homePage.helpCommunityColumn).toBeVisible();
+ // Check that the column contains help and community content
+ const helpHeading = homePage.helpCommunityColumn.locator('h3').first();
+ await expect(helpHeading).toBeVisible();
+ const helpText = await helpHeading.textContent();
+ expect(helpText).toContain('Help');
+ });
+ });
+
+ test('should maintain layout structure across different viewport sizes',
async ({ page }) => {
+ const homePage = new HomePage(page);
+
+ await test.step('Given I am on the home page', async () => {
+ await homePage.navigateToHome();
+ });
+
+ await test.step('When I resize to tablet view', async () => {
+ await page.setViewportSize({ width: 768, height: 1024 });
+ await page.waitForTimeout(500);
+ });
+
+ await test.step('Then the grid should still be visible and functional',
async () => {
+ await expect(homePage.moreInfoGrid).toBeVisible();
+ await expect(homePage.notebookColumn).toBeVisible();
+ await expect(homePage.helpCommunityColumn).toBeVisible();
+ });
+
+ await test.step('When I resize to mobile view', async () => {
+ await page.setViewportSize({ width: 375, height: 667 });
+ await page.waitForTimeout(500);
+ });
+
+ await test.step('Then the grid should adapt to mobile layout', async ()
=> {
+ await expect(homePage.moreInfoGrid).toBeVisible();
+ await expect(homePage.notebookColumn).toBeVisible();
+ await expect(homePage.helpCommunityColumn).toBeVisible();
+
+ // Verify content is still accessible in mobile view
+ const notebookHeading = homePage.notebookColumn.locator('h3');
+ const helpHeading = homePage.helpCommunityColumn.locator('h3').first();
+ await expect(notebookHeading).toBeVisible();
+ await expect(helpHeading).toBeVisible();
+ });
+ });
+ });
+
+ test.describe('Content Organization', () => {
+ test('should organize content in logical sections', async ({ page }) => {
+ const homePage = new HomePage(page);
+
+ await test.step('Given I am on the home page', async () => {
+ await homePage.navigateToHome();
+ });
+
+ await test.step('When I examine the content organization', async () => {
+ await waitForZeppelinReady(page);
+ });
+
+ await test.step('Then I should see the welcome section at the top',
async () => {
+ await expect(homePage.welcomeSection).toBeVisible();
+ });
+
+ await test.step('And I should see the notebook section in the left
column', async () => {
+ const notebookInColumn = homePage.notebookColumn.locator('h3');
+ await expect(notebookInColumn).toBeVisible();
+ const text = await notebookInColumn.textContent();
+ expect(text).toContain('Notebook');
+ });
+
+ await test.step('And I should see help and community sections in the
right column', async () => {
+ const helpHeading =
homePage.helpCommunityColumn.locator('h3').filter({ hasText: 'Help' });
+ const communityHeading =
homePage.helpCommunityColumn.locator('h3').filter({ hasText: 'Community' });
+ await expect(helpHeading).toBeVisible();
+ await expect(communityHeading).toBeVisible();
+ });
+ });
+ });
+});
diff --git
a/zeppelin-web-angular/e2e/tests/home/home-page-note-operations.spec.ts
b/zeppelin-web-angular/e2e/tests/home/home-page-note-operations.spec.ts
new file mode 100644
index 0000000000..f7be8fd9d6
--- /dev/null
+++ b/zeppelin-web-angular/e2e/tests/home/home-page-note-operations.spec.ts
@@ -0,0 +1,260 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { expect, test } from '@playwright/test';
+import { HomePage } from '../../models/home-page';
+import { addPageAnnotationBeforeEach, performLoginIfRequired,
waitForZeppelinReady, PAGES } from '../../utils';
+
+addPageAnnotationBeforeEach(PAGES.WORKSPACE.HOME);
+
+test.describe('Home Page Note Operations', () => {
+ let homePage: HomePage;
+
+ test.beforeEach(async ({ page }) => {
+ homePage = new HomePage(page);
+ await page.goto('/');
+ await waitForZeppelinReady(page);
+ await performLoginIfRequired(page);
+ await page.waitForSelector('zeppelin-node-list', { timeout: 15000 });
+ });
+
+ test.describe('Given note operations are available', () => {
+ test('When note list loads Then should show note action buttons on hover',
async ({ page }) => {
+ const notesExist = await page.locator('.node .file').count();
+
+ if (notesExist > 0) {
+ const firstNote = page.locator('.node .file').first();
+ await firstNote.hover();
+
+ await expect(page.locator('.file .operation a[nztooltiptitle*="Rename
note"]').first()).toBeVisible();
+ await expect(page.locator('.file .operation a[nztooltiptitle*="Clear
output"]').first()).toBeVisible();
+ await expect(page.locator('.file .operation a[nztooltiptitle*="Move
note to Trash"]').first()).toBeVisible();
+ } else {
+ console.log('No notes available for testing operations');
+ }
+ });
+
+ test('When hovering over note actions Then should show tooltip
descriptions', async ({ page }) => {
+ const noteExists = await page
+ .locator('.node .file')
+ .first()
+ .isVisible()
+ .catch(() => false);
+
+ if (noteExists) {
+ const firstNote = page.locator('.node .file').first();
+ await firstNote.hover();
+
+ const renameIcon = page.locator('.file .operation
a[nztooltiptitle*="Rename note"]').first();
+ const clearIcon = page.locator('.file .operation
a[nztooltiptitle*="Clear output"]').first();
+ const deleteIcon = page.locator('.file .operation
a[nztooltiptitle*="Move note to Trash"]').first();
+
+ await expect(renameIcon).toBeVisible();
+ await expect(clearIcon).toBeVisible();
+ await expect(deleteIcon).toBeVisible();
+
+ // Test tooltip visibility by hovering over each icon
+ await renameIcon.hover();
+ await expect(page.locator('.ant-tooltip', { hasText: 'Rename note'
})).toBeVisible();
+
+ await clearIcon.hover();
+ await expect(page.locator('.ant-tooltip', { hasText: 'Clear output'
})).toBeVisible();
+
+ await deleteIcon.hover();
+ await expect(page.locator('.ant-tooltip', { hasText: 'Move note to
Trash' })).toBeVisible();
+ }
+ });
+ });
+
+ test.describe('Given rename note functionality', () => {
+ test('When rename button is clicked Then should trigger rename workflow',
async ({ page }) => {
+ const noteExists = await page
+ .locator('.node .file')
+ .first()
+ .isVisible()
+ .catch(() => false);
+
+ if (noteExists) {
+ const noteItem = page.locator('.node .file').first();
+ await noteItem.hover();
+
+ const renameButton = page.locator('.file .operation
a[nztooltiptitle*="Rename note"]').first();
+ await expect(renameButton).toBeVisible();
+ await renameButton.click();
+
+ await page
+ .waitForFunction(
+ () => {
+ return (
+ document.querySelector('zeppelin-note-rename') !== null ||
+ document.querySelector('[role="dialog"]') !== null ||
+ document.querySelector('.ant-modal') !== null
+ );
+ },
+ { timeout: 5000 }
+ )
+ .catch(() => {
+ console.log('Rename modal did not appear - might need different
trigger');
+ });
+ }
+ });
+ });
+
+ test.describe('Given clear output functionality', () => {
+ test('When clear output button is clicked Then should show confirmation
dialog', async ({ page }) => {
+ const noteExists = await page
+ .locator('.node .file')
+ .first()
+ .isVisible()
+ .catch(() => false);
+
+ if (noteExists) {
+ const noteItem = page.locator('.node .file').first();
+ await noteItem.hover();
+
+ const clearButton = page.locator('.file .operation
a[nztooltiptitle*="Clear output"]').first();
+ await expect(clearButton).toBeVisible();
+ await clearButton.click();
+
+ await expect(page.locator('text=Do you want to clear all
output?')).toBeVisible();
+ }
+ });
+
+ test('When clear output is confirmed Then should execute clear operation',
async ({ page }) => {
+ const noteExists = await page
+ .locator('.node .file')
+ .first()
+ .isVisible()
+ .catch(() => false);
+
+ if (noteExists) {
+ const noteItem = page.locator('.node .file').first();
+ await noteItem.hover();
+
+ const clearButton = page.locator('.file .operation
a[nztooltiptitle*="Clear output"]').first();
+ await expect(clearButton).toBeVisible();
+ await clearButton.click();
+
+ const confirmButton = page.locator('button:has-text("Yes")');
+ if (await confirmButton.isVisible()) {
+ await confirmButton.click();
+ }
+ }
+ });
+ });
+
+ test.describe('Given move to trash functionality', () => {
+ test('When delete button is clicked Then should show trash confirmation',
async ({ page }) => {
+ const noteExists = await page
+ .locator('.node .file')
+ .first()
+ .isVisible()
+ .catch(() => false);
+
+ if (noteExists) {
+ const noteItem = page.locator('.node .file').first();
+ await noteItem.hover();
+
+ const deleteButton = page.locator('.file .operation
a[nztooltiptitle*="Move note to Trash"]').first();
+ await expect(deleteButton).toBeVisible();
+ await deleteButton.click();
+
+ await expect(page.locator('text=This note will be moved to
trash.')).toBeVisible();
+ }
+ });
+
+ test('When move to trash is confirmed Then should move note to trash
folder', async ({ page }) => {
+ const noteExists = await page
+ .locator('.node .file')
+ .first()
+ .isVisible()
+ .catch(() => false);
+
+ if (noteExists) {
+ const noteItem = page.locator('.node .file').first();
+ await noteItem.hover();
+
+ const deleteButton = page.locator('.file .operation
a[nztooltiptitle*="Move note to Trash"]').first();
+ await expect(deleteButton).toBeVisible();
+ await deleteButton.click();
+
+ const confirmButton = page.locator('button:has-text("Yes")');
+ if (await confirmButton.isVisible()) {
+ await confirmButton.click();
+
+ await page.waitForTimeout(2000);
+
+ const trashFolder = page.locator('.node .folder').filter({ hasText:
'Trash' });
+ await expect(trashFolder).toBeVisible();
+ }
+ }
+ });
+ });
+
+ test.describe('Given trash folder operations', () => {
+ test('When trash folder exists Then should show restore and empty
options', async ({ page }) => {
+ const trashExists = await page
+ .locator('.node .folder')
+ .filter({ hasText: 'Trash' })
+ .isVisible()
+ .catch(() => false);
+
+ if (trashExists) {
+ const trashFolder = page.locator('.node .folder').filter({ hasText:
'Trash' });
+ await trashFolder.hover();
+
+ await expect(page.locator('.folder .operation
a[nztooltiptitle*="Restore all"]')).toBeVisible();
+ await expect(page.locator('.folder .operation a[nztooltiptitle*="Empty
all"]')).toBeVisible();
+ }
+ });
+
+ test('When restore all is clicked Then should show confirmation dialog',
async ({ page }) => {
+ const trashExists = await page
+ .locator('.node .folder')
+ .filter({ hasText: 'Trash' })
+ .isVisible()
+ .catch(() => false);
+
+ if (trashExists) {
+ const trashFolder = page.locator('.node .folder').filter({ hasText:
'Trash' });
+ await trashFolder.hover();
+
+ const restoreButton = page.locator('.folder .operation
a[nztooltiptitle*="Restore all"]').first();
+ await expect(restoreButton).toBeVisible();
+ await restoreButton.click();
+
+ await expect(
+ page.locator('text=Folders and notes in the trash will be merged
into their original position.')
+ ).toBeVisible();
+ }
+ });
+
+ test('When empty trash is clicked Then should show permanent deletion
warning', async ({ page }) => {
+ const trashExists = await page
+ .locator('.node .folder')
+ .filter({ hasText: 'Trash' })
+ .isVisible()
+ .catch(() => false);
+
+ if (trashExists) {
+ const trashFolder = page.locator('.node .folder').filter({ hasText:
'Trash' });
+ await trashFolder.hover();
+
+ const emptyButton = page.locator('.folder .operation
a[nztooltiptitle*="Empty all"]').first();
+ await expect(emptyButton).toBeVisible();
+ await emptyButton.click();
+
+ await expect(page.locator('text=This cannot be undone. Are you
sure?')).toBeVisible();
+ }
+ });
+ });
+});
diff --git
a/zeppelin-web-angular/e2e/tests/home/home-page-notebook-actions.spec.ts
b/zeppelin-web-angular/e2e/tests/home/home-page-notebook-actions.spec.ts
new file mode 100644
index 0000000000..c3e7e1388b
--- /dev/null
+++ b/zeppelin-web-angular/e2e/tests/home/home-page-notebook-actions.spec.ts
@@ -0,0 +1,68 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { expect, test } from '@playwright/test';
+import { HomePageUtil } from '../../models/home-page.util';
+import { addPageAnnotationBeforeEach, performLoginIfRequired,
waitForZeppelinReady, PAGES } from '../../utils';
+
+addPageAnnotationBeforeEach(PAGES.WORKSPACE.HOME);
+
+test.describe('Home Page Notebook Actions', () => {
+ let homeUtil: HomePageUtil;
+
+ test.beforeEach(async ({ page }) => {
+ homeUtil = new HomePageUtil(page);
+ await page.goto('/');
+ await waitForZeppelinReady(page);
+ await performLoginIfRequired(page);
+ });
+
+ test.describe('Given notebook list is displayed', () => {
+ test('When page loads Then should show notebook actions', async ({ page })
=> {
+ await homeUtil.verifyNotebookActions();
+ });
+
+ test('When refresh button is clicked Then should trigger reload with
loading state', async ({ page }) => {
+ await homeUtil.testNotebookRefreshLoadingState();
+ });
+
+ test('When filter is used Then should filter notebook list', async ({ page
}) => {
+ await homeUtil.testFilterFunctionality('test');
+ });
+ });
+
+ test.describe('Given create new note action', () => {
+ test('When create new note is clicked Then should open note creation
modal', async ({ page }) => {
+ try {
+ await homeUtil.verifyCreateNewNoteWorkflow();
+ } catch (error) {
+ console.log('Note creation modal might not appear immediately');
+ }
+ });
+ });
+
+ test.describe('Given import note action', () => {
+ test('When import note is clicked Then should open import modal', async ({
page }) => {
+ try {
+ await homeUtil.verifyImportNoteWorkflow();
+ } catch (error) {
+ console.log('Import modal might not appear immediately');
+ }
+ });
+ });
+
+ test.describe('Given notebook refresh functionality', () => {
+ test('When refresh is triggered Then should maintain notebook list
visibility', async ({ page }) => {
+ await homeUtil.verifyNotebookRefreshFunctionality();
+ });
+ });
+});
diff --git a/zeppelin-web-angular/e2e/tests/workspace/workspace-main.spec.ts
b/zeppelin-web-angular/e2e/tests/workspace/workspace-main.spec.ts
new file mode 100644
index 0000000000..c6292cbaec
--- /dev/null
+++ b/zeppelin-web-angular/e2e/tests/workspace/workspace-main.spec.ts
@@ -0,0 +1,69 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { test } from '@playwright/test';
+import { WorkspaceTestUtil } from '../../models/workspace-page.util';
+import { addPageAnnotationBeforeEach, PAGES } from '../../utils';
+
+addPageAnnotationBeforeEach(PAGES.WORKSPACE.MAIN);
+
+test.describe('Workspace Main Component', () => {
+ let workspaceUtil: WorkspaceTestUtil;
+
+ test.beforeEach(async ({ page }) => {
+ workspaceUtil = new WorkspaceTestUtil(page);
+ });
+
+ test.describe('Given user accesses workspace container', () => {
+ test('When workspace loads Then should display main container structure',
async () => {
+ await workspaceUtil.navigateAndWaitForLoad();
+
+ await workspaceUtil.verifyWorkspaceLayout();
+ await workspaceUtil.verifyWorkspaceContainer();
+ });
+
+ test('When workspace loads Then should display header component', async ()
=> {
+ await workspaceUtil.navigateAndWaitForLoad();
+
+ await workspaceUtil.verifyHeaderVisibility(true);
+ });
+
+ test('When workspace loads Then should activate router outlet', async ()
=> {
+ await workspaceUtil.navigateAndWaitForLoad();
+
+ await workspaceUtil.verifyRouterOutletActivation();
+ });
+
+ test('When component activates Then should trigger onActivate event',
async () => {
+ await workspaceUtil.navigateAndWaitForLoad();
+
+ await workspaceUtil.waitForComponentActivation();
+ });
+ });
+
+ test.describe('Given workspace header visibility', () => {
+ test('When not in publish mode Then should show header', async () => {
+ await workspaceUtil.navigateAndWaitForLoad();
+
+ await workspaceUtil.verifyHeaderVisibility(true);
+ });
+ });
+
+ test.describe('Given router outlet functionality', () => {
+ test('When navigating to workspace Then should load child components',
async () => {
+ await workspaceUtil.navigateAndWaitForLoad();
+
+ await workspaceUtil.verifyRouterOutletActivation();
+ await workspaceUtil.waitForComponentActivation();
+ });
+ });
+});