This is an automated email from the ASF dual-hosted git repository.
jongyoul pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/zeppelin.git
The following commit(s) were added to refs/heads/master by this push:
new 076676aa87 [ZEPPELIN-6358] Remove anti-patterns of E2E and tidy test
suite
076676aa87 is described below
commit 076676aa87f92b052619691c2788bd53771a266e
Author: YONGJAE LEE(이용재) <[email protected]>
AuthorDate: Sat Apr 4 21:04:45 2026 +0900
[ZEPPELIN-6358] Remove anti-patterns of E2E and tidy test suite
### What is this PR for?
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()`
### What type of PR is it?
Improvement
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 #5180 from dididy/tidy-e2e.
Signed-off-by: Jongyoul Lee <[email protected]>
---
.../e2e/models/about-zeppelin-modal.ts | 4 -
zeppelin-web-angular/e2e/models/base-page.ts | 34 +-
zeppelin-web-angular/e2e/models/home-page.ts | 70 ++--
zeppelin-web-angular/e2e/models/node-list-page.ts | 14 +-
.../e2e/models/note-create-modal.ts | 4 -
.../e2e/models/note-import-modal.ts | 22 --
.../e2e/models/notebook-repo-item.util.ts | 6 +-
.../e2e/models/notebook-repos-page.ts | 36 +--
zeppelin-web-angular/e2e/models/notebook.util.ts | 6 +-
zeppelin-web-angular/e2e/models/workspace-page.ts | 23 --
.../e2e/models/workspace-page.util.ts | 60 ----
zeppelin-web-angular/e2e/tests/app.spec.ts | 128 +++-----
.../anonymous-login-redirect.spec.ts | 117 ++++---
.../e2e/tests/home/home-page-elements.spec.ts | 55 +---
.../home/home-page-enhanced-functionality.spec.ts | 92 ------
.../tests/home/home-page-external-links.spec.ts | 46 +--
.../e2e/tests/home/home-page-layout.spec.ts | 22 +-
.../tests/home/home-page-note-operations.spec.ts | 357 ++++++++++-----------
.../tests/home/home-page-notebook-actions.spec.ts | 57 ++--
.../notebook/published/published-paragraph.spec.ts | 105 +++---
.../about-zeppelin/about-zeppelin-modal.spec.ts | 18 +-
.../node-list/node-list-functionality.spec.ts | 36 ++-
.../share/note-create/note-create-modal.spec.ts | 21 +-
.../share/note-import/note-import-modal.spec.ts | 18 +-
.../e2e/tests/theme/dark-mode.spec.ts | 12 +-
.../notebook-repo-item-display.spec.ts | 13 +-
.../notebook-repos/notebook-repo-item-edit.spec.ts | 36 +--
.../notebook-repo-item-form-validation.spec.ts | 34 +-
.../notebook-repo-item-settings.spec.ts | 84 +++--
.../notebook-repo-item-workflow.spec.ts | 22 +-
.../notebook-repos-page-structure.spec.ts | 16 +-
.../e2e/tests/workspace/workspace-main.spec.ts | 48 +--
zeppelin-web-angular/e2e/utils.ts | 12 +-
zeppelin-web-angular/playwright.config.js | 2 +-
.../projects/zeppelin-react/package-lock.json | 6 +-
.../notebook/action-bar/action-bar.component.html | 2 +-
36 files changed, 549 insertions(+), 1089 deletions(-)
diff --git a/zeppelin-web-angular/e2e/models/about-zeppelin-modal.ts
b/zeppelin-web-angular/e2e/models/about-zeppelin-modal.ts
index d5a44add77..a61632d3e6 100644
--- a/zeppelin-web-angular/e2e/models/about-zeppelin-modal.ts
+++ b/zeppelin-web-angular/e2e/models/about-zeppelin-modal.ts
@@ -43,10 +43,6 @@ export class AboutZeppelinModal extends BasePage {
return (await this.versionText.textContent()) || '';
}
- async isLogoVisible(): Promise<boolean> {
- return this.logo.isVisible();
- }
-
async getGetInvolvedHref(): Promise<string | null> {
return this.getInvolvedLink.getAttribute('href');
}
diff --git a/zeppelin-web-angular/e2e/models/base-page.ts
b/zeppelin-web-angular/e2e/models/base-page.ts
index 539f096cbb..aa37f1a138 100644
--- a/zeppelin-web-angular/e2e/models/base-page.ts
+++ b/zeppelin-web-angular/e2e/models/base-page.ts
@@ -24,8 +24,6 @@ export class BasePage {
readonly zeppelinHeader: Locator;
readonly modalTitle: Locator;
- readonly modalBody: Locator;
- readonly modalContent: Locator;
readonly okButton: Locator;
readonly cancelButton: Locator;
@@ -41,8 +39,6 @@ export class BasePage {
this.zeppelinHeader = page.locator('zeppelin-header');
this.modalTitle = page.locator('.ant-modal-confirm-title,
.ant-modal-title');
- this.modalBody = page.locator('.ant-modal-confirm-content,
.ant-modal-body');
- this.modalContent = page.locator('.ant-modal-body');
this.okButton = page.locator('button:has-text("OK")');
this.cancelButton = page.locator('button:has-text("Cancel")');
@@ -71,11 +67,6 @@ export class BasePage {
await this.navigateToRoute('/');
}
- getCurrentPath(): string {
- const url = new URL(this.page.url());
- return url.hash || url.pathname;
- }
-
async waitForUrlNotContaining(fragment: string): Promise<void> {
await this.page.waitForURL(url => !url.toString().includes(fragment));
}
@@ -85,14 +76,8 @@ export class BasePage {
}
async waitForFormLabels(labelTexts: string[], timeout = 10000):
Promise<void> {
- await this.page.waitForFunction(
- texts => {
- const labels = Array.from(document.querySelectorAll('nz-form-label'));
- return texts.some(text => labels.some(l =>
l.textContent?.includes(text)));
- },
- labelTexts,
- { timeout }
- );
+ const locators = labelTexts.map(text => this.page.locator('nz-form-label',
{ hasText: text }));
+ await Promise.race(locators.map(l => l.waitFor({ state: 'attached',
timeout })));
}
async waitForElementAttribute(
@@ -109,25 +94,20 @@ export class BasePage {
}
}
- async waitForRouterOutletChild(timeout = 10000): Promise<void> {
- await expect(this.page.locator('zeppelin-workspace router-outlet +
*')).toHaveCount(1, { timeout });
- }
-
async fillAndVerifyInput(
locator: Locator,
value: string,
options?: { timeout?: number; clearFirst?: boolean }
): Promise<void> {
- const { timeout = 10000, clearFirst = true } = options || {};
+ const { timeout = 10000 } = options || {};
await expect(locator).toBeVisible({ timeout });
await expect(locator).toBeEnabled({ timeout: 5000 });
- if (clearFirst) {
- await locator.clear();
- }
-
+ // Click first so Angular's form control is focused and its initial
setValue cycle
+ // has completed before we overwrite it. Then fill() atomically sets the
value.
+ await locator.click();
await locator.fill(value);
- await expect(locator).toHaveValue(value);
+ await expect(locator).toHaveValue(value, { timeout: 10000 });
}
}
diff --git a/zeppelin-web-angular/e2e/models/home-page.ts
b/zeppelin-web-angular/e2e/models/home-page.ts
index 81f5085790..3222fc3964 100644
--- a/zeppelin-web-angular/e2e/models/home-page.ts
+++ b/zeppelin-web-angular/e2e/models/home-page.ts
@@ -17,8 +17,6 @@ export class HomePage extends BasePage {
readonly notebookSection: Locator;
readonly helpSection: Locator;
readonly communitySection: Locator;
- readonly zeppelinLogo: Locator;
- readonly anonymousUserIndicator: Locator;
readonly welcomeSection: Locator;
readonly moreInfoGrid: Locator;
readonly notebookColumn: Locator;
@@ -28,9 +26,6 @@ export class HomePage extends BasePage {
readonly notebookHeading: Locator;
readonly helpHeading: Locator;
readonly communityHeading: Locator;
- readonly createNoteModal: Locator;
- readonly createNoteButton: Locator;
- readonly notebookNameInput: Locator;
readonly externalLinks: {
documentation: Locator;
mailingList: Locator;
@@ -41,13 +36,12 @@ export class HomePage extends BasePage {
createNewNoteLink: Locator;
importNoteLink: Locator;
filterInput: Locator;
- tree: Locator;
- noteActions: {
- renameNote: Locator;
- clearOutput: Locator;
- moveToTrash: Locator;
- };
};
+ readonly anonymousUserIndicator: Locator;
+ private readonly zeppelinLogo: Locator;
+ private readonly createNoteModal: Locator;
+ private readonly createNoteButton: Locator;
+ private readonly notebookNameInput: Locator;
constructor(page: Page) {
super(page);
@@ -58,8 +52,8 @@ export class HomePage extends BasePage {
this.anonymousUserIndicator = page.locator('text=anonymous');
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.notebookColumn = page.locator('[nz-col]').first(); // first() — left
column contains the Notebook section
+ this.helpCommunityColumn = page.locator('[nz-col]').last(); // last() —
right column contains Help and Community sections
this.welcomeDescription = page.locator('.welcome').getByText('Zeppelin is
web-based notebook');
this.refreshNoteButton = page.locator('a.refresh-note');
this.notebookHeading = this.notebookColumn.locator('h3');
@@ -79,13 +73,7 @@ export class HomePage extends BasePage {
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"]')
- }
+ filterInput: page.locator('zeppelin-node-list
input[placeholder*="Filter"]')
};
}
@@ -100,14 +88,6 @@ export class HomePage extends BasePage {
await this.waitForUrlNotContaining('#/login');
}
- async isHomeContentDisplayed(): Promise<boolean> {
- return this.welcomeTitle.isVisible();
- }
-
- async isAnonymousUser(): Promise<boolean> {
- return this.anonymousUserIndicator.isVisible();
- }
-
async clickZeppelinLogo(): Promise<void> {
await this.zeppelinLogo.click({ timeout: 15000 });
}
@@ -117,19 +97,11 @@ export class HomePage extends BasePage {
return text || '';
}
- async getWelcomeDescriptionText(): Promise<string> {
- const text = await this.welcomeDescription.textContent();
- return text || '';
- }
-
async clickRefreshNotes(): Promise<void> {
+ await this.refreshNoteButton.waitFor({ state: 'visible', timeout: 10000 });
await this.refreshNoteButton.click({ timeout: 15000 });
}
- async isNotebookListVisible(): Promise<boolean> {
- return this.zeppelinNodeList.isVisible();
- }
-
async clickCreateNewNote(): Promise<void> {
await this.nodeList.createNewNoteLink.click({ timeout: 15000 });
await this.createNoteModal.waitFor({ state: 'visible' });
@@ -141,7 +113,7 @@ export class HomePage extends BasePage {
// Wait for the modal form to be fully rendered with proper labels
await this.page.waitForSelector('nz-form-label', { timeout: 10000 });
- await this.waitForFormLabels(['Note Name', 'Clone Note']);
+ await this.waitForFormLabels(['Note Name']);
// Fill and verify the notebook name input
await this.fillAndVerifyInput(this.notebookNameInput, notebookName);
@@ -149,7 +121,9 @@ export class HomePage extends BasePage {
// Click the 'Create' button in the modal
await expect(this.createNoteButton).toBeEnabled({ timeout: 5000 });
await this.createNoteButton.click({ timeout: 15000 });
- await this.waitForPageLoad();
+ // Wait for navigation to the notebook page — confirms the note was
created server-side.
+ // waitForPageLoad() (domcontentloaded) fires instantly on SPA routing and
does not guarantee this.
+ await this.page.waitForURL(/\/notebook\//, { timeout: 45000 });
}
async clickImportNote(): Promise<void> {
@@ -159,19 +133,17 @@ export class HomePage extends BasePage {
async filterNotes(searchTerm: string): Promise<void> {
await this.page.waitForLoadState('domcontentloaded', { timeout: 10000 });
await this.nodeList.filterInput.waitFor({ state: 'visible', timeout: 5000
});
- await this.nodeList.filterInput.fill(searchTerm, { timeout: 15000 });
- }
-
- async isRefreshIconSpinning(): Promise<boolean> {
- const spinAttribute = await this.refreshIcon.getAttribute('nzSpin');
- return spinAttribute === 'true' || spinAttribute === '';
+ // pressSequentially fires real key events so Angular's ngModel detects
the change (fill() does not).
+ // Triple-click to select all, then type to replace or Backspace to clear.
+ await this.nodeList.filterInput.click({ clickCount: 3 });
+ if (searchTerm) {
+ await this.nodeList.filterInput.pressSequentially(searchTerm);
+ } else {
+ await this.nodeList.filterInput.press('Backspace');
+ }
}
async waitForRefreshToComplete(): Promise<void> {
await this.waitForElementAttribute('a.refresh-note i[nz-icon]', 'nzSpin',
false);
}
-
- async getDocumentationLinkHref(): Promise<string | null> {
- return this.externalLinks.documentation.getAttribute('href');
- }
}
diff --git a/zeppelin-web-angular/e2e/models/node-list-page.ts
b/zeppelin-web-angular/e2e/models/node-list-page.ts
index 17bd93de33..9c81bda02f 100644
--- a/zeppelin-web-angular/e2e/models/node-list-page.ts
+++ b/zeppelin-web-angular/e2e/models/node-list-page.ts
@@ -41,11 +41,7 @@ export class NodeListPage extends BasePage {
await this.createNewNoteButton.click();
}
- getFolderByName(folderName: string): Locator {
- return this.page.locator('nz-tree-node').filter({ hasText: folderName
}).first();
- }
-
- getNoteByName(noteName: string): Locator {
+ private getNoteByName(noteName: string): Locator {
return this.page.locator('nz-tree-node').filter({ hasText: noteName
}).first();
}
@@ -56,14 +52,6 @@ export class NodeListPage extends BasePage {
await noteLink.click();
}
- async isFilterInputVisible(): Promise<boolean> {
- return this.filterInput.isVisible();
- }
-
- async isTrashFolderVisible(): Promise<boolean> {
- return this.trashFolder.isVisible();
- }
-
async getAllVisibleNoteNames(): Promise<string[]> {
const noteElements = await this.notes.all();
const names: string[] = [];
diff --git a/zeppelin-web-angular/e2e/models/note-create-modal.ts
b/zeppelin-web-angular/e2e/models/note-create-modal.ts
index 1e1a0c4808..a00ef19219 100644
--- a/zeppelin-web-angular/e2e/models/note-create-modal.ts
+++ b/zeppelin-web-angular/e2e/models/note-create-modal.ts
@@ -47,8 +47,4 @@ export class NoteCreateModal extends BasePage {
async clickCreate(): Promise<void> {
await this.createButton.click();
}
-
- async isFolderInfoVisible(): Promise<boolean> {
- return this.folderInfoAlert.isVisible();
- }
}
diff --git a/zeppelin-web-angular/e2e/models/note-import-modal.ts
b/zeppelin-web-angular/e2e/models/note-import-modal.ts
index 11db6d5da4..e4634f94bc 100644
--- a/zeppelin-web-angular/e2e/models/note-import-modal.ts
+++ b/zeppelin-web-angular/e2e/models/note-import-modal.ts
@@ -59,16 +59,6 @@ export class NoteImportModal extends BasePage {
await this.urlTab.click();
}
- async isJsonFileTabSelected(): Promise<boolean> {
- const ariaSelected = await this.jsonFileTab.getAttribute('aria-selected');
- return ariaSelected === 'true';
- }
-
- async isUrlTabSelected(): Promise<boolean> {
- const ariaSelected = await this.urlTab.getAttribute('aria-selected');
- return ariaSelected === 'true';
- }
-
async setImportUrl(url: string): Promise<void> {
await this.urlInput.fill(url);
}
@@ -77,19 +67,7 @@ export class NoteImportModal extends BasePage {
await this.importNoteButton.click();
}
- async isImportNoteButtonDisabled(): Promise<boolean> {
- return this.importNoteButton.isDisabled();
- }
-
async getFileSizeLimit(): Promise<string> {
return (await this.fileSizeLimit.textContent()) || '';
}
-
- async isErrorAlertVisible(): Promise<boolean> {
- return this.errorAlert.isVisible();
- }
-
- async getErrorMessage(): Promise<string> {
- return (await this.errorAlert.textContent()) || '';
- }
}
diff --git a/zeppelin-web-angular/e2e/models/notebook-repo-item.util.ts
b/zeppelin-web-angular/e2e/models/notebook-repo-item.util.ts
index 06cdab7ed2..333af7c417 100644
--- a/zeppelin-web-angular/e2e/models/notebook-repo-item.util.ts
+++ b/zeppelin-web-angular/e2e/models/notebook-repo-item.util.ts
@@ -24,14 +24,12 @@ export class NotebookRepoItemUtil extends BasePage {
async verifyDisplayMode(): Promise<void> {
await expect(this.repoItemPage.editButton).toBeVisible();
- const isEditMode = await this.repoItemPage.isEditMode();
- expect(isEditMode).toBe(false);
+ await expect(this.repoItemPage.repositoryCard).not.toHaveClass(/\bedit\b/);
}
async verifyEditMode(): Promise<void> {
await expect(this.repoItemPage.saveButton).toBeVisible();
await expect(this.repoItemPage.cancelButton).toBeVisible();
- const isEditMode = await this.repoItemPage.isEditMode();
- expect(isEditMode).toBe(true);
+ await expect(this.repoItemPage.repositoryCard).toHaveClass(/\bedit\b/);
}
}
diff --git a/zeppelin-web-angular/e2e/models/notebook-repos-page.ts
b/zeppelin-web-angular/e2e/models/notebook-repos-page.ts
index 3272df477d..66234f5b61 100644
--- a/zeppelin-web-angular/e2e/models/notebook-repos-page.ts
+++ b/zeppelin-web-angular/e2e/models/notebook-repos-page.ts
@@ -34,10 +34,6 @@ export class NotebookReposPage extends BasePage {
this.page.waitForSelector('zeppelin-notebook-repo-item', { state:
'visible' })
]);
}
-
- async getRepositoryItemCount(): Promise<number> {
- return await this.repositoryItems.count();
- }
}
export class NotebookRepoItemPage extends BasePage {
@@ -72,20 +68,6 @@ export class NotebookRepoItemPage extends BasePage {
await this.cancelButton.click({ timeout: 15000 });
}
- async isEditMode(): Promise<boolean> {
- return await this.repositoryCard.evaluate(el =>
el.classList.contains('edit'));
- }
-
- async isSaveButtonEnabled(): Promise<boolean> {
- return await this.saveButton.isEnabled();
- }
-
- async getSettingValue(settingName: string): Promise<string> {
- const row = this.repositoryCard.locator('tbody tr').filter({ hasText:
settingName });
- const valueCell = row.locator('td').nth(1);
- return (await valueCell.textContent()) || '';
- }
-
async fillSettingInput(settingName: string, value: string): Promise<void> {
const row = this.repositoryCard.locator('tbody tr').filter({ hasText:
settingName });
const input = row.locator('input[nz-input]');
@@ -93,29 +75,15 @@ export class NotebookRepoItemPage extends BasePage {
await input.fill(value);
}
- async selectSettingDropdown(settingName: string, optionValue: string):
Promise<void> {
- const row = this.repositoryCard.locator('tbody tr').filter({ hasText:
settingName });
- const select = row.locator('nz-select');
- await select.click({ timeout: 15000 });
- await this.page.locator(`nz-option[nzvalue="${optionValue}"]`).click({
timeout: 15000 });
- }
-
async getSettingInputValue(settingName: string): Promise<string> {
const row = this.repositoryCard.locator('tbody tr').filter({ hasText:
settingName });
const input = row.locator('input[nz-input]');
return await input.inputValue();
}
- async isInputVisible(settingName: string): Promise<boolean> {
- const row = this.repositoryCard.locator('tbody tr').filter({ hasText:
settingName });
- const input = row.locator('input[nz-input]');
- return await input.isVisible();
- }
-
- async isDropdownVisible(settingName: string): Promise<boolean> {
+ async getSettingValue(settingName: string): Promise<string> {
const row = this.repositoryCard.locator('tbody tr').filter({ hasText:
settingName });
- const select = row.locator('nz-select');
- return await select.isVisible();
+ return (await row.locator('td').nth(1).textContent()) || '';
}
async getSettingCount(): Promise<number> {
diff --git a/zeppelin-web-angular/e2e/models/notebook.util.ts
b/zeppelin-web-angular/e2e/models/notebook.util.ts
index 00e8dbc183..98ee1d9648 100644
--- a/zeppelin-web-angular/e2e/models/notebook.util.ts
+++ b/zeppelin-web-angular/e2e/models/notebook.util.ts
@@ -33,10 +33,8 @@ export class NotebookUtil extends BasePage {
await waitForZeppelinReady(this.page);
// Wait for URL to not contain 'login' and for the notebook list to appear
- await this.page.waitForFunction(
- () => !window.location.href.includes('#/login') &&
document.querySelector('zeppelin-node-list') !== null,
- { timeout: 30000 }
- );
+ await this.page.waitForURL(url => !url.toString().includes('#/login'), {
timeout: 30000 });
+ await this.page.locator('zeppelin-node-list').waitFor({ state: 'attached',
timeout: 30000 });
await expect(this.homePage.zeppelinNodeList).toBeVisible({ timeout: 90000
});
await this.homePage.createNote(notebookName);
diff --git a/zeppelin-web-angular/e2e/models/workspace-page.ts
b/zeppelin-web-angular/e2e/models/workspace-page.ts
deleted file mode 100644
index 1fdcf9e5a7..0000000000
--- a/zeppelin-web-angular/e2e/models/workspace-page.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * 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 routerOutlet: Locator;
-
- constructor(page: Page) {
- super(page);
- this.routerOutlet = page.locator('zeppelin-workspace router-outlet');
- }
-}
diff --git a/zeppelin-web-angular/e2e/models/workspace-page.util.ts
b/zeppelin-web-angular/e2e/models/workspace-page.util.ts
deleted file mode 100644
index 8ed557b66d..0000000000
--- a/zeppelin-web-angular/e2e/models/workspace-page.util.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * 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 { BasePage } from './base-page';
-import { WorkspacePage } from './workspace-page';
-import { performLoginIfRequired, waitForZeppelinReady } from '../utils';
-
-export class WorkspaceUtil extends BasePage {
- private workspacePage: WorkspacePage;
-
- constructor(page: Page) {
- super(page);
- this.workspacePage = new WorkspacePage(page);
- }
-
- async navigateAndWaitForLoad(): Promise<void> {
- await this.workspacePage.navigateToWorkspace();
- await performLoginIfRequired(this.page);
- await waitForZeppelinReady(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.zeppelinHeader).toBeVisible();
- } else {
- await expect(this.workspacePage.zeppelinHeader).toBeHidden();
- }
- }
-
- async verifyRouterOutletActivation(): Promise<void> {
- await expect(this.workspacePage.routerOutlet).toBeAttached();
- await this.waitForRouterOutletChild();
- }
-
- 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/app.spec.ts
b/zeppelin-web-angular/e2e/tests/app.spec.ts
index 5d956c747f..d637896e0b 100644
--- a/zeppelin-web-angular/e2e/tests/app.spec.ts
+++ b/zeppelin-web-angular/e2e/tests/app.spec.ts
@@ -29,64 +29,47 @@ test.describe('Zeppelin App Component', () => {
test('should have correct component selector and structure', async ({ page
}) => {
await basePage.waitForPageLoad();
- // Test zeppelin-root selector
- const zeppelinRoot = page.locator('zeppelin-root');
- await expect(zeppelinRoot).toBeAttached();
-
await waitForZeppelinReady(page);
// Verify router-outlet is inside zeppelin-root (use first to avoid
multiple elements)
- const routerOutlet = zeppelinRoot.locator('router-outlet').first();
- await expect(routerOutlet).toBeAttached();
+ const zeppelinRoot = page.locator('zeppelin-root');
+
+ // Verify routing has activated by checking that actual content is
rendered inside the workspace
+ await expect(zeppelinRoot.locator('zeppelin-workspace')).toBeVisible();
// Check for loading spinner
const loadingSpinner = zeppelinRoot.locator('zeppelin-spin').filter({
hasText: 'Getting Ticket Data' });
const logoutSpinner = zeppelinRoot.locator('zeppelin-spin').filter({
hasText: 'Logging out' });
- // Loading spinner should exist, logout spinner may or may not exist
depending on conditions
- const loadingSpinnerCount = await loadingSpinner.count();
- const logoutSpinnerCount = await logoutSpinner.count();
-
- expect(loadingSpinnerCount).toBeGreaterThanOrEqual(0);
- expect(logoutSpinnerCount).toBeGreaterThanOrEqual(0);
+ // After waitForZeppelinReady, both spinners must be gone
+ await expect(loadingSpinner).toHaveCount(0);
+ await expect(logoutSpinner).toHaveCount(0);
});
test('should have proper page title', async ({ page }) => {
await expect(page).toHaveTitle(/Zeppelin/);
});
- test('should display workspace after loading', async ({ page }) => {
+ test('should display home content after loading', async ({ page }) => {
await waitForZeppelinReady(page);
// After the `beforeEach` hook, which handles login, the workspace should
be visible.
await expect(basePage.zeppelinWorkspace).toBeVisible();
+ // Verify the home page content is rendered (not just a blank shell)
+ await
expect(basePage.zeppelinWorkspace.locator('zeppelin-home')).toBeVisible();
});
- test('should handle navigation events correctly', async ({ page }) => {
+ test('should hide loading spinner after navigation', async ({ page }) => {
await waitForZeppelinReady(page);
- // Test navigation back to root path
- try {
- await page.goto('/', { waitUntil: 'load', timeout: 10000 });
-
- // Check if loading spinner appears during navigation
- const loadingSpinner = page.locator('zeppelin-spin').filter({ hasText:
'Getting Ticket Data' });
-
- // Loading might be very fast, so we check if it exists
- const spinnerCount = await loadingSpinner.count();
- expect(spinnerCount).toBeGreaterThanOrEqual(0);
-
- await waitForZeppelinReady(page);
+ await page.goto('/', { waitUntil: 'load', timeout: 10000 });
+ await waitForZeppelinReady(page);
- // After ready, loading should be hidden if it was visible
- if (await loadingSpinner.isVisible()) {
- await expect(loadingSpinner).toBeHidden();
- }
- } catch (error) {
- console.log('Navigation test skipped due to timeout:', error);
- }
+ // After the app is ready, the loading spinner must be hidden
+ const loadingSpinner = page.locator('zeppelin-spin').filter({ hasText:
'Getting Ticket Data' });
+ await expect(loadingSpinner).toBeHidden();
});
- test('should properly manage loading state observable', async ({ page }) => {
+ test('should hide loading spinner after page reload', async ({ page }) => {
await basePage.waitForPageLoad();
// Test that loading$ observable works correctly
@@ -95,55 +78,44 @@ test.describe('Zeppelin App Component', () => {
// Reload page to trigger loading state
await page.reload({ waitUntil: 'load' });
- // Check loading state during page load
- const initialLoadingVisible = await loadingSpinner.isVisible();
-
- if (initialLoadingVisible) {
- await expect(loadingSpinner).toBeVisible();
- await expect(loadingSpinner).toContainText('Getting Ticket Data ...');
- }
+ // If the spinner is briefly visible during reload, it will resolve; just
wait for ready
// Wait for loading to complete
await waitForZeppelinReady(page);
await expect(loadingSpinner).toBeHidden();
});
- test('should handle logout observable correctly', async ({ page }) => {
+ test('should show logout spinner when logging out', async ({ page }) => {
await waitForZeppelinReady(page);
+ // Only test logout flow for authenticated (non-anonymous) users — skip
before any assertions
+ const statusElement = page.locator('.status');
+ await expect(statusElement).toBeVisible();
+ const statusText = await statusElement.textContent();
+ test.skip(statusText?.includes('anonymous') ?? false, 'Logout spinner only
applies to authenticated users');
+
const logoutSpinner = page.locator('zeppelin-spin').filter({ hasText:
'Logging out' });
// Initially logout spinner should be hidden
await expect(logoutSpinner).toBeHidden();
- // Check if we have a logout mechanism available
- const statusElement = page.locator('.status');
- if (await statusElement.isVisible()) {
- const statusText = await statusElement.textContent();
-
- if (statusText && !statusText.includes('anonymous')) {
- // If not anonymous user, test logout spinner
- await statusElement.click();
- const logoutButton = page.getByRole('link', { name: 'Logout' });
-
- if (await logoutButton.isVisible()) {
- await logoutButton.click();
-
- // Logout spinner should appear
- await expect(logoutSpinner).toBeVisible();
- await expect(logoutSpinner).toContainText('Logging out ...');
- }
- }
- }
+ await statusElement.click();
+ const logoutButton = page.getByRole('link', { name: 'Logout' });
+
+ // If the dropdown has no Logout link, auth is not configured — skip
gracefully
+ const logoutCount = await logoutButton.count();
+ test.skip(logoutCount === 0, 'Logout option not available — auth not
configured in this environment');
+
+ await logoutButton.click();
+
+ await expect(logoutSpinner).toBeVisible();
+ await expect(logoutSpinner).toContainText('Logging out ...');
});
test('should maintain component integrity during navigation', async ({ page
}) => {
await waitForZeppelinReady(page);
await performLoginIfRequired(page);
- const zeppelinRoot = page.locator('zeppelin-root');
- const routerOutlet = zeppelinRoot.locator('router-outlet').first();
-
// Navigate to different pages and ensure component remains intact
const testPaths = ['/#/notebook', '/#/jobmanager', '/#/configuration'];
@@ -151,36 +123,12 @@ test.describe('Zeppelin App Component', () => {
await page.goto(path, { waitUntil: 'load', timeout: 10000 });
await waitForZeppelinReady(page);
- // Component should still be attached
- await expect(zeppelinRoot).toBeAttached();
-
- // Router outlet should still be present
- await expect(routerOutlet).toBeAttached();
+ // Workspace must render visible content after each navigation (confirms
Angular didn't unmount the root component)
+ await expect(page.locator('zeppelin-workspace')).toBeVisible();
}
// Return to home
await page.goto('/', { waitUntil: 'load' });
await waitForZeppelinReady(page);
- await expect(zeppelinRoot).toBeAttached();
- });
-
- test('should verify spinner text content and visibility', async ({ page })
=> {
- await basePage.waitForPageLoad();
-
- // Check exact text content of spinners
- const loadingSpinner = page.locator('zeppelin-spin').filter({ hasText:
'Getting Ticket Data' });
- const logoutSpinner = page.locator('zeppelin-spin').filter({ hasText:
'Logging out' });
-
- // Verify spinner elements exist
- expect(await loadingSpinner.count()).toBeGreaterThanOrEqual(0);
- expect(await logoutSpinner.count()).toBeGreaterThanOrEqual(0);
-
- // If loading spinner is visible, check its exact text
- if (await loadingSpinner.isVisible()) {
- await expect(loadingSpinner).toHaveText('Getting Ticket Data ...');
- }
-
- // Logout spinner should not be visible initially
- await expect(logoutSpinner).toBeHidden();
});
});
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 5e73dc036d..3189863e13 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
@@ -54,27 +54,35 @@ test.describe('Anonymous User Login Redirect', () => {
const currentPath = getCurrentPath(page);
const isLoginUrlMaintained = currentPath.includes('#/login');
- const isHomeContentDisplayed = await homePage.isHomeContentDisplayed();
- const isAnonymousUser = await homePage.isAnonymousUser();
expect(isLoginUrlMaintained).toBe(false);
- expect(isHomeContentDisplayed).toBe(true);
- expect(isAnonymousUser).toBe(true);
+ await expect(homePage.welcomeTitle).toBeVisible();
+ await expect(homePage.anonymousUserIndicator).toBeVisible();
expect(currentPath).toContain('#/');
expect(currentPath).not.toContain('#/login');
});
- test('When accessing login page directly, Then should display all home
page elements correctly', async ({
+ test('When accessing login page directly, Then should display full home
page with all sections and links', async ({
page
}) => {
await page.goto('/#/login');
await waitForZeppelinReady(page);
await page.waitForURL(url => !url.toString().includes('#/login'));
+ // Sections
await expect(homePage.welcomeTitle).toBeVisible();
await expect(homePage.notebookSection).toBeVisible();
await expect(homePage.helpSection).toBeVisible();
await expect(homePage.communitySection).toBeVisible();
+ // Notebook actions
+ await expect(homePage.nodeList.createNewNoteLink).toBeVisible();
+ await expect(homePage.nodeList.importNoteLink).toBeVisible();
+ await expect(homePage.nodeList.filterInput).toBeVisible();
+ // External links
+ await expect(homePage.externalLinks.documentation).toBeVisible();
+ await expect(homePage.externalLinks.mailingList).toBeVisible();
+ await expect(homePage.externalLinks.issuesTracking).toBeVisible();
+ await expect(homePage.externalLinks.github).toBeVisible();
});
test('When clicking Zeppelin logo after redirect, Then should maintain
home URL and content', async ({ page }) => {
@@ -86,12 +94,11 @@ test.describe('Anonymous User Login Redirect', () => {
await homePage.clickZeppelinLogo();
await basePage.waitForPageLoad();
const pathAfterClick = getCurrentPath(page);
- const homeContentMaintained = await homePage.isHomeContentDisplayed();
expect(pathBeforeClick).toContain('#/');
expect(pathBeforeClick).not.toContain('#/login');
expect(pathAfterClick).toContain('#/');
- expect(homeContentMaintained).toBe(true);
+ await expect(homePage.welcomeTitle).toBeVisible();
});
test('When accessing login page, Then should redirect and maintain
anonymous user state', async ({ page }) => {
@@ -100,89 +107,75 @@ test.describe('Anonymous User Login Redirect', () => {
await page.waitForURL(url => !url.toString().includes('#/login'));
const basicMetadata = await getBasicPageMetadata(page);
- const isAnonymous = await homePage.isAnonymousUser();
expect(basicMetadata.title).toContain('Zeppelin');
expect(basicMetadata.path).toContain('#/');
expect(basicMetadata.path).not.toContain('#/login');
- expect(isAnonymous).toBe(true);
+ await expect(homePage.anonymousUserIndicator).toBeVisible();
});
- test('When accessing login page, Then should display welcome heading and
main sections', async ({ page }) => {
- await page.goto('/#/login');
+ test('When navigating between home and login URLs, Then should maintain
consistent user experience', async ({
+ page
+ }) => {
+ await page.goto('/#/');
await waitForZeppelinReady(page);
- await page.waitForURL(url => !url.toString().includes('#/login'));
- await expect(basePage.welcomeTitle).toBeVisible();
- await expect(page.locator('text=Notebook').first()).toBeVisible();
- await expect(page.locator('text=Help').first()).toBeVisible();
- await expect(page.locator('text=Community').first()).toBeVisible();
- });
+ const homeMetadata = await getBasicPageMetadata(page);
+ expect(homeMetadata.path).toContain('#/');
+ await expect(homePage.anonymousUserIndicator).toBeVisible();
- test('When accessing login page, Then should display notebook
functionalities', async ({ page }) => {
await page.goto('/#/login');
await waitForZeppelinReady(page);
await page.waitForURL(url => !url.toString().includes('#/login'));
- await expect(page.locator('text=Create new Note')).toBeVisible();
- await expect(page.locator('text=Import Note')).toBeVisible();
+ const loginMetadata = await getBasicPageMetadata(page);
+ expect(loginMetadata.path).toContain('#/');
+ expect(loginMetadata.path).not.toContain('#/login');
+ await expect(homePage.anonymousUserIndicator).toBeVisible();
- const filterInput = page.locator('input[placeholder*="Filter"]');
- if ((await filterInput.count()) > 0) {
- await expect(filterInput).toBeVisible();
- }
+ await homePage.navigateToLogin();
+ await expect(homePage.welcomeTitle).toBeVisible();
});
- test('When accessing login page, Then should display external links in
help and community sections', async ({
+ test('When accessing protected route directly, Then should load home
content for anonymous user', async ({
page
}) => {
- await page.goto('/#/login');
+ // Notebook-repos is a management route; anonymous users should either
access it or be redirected home
+ await page.goto('/#/notebook-repos');
await waitForZeppelinReady(page);
- await page.waitForURL(url => !url.toString().includes('#/login'));
- const docLinks = page.locator('a[href*="zeppelin.apache.org/docs"]');
- const communityLinks = page.locator('a[href*="community.html"]');
- const issuesLinks = page.locator('a[href*="issues.apache.org"]');
- const githubLinks =
page.locator('a[href*="github.com/apache/zeppelin"]');
+ // Then: Either the notebook-repos page loads (anonymous mode allows it)
OR
+ // the user is redirected back to home — both are valid; the app must
not crash or show an empty shell
+ const currentPath = getCurrentPath(page);
- if ((await docLinks.count()) > 0) {
- await expect(docLinks).toBeVisible();
- }
- if ((await communityLinks.count()) > 0) {
- await expect(communityLinks).toBeVisible();
- }
- if ((await issuesLinks.count()) > 0) {
- await expect(issuesLinks).toBeVisible();
- }
- if ((await githubLinks.count()) > 0) {
- await expect(githubLinks).toBeVisible();
+ await expect(homePage.anonymousUserIndicator).toBeVisible();
+ // The app root must still be rendering — not a blank white page
+ await expect(basePage.zeppelinWorkspace).toBeVisible();
+ // If redirected, must land on home (not an error page)
+ if (!currentPath.includes('#/notebook-repos')) {
+ // JUSTIFIED: both states are valid — notebook-repos accessible OR
redirect to home; only assert welcomeTitle on redirect path
+ await expect(basePage.welcomeTitle).toBeVisible();
}
});
- test('When navigating between home and login URLs, Then should maintain
consistent user experience', async ({
+ test('When accessing configuration route directly, Then should handle
navigation for anonymous user', async ({
page
}) => {
- await page.goto('/#/');
+ // Configuration is a management route; anonymous users should either
access it or be redirected home
+ await page.goto('/#/configuration');
await waitForZeppelinReady(page);
- const homeMetadata = await getBasicPageMetadata(page);
- const isHomeAnonymous = await homePage.isAnonymousUser();
- expect(homeMetadata.path).toContain('#/');
- expect(isHomeAnonymous).toBe(true);
-
- await page.goto('/#/login');
- await waitForZeppelinReady(page);
- await page.waitForURL(url => !url.toString().includes('#/login'));
-
- const loginMetadata = await getBasicPageMetadata(page);
- const isLoginAnonymous = await homePage.isAnonymousUser();
- expect(loginMetadata.path).toContain('#/');
- expect(loginMetadata.path).not.toContain('#/login');
- expect(isLoginAnonymous).toBe(true);
+ // Then: Either the configuration page loads (anonymous mode allows it)
OR
+ // the user is redirected back to home — both are valid; the app must
not crash
+ const currentPath = getCurrentPath(page);
- await homePage.navigateToLogin();
- const isHomeContentDisplayed = await homePage.isHomeContentDisplayed();
- expect(isHomeContentDisplayed).toBe(true);
+ await expect(homePage.anonymousUserIndicator).toBeVisible();
+ await expect(basePage.zeppelinWorkspace).toBeVisible();
+ if (!currentPath.includes('#/configuration')) {
+ // JUSTIFIED: both states are valid — in anonymous mode (no shiro.ini)
all routes including
+ // /configuration are accessible; shiro.ini url rules control whether
this route is restricted
+ await expect(basePage.welcomeTitle).toBeVisible({ timeout: 15000 });
+ }
});
test('When multiple page loads occur on login URL, Then should
consistently redirect to home', async ({ page }) => {
@@ -192,7 +185,7 @@ test.describe('Anonymous User Login Redirect', () => {
await waitForUrlNotContaining(page, '#/login');
await expect(basePage.welcomeTitle).toBeVisible();
- await expect(page.locator('text=anonymous')).toBeVisible();
+ await expect(page.getByText('anonymous', { exact: true
})).toBeVisible();
const path = getCurrentPath(page);
expect(path).toContain('#/');
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
index f41c00c544..cac761ae85 100644
--- a/zeppelin-web-angular/e2e/tests/home/home-page-elements.spec.ts
+++ b/zeppelin-web-angular/e2e/tests/home/home-page-elements.spec.ts
@@ -47,29 +47,6 @@ test.describe('Home Page - Core Elements', () => {
expect(welcomeText).toContain('interactive data analytics');
});
});
-
- test('should have proper welcome message structure', async () => {
- 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.welcomeTitle).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', () => {
@@ -85,13 +62,13 @@ test.describe('Home Page - Core Elements', () => {
await test.step('Then I should see all notebook section components',
async () => {
await expect(homePage.notebookSection).toBeVisible();
await expect(homePage.notebookHeading).toBeVisible();
+ await expect(homePage.notebookHeading).toContainText('Notebook');
await expect(homePage.refreshNoteButton).toBeVisible();
- await page.waitForSelector('zeppelin-node-list', { timeout: 10000 });
await expect(homePage.zeppelinNodeList).toBeVisible();
});
});
- test('should have functional refresh notes button', async () => {
+ test('should keep notebook list visible after refresh', async () => {
await test.step('Given I am on the home page with notebook section
visible', async () => {
await homePage.navigateToHome();
await expect(homePage.refreshNoteButton).toBeVisible();
@@ -104,24 +81,6 @@ test.describe('Home Page - Core Elements', () => {
await test.step('Then the notebook list should still be visible', async
() => {
await homePage.waitForRefreshToComplete();
await expect(homePage.zeppelinNodeList).toBeVisible();
- const isStillVisible = await homePage.zeppelinNodeList.isVisible();
- expect(isStillVisible).toBe(true);
- });
- });
-
- test('should display notebook list component', async ({ 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.zeppelinNodeList).toBeVisible();
- const isVisible = await homePage.isNotebookListVisible();
- expect(isVisible).toBe(true);
});
});
});
@@ -148,7 +107,7 @@ test.describe('Home Page - Core Elements', () => {
});
test.describe('Community Section', () => {
- test('should display community section with all links', async ({ page })
=> {
+ test('should display community section heading', async ({ page }) => {
await test.step('Given I am on the home page', async () => {
await homePage.navigateToHome();
});
@@ -160,14 +119,10 @@ test.describe('Home Page - Core Elements', () => {
await test.step('Then I should see the community section', async () => {
await expect(homePage.communitySection).toBeVisible();
await expect(homePage.communityHeading).toBeVisible();
+ await expect(homePage.communityHeading).toContainText('Community');
});
- await test.step('And I should see all community 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();
- });
+ // External link href/target/icon assertions are covered by
home-page-external-links.spec.ts
});
});
});
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
deleted file mode 100644
index fb3a56cbc2..0000000000
---
a/zeppelin-web-angular/e2e/tests/home/home-page-enhanced-functionality.spec.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * 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 Enhanced Functionality', () => {
- let homePage: HomePage;
-
- test.beforeEach(async ({ page }) => {
- homePage = new HomePage(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 () => {
- const href = await homePage.getDocumentationLinkHref();
- expect(href).toContain('zeppelin.apache.org/docs');
- expect(href).toMatch(/\/docs\/\d+\.\d+\.\d+(-SNAPSHOT)?\//);
- });
-
- test('When external links are checked Then should all open in new tab',
async () => {
- const links = [
- homePage.externalLinks.documentation,
- homePage.externalLinks.mailingList,
- homePage.externalLinks.issuesTracking,
- homePage.externalLinks.github
- ];
-
- for (const link of links) {
- const target = await link.getAttribute('target');
- expect(target).toBe('_blank');
- }
- });
- });
-
- test.describe('Given welcome section display', () => {
- test('When page loads Then should show welcome content with proper text',
async () => {
- await expect(homePage.welcomeSection).toBeVisible();
- await expect(homePage.welcomeTitle).toBeVisible();
- const headingText = await homePage.getWelcomeHeadingText();
- expect(headingText.trim()).toBe('Welcome to Zeppelin!');
- await expect(homePage.welcomeDescription).toBeVisible();
- const welcomeText = await homePage.welcomeDescription.textContent();
- expect(welcomeText).toContain('web-based notebook');
- expect(welcomeText).toContain('interactive data analytics');
- });
-
- test('When welcome section is displayed Then should contain interactive
elements', async ({ page }) => {
- await expect(homePage.notebookSection).toBeVisible();
- await expect(homePage.notebookHeading).toBeVisible();
- await expect(homePage.refreshNoteButton).toBeVisible();
- await page.waitForSelector('zeppelin-node-list', { timeout: 10000 });
- await expect(homePage.zeppelinNodeList).toBeVisible();
- });
- });
-
- test.describe('Given community section content', () => {
- test('When community section loads Then should display help and community
headings', async () => {
- await expect(homePage.helpSection).toBeVisible();
- await expect(homePage.helpHeading).toBeVisible();
- await expect(homePage.communitySection).toBeVisible();
- await expect(homePage.communityHeading).toBeVisible();
- });
-
- test('When external links are displayed Then should show correct targets',
async () => {
- const docHref = await
homePage.externalLinks.documentation.getAttribute('href');
- const mailHref = await
homePage.externalLinks.mailingList.getAttribute('href');
- const issuesHref = await
homePage.externalLinks.issuesTracking.getAttribute('href');
- const githubHref = await
homePage.externalLinks.github.getAttribute('href');
-
- expect(docHref).toContain('zeppelin.apache.org/docs');
- expect(mailHref).toContain('community.html');
- expect(issuesHref).toContain('issues.apache.org');
- expect(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
index ce44eb967b..97a250d6ab 100644
--- 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
@@ -28,18 +28,13 @@ test.describe('Home Page - External Links', () => {
test.describe('Documentation Link', () => {
test('should have correct documentation link with dynamic version', async
() => {
- 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 href = await
homePage.externalLinks.documentation.getAttribute('href');
- expect(href).toContain('zeppelin.apache.org/docs');
- expect(href).toContain('index.html');
+ expect(href).toMatch(/\/docs\/\d+\.\d+\.\d+(-SNAPSHOT)?\/index\.html/);
});
await test.step('And it should open in a new tab', async () => {
@@ -51,10 +46,6 @@ test.describe('Home Page - External Links', () => {
test.describe('Community Links', () => {
test('should have correct mailing list link', async () => {
- 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();
});
@@ -76,10 +67,6 @@ test.describe('Home Page - External Links', () => {
});
test('should have correct issues tracking link', async () => {
- 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();
});
@@ -101,10 +88,6 @@ test.describe('Home Page - External Links', () => {
});
test('should have correct GitHub link', async () => {
- 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();
});
@@ -125,31 +108,4 @@ test.describe('Home Page - External Links', () => {
});
});
});
-
- test.describe('Link Verification', () => {
- test('should have all external links with proper attributes', async () => {
- 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
index e960c3c6cb..5a12f6ea4e 100644
--- a/zeppelin-web-angular/e2e/tests/home/home-page-layout.spec.ts
+++ b/zeppelin-web-angular/e2e/tests/home/home-page-layout.spec.ts
@@ -27,16 +27,6 @@ test.describe('Home Page - Layout and Grid', () => {
});
test.describe('Responsive Grid Layout', () => {
- test('should display responsive grid structure', async ({ page }) => {
- await test.step('Given I am on the home page', async () => {
- await homePage.navigateToHome();
- });
-
- await test.step('When the page loads', async () => {
- await waitForZeppelinReady(page);
- });
- });
-
test('should have proper column distribution', async () => {
await test.step('Given I am on the home page', async () => {
await homePage.navigateToHome();
@@ -58,6 +48,7 @@ test.describe('Home Page - Layout and Grid', () => {
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
+ // JUSTIFIED: Help heading comes before Community heading in the
right-column DOM order
const helpHeading = homePage.helpCommunityColumn.locator('h3').first();
await expect(helpHeading).toBeVisible();
const helpText = await helpHeading.textContent();
@@ -78,6 +69,12 @@ test.describe('Home Page - Layout and Grid', () => {
await expect(homePage.moreInfoGrid).toBeVisible();
await expect(homePage.notebookColumn).toBeVisible();
await expect(homePage.helpCommunityColumn).toBeVisible();
+ // Verify headings are readable and contain expected text at tablet
width
+ const notebookHeading = homePage.notebookColumn.locator('h3');
+ // JUSTIFIED: Help heading comes before Community heading in the
right-column DOM order
+ const helpHeading = homePage.helpCommunityColumn.locator('h3').first();
+ await expect(notebookHeading).toContainText('Notebook');
+ await expect(helpHeading).toContainText('Help');
});
await test.step('When I resize to mobile view', async () => {
@@ -89,11 +86,14 @@ test.describe('Home Page - Layout and Grid', () => {
await expect(homePage.notebookColumn).toBeVisible();
await expect(homePage.helpCommunityColumn).toBeVisible();
- // Verify content is still accessible in mobile view
+ // Verify headings are readable and contain expected text in mobile
view
const notebookHeading = homePage.notebookColumn.locator('h3');
+ // JUSTIFIED: Help heading comes before Community heading in the
right-column DOM order
const helpHeading = homePage.helpCommunityColumn.locator('h3').first();
await expect(notebookHeading).toBeVisible();
+ await expect(notebookHeading).toContainText('Notebook');
await expect(helpHeading).toBeVisible();
+ await expect(helpHeading).toContainText('Help');
});
});
});
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
index f385de5f58..66be0f6f4d 100644
--- 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
@@ -18,235 +18,220 @@ addPageAnnotationBeforeEach(PAGES.WORKSPACE.HOME);
test.describe('Home Page Note Operations', () => {
let homePage: HomePage;
+ let testNoteName: string;
test.beforeEach(async ({ page }) => {
homePage = new HomePage(page);
+ testNoteName = `_e2e_ops_test_${Date.now()}`;
+
await page.goto('/#/');
await waitForZeppelinReady(page);
await performLoginIfRequired(page);
- const noteListLocator = page.locator('zeppelin-node-list');
- await expect(noteListLocator).toBeVisible({ 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();
+ // Create a test note so all operation tests have a real target
+ await homePage.createNote(testNoteName);
+ await page.goto('/#/');
+ await waitForZeppelinReady(page);
- if (notesExist > 0) {
- const firstNote = page.locator('.node .file').first();
- await firstNote.hover();
+ await expect(page.locator('zeppelin-node-list')).toBeVisible({ timeout:
15000 });
- await
expect(homePage.nodeList.noteActions.renameNote.first()).toBeVisible();
- await
expect(homePage.nodeList.noteActions.clearOutput.first()).toBeVisible();
- await
expect(homePage.nodeList.noteActions.moveToTrash.first()).toBeVisible();
- } else {
- console.log('No notes available for testing operations');
- }
+ // Force a note list refresh so the newly created note is guaranteed to
appear
+ await homePage.clickRefreshNotes();
+ await expect(page.locator('.node .file').filter({ hasText: testNoteName
})).toBeVisible({ timeout: 15000 });
+ });
+
+ test.describe('Given note operations are available', () => {
+ test('When hovering over note Then should show rename, clear, and delete
action buttons', async ({ page }) => {
+ const testNote = page.locator('.node .file').filter({ hasText:
testNoteName });
+ await testNote.hover();
+
+ // Scoped to testNote: CSS .node:hover reveals .operation icons only for
the hovered node
+ await expect(testNote.locator('.operation a[nztooltiptitle="Rename
note"]')).toBeVisible();
+ await expect(testNote.locator('.operation a[nztooltiptitle="Clear
output"]')).toBeVisible();
+ await expect(testNote.locator('.operation a[nztooltiptitle="Move note to
Trash"]')).toBeVisible();
});
test('When hovering over note actions Then should show tooltip
descriptions', async ({ page }) => {
- const noteExists = await page
- .locator('.node .file')
- .first()
- .isVisible()
- .catch(() => false);
+ const testNote = page.locator('.node .file').filter({ hasText:
testNoteName });
+ await testNote.hover();
+ // Wait for the buttons to become visible — CSS .node:hover makes <i>
display:inline-block
+ const renameBtn = testNote.locator('.operation a[nztooltiptitle="Rename
note"]');
+ const clearBtn = testNote.locator('.operation a[nztooltiptitle="Clear
output"]');
+ const trashBtn = testNote.locator('.operation a[nztooltiptitle="Move
note to Trash"]');
+ await renameBtn.waitFor({ state: 'visible' });
+
+ // dispatchEvent mouseenter — justified: nz-tooltip listens to
mouseenter; direct hover() on a child
+ // causes a CSS :hover race where the mouse leaves the parent .node
.file mid-movement, hiding the button.
+ await renameBtn.dispatchEvent('mouseenter');
+ await expect(page.locator('.ant-tooltip', { hasText: 'Rename note'
})).toBeVisible();
+ await renameBtn.dispatchEvent('mouseleave');
+
+ await clearBtn.dispatchEvent('mouseenter');
+ await expect(page.locator('.ant-tooltip', { hasText: 'Clear output'
})).toBeVisible();
+ await clearBtn.dispatchEvent('mouseleave');
+
+ await trashBtn.dispatchEvent('mouseenter');
+ await expect(page.locator('.ant-tooltip', { hasText: 'Move note to
Trash' })).toBeVisible();
+ await trashBtn.dispatchEvent('mouseleave');
+ });
+ });
- if (noteExists) {
- const firstNote = page.locator('.node .file').first();
- await firstNote.hover();
+ test.describe('Given rename note functionality', () => {
+ test('When rename button is clicked Then should open rename dialog', async
({ page }) => {
+ const testNote = page.locator('.node .file').filter({ hasText:
testNoteName });
+ await testNote.hover();
+
+ const renameButton = testNote.locator('.operation
a[nztooltiptitle="Rename note"]');
+ await expect(renameButton).toBeVisible();
+ await renameButton.click();
+
+ // JUSTIFIED: compound selector targets rename dialog; first() picks the
visible modal instance
+ await expect(page.locator('zeppelin-note-rename,
[role="dialog"].ant-modal').first()).toBeVisible({
+ timeout: 5000
+ });
+ });
+ });
- await
expect(homePage.nodeList.noteActions.renameNote.first()).toBeVisible();
- await
expect(homePage.nodeList.noteActions.clearOutput.first()).toBeVisible();
- await
expect(homePage.nodeList.noteActions.moveToTrash.first()).toBeVisible();
+ test.describe('Given clear output functionality', () => {
+ test('When clear output button is clicked Then should show and dismiss
confirmation dialog', async ({ page }) => {
+ const testNote = page.locator('.node .file').filter({ hasText:
testNoteName });
+ await testNote.hover();
- // Test tooltip visibility by hovering over each icon
- await homePage.nodeList.noteActions.renameNote.first().hover();
- await expect(page.locator('.ant-tooltip', { hasText: 'Rename note'
})).toBeVisible();
+ const clearButton = testNote.locator('.operation a[nztooltiptitle="Clear
output"]');
+ await expect(clearButton).toBeVisible();
+ await clearButton.click();
- await homePage.nodeList.noteActions.clearOutput.first().hover();
- await expect(page.locator('.ant-tooltip', { hasText: 'Clear output'
})).toBeVisible();
+ await expect(page.locator('text=Do you want to clear all
output?')).toBeVisible();
+ await page.locator('.ant-popover button:has-text("OK")').click();
- await homePage.nodeList.noteActions.moveToTrash.first().hover();
- await expect(page.locator('.ant-tooltip', { hasText: 'Move note to
Trash' })).toBeVisible();
- }
+ // Popover should close after confirming the operation
+ await expect(page.locator('text=Do you want to clear all
output?')).not.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 = homePage.nodeList.noteActions.renameNote.first();
- await expect(renameButton).toBeVisible();
- await renameButton.click();
-
- await page
- .waitForFunction(
- () =>
- 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 move to trash functionality', () => {
+ test('When move to trash is confirmed Then should move note to trash
folder', async ({ page }) => {
+ const testNote = page.locator('.node .file').filter({ hasText:
testNoteName });
+ await testNote.hover();
+
+ const deleteButton = testNote.locator('.operation a[nztooltiptitle="Move
note to Trash"]');
+ await expect(deleteButton).toBeVisible();
+ await deleteButton.click();
+
+ await expect(page.locator('text=This note will be moved to
trash.')).toBeVisible();
+ await page.locator('.ant-popover button:has-text("OK")').click();
+
+ // Source note must disappear from the list — not just that Trash folder
appears
+ await expect(testNote).not.toBeVisible({ timeout: 10000 });
+ await expect(page.locator('.node .folder').filter({ hasText: 'Trash'
})).toBeVisible({ timeout: 10000 });
});
});
- 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 = homePage.nodeList.noteActions.clearOutput.first();
- await expect(clearButton).toBeVisible();
- await clearButton.click();
-
- await expect(page.locator('text=Do you want to clear all
output?')).toBeVisible();
+ test.describe('Given note filter with special characters', () => {
+ test('When filtering with special characters Then should not crash and
should dim non-matching results', async ({
+ page
+ }) => {
+ for (const char of ['#', '%', '"']) {
+ await homePage.filterNotes(char);
+
+ // App must not crash — node list container remains present
+ await expect(page.locator('zeppelin-node-list')).toBeVisible();
+ // Test note name contains none of these chars, so it must be dimmed
(.not-matched class)
+ await expect(page.locator('.node').filter({ hasText: testNoteName
})).toHaveClass(/not-matched/, {
+ timeout: 10000
+ });
}
- });
- 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 = homePage.nodeList.noteActions.clearOutput.first();
- await expect(clearButton).toBeVisible();
- await clearButton.click();
-
- const confirmButton = page.locator('button:has-text("Yes")');
- if (await confirmButton.isVisible()) {
- await confirmButton.click();
- }
- }
+ // Clearing filter restores the note (removes .not-matched)
+ await homePage.filterNotes('');
+ await expect(page.locator('.node').filter({ hasText: testNoteName
})).not.toHaveClass(/not-matched/, {
+ timeout: 5000
+ });
});
});
- 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 = homePage.nodeList.noteActions.moveToTrash.first();
- await expect(deleteButton).toBeVisible();
- await deleteButton.click();
-
- await expect(page.locator('text=This note will be moved to
trash.')).toBeVisible();
- }
- });
+ test.describe('Given max length note name input', () => {
+ test('When note name input is filled with a very long string Then input
should cap or accept gracefully', async ({
+ page
+ }) => {
+ await homePage.clickCreateNewNote();
+ await page.waitForSelector('nz-form-label', { timeout: 10000 });
- 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 = homePage.nodeList.noteActions.moveToTrash.first();
- await expect(deleteButton).toBeVisible();
- await deleteButton.click();
-
- const confirmButton = page.locator('button:has-text("Yes")');
- if (await confirmButton.isVisible()) {
- await confirmButton.click();
-
- const trashFolder = page.locator('.node .folder').filter({ hasText:
'Trash' });
- await expect(trashFolder).toBeVisible();
- }
+ const notebookNameInput = page.locator('div.ant-modal-content
input[name="noteName"]');
+ const maxLengthAttr = await notebookNameInput.getAttribute('maxlength');
+ const longName = `_e2e_ml_${'a'.repeat(300)}`;
+
+ await notebookNameInput.fill(longName);
+ const actualValue = await notebookNameInput.inputValue();
+
+ // Must have content — input did not silently reject the fill
+ expect(actualValue.length).toBeGreaterThan(0);
+
+ if (maxLengthAttr !== null) {
+ // If the element enforces maxlength, the value must be capped at that
limit
+ expect(actualValue.length).toBeLessThanOrEqual(parseInt(maxLengthAttr,
10));
+ } else {
+ // No client-side cap — the full value passes through
+ expect(actualValue).toBe(longName);
}
+
+ // Dismiss the modal without creating
+ await page.keyboard.press('Escape');
+ await page.locator('div.ant-modal-content').waitFor({ state: 'detached',
timeout: 5000 });
});
});
test.describe('Given trash folder operations', () => {
+ test.beforeEach(async ({ page }) => {
+ // Move the test note to trash to put the trash folder into a known state
+ const testNote = page.locator('.node .file').filter({ hasText:
testNoteName });
+ await testNote.hover();
+ await testNote.locator('.operation').waitFor({ state: 'visible' });
+
+ const deleteButton = testNote.locator('.operation a[nztooltiptitle="Move
note to Trash"]');
+ await deleteButton.click();
+
+ await expect(page.locator('text=This note will be moved to
trash.')).toBeVisible();
+ await page.locator('.ant-popover button:has-text("OK")').click();
+ await expect(page.locator('.node .folder').filter({ hasText: 'Trash'
})).toBeVisible({ timeout: 10000 });
+ });
+
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();
- }
+ const trashFolder = page.locator('.node .folder').filter({ hasText:
'Trash' });
+ await trashFolder.hover();
+ await trashFolder.locator('.operation').waitFor({ state: 'visible' });
+
+ // Scoped to trashFolder: same CSS hover rule applies to .node:hover for
folder nodes
+ await expect(trashFolder.locator('.operation a[nztooltiptitle*="Restore
all"]')).toBeVisible();
+ await expect(trashFolder.locator('.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();
- }
+ const trashFolder = page.locator('.node .folder').filter({ hasText:
'Trash' });
+ await trashFolder.hover();
+ await trashFolder.locator('.operation').waitFor({ state: 'visible' });
+
+ const restoreButton = trashFolder.locator('.operation
a[nztooltiptitle*="Restore all"]');
+ await expect(restoreButton).toBeVisible();
+ // JUSTIFIED: hovering restoreButton directly can briefly exit
.folder:hover and hide it
+ await restoreButton.click({ force: true });
+
+ 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);
+ const trashFolder = page.locator('.node .folder').filter({ hasText:
'Trash' });
+ await trashFolder.hover();
+ await trashFolder.locator('.operation').waitFor({ state: 'visible' });
- if (trashExists) {
- const trashFolder = page.locator('.node .folder').filter({ hasText:
'Trash' });
- await trashFolder.hover();
+ const emptyButton = trashFolder.locator('.operation
a[nztooltiptitle*="Empty all"]');
+ await expect(emptyButton).toBeVisible();
+ await emptyButton.hover();
+ await emptyButton.click();
- 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();
- }
+ 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
index 3cb9725dcb..c14a5474e2 100644
--- 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
@@ -27,14 +27,7 @@ test.describe('Home Page Notebook Actions', () => {
});
test.describe('Given notebook list is displayed', () => {
- test('When page loads Then should show notebook actions', async () => {
- await expect(homePage.nodeList.createNewNoteLink).toBeVisible();
- await expect(homePage.nodeList.importNoteLink).toBeVisible();
- await expect(homePage.nodeList.filterInput).toBeVisible();
- await expect(homePage.nodeList.tree).toBeVisible();
- });
-
- test('When refresh button is clicked Then should trigger reload with
loading state', async ({ page }) => {
+ test('When refresh button is clicked Then should keep refresh icon
visible', async ({ page }) => {
const refreshButton = page.locator('a.refresh-note');
const refreshIcon = page.locator('a.refresh-note i[nz-icon]');
@@ -42,48 +35,36 @@ test.describe('Home Page Notebook Actions', () => {
await expect(refreshIcon).toBeVisible();
await homePage.clickRefreshNotes();
-
- await page.waitForTimeout(500);
+ await homePage.waitForRefreshToComplete();
await expect(refreshIcon).toBeVisible();
});
test('When filter is used Then should filter notebook list', async ({ page
}) => {
- // Note (ZEPPELIN-6386):
- // The Notebook search filter in the New UI is currently too slow,
- // so this test is temporarily skipped. The skip will be removed
- // once the performance issue is resolved.
- test.skip();
+ test.skip(true, 'ZEPPELIN-6386: Notebook search filter in the New UI is
too slow — re-enable when fixed');
await homePage.filterNotes('test');
await page.waitForLoadState('networkidle', { timeout: 15000 });
const filteredResults = await page.locator('nz-tree .node').count();
- expect(filteredResults).toBeGreaterThanOrEqual(0);
+ expect(filteredResults).toBeGreaterThan(0);
});
- });
- test.describe('Given create new note action', () => {
- test('When create new note is clicked Then should open note creation
modal', async ({ page }) => {
- await homePage.clickCreateNewNote();
- await page.waitForSelector('zeppelin-note-create', { timeout: 10000 });
- await expect(page.locator('zeppelin-note-create')).toBeVisible();
- });
- });
+ test('When filter input receives special characters Then page should not
crash', async ({ page }) => {
+ // Given: The filter input is visible
+ await expect(homePage.nodeList.filterInput).toBeVisible();
- test.describe('Given import note action', () => {
- test('When import note is clicked Then should open import modal', async ({
page }) => {
- await homePage.clickImportNote();
- await page.waitForSelector('zeppelin-note-import', { timeout: 10000 });
- await expect(page.locator('zeppelin-note-import')).toBeVisible();
- });
- });
+ // When: User types special characters that could break regex or URL
encoding
+ for (const specialInput of ['[test]', '*.note', '/folder/sub', 'a?b=c'])
{
+ await homePage.nodeList.filterInput.fill(specialInput);
+ // Then: The page must still render without crashing — no blank
screen, input remains editable.
+ // Note: nz-tree may be hidden when the filter returns 0 results; that
is valid behavior.
+ await expect(page.locator('zeppelin-node-list')).toBeVisible();
+ await expect(homePage.nodeList.filterInput).toBeEditable();
+ await expect(homePage.nodeList.filterInput).toHaveValue(specialInput);
+ await expect(page.locator('zeppelin-header')).toBeVisible();
+ }
- test.describe('Given notebook refresh functionality', () => {
- test('When refresh is triggered Then should maintain notebook list
visibility', async () => {
- await homePage.clickRefreshNotes();
- await homePage.waitForRefreshToComplete();
- await expect(homePage.zeppelinNodeList).toBeVisible();
- const isStillVisible = await homePage.zeppelinNodeList.isVisible();
- expect(isStillVisible).toBe(true);
+ // Clean up: clear the filter so other tests start fresh
+ await homePage.nodeList.filterInput.fill('');
});
});
});
diff --git
a/zeppelin-web-angular/e2e/tests/notebook/published/published-paragraph.spec.ts
b/zeppelin-web-angular/e2e/tests/notebook/published/published-paragraph.spec.ts
index b15facedc7..475da43f3b 100644
---
a/zeppelin-web-angular/e2e/tests/notebook/published/published-paragraph.spec.ts
+++
b/zeppelin-web-angular/e2e/tests/notebook/published/published-paragraph.spec.ts
@@ -51,6 +51,7 @@ test.describe('Published Paragraph', () => {
await
publishedParagraphPage.navigateToPublishedParagraph(nonExistentIds.noteId,
nonExistentIds.paragraphId);
+ // JUSTIFIED: last() handles stacked modals where the most recent error
modal appears on top
const modal = page.locator('.ant-modal', { hasText: /not found/i
}).last();
await expect(modal).toBeVisible({ timeout: 10000 });
await expect(modal).toContainText(/not found/i);
@@ -77,7 +78,7 @@ test.describe('Published Paragraph', () => {
await
publishedParagraphPage.navigateToPublishedParagraph(nonExistentIds.noteId,
nonExistentIds.paragraphId);
// Modal must appear — we navigated to non-existent IDs
- const modal = page.locator('.ant-modal').last();
+ const modal = page.locator('.ant-modal').filter({ hasText: /not found/i
});
await expect(modal).toBeVisible({ timeout: 10000 });
await publishedParagraphPage.okButton.click();
@@ -93,7 +94,7 @@ test.describe('Published Paragraph', () => {
await page.goto(`/#/notebook/${noteId}`);
await page.waitForLoadState('networkidle');
- // createTestNotebook creates a single paragraph, so .first() is the
target
+ // JUSTIFIED: createTestNotebook creates a single paragraph; first() is
deterministic
const paragraphElement =
page.locator('zeppelin-notebook-paragraph').first();
await expect(paragraphElement).toBeVisible({ timeout: 10000 });
@@ -115,11 +116,19 @@ test.describe('Published Paragraph', () => {
test('should load published paragraph component by direct URL navigation',
async ({ page }) => {
await
page.goto(`/#/notebook/${testNotebook.noteId}/paragraph/${testNotebook.paragraphId}`);
- await page.waitForLoadState('networkidle');
+
+ // Wait for the confirmation modal — it signals NOTE was received and
the component is fully rendered.
+ // networkidle fires before the NOTE WebSocket response, so the modal is
the reliable ready signal.
+ const confirmModal = page.locator('.ant-modal-confirm');
+ await expect(confirmModal).toBeVisible({ timeout: 15000 });
+ await publishedParagraphPage.cancelButton.click();
+ await expect(confirmModal).toBeHidden({ timeout: 5000 });
await expect(page).toHaveURL(
new
RegExp(`/notebook/${testNotebook.noteId}/paragraph/${testNotebook.paragraphId}`)
);
+ // JUSTIFIED: paragraph has no results yet so the component renders 0×0
— toBeAttached confirms
+ // the route is active without requiring visible content.
await expect(page.locator('zeppelin-publish-paragraph')).toBeAttached({
timeout: 10000 });
});
@@ -127,19 +136,17 @@ test.describe('Published Paragraph', () => {
const { noteId, paragraphId } = testNotebook;
await page.goto(`/#/notebook/${noteId}/paragraph/${paragraphId}`);
- await page.waitForLoadState('networkidle');
- const publishedContainer = page.locator('zeppelin-publish-paragraph');
- await expect(publishedContainer).toBeAttached({ timeout: 10000 });
-
- // Confirmation modal should appear for paragraph execution
+ // Confirmation modal signals NOTE was received and component is fully
rendered.
const modal = page.locator('.ant-modal');
await expect(modal).toBeVisible({ timeout: 20000 });
await publishedParagraphPage.runButton.click();
await expect(modal).not.toBeVisible({ timeout: 10000 });
- // Published container should remain attached after modal dismissal
+ const publishedContainer = page.locator('zeppelin-publish-paragraph');
+ // JUSTIFIED: paragraph has no results yet so the component renders 0×0
— toBeAttached confirms
+ // the route is still active (not navigated away) without requiring
visible content.
await expect(publishedContainer).toBeAttached({ timeout: 10000 });
});
@@ -156,6 +163,7 @@ test.describe('Published Paragraph', () => {
await test.step('And React widget should be mounted in the container',
async () => {
// React mount() renders <div data-testid="react-published-paragraph">
or <Empty> (Alert)
const reactContent =
page.locator('[data-testid="react-published-paragraph"], .ant-alert');
+ // JUSTIFIED: compound selector covers React success + error fallback
(.ant-alert); either may render
await expect(reactContent).toBeAttached({ timeout: 15000 });
});
});
@@ -166,8 +174,15 @@ test.describe('Published Paragraph', () => {
const { noteId, paragraphId } = testNotebook;
await page.goto(`/#/notebook/${noteId}/paragraph/${paragraphId}`);
- await page.waitForLoadState('networkidle');
+ // Wait for modal then dismiss — component visibility is unreliable
while modal is animating open.
+ const confirmModal = page.locator('.ant-modal-confirm');
+ await expect(confirmModal).toBeVisible({ timeout: 15000 });
+ await publishedParagraphPage.cancelButton.click();
+ await expect(confirmModal).toBeHidden({ timeout: 5000 });
+
+ // JUSTIFIED: paragraph has no results yet so the component renders 0×0
— toBeAttached confirms
+ // the route is active without requiring visible content.
await expect(page.locator('zeppelin-publish-paragraph')).toBeAttached({
timeout: 10000 });
await
expect(page.locator('zeppelin-notebook-paragraph-code-editor')).toBeHidden();
await
expect(page.locator('zeppelin-notebook-paragraph-control')).toBeHidden();
@@ -175,59 +190,26 @@ test.describe('Published Paragraph', () => {
});
test.describe('Confirmation Modal and Execution', () => {
- test('should show confirmation modal with code preview and allow running',
async ({ page }) => {
- const { noteId, paragraphId } = testNotebook;
-
- await publishedParagraphPage.navigateToNotebook(noteId);
-
- // Verify paragraph has no results yet
- const paragraphElement =
page.locator('zeppelin-notebook-paragraph').first();
- await
expect(paragraphElement.locator('zeppelin-notebook-paragraph-result')).toBeHidden();
-
- await publishedParagraphPage.navigateToPublishedParagraph(noteId,
paragraphId);
-
- await expect(page).toHaveURL(new RegExp(`/paragraph/${paragraphId}`));
+ for (const reactMode of [false, true]) {
+ test(`should show confirmation modal with code preview and allow
running${reactMode ? ' (React mode)' : ''}`, async ({
+ page
+ }) => {
+ const { noteId, paragraphId } = testNotebook;
- const modal = publishedParagraphPage.confirmationModal;
- await expect(modal).toBeVisible();
-
- // Modal title
- await expect(publishedParagraphPage.modalTitle).toHaveText('Run
Paragraph?');
-
- // Code preview content
- const modalContent = modal.locator('.ant-modal-confirm-content');
- await expect(modalContent).toContainText('This paragraph contains the
following code:');
- await expect(modalContent).toContainText('Would you like to execute this
code?');
-
- // Code preview element
- const codePreview = modalContent.locator('pre, code, .code-preview,
[class*="code"]').first();
- await expect(codePreview).toBeVisible();
-
- // Run and Cancel buttons
- await expect(publishedParagraphPage.runButton).toBeVisible();
- await expect(publishedParagraphPage.cancelButton).toBeVisible();
-
- // Execute and verify modal dismissal
- await publishedParagraphPage.runButton.click();
- await expect(modal).toBeHidden();
- });
-
- test('should show confirmation modal in React mode and allow running',
async ({ page }) => {
- const { noteId, paragraphId } = testNotebook;
-
- await test.step('Given paragraph has no results in normal notebook
view', async () => {
await publishedParagraphPage.navigateToNotebook(noteId);
+ // JUSTIFIED: createTestNotebook creates a single paragraph; first()
is deterministic
const paragraphElement =
page.locator('zeppelin-notebook-paragraph').first();
await
expect(paragraphElement.locator('zeppelin-notebook-paragraph-result')).toBeHidden();
- });
- await test.step('When I navigate to React mode published paragraph URL',
async () => {
- await
page.goto(`/#/notebook/${noteId}/paragraph/${paragraphId}?react=true`);
+ const urlSuffix = reactMode ? '?react=true' : '';
+ await
page.goto(`/#/notebook/${noteId}/paragraph/${paragraphId}${urlSuffix}`);
await waitForZeppelinReady(page);
- });
- await test.step('Then confirmation modal should appear and allow
execution', async () => {
+ if (!reactMode) {
+ await expect(page).toHaveURL(new
RegExp(`/paragraph/${paragraphId}`));
+ }
+
const modal = publishedParagraphPage.confirmationModal;
await expect(modal).toBeVisible({ timeout: 30000 });
@@ -237,9 +219,20 @@ test.describe('Published Paragraph', () => {
await expect(modalContent).toContainText('This paragraph contains the
following code:');
await expect(modalContent).toContainText('Would you like to execute
this code?');
+ if (!reactMode) {
+ // Code preview element only checked in Angular mode
+ // JUSTIFIED: compound fallback selector; first() picks any element
that confirms code preview is rendered
+ const codePreview = modalContent.locator('pre, code, .code-preview,
[class*="code"]').first();
+ await expect(codePreview).toBeVisible();
+ await expect(codePreview).not.toBeEmpty(); // code must have
content, not just an empty container
+
+ await expect(publishedParagraphPage.runButton).toBeVisible();
+ await expect(publishedParagraphPage.cancelButton).toBeVisible();
+ }
+
await publishedParagraphPage.runButton.click();
await expect(modal).toBeHidden();
});
- });
+ }
});
});
diff --git
a/zeppelin-web-angular/e2e/tests/share/about-zeppelin/about-zeppelin-modal.spec.ts
b/zeppelin-web-angular/e2e/tests/share/about-zeppelin/about-zeppelin-modal.spec.ts
index 2e8ab234a7..1f2d6fed08 100644
---
a/zeppelin-web-angular/e2e/tests/share/about-zeppelin/about-zeppelin-modal.spec.ts
+++
b/zeppelin-web-angular/e2e/tests/share/about-zeppelin/about-zeppelin-modal.spec.ts
@@ -35,18 +35,17 @@ test.describe('About Zeppelin Modal', () => {
test('Given user clicks About Zeppelin menu item, When modal opens, Then
modal should display all required elements', async () => {
await expect(aboutModal.modal).toBeVisible();
- await expect(aboutModal.modalTitle).toBeVisible();
- await expect(aboutModal.heading).toBeVisible();
+ await expect(aboutModal.modalTitle).toContainText('About Zeppelin');
+ await expect(aboutModal.heading).toContainText('Apache Zeppelin');
await expect(aboutModal.logo).toBeVisible();
- await expect(aboutModal.versionText).toBeVisible();
+ await expect(aboutModal.versionText).not.toBeEmpty();
await expect(aboutModal.getInvolvedLink).toBeVisible();
await expect(aboutModal.licenseLink).toBeVisible();
});
test('Given About Zeppelin modal is open, When viewing version information,
Then version should be displayed', async () => {
const version = await aboutModal.getVersionText();
- expect(version).toBeTruthy();
- expect(version.length).toBeGreaterThan(0);
+ expect(version).toMatch(/\d+\.\d+/);
});
test('Given About Zeppelin modal is open, When checking external links, Then
links should have correct URLs', async () => {
@@ -62,8 +61,11 @@ test.describe('About Zeppelin Modal', () => {
await expect(aboutModal.modal).not.toBeVisible();
});
- test('Given About Zeppelin modal is open, When checking logo, Then logo
should be visible and properly loaded', async () => {
- const isLogoVisible = await aboutModal.isLogoVisible();
- expect(isLogoVisible).toBe(true);
+ test('Given About Zeppelin modal is open, When checking logo, Then logo
should be visible and its image loaded', async () => {
+ await expect(aboutModal.logo).toBeVisible();
+ // JUSTIFIED: naturalWidth is the only reliable way to verify image has
loaded;
+ // Playwright has no built-in assertion for image load status
+ const naturalWidth = await aboutModal.logo.evaluate((img:
HTMLImageElement) => img.naturalWidth);
+ expect(naturalWidth).toBeGreaterThan(0);
});
});
diff --git
a/zeppelin-web-angular/e2e/tests/share/node-list/node-list-functionality.spec.ts
b/zeppelin-web-angular/e2e/tests/share/node-list/node-list-functionality.spec.ts
index 111d01011f..2ef30aa82f 100644
---
a/zeppelin-web-angular/e2e/tests/share/node-list/node-list-functionality.spec.ts
+++
b/zeppelin-web-angular/e2e/tests/share/node-list/node-list-functionality.spec.ts
@@ -31,16 +31,20 @@ test.describe('Node List Functionality', () => {
test('Given user is on home page, When viewing node list, Then node list
should display tree structure', async () => {
await expect(nodeListPage.nodeListContainer).toBeVisible();
await expect(nodeListPage.treeView).toBeVisible();
+ // JUSTIFIED: first() confirms at least one tree node is rendered in the
list
+ await
expect(nodeListPage.treeView.locator('nz-tree-node').first()).toBeVisible();
});
test('Given user is on home page, When viewing node list, Then action
buttons should be visible', async () => {
await expect(nodeListPage.createNewNoteButton).toBeVisible();
+ await expect(nodeListPage.createNewNoteButton).toContainText('Create new
Note');
await expect(nodeListPage.importNoteButton).toBeVisible();
+ await expect(nodeListPage.importNoteButton).toContainText('Import Note');
});
- test('Given user is on home page, When viewing node list, Then filter input
should be visible', async () => {
- const isFilterVisible = await nodeListPage.isFilterInputVisible();
- expect(isFilterVisible).toBe(true);
+ test('Given user is on home page, When viewing node list, Then filter input
should be visible with placeholder', async () => {
+ await expect(nodeListPage.filterInput).toBeVisible();
+ await expect(nodeListPage.filterInput).toHaveAttribute('placeholder',
/[Ff]ilter/);
});
test('Given a note has been moved to trash, When viewing node list, Then
trash folder should be visible', async ({
@@ -71,24 +75,28 @@ test.describe('Node List Functionality', () => {
// Wait for the trash folder to appear and verify
await expect(nodeListPage.trashFolder).toBeVisible({ timeout: 10000 });
- const isTrashVisible = await nodeListPage.isTrashFolderVisible();
- expect(isTrashVisible).toBe(true);
});
test('Given there are notes in node list, When clicking a note, Then user
should navigate to that note', async ({
page
}) => {
- await expect(nodeListPage.treeView).toBeVisible();
- const notes = await nodeListPage.getAllVisibleNoteNames();
-
- if (notes.length > 0 && notes[0]) {
- const noteName = notes[0].trim();
-
- await nodeListPage.clickNote(noteName);
- await page.waitForURL(/notebook\//);
+ const homePage = new HomePage(page);
- expect(page.url()).toContain('notebook/');
+ await expect(nodeListPage.treeView).toBeVisible();
+ let notes = await nodeListPage.getAllVisibleNoteNames();
+
+ if (notes.length === 0) {
+ // Seed a note so the test always runs — critical navigation path must
not be skipped
+ await homePage.createNote(`_e2e_nav_${Date.now()}`);
+ await page.goto('/');
+ await waitForZeppelinReady(page);
+ notes = await nodeListPage.getAllVisibleNoteNames();
}
+
+ const noteName = notes[0].trim();
+ await nodeListPage.clickNote(noteName);
+ await page.waitForURL(/notebook\//);
+ expect(page.url()).toContain('notebook/');
});
test('Given user clicks Create New Note button, When modal opens, Then note
create modal should be displayed', async ({
diff --git
a/zeppelin-web-angular/e2e/tests/share/note-create/note-create-modal.spec.ts
b/zeppelin-web-angular/e2e/tests/share/note-create/note-create-modal.spec.ts
index a2674b4c4a..640e60899c 100644
--- a/zeppelin-web-angular/e2e/tests/share/note-create/note-create-modal.spec.ts
+++ b/zeppelin-web-angular/e2e/tests/share/note-create/note-create-modal.spec.ts
@@ -60,12 +60,13 @@ test.describe('Note Create Modal', () => {
expect(page.url()).toContain('notebook/');
// Verify the note was created with the correct name
- const notebookTitle = page.locator('p, .notebook-title, .note-title, h1,
[data-testid="notebook-title"]').first();
+ const notebookTitle = page.locator('[data-testid="notebook-title"]');
await expect(notebookTitle).toContainText(uniqueName);
// Verify in the navigation tree if available
- await page.goto('/');
- await page.waitForLoadState('networkidle');
+ await page.goto('/#/');
+ await waitForZeppelinReady(page);
+ await page.locator('zeppelin-node-list').waitFor({ state: 'visible',
timeout: 15000 });
const noteInTree = page.getByRole('link', { name: uniqueName });
await expect(noteInTree).toBeVisible();
});
@@ -87,14 +88,14 @@ test.describe('Note Create Modal', () => {
expect(page.url()).toContain('notebook/');
// Verify the note was created with the correct name (without folder path)
- const notebookTitle = page.locator('p, .notebook-title, .note-title, h1,
[data-testid="notebook-title"]').first();
+ const notebookTitle = page.locator('[data-testid="notebook-title"]');
await expect(notebookTitle).toContainText(noteName);
// Verify the folder structure was created
- await page.goto('/');
- await page.waitForLoadState('networkidle');
- const folder = page.locator('nz-tree-node').filter({ hasText: 'TestFolder'
});
- await expect(folder).toBeVisible();
+ await page.goto('/#/');
+ await waitForZeppelinReady(page);
+ await page.locator('zeppelin-node-list').waitFor({ state: 'visible',
timeout: 15000 });
+ await
expect(page.locator('a.name[data-testid="folder-TestFolder"]')).toBeVisible();
});
test('Given Create Note modal is open, When clicking close button, Then
modal should close', async () => {
@@ -102,7 +103,7 @@ test.describe('Note Create Modal', () => {
});
test('Given Create Note modal is open, When viewing folder info alert, Then
alert should contain folder creation instructions', async () => {
- const isInfoVisible = await noteCreateModal.isFolderInfoVisible();
- expect(isInfoVisible).toBe(true);
+ await expect(noteCreateModal.folderInfoAlert).toBeVisible();
+ await expect(noteCreateModal.folderInfoAlert).toContainText("Use '/' to
create folders");
});
});
diff --git
a/zeppelin-web-angular/e2e/tests/share/note-import/note-import-modal.spec.ts
b/zeppelin-web-angular/e2e/tests/share/note-import/note-import-modal.spec.ts
index b20bee0902..2100d56a39 100644
--- a/zeppelin-web-angular/e2e/tests/share/note-import/note-import-modal.spec.ts
+++ b/zeppelin-web-angular/e2e/tests/share/note-import/note-import-modal.spec.ts
@@ -42,8 +42,7 @@ test.describe('Note Import Modal', () => {
});
test('Given Import Note modal is open, When viewing default tab, Then JSON
File tab should be selected', async () => {
- const isJsonTabSelected = await noteImportModal.isJsonFileTabSelected();
- expect(isJsonTabSelected).toBe(true);
+ await expect(noteImportModal.jsonFileTab).toHaveAttribute('aria-selected',
'true');
await expect(noteImportModal.uploadArea).toBeVisible();
await expect(noteImportModal.uploadText).toBeVisible();
@@ -52,8 +51,7 @@ test.describe('Note Import Modal', () => {
test('Given Import Note modal is open, When switching to URL tab, Then URL
input should be visible', async () => {
await noteImportModal.switchToUrlTab();
- const isUrlTabSelected = await noteImportModal.isUrlTabSelected();
- expect(isUrlTabSelected).toBe(true);
+ await expect(noteImportModal.urlTab).toHaveAttribute('aria-selected',
'true');
await expect(noteImportModal.urlInput).toBeVisible();
await expect(noteImportModal.importNoteButton).toBeVisible();
@@ -62,16 +60,14 @@ test.describe('Note Import Modal', () => {
test('Given URL tab is selected, When URL is empty, Then import button
should be disabled', async () => {
await noteImportModal.switchToUrlTab();
- const isDisabled = await noteImportModal.isImportNoteButtonDisabled();
- expect(isDisabled).toBe(true);
+ await expect(noteImportModal.importNoteButton).toBeDisabled();
});
test('Given URL tab is selected, When entering URL, Then import button
should be enabled', async () => {
await noteImportModal.switchToUrlTab();
await noteImportModal.setImportUrl('https://example.com/note.json');
- const isDisabled = await noteImportModal.isImportNoteButtonDisabled();
- expect(isDisabled).toBe(false);
+ await expect(noteImportModal.importNoteButton).toBeEnabled();
});
test('Given Import Note modal is open, When entering import name, Then name
should be set', async () => {
@@ -84,8 +80,7 @@ test.describe('Note Import Modal', () => {
test('Given JSON File tab is selected, When viewing file size limit, Then
limit should be displayed', async () => {
const fileSizeLimit = await noteImportModal.getFileSizeLimit();
- expect(fileSizeLimit).toBeTruthy();
- expect(fileSizeLimit.length).toBeGreaterThan(0);
+ expect(fileSizeLimit).toMatch(/\d+\s*(MB|KB|GB)/i);
});
test('Given Import Note modal is open, When clicking close button, Then
modal should close', async () => {
@@ -99,7 +94,6 @@ test.describe('Note Import Modal', () => {
await noteImportModal.clickImportNote();
await expect(noteImportModal.errorAlert).toBeVisible();
- const errorMessage = await noteImportModal.getErrorMessage();
- expect(errorMessage).toBeTruthy();
+ await expect(noteImportModal.errorAlert).not.toBeEmpty();
});
});
diff --git a/zeppelin-web-angular/e2e/tests/theme/dark-mode.spec.ts
b/zeppelin-web-angular/e2e/tests/theme/dark-mode.spec.ts
index 61a15e26d1..13b03fbdd3 100644
--- a/zeppelin-web-angular/e2e/tests/theme/dark-mode.spec.ts
+++ b/zeppelin-web-angular/e2e/tests/theme/dark-mode.spec.ts
@@ -34,16 +34,15 @@ test.describe('Dark Mode Theme Switching', () => {
await darkModePage.clearLocalStorage();
});
- test('Scenario: User can switch to dark mode and persistence is maintained',
async ({ page, browserName }) => {
+ test('Scenario: Dark mode persists across page reload when set via
localStorage', async ({ page, browserName }) => {
// GIVEN: User is on the main page, which starts in 'system' mode by
default (localStorage cleared).
await test.step('GIVEN the page starts in system mode', async () => {
await darkModePage.assertSystemTheme(); // Robot icon for system theme
});
- // WHEN: Explicitly set theme to light mode for the rest of the test.
- await test.step('WHEN the user explicitly sets theme to light mode', async
() => {
+ // WHEN: Set theme to light via localStorage and reload (bypasses UI
toggle for test setup).
+ await test.step('WHEN localStorage theme is set to light and page
reloads', async () => {
await darkModePage.setThemeInLocalStorage('light');
- await page.waitForTimeout(500);
// Reload the page to apply localStorage theme changes
if (browserName === 'webkit') {
const currentUrl = page.url();
@@ -55,10 +54,9 @@ test.describe('Dark Mode Theme Switching', () => {
await darkModePage.assertLightTheme(); // Now it should be light mode
with sun icon
});
- // WHEN: User switches to dark mode by setting localStorage and reloading.
- await test.step('WHEN the user explicitly sets theme to dark mode', async
() => {
+ // WHEN: Set theme to dark via localStorage and reload.
+ await test.step('WHEN localStorage theme is set to dark and page reloads',
async () => {
await darkModePage.setThemeInLocalStorage('dark');
- await page.waitForTimeout(500);
// Reload the page to apply localStorage theme changes
if (browserName === 'webkit') {
const currentUrl = page.url();
diff --git
a/zeppelin-web-angular/e2e/tests/workspace/notebook-repos/notebook-repo-item-display.spec.ts
b/zeppelin-web-angular/e2e/tests/workspace/notebook-repos/notebook-repo-item-display.spec.ts
index 6e887b1924..1796e1578a 100644
---
a/zeppelin-web-angular/e2e/tests/workspace/notebook-repos/notebook-repo-item-display.spec.ts
+++
b/zeppelin-web-angular/e2e/tests/workspace/notebook-repos/notebook-repo-item-display.spec.ts
@@ -28,8 +28,10 @@ test.describe('Notebook Repository Item - Display Mode', ()
=> {
notebookReposPage = new NotebookReposPage(page);
await notebookReposPage.navigate();
+ // JUSTIFIED: .first() picks the first configured repo; tests require at
least one repo to be present
const firstCard = notebookReposPage.repositoryItems.first();
firstRepoName = (await
firstCard.locator('.ant-card-head-title').textContent()) || '';
+ expect(firstRepoName, 'No repository found — ensure at least one repo is
configured').not.toBe('');
repoItemPage = new NotebookRepoItemPage(page, firstRepoName);
});
@@ -40,14 +42,7 @@ test.describe('Notebook Repository Item - Display Mode', ()
=> {
test('should show edit button in display mode', async () => {
await expect(repoItemPage.editButton).toBeVisible();
- });
-
- test('should display settings table', async () => {
- await expect(repoItemPage.settingTable).toBeVisible();
- });
-
- test('should show all settings in display mode', async () => {
- const settingCount = await repoItemPage.getSettingCount();
- expect(settingCount).toBeGreaterThan(0);
+ await expect(repoItemPage.editButton).toBeEnabled();
+ await expect(repoItemPage.editButton).toContainText('Edit');
});
});
diff --git
a/zeppelin-web-angular/e2e/tests/workspace/notebook-repos/notebook-repo-item-edit.spec.ts
b/zeppelin-web-angular/e2e/tests/workspace/notebook-repos/notebook-repo-item-edit.spec.ts
index 1ee350c21d..5fd6af53b9 100644
---
a/zeppelin-web-angular/e2e/tests/workspace/notebook-repos/notebook-repo-item-edit.spec.ts
+++
b/zeppelin-web-angular/e2e/tests/workspace/notebook-repos/notebook-repo-item-edit.spec.ts
@@ -30,8 +30,10 @@ test.describe('Notebook Repository Item - Edit Mode', () => {
notebookReposPage = new NotebookReposPage(page);
await notebookReposPage.navigate();
+ // JUSTIFIED: .first() picks the first configured repo; tests require at
least one repo to be present
const firstCard = notebookReposPage.repositoryItems.first();
firstRepoName = (await
firstCard.locator('.ant-card-head-title').textContent()) || '';
+ expect(firstRepoName, 'No repository found — ensure at least one repo is
configured').not.toBe('');
repoItemPage = new NotebookRepoItemPage(page, firstRepoName);
repoItemUtil = new NotebookRepoItemUtil(page, firstRepoName);
});
@@ -41,42 +43,8 @@ test.describe('Notebook Repository Item - Edit Mode', () => {
await repoItemUtil.verifyEditMode();
});
- test('should show save and cancel buttons in edit mode', async () => {
- await repoItemPage.clickEdit();
- await expect(repoItemPage.saveButton).toBeVisible();
- await expect(repoItemPage.cancelButton).toBeVisible();
- });
-
test('should hide edit button in edit mode', async () => {
await repoItemPage.clickEdit();
await expect(repoItemPage.editButton).toBeHidden();
});
-
- test('should apply edit CSS class to card in edit mode', async () => {
- await repoItemPage.clickEdit();
- const isEditMode = await repoItemPage.isEditMode();
- expect(isEditMode).toBe(true);
- });
-
- test('should exit edit mode when cancel button is clicked', async () => {
- await repoItemPage.clickEdit();
- await repoItemUtil.verifyEditMode();
- await repoItemPage.clickCancel();
- await repoItemUtil.verifyDisplayMode();
- });
-
- test('should reset form when cancel is clicked', async () => {
- const firstRow = repoItemPage.settingRows.first();
- const settingName = (await firstRow.locator('td').first().textContent())
|| '';
- const originalValue = await repoItemPage.getSettingValue(settingName);
-
- await repoItemPage.clickEdit();
-
- await repoItemPage.fillSettingInput(settingName, 'temp-value');
-
- await repoItemPage.clickCancel();
-
- const currentValue = await repoItemPage.getSettingValue(settingName);
- expect(currentValue.trim()).toBe(originalValue.trim());
- });
});
diff --git
a/zeppelin-web-angular/e2e/tests/workspace/notebook-repos/notebook-repo-item-form-validation.spec.ts
b/zeppelin-web-angular/e2e/tests/workspace/notebook-repos/notebook-repo-item-form-validation.spec.ts
index aedf7e1675..e4fc940322 100644
---
a/zeppelin-web-angular/e2e/tests/workspace/notebook-repos/notebook-repo-item-form-validation.spec.ts
+++
b/zeppelin-web-angular/e2e/tests/workspace/notebook-repos/notebook-repo-item-form-validation.spec.ts
@@ -28,55 +28,55 @@ test.describe('Notebook Repository Item - Form Validation',
() => {
notebookReposPage = new NotebookReposPage(page);
await notebookReposPage.navigate();
+ // JUSTIFIED: .first() picks the first configured repo; tests require at
least one repo to be present
const firstCard = notebookReposPage.repositoryItems.first();
firstRepoName = (await
firstCard.locator('.ant-card-head-title').textContent()) || '';
+ expect(firstRepoName, 'No repository found — ensure at least one repo is
configured').not.toBe('');
repoItemPage = new NotebookRepoItemPage(page, firstRepoName);
});
test('should disable save button when form is invalid', async () => {
await repoItemPage.clickEdit();
+ // JUSTIFIED: any row is sufficient — all rows share the same
save-disable-on-empty behavior
const firstRow = repoItemPage.settingRows.first();
+ // JUSTIFIED: td.first() is the Name column in the fixed 2-column settings
table
const settingName = (await firstRow.locator('td').first().textContent())
|| '';
await repoItemPage.fillSettingInput(settingName, '');
- const isSaveEnabled = await repoItemPage.isSaveButtonEnabled();
- expect(isSaveEnabled).toBe(false);
+ await expect(repoItemPage.saveButton).not.toBeEnabled();
});
test('should enable save button when form is valid', async () => {
await repoItemPage.clickEdit();
+ // JUSTIFIED: any row is sufficient — all rows share the same
save-enable-on-valid behavior
const firstRow = repoItemPage.settingRows.first();
+ // JUSTIFIED: td.first() is the Name column in the fixed 2-column settings
table
const settingName = (await firstRow.locator('td').first().textContent())
|| '';
const originalValue = await repoItemPage.getSettingInputValue(settingName);
await repoItemPage.fillSettingInput(settingName, originalValue ||
'valid-value');
- const isSaveEnabled = await repoItemPage.isSaveButtonEnabled();
- expect(isSaveEnabled).toBe(true);
+ await expect(repoItemPage.saveButton).toBeEnabled();
});
- test('should validate required fields on form controls', async () => {
+ test('should have editable controls for every setting row in edit mode',
async () => {
const settingRows = await repoItemPage.settingRows.count();
await repoItemPage.clickEdit();
for (let i = 0; i < settingRows; i++) {
+ // JUSTIFIED: nth(i) iterates all rows deterministically; order matches
server-defined settings
const row = repoItemPage.settingRows.nth(i);
- const settingName = (await row.locator('td').first().textContent()) ||
'';
-
- const isInputVisible = await repoItemPage.isInputVisible(settingName);
- if (isInputVisible) {
- const input = row.locator('input[nz-input]');
- await expect(input).toBeVisible();
- }
-
- const isDropdownVisible = await
repoItemPage.isDropdownVisible(settingName);
- if (isDropdownVisible) {
- const select = row.locator('nz-select');
- await expect(select).toBeVisible();
+ const input = row.locator('input[nz-input]');
+ const select = row.locator('nz-select');
+ await expect(input.or(select), `Row ${i} must have an editable control
in edit mode`).toBeVisible();
+ const isInputRow = await input.isVisible();
+ if (isInputRow) {
+ // JUSTIFIED: attribute check only applies to INPUT-type rows;
DROPDOWN-type rows use nz-select and have no nz-input
+ await expect(input).toHaveAttribute('nz-input');
}
}
});
diff --git
a/zeppelin-web-angular/e2e/tests/workspace/notebook-repos/notebook-repo-item-settings.spec.ts
b/zeppelin-web-angular/e2e/tests/workspace/notebook-repos/notebook-repo-item-settings.spec.ts
index 68cc608bb3..db65b97063 100644
---
a/zeppelin-web-angular/e2e/tests/workspace/notebook-repos/notebook-repo-item-settings.spec.ts
+++
b/zeppelin-web-angular/e2e/tests/workspace/notebook-repos/notebook-repo-item-settings.spec.ts
@@ -28,8 +28,10 @@ test.describe('Notebook Repository Item - Settings', () => {
notebookReposPage = new NotebookReposPage(page);
await notebookReposPage.navigate();
+ // JUSTIFIED: .first() picks the first configured repo; tests require at
least one repo to be present
const firstCard = notebookReposPage.repositoryItems.first();
firstRepoName = (await
firstCard.locator('.ant-card-head-title').textContent()) || '';
+ expect(firstRepoName, 'No repository found — ensure at least one repo is
configured').not.toBe('');
repoItemPage = new NotebookRepoItemPage(page, firstRepoName);
});
@@ -37,79 +39,67 @@ test.describe('Notebook Repository Item - Settings', () => {
await expect(repoItemPage.settingTable).toBeVisible();
const headers = repoItemPage.settingTable.locator('thead th');
- await expect(headers.nth(0)).toContainText('Name');
- await expect(headers.nth(1)).toContainText('Value');
+ await expect(headers.filter({ hasText: 'Name' })).toBeVisible();
+ await expect(headers.filter({ hasText: 'Value' })).toBeVisible();
});
- test('should display all setting rows', async () => {
- const settingCount = await repoItemPage.getSettingCount();
- expect(settingCount).toBeGreaterThan(0);
- });
-
- test('should show input controls for INPUT type settings in edit mode',
async () => {
- const settingRows = await repoItemPage.settingRows.count();
-
+ test('should show input controls for INPUT type settings in edit mode',
async ({ page }) => {
await repoItemPage.clickEdit();
- for (let i = 0; i < settingRows; i++) {
- const row = repoItemPage.settingRows.nth(i);
- const settingName = (await row.locator('td').first().textContent()) ||
'';
+ const inputRows = repoItemPage.settingRows.filter({ has:
page.locator('input[nz-input]') });
+ await expect(inputRows).not.toHaveCount(0); // repo must have at least one
INPUT-type setting
- const isInputVisible = await repoItemPage.isInputVisible(settingName);
- if (isInputVisible) {
- const input = row.locator('input[nz-input]');
- await expect(input).toBeVisible();
- await expect(input).toHaveAttribute('nz-input');
- }
+ const count = await inputRows.count();
+ for (let i = 0; i < count; i++) {
+ // JUSTIFIED: nth(i) iterates all INPUT-type rows deterministically;
order matches server-defined settings
+ const input = inputRows.nth(i).locator('input[nz-input]');
+ await expect(input).toBeVisible();
+ await expect(input).toHaveAttribute('nz-input');
}
});
- test('should show dropdown controls for DROPDOWN type settings in edit
mode', async () => {
- const settingRows = await repoItemPage.settingRows.count();
-
+ test('should show dropdown controls for DROPDOWN type settings in edit
mode', async ({ page }) => {
await repoItemPage.clickEdit();
- for (let i = 0; i < settingRows; i++) {
- const row = repoItemPage.settingRows.nth(i);
- const settingName = (await row.locator('td').first().textContent()) ||
'';
+ const dropdownRows = repoItemPage.settingRows.filter({ has:
page.locator('nz-select') });
+ const count = await dropdownRows.count();
+ test.skip(count === 0, 'VFSNotebookRepo has no DROPDOWN-type settings in
this environment');
- const isDropdownVisible = await
repoItemPage.isDropdownVisible(settingName);
- if (isDropdownVisible) {
- const select = row.locator('nz-select');
- await expect(select).toBeVisible();
- }
+ for (let i = 0; i < count; i++) {
+ // JUSTIFIED: nth(i) iterates all DROPDOWN-type rows deterministically;
order matches server-defined settings
+ await expect(dropdownRows.nth(i).locator('nz-select')).toBeVisible();
}
});
- test('should update input value in edit mode', async () => {
- const settingRows = await repoItemPage.settingRows.count();
-
+ test('should update input value in edit mode', async ({ page }) => {
await repoItemPage.clickEdit();
- for (let i = 0; i < settingRows; i++) {
- const row = repoItemPage.settingRows.nth(i);
- const settingName = (await row.locator('td').first().textContent()) ||
'';
-
- const isInputVisible = await repoItemPage.isInputVisible(settingName);
- if (isInputVisible) {
- const testValue = 'test-value';
- await repoItemPage.fillSettingInput(settingName, testValue);
- const inputValue = await
repoItemPage.getSettingInputValue(settingName);
- expect(inputValue).toBe(testValue);
- break;
- }
- }
+ const inputRows = repoItemPage.settingRows.filter({ has:
page.locator('input[nz-input]') });
+ await expect(inputRows).not.toHaveCount(0); // repo must have at least one
INPUT-type setting
+
+ // JUSTIFIED: any INPUT-type row works — all share the same input control
structure
+ const firstRow = inputRows.first();
+ // JUSTIFIED: td.first() is the Name column in the fixed 2-column settings
table
+ const settingName = (await firstRow.locator('td').first().textContent())
|| '';
+ const testValue = 'test-value';
+ await repoItemPage.fillSettingInput(settingName, testValue);
+ expect(await
repoItemPage.getSettingInputValue(settingName)).toBe(testValue);
});
test('should display setting name and value in display mode', async () => {
+ // JUSTIFIED: any row is sufficient — testing Name/Value column structure
shared by all rows
const firstRow = repoItemPage.settingRows.first();
+ // JUSTIFIED: td.first() = Name column in the fixed 2-column settings table
const nameCell = firstRow.locator('td').first();
+ // JUSTIFIED: td.nth(1) = Value column in the fixed 2-column settings table
const valueCell = firstRow.locator('td').nth(1);
await expect(nameCell).toBeVisible();
await expect(valueCell).toBeVisible();
const nameText = await nameCell.textContent();
- expect(nameText).toBeTruthy();
+ expect(nameText).not.toBe('');
+ const valueText = await valueCell.textContent();
+ expect(valueText).not.toBe('');
});
});
diff --git
a/zeppelin-web-angular/e2e/tests/workspace/notebook-repos/notebook-repo-item-workflow.spec.ts
b/zeppelin-web-angular/e2e/tests/workspace/notebook-repos/notebook-repo-item-workflow.spec.ts
index 52f3e42909..0fd368e493 100644
---
a/zeppelin-web-angular/e2e/tests/workspace/notebook-repos/notebook-repo-item-workflow.spec.ts
+++
b/zeppelin-web-angular/e2e/tests/workspace/notebook-repos/notebook-repo-item-workflow.spec.ts
@@ -30,6 +30,7 @@ test.describe('Notebook Repository Item - Edit Workflow', ()
=> {
notebookReposPage = new NotebookReposPage(page);
await notebookReposPage.navigate();
+ // JUSTIFIED: .first() picks the first configured repo; tests require at
least one repo to be present
const firstCard = notebookReposPage.repositoryItems.first();
firstRepoName = (await
firstCard.locator('.ant-card-head-title').textContent()) || '';
repoItemPage = new NotebookRepoItemPage(page, firstRepoName);
@@ -44,30 +45,41 @@ test.describe('Notebook Repository Item - Edit Workflow',
() => {
await repoItemPage.clickEdit();
await repoItemUtil.verifyEditMode();
+ let savedSettingName = '';
+ let savedValue = '';
for (let i = 0; i < settingRows; i++) {
+ // JUSTIFIED: nth(i) iterates all rows deterministically to find the
first INPUT-type row
const row = repoItemPage.settingRows.nth(i);
+ // JUSTIFIED: td.first() is the Name column in the fixed 2-column
settings table
const settingName = (await row.locator('td').first().textContent()) ||
'';
- const isInputVisible = await repoItemPage.isInputVisible(settingName);
+ const isInputVisible = await row.locator('input[nz-input]').isVisible();
if (isInputVisible) {
- const originalValue = await
repoItemPage.getSettingInputValue(settingName);
- await repoItemPage.fillSettingInput(settingName, originalValue ||
'test-value');
+ savedValue = (await repoItemPage.getSettingInputValue(settingName)) ||
'test-value';
+ await repoItemPage.fillSettingInput(settingName, savedValue);
+ savedSettingName = settingName;
break;
}
}
- const isSaveEnabled = await repoItemPage.isSaveButtonEnabled();
- expect(isSaveEnabled).toBe(true);
+ expect(savedSettingName, 'No INPUT-type setting found — cannot verify save
result').not.toBe('');
+ await expect(repoItemPage.saveButton).toBeEnabled();
await repoItemPage.clickSave();
await repoItemUtil.verifyDisplayMode();
+
+ // Verify the saved value is shown in display mode — not just that mode
switched
+ const displayValue = await repoItemPage.getSettingValue(savedSettingName);
+ expect(displayValue.trim()).toBe(savedValue.trim());
});
test('should complete full edit workflow with cancel', async () => {
await repoItemUtil.verifyDisplayMode();
+ // JUSTIFIED: any row is representative — testing that cancel reverts all
changes
const firstRow = repoItemPage.settingRows.first();
+ // JUSTIFIED: td.first() is the Name column in the fixed 2-column settings
table
const settingName = (await firstRow.locator('td').first().textContent())
|| '';
const originalValue = await repoItemPage.getSettingValue(settingName);
diff --git
a/zeppelin-web-angular/e2e/tests/workspace/notebook-repos/notebook-repos-page-structure.spec.ts
b/zeppelin-web-angular/e2e/tests/workspace/notebook-repos/notebook-repos-page-structure.spec.ts
index 747037ef47..39cccf2c58 100644
---
a/zeppelin-web-angular/e2e/tests/workspace/notebook-repos/notebook-repos-page-structure.spec.ts
+++
b/zeppelin-web-angular/e2e/tests/workspace/notebook-repos/notebook-repos-page-structure.spec.ts
@@ -29,16 +29,14 @@ test.describe('Notebook Repository Page - Structure', () =>
{
test('should display page header with correct title and description', async
() => {
await expect(notebookReposPage.zeppelinPageHeader).toBeVisible();
- await expect(notebookReposPage.pageDescription).toBeVisible();
+ await expect(notebookReposPage.zeppelinPageHeader).toContainText('Notebook
Repository');
+ await expect(notebookReposPage.pageDescription).toContainText("Manage your
Notebook Repositories' settings.");
});
- test('should render repository list container', async () => {
- const count = await notebookReposPage.getRepositoryItemCount();
- expect(count).toBeGreaterThanOrEqual(0);
- });
-
- test('should display all repository items', async () => {
- const count = await notebookReposPage.getRepositoryItemCount();
- expect(count).toBeGreaterThan(0);
+ test('should display all repository items with names', async () => {
+ await expect(notebookReposPage.repositoryItems).not.toHaveCount(0);
+ // JUSTIFIED: .first() samples the first repo card; all cards share the
same title structure
+ const firstTitle =
notebookReposPage.repositoryItems.first().locator('.ant-card-head-title');
+ await expect(firstTitle).not.toBeEmpty();
});
});
diff --git a/zeppelin-web-angular/e2e/tests/workspace/workspace-main.spec.ts
b/zeppelin-web-angular/e2e/tests/workspace/workspace-main.spec.ts
index a3e42474c0..106345fd2e 100644
--- a/zeppelin-web-angular/e2e/tests/workspace/workspace-main.spec.ts
+++ b/zeppelin-web-angular/e2e/tests/workspace/workspace-main.spec.ts
@@ -11,58 +11,40 @@
*/
import { expect, test } from '@playwright/test';
-import { WorkspacePage } from 'e2e/models/workspace-page';
-import { WorkspaceUtil } from '../../models/workspace-page.util';
+import { BasePage } from 'e2e/models/base-page';
import { addPageAnnotationBeforeEach, PAGES, performLoginIfRequired,
waitForZeppelinReady } from '../../utils';
addPageAnnotationBeforeEach(PAGES.WORKSPACE.MAIN);
test.describe('Workspace Main Component', () => {
- let workspaceUtil: WorkspaceUtil;
- let workspacePage: WorkspacePage;
+ let basePage: BasePage;
test.beforeEach(async ({ page }) => {
await page.goto('/#/');
await waitForZeppelinReady(page);
await performLoginIfRequired(page);
- workspacePage = new WorkspacePage(page);
- workspaceUtil = new WorkspaceUtil(page);
+ basePage = new BasePage(page);
});
test.describe('Given user accesses workspace container', () => {
- test('When workspace loads Then should display main container structure',
async ({ page }) => {
- await expect(workspacePage.zeppelinWorkspace).toBeVisible();
- await expect(workspacePage.routerOutlet).toBeAttached();
-
- await expect(workspacePage.zeppelinWorkspace).toBeVisible();
- const contentElements = await page.locator('.content').count();
- expect(contentElements).toBeGreaterThan(0);
+ test('When workspace loads Then should display main container structure',
async () => {
+ await expect(basePage.zeppelinWorkspace).toBeVisible();
+ // Verify workspace contains the header — not just that the elements
exist in isolation
+ await
expect(basePage.zeppelinWorkspace.locator('zeppelin-header')).toBeVisible();
});
test('When workspace loads Then should display header component', async ()
=> {
- await workspaceUtil.verifyHeaderVisibility(true);
- });
-
- test('When workspace loads Then should activate router outlet', async ()
=> {
- await workspaceUtil.verifyRouterOutletActivation();
- });
-
- test('When component activates Then should trigger onActivate event',
async () => {
- await workspaceUtil.waitForComponentActivation();
+ await expect(basePage.zeppelinHeader).toBeVisible();
+ // Header must contain navigable content, not just be an empty shell
+ await expect(basePage.zeppelinHeader).toContainText('Zeppelin');
});
- });
-
- test.describe('Given workspace header visibility', () => {
- test('When not in publish mode Then should show header', async () => {
- await workspaceUtil.verifyHeaderVisibility(true);
- });
- });
- test.describe('Given router outlet functionality', () => {
- test('When navigating to workspace Then should load child components',
async () => {
- await workspaceUtil.verifyRouterOutletActivation();
- await workspaceUtil.waitForComponentActivation();
+ test('When workspace loads Then should have router outlet attached with
home component', async ({ page }) => {
+ // Router outlet must have an activated child, not just exist as an
empty outlet
+ await expect(page.locator('zeppelin-workspace router-outlet +
*')).toHaveCount(1);
+ // Activated route must have rendered the home component
+ await
expect(basePage.zeppelinWorkspace.locator('zeppelin-home')).toBeVisible();
});
});
});
diff --git a/zeppelin-web-angular/e2e/utils.ts
b/zeppelin-web-angular/e2e/utils.ts
index efd6096532..18a66a0ee6 100644
--- a/zeppelin-web-angular/e2e/utils.ts
+++ b/zeppelin-web-angular/e2e/utils.ts
@@ -184,8 +184,6 @@ export const performLoginIfRequired = async (page: Page):
Promise<boolean> => {
const loginPage = new LoginPage(page);
await loginPage.login(testUser.username, testUser.password);
- // for webkit
- await page.waitForTimeout(200);
await page.evaluate(() => {
if (window.location.hash.includes('login')) {
window.location.hash = '#/';
@@ -220,6 +218,7 @@ export const waitForZeppelinReady = async (page: Page):
Promise<void> => {
// If we're on login page, this is expected when authentication is
required
// Just wait for login elements to be ready instead of waiting for app
content
await page.waitForFunction(
+ // JUSTIFIED: multi-condition AND — Angular presence + login element
OR across three selectors; can't express as single locator wait
() => {
const hasAngular = document.querySelector('[ng-version]') !== null;
const hasLoginElements =
@@ -236,6 +235,7 @@ export const waitForZeppelinReady = async (page: Page):
Promise<void> => {
// Wait for Angular and Zeppelin to be ready with more robust checks
await page.waitForFunction(
+ // JUSTIFIED: multi-condition OR across DOM + textContent checks;
textContent not expressible via Playwright locator API
() => {
// Check for Angular framework
const hasAngular = document.querySelector('[ng-version]') !== null;
@@ -347,9 +347,7 @@ const navigateViaHomePageFallback = async (page: Page,
baseNotebookName: string)
await page.waitForLoadState('networkidle', { timeout: 15000 });
await page.waitForSelector('zeppelin-node-list', { timeout: 15000 });
- await page.waitForFunction(() =>
document.querySelectorAll(NOTEBOOK_PATTERNS.LINK_SELECTOR).length > 0, {
- timeout: 15000
- });
+ await page.locator(NOTEBOOK_PATTERNS.LINK_SELECTOR).first().waitFor({ state:
'attached', timeout: 15000 });
await page.waitForLoadState('domcontentloaded', { timeout: 15000 });
const notebookLink = page.locator(NOTEBOOK_PATTERNS.LINK_SELECTOR).filter({
hasText: baseNotebookName });
@@ -386,6 +384,10 @@ const extractFirstParagraphId = async (page: Page):
Promise<string> => {
await paragraphLink.waitFor({ state: 'attached', timeout: 15000 });
const paragraphId = await paragraphLink.textContent();
+
+ // Close the dropdown before returning — leaving it open leaks state into
subsequent tests
+ await page.keyboard.press('Escape');
+
if (!paragraphId || !paragraphId.startsWith('paragraph_')) {
throw new Error(`Invalid paragraph ID found: ${paragraphId}`);
}
diff --git a/zeppelin-web-angular/playwright.config.js
b/zeppelin-web-angular/playwright.config.js
index 6e3e664bf0..06e9270385 100644
--- a/zeppelin-web-angular/playwright.config.js
+++ b/zeppelin-web-angular/playwright.config.js
@@ -20,7 +20,7 @@ module.exports = defineConfig({
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 1,
- workers: process.env.CI ? 2 : 10,
+ workers: process.env.CI ? 2 : 5,
timeout: 300000,
expect: {
timeout: 60000
diff --git a/zeppelin-web-angular/projects/zeppelin-react/package-lock.json
b/zeppelin-web-angular/projects/zeppelin-react/package-lock.json
index ff9ba13cd0..2fa8a69a4b 100644
--- a/zeppelin-web-angular/projects/zeppelin-react/package-lock.json
+++ b/zeppelin-web-angular/projects/zeppelin-react/package-lock.json
@@ -4824,9 +4824,9 @@
}
},
"node_modules/flatted": {
- "version": "3.3.3",
- "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
- "integrity":
"sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
+ "version": "3.4.1",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz",
+ "integrity":
"sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==",
"dev": true,
"license": "ISC"
},
diff --git
a/zeppelin-web-angular/src/app/pages/workspace/notebook/action-bar/action-bar.component.html
b/zeppelin-web-angular/src/app/pages/workspace/notebook/action-bar/action-bar.component.html
index 8e3bdea00b..54f4e46853 100644
---
a/zeppelin-web-angular/src/app/pages/workspace/notebook/action-bar/action-bar.component.html
+++
b/zeppelin-web-angular/src/app/pages/workspace/notebook/action-bar/action-bar.component.html
@@ -11,7 +11,7 @@
-->
<div class="bar" [class.simple]="looknfeel !== 'default'">
- <div class="title" role="heading" aria-level="1">
+ <div class="title" role="heading" aria-level="1"
data-testid="notebook-title">
<zeppelin-elastic-input
nz-tooltip
[nzTooltipTitle]="note.path"