This is an automated email from the ASF dual-hosted git repository.

tbonelee 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 0539c8a386 [ZEPPELIN-6422] Stabilize flaky Playwright E2E tests
0539c8a386 is described below

commit 0539c8a386c198985bd69acefbe8208bcfa12412
Author: YONGJAE LEE (이용재) <[email protected]>
AuthorDate: Wed Jun 3 19:55:46 2026 +0900

    [ZEPPELIN-6422] Stabilize flaky Playwright E2E tests
    
    ### What is this PR for?
    Three races caused flaky `frontend / run-playwright-e2e-tests`:
    
    1. Per-test login raced on the shared session cookie under parallel workers 
-> moved to a single `setup` project + `storageState`.
    2. `locator.fill` on Ant modal inputs landed before Angular bound the 
form-control -> new `BasePage.fillAndVerifyInput()` retries via `expect.toPass` 
until the input value sticks.
    3. Modal/dropdown/theme/logout transitions had no explicit wait -> targeted 
waits added at each boundary.
    
    Inline comments on the diff for the non-obvious bits.
    
    
    ### What type of PR is it?
    Bug Fix
    
    ### Todos
    
    ### What is the Jira issue?
    ZEPPELIN-6422
    
    ### 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 #5262 from voidmatcha/fix/e2e-flaky-final.
    
    Signed-off-by: ChanHo Lee <[email protected]>
---
 zeppelin-web-angular/.gitignore                    |   1 +
 zeppelin-web-angular/e2e/global.setup.ts           |  45 +++
 zeppelin-web-angular/e2e/models/base-page.ts       |  22 +-
 zeppelin-web-angular/e2e/models/dark-mode-page.ts  |   5 +
 .../e2e/models/folder-rename-page.ts               |  40 +--
 .../e2e/models/folder-rename-page.util.ts          |   2 +-
 zeppelin-web-angular/e2e/models/home-page.ts       |   5 +-
 zeppelin-web-angular/e2e/models/node-list-page.ts  |   9 +-
 .../e2e/models/note-create-modal.ts                |   3 +-
 .../e2e/models/note-import-modal.ts                |   4 +-
 .../e2e/models/note-rename-page.ts                 |   2 +-
 .../e2e/models/notebook-repos-page.ts              |   9 +-
 zeppelin-web-angular/e2e/models/notebook.util.ts   |   6 +-
 zeppelin-web-angular/e2e/tests/app.spec.ts         |  71 +++--
 .../e2e/tests/home/home-page-elements.spec.ts      |   3 +-
 .../tests/home/home-page-external-links.spec.ts    |   3 +-
 .../e2e/tests/home/home-page-layout.spec.ts        |   3 +-
 .../tests/home/home-page-note-operations.spec.ts   |  31 +-
 .../tests/home/home-page-notebook-actions.spec.ts  |   7 +-
 zeppelin-web-angular/e2e/tests/login/login.spec.ts |  17 +-
 .../action-bar/action-bar-functionality.spec.ts    |  14 +-
 .../keyboard/notebook-keyboard-shortcuts.spec.ts   |   9 +-
 .../tests/notebook/main/notebook-container.spec.ts |  11 +-
 .../notebook/main/notebook-navigation.spec.ts      |   3 +-
 .../notebook/published/published-paragraph.spec.ts |   8 +-
 .../notebook/sidebar/sidebar-functionality.spec.ts |  11 +-
 .../about-zeppelin/about-zeppelin-modal.spec.ts    |   3 +-
 .../share/folder-rename/folder-rename.spec.ts      |  35 ++-
 .../tests/share/header/header-navigation.spec.ts   |  21 +-
 .../e2e/tests/share/header/header-search.spec.ts   |   9 +-
 .../node-list/node-list-functionality.spec.ts      |  27 +-
 .../share/note-create/note-create-modal.spec.ts    |  11 +-
 .../share/note-import/note-import-modal.spec.ts    |   3 +-
 .../tests/share/note-rename/note-rename.spec.ts    |  17 +-
 .../e2e/tests/share/note-toc/note-toc.spec.ts      |  14 +-
 .../e2e/tests/theme/dark-mode.spec.ts              |  15 +-
 .../notebook-repo-item-display.spec.ts             |   3 +-
 .../notebook-repos/notebook-repo-item-edit.spec.ts |   3 +-
 .../notebook-repo-item-form-validation.spec.ts     |   3 +-
 .../notebook-repo-item-settings.spec.ts            |   3 +-
 .../notebook-repo-item-workflow.spec.ts            |   3 +-
 .../notebook-repos-page-structure.spec.ts          |   3 +-
 .../tests/workspace/user-menu-navigation.spec.ts   |   3 +-
 .../e2e/tests/workspace/workspace-main.spec.ts     |   3 +-
 zeppelin-web-angular/e2e/utils.ts                  | 325 +++++++++++++--------
 zeppelin-web-angular/playwright.config.js          |  44 ++-
 .../src/app/share/header/header.component.html     |   1 +
 .../src/app/share/header/header.component.less     |  11 +
 48 files changed, 560 insertions(+), 344 deletions(-)

diff --git a/zeppelin-web-angular/.gitignore b/zeppelin-web-angular/.gitignore
index f285d87e9b..42b640c2fd 100644
--- a/zeppelin-web-angular/.gitignore
+++ b/zeppelin-web-angular/.gitignore
@@ -48,6 +48,7 @@ Thumbs.db
 /playwright-coverage/
 /test-results/
 /playwright/.cache/
+/playwright/.auth/
 
 #
 .env
diff --git a/zeppelin-web-angular/e2e/global.setup.ts 
b/zeppelin-web-angular/e2e/global.setup.ts
new file mode 100644
index 0000000000..234d0dbea1
--- /dev/null
+++ b/zeppelin-web-angular/e2e/global.setup.ts
@@ -0,0 +1,45 @@
+/*
+ * 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 * as fs from 'fs';
+import * as path from 'path';
+import { test as setup, expect } from '@playwright/test';
+import { LoginTestUtil } from './models/login-page.util';
+import { performLoginIfRequired, waitForZeppelinReady } from './utils';
+
+// Resolved against the Playwright project rootDir (zeppelin-web-angular/).
+// Must match the `storageState` value declared for browser projects in 
playwright.config.js.
+export const STORAGE_STATE = path.join('playwright', '.auth', 'user.json');
+
+setup('authenticate', async ({ page }) => {
+  fs.mkdirSync(path.dirname(STORAGE_STATE), { recursive: true });
+
+  const isShiroEnabled = await LoginTestUtil.isShiroEnabled();
+  if (!isShiroEnabled) {
+    // Auth variant disabled — write an empty storage state so dependent 
projects load,
+    // then exit. This keeps the setup-project pattern uniform across CI 
matrix variants.
+    await page.context().storageState({ path: STORAGE_STATE });
+    return;
+  }
+
+  await page.goto('/');
+  await waitForZeppelinReady(page);
+
+  await performLoginIfRequired(page);
+
+  // Verify we are authenticated. Don't rely on performLoginIfRequired's 
return value —
+  // it returns false both for "no work to do" and "login attempt failed".
+  await expect(page.locator('zeppelin-login')).toBeHidden({ timeout: 30000 });
+  await expect(page.getByRole('heading', { name: 'Welcome to Zeppelin!' 
})).toBeVisible({ timeout: 30000 });
+
+  await page.context().storageState({ path: STORAGE_STATE });
+});
diff --git a/zeppelin-web-angular/e2e/models/base-page.ts 
b/zeppelin-web-angular/e2e/models/base-page.ts
index aa37f1a138..403be0f780 100644
--- a/zeppelin-web-angular/e2e/models/base-page.ts
+++ b/zeppelin-web-angular/e2e/models/base-page.ts
@@ -104,10 +104,22 @@ export class BasePage {
     await expect(locator).toBeVisible({ timeout });
     await expect(locator).toBeEnabled({ timeout: 5000 });
 
-    // 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, { timeout: 10000 });
+    // Ant-modal autofocus + Angular form initialization race: any of fill / 
type /
+    // pressSequentially can land BEFORE the form-control's initial value sync,
+    // after which Angular silently resets the input back to the model's 
initial
+    // value (placeholder). ng-dirty is set but the visible value is wrong.
+    await expect(async () => {
+      await locator.click();
+      await locator.fill(value);
+      await locator.evaluate((el: HTMLInputElement) => {
+        el.dispatchEvent(new Event('input', { bubbles: true }));
+        el.dispatchEvent(new Event('change', { bubbles: true }));
+      });
+      // Verify the value stuck — if Angular reset it, this throws and toPass 
retries.
+      const actual = await locator.inputValue();
+      if (actual !== value) {
+        throw new Error(`fillAndVerifyInput retry: expected "${value}" got 
"${actual}"`);
+      }
+    }).toPass({ timeout: 15000, intervals: [200, 500, 1000, 2000] });
   }
 }
diff --git a/zeppelin-web-angular/e2e/models/dark-mode-page.ts 
b/zeppelin-web-angular/e2e/models/dark-mode-page.ts
index 98f77c8933..bedea19f57 100644
--- a/zeppelin-web-angular/e2e/models/dark-mode-page.ts
+++ b/zeppelin-web-angular/e2e/models/dark-mode-page.ts
@@ -40,6 +40,11 @@ export class DarkModePage extends BasePage {
   }
 
   async assertSystemTheme() {
+    // After page reload, Angular re-bootstraps and reads theme from 
localStorage. The
+    // toggle-button icon refresh races against that bootstrap window. Wait 
for the
+    // root element's data-theme attribute to be set first — that guarantees 
Angular's
+    // theme-init has completed — then assert the icon.
+    await expect(this.rootElement).toHaveAttribute('data-theme', /light|dark/, 
{ timeout: 15000 });
     await expect(this.themeToggleButton).toHaveText('smart_toy', { timeout: 
60000 });
   }
 
diff --git a/zeppelin-web-angular/e2e/models/folder-rename-page.ts 
b/zeppelin-web-angular/e2e/models/folder-rename-page.ts
index 58f9327c6f..93f49d30be 100644
--- a/zeppelin-web-angular/e2e/models/folder-rename-page.ts
+++ b/zeppelin-web-angular/e2e/models/folder-rename-page.ts
@@ -31,23 +31,12 @@ export class FolderRenamePage extends BasePage {
     this.deleteConfirmation = page.locator('.ant-popover').filter({ hasText: 
'This folder will be moved to trash.' });
   }
 
-  private getFolderNode(folderName: string): Locator {
-    return this.page
-      .locator('.folder')
-      .filter({
-        has: this.page.locator('a.name', {
-          hasText: new RegExp(`^\\s*${folderName}\\s*$`, 'i')
-        })
-      })
-      .first();
-  }
-
   async hoverOverFolder(folderName: string): Promise<void> {
     await this.page.waitForSelector('zeppelin-node-list', { state: 'visible' 
});
-    const folderNode = this.getFolderNode(folderName);
     // Hover a.name (not .folder) — CSS :hover on .operation is triggered by 
the text link, same as clickRenameMenuItem()
-    const nameLink = folderNode.locator('a.name');
-    await nameLink.scrollIntoViewIfNeeded();
+    const nameLink = this.getFolderNameLink(folderName);
+    await expect(nameLink).toBeVisible({ timeout: 60000 });
+    await nameLink.scrollIntoViewIfNeeded({ timeout: 10000 });
     await nameLink.hover({ force: true }); // JUSTIFIED: .operation buttons 
are CSS-:hover-revealed; force required to trigger the hover event on the text 
link that activates the context menu
   }
 
@@ -63,9 +52,10 @@ export class FolderRenamePage extends BasePage {
 
   async clickRenameMenuItem(folderName: string): Promise<void> {
     const folderNode = this.getFolderNode(folderName);
-    const nameLink = folderNode.locator('a.name');
+    const nameLink = this.getFolderNameLink(folderName);
 
-    await nameLink.scrollIntoViewIfNeeded();
+    await expect(nameLink).toBeVisible({ timeout: 60000 });
+    await nameLink.scrollIntoViewIfNeeded({ timeout: 10000 });
     await nameLink.hover({ force: true }); // JUSTIFIED: .operation buttons 
are CSS-:hover-revealed; force required to trigger the hover event on the text 
link that activates the context menu
 
     const renameIcon = folderNode.locator('.operation a[nztooltiptitle="Rename 
folder"]');
@@ -77,12 +67,11 @@ export class FolderRenamePage extends BasePage {
   }
 
   async enterNewName(name: string): Promise<void> {
-    await this.renameInput.fill(name);
+    await this.fillAndVerifyInput(this.renameInput, name);
   }
 
   async clearNewName(): Promise<void> {
-    await this.renameInput.clear();
-    await expect(this.renameInput).toHaveValue('');
+    await this.fillAndVerifyInput(this.renameInput, '');
   }
 
   async clickConfirm(): Promise<void> {
@@ -97,4 +86,17 @@ export class FolderRenamePage extends BasePage {
   async clickCancel(): Promise<void> {
     await this.cancelButton.click();
   }
+
+  private getFolderNameLink(folderName: string): Locator {
+    return this.page.getByTestId(`folder-${folderName}`).first();
+  }
+
+  private getFolderNode(folderName: string): Locator {
+    return this.page
+      .locator('.node')
+      .filter({
+        has: this.getFolderNameLink(folderName)
+      })
+      .first();
+  }
 }
diff --git a/zeppelin-web-angular/e2e/models/folder-rename-page.util.ts 
b/zeppelin-web-angular/e2e/models/folder-rename-page.util.ts
index f1e32b1ded..c248e3e376 100644
--- a/zeppelin-web-angular/e2e/models/folder-rename-page.util.ts
+++ b/zeppelin-web-angular/e2e/models/folder-rename-page.util.ts
@@ -31,7 +31,7 @@ export class FolderRenamePageUtil {
     return this.folderRenamePage.page
       .locator('.node')
       .filter({
-        has: this.folderRenamePage.page.locator('.folder .name', { hasText: 
folderName })
+        has: this.folderRenamePage.page.getByTestId(`folder-${folderName}`)
       })
       .first();
   }
diff --git a/zeppelin-web-angular/e2e/models/home-page.ts 
b/zeppelin-web-angular/e2e/models/home-page.ts
index 3222fc3964..412642df44 100644
--- a/zeppelin-web-angular/e2e/models/home-page.ts
+++ b/zeppelin-web-angular/e2e/models/home-page.ts
@@ -122,8 +122,9 @@ export class HomePage extends BasePage {
     await expect(this.createNoteButton).toBeEnabled({ timeout: 5000 });
     await this.createNoteButton.click({ timeout: 15000 });
     // 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 });
+    // This is an Angular hash-route transition, so polling the URL is more 
reliable than
+    // waitForURL()'s default "load" wait, which can hang on same-document SPA 
navigation.
+    await expect(this.page).toHaveURL(/\/notebook\//, { timeout: 45000 });
   }
 
   async clickImportNote(): Promise<void> {
diff --git a/zeppelin-web-angular/e2e/models/node-list-page.ts 
b/zeppelin-web-angular/e2e/models/node-list-page.ts
index 9c81bda02f..88fa2721cd 100644
--- a/zeppelin-web-angular/e2e/models/node-list-page.ts
+++ b/zeppelin-web-angular/e2e/models/node-list-page.ts
@@ -41,15 +41,12 @@ export class NodeListPage extends BasePage {
     await this.createNewNoteButton.click();
   }
 
-  private getNoteByName(noteName: string): Locator {
-    return this.page.locator('nz-tree-node').filter({ hasText: noteName 
}).first();
+  noteLinkByName(noteName: string): Locator {
+    return this.nodeListContainer.getByRole('link', { name: noteName, exact: 
true });
   }
 
   async clickNote(noteName: string): Promise<void> {
-    const note = this.getNoteByName(noteName);
-    // Target the specific link that navigates to the notebook (has href with 
"#/notebook/")
-    const noteLink = note.locator('a[href*="#/notebook/"]');
-    await noteLink.click();
+    await this.noteLinkByName(noteName).click();
   }
 
   async getAllVisibleNoteNames(): Promise<string[]> {
diff --git a/zeppelin-web-angular/e2e/models/note-create-modal.ts 
b/zeppelin-web-angular/e2e/models/note-create-modal.ts
index a00ef19219..b39d62d85b 100644
--- a/zeppelin-web-angular/e2e/models/note-create-modal.ts
+++ b/zeppelin-web-angular/e2e/models/note-create-modal.ts
@@ -40,8 +40,7 @@ export class NoteCreateModal extends BasePage {
   }
 
   async setNoteName(name: string): Promise<void> {
-    await this.noteNameInput.clear();
-    await this.noteNameInput.fill(name);
+    await this.fillAndVerifyInput(this.noteNameInput, name);
   }
 
   async clickCreate(): Promise<void> {
diff --git a/zeppelin-web-angular/e2e/models/note-import-modal.ts 
b/zeppelin-web-angular/e2e/models/note-import-modal.ts
index e4634f94bc..92bfe0e8a0 100644
--- a/zeppelin-web-angular/e2e/models/note-import-modal.ts
+++ b/zeppelin-web-angular/e2e/models/note-import-modal.ts
@@ -48,7 +48,7 @@ export class NoteImportModal extends BasePage {
   }
 
   async setImportAsName(name: string): Promise<void> {
-    await this.importAsInput.fill(name);
+    await this.fillAndVerifyInput(this.importAsInput, name);
   }
 
   async getImportAsName(): Promise<string> {
@@ -60,7 +60,7 @@ export class NoteImportModal extends BasePage {
   }
 
   async setImportUrl(url: string): Promise<void> {
-    await this.urlInput.fill(url);
+    await this.fillAndVerifyInput(this.urlInput, url);
   }
 
   async clickImportNote(): Promise<void> {
diff --git a/zeppelin-web-angular/e2e/models/note-rename-page.ts 
b/zeppelin-web-angular/e2e/models/note-rename-page.ts
index 932babc409..9999bfcab9 100644
--- a/zeppelin-web-angular/e2e/models/note-rename-page.ts
+++ b/zeppelin-web-angular/e2e/models/note-rename-page.ts
@@ -31,7 +31,7 @@ export class NoteRenamePage extends BasePage {
 
   async enterTitle(title: string): Promise<void> {
     await this.ensureEditMode();
-    await this.noteTitleInput.fill(title, { timeout: 15000 });
+    await this.fillAndVerifyInput(this.noteTitleInput, title);
   }
 
   async clearTitle(): Promise<void> {
diff --git a/zeppelin-web-angular/e2e/models/notebook-repos-page.ts 
b/zeppelin-web-angular/e2e/models/notebook-repos-page.ts
index 66234f5b61..76cbf00f7b 100644
--- a/zeppelin-web-angular/e2e/models/notebook-repos-page.ts
+++ b/zeppelin-web-angular/e2e/models/notebook-repos-page.ts
@@ -58,21 +58,26 @@ export class NotebookRepoItemPage extends BasePage {
 
   async clickEdit(): Promise<void> {
     await this.editButton.click({ timeout: 15000 });
+    // Wait for Angular to swap to edit mode before returning. Without this,
+    // a follow-up assertion like `expect(editButton).not.toBeVisible()` races
+    // against the re-render and intermittently sees the button still present.
+    await this.saveButton.waitFor({ state: 'visible', timeout: 10000 });
   }
 
   async clickSave(): Promise<void> {
     await this.saveButton.click({ timeout: 15000 });
+    await this.editButton.waitFor({ state: 'visible', timeout: 10000 });
   }
 
   async clickCancel(): Promise<void> {
     await this.cancelButton.click({ timeout: 15000 });
+    await this.editButton.waitFor({ state: 'visible', timeout: 10000 });
   }
 
   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]');
-    await input.clear();
-    await input.fill(value);
+    await this.fillAndVerifyInput(input, value);
   }
 
   async getSettingInputValue(settingName: string): Promise<string> {
diff --git a/zeppelin-web-angular/e2e/models/notebook.util.ts 
b/zeppelin-web-angular/e2e/models/notebook.util.ts
index 98ee1d9648..1070618264 100644
--- a/zeppelin-web-angular/e2e/models/notebook.util.ts
+++ b/zeppelin-web-angular/e2e/models/notebook.util.ts
@@ -11,7 +11,7 @@
  */
 
 import { expect, Page } from '@playwright/test';
-import { performLoginIfRequired, waitForZeppelinReady } from '../utils';
+import { waitForZeppelinReady } from '../utils';
 import { BasePage } from './base-page';
 import { HomePage } from './home-page';
 
@@ -26,9 +26,7 @@ export class NotebookUtil extends BasePage {
   async createNotebook(notebookName: string): Promise<void> {
     await this.homePage.navigateToHome();
 
-    // Perform login if required
-    await performLoginIfRequired(this.page);
-
+    // Auth is handled by the `setup` Playwright project + storageState; no 
per-call login here.
     // Wait for Zeppelin to be fully ready
     await waitForZeppelinReady(this.page);
 
diff --git a/zeppelin-web-angular/e2e/tests/app.spec.ts 
b/zeppelin-web-angular/e2e/tests/app.spec.ts
index d637896e0b..759fde133a 100644
--- a/zeppelin-web-angular/e2e/tests/app.spec.ts
+++ b/zeppelin-web-angular/e2e/tests/app.spec.ts
@@ -12,7 +12,9 @@
 
 import { expect, test } from '@playwright/test';
 import { BasePage } from '../models/base-page';
-import { addPageAnnotationBeforeEach, waitForZeppelinReady, PAGES, 
performLoginIfRequired } from '../utils';
+import { LoginPage } from '../models/login-page';
+import { LoginTestUtil, TestCredentials } from '../models/login-page.util';
+import { addPageAnnotationBeforeEach, waitForZeppelinReady, PAGES } from 
'../utils';
 
 test.describe('Zeppelin App Component', () => {
   addPageAnnotationBeforeEach(PAGES.APP);
@@ -23,7 +25,6 @@ test.describe('Zeppelin App Component', () => {
 
     await page.goto('/', { waitUntil: 'load' });
     await waitForZeppelinReady(page);
-    await performLoginIfRequired(page);
   });
 
   test('should have correct component selector and structure', async ({ page 
}) => {
@@ -85,7 +86,7 @@ test.describe('Zeppelin App Component', () => {
     await expect(loadingSpinner).toBeHidden();
   });
 
-  test('should show logout spinner when logging out', async ({ page }) => {
+  test('should show logout spinner when logging out', async ({ page, browser, 
baseURL }) => {
     await waitForZeppelinReady(page);
 
     // Only test logout flow for authenticated (non-anonymous) users — skip 
before any assertions
@@ -94,27 +95,52 @@ test.describe('Zeppelin App Component', () => {
     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();
-
-    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 ...');
+    const credentials = await LoginTestUtil.getTestCredentials();
+    const logoutUser = getIsolatedLogoutUser(credentials);
+    test.skip(!logoutUser, 'No non-shared logout test user available');
+
+    // The default auth storage state is shared by the whole parallel suite. 
Logging
+    // out from that shared user invalidates the server-side Shiro session for 
many
+    // still-running tests, so exercise logout from a throwaway user/session 
instead.
+    const context = await browser.newContext({
+      baseURL: baseURL ?? 'http://localhost:4200',
+      storageState: { cookies: [], origins: [] }
+    });
+
+    try {
+      const logoutPage = await context.newPage();
+      const loginPage = new LoginPage(logoutPage);
+      await loginPage.navigate();
+      await loginPage.login(logoutUser!.username, logoutUser!.password);
+      await logoutPage.waitForURL('/#/', { timeout: 30000 });
+      await waitForZeppelinReady(logoutPage);
+
+      const isolatedStatusElement = logoutPage.locator('.status');
+      const logoutSpinner = logoutPage.locator('zeppelin-spin').filter({ 
hasText: 'Logging out' });
+
+      await expect(logoutSpinner).toBeHidden();
+
+      await isolatedStatusElement.click();
+      const logoutButton = logoutPage.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();
+
+      // `toBeVisible` can resolve briefly before the spinner mounts then 
misses the
+      // narrow visibility window. `toHaveCount(1)` polls the DOM for the 
spinner's
+      // presence which is more tolerant of the transient mount.
+      await expect(logoutSpinner).toHaveCount(1, { timeout: 10000 });
+      await expect(logoutSpinner).toContainText('Logging out ...');
+    } finally {
+      await context.close();
+    }
   });
 
   test('should maintain component integrity during navigation', async ({ page 
}) => {
     await waitForZeppelinReady(page);
-    await performLoginIfRequired(page);
 
     // Navigate to different pages and ensure component remains intact
     const testPaths = ['/#/notebook', '/#/jobmanager', '/#/configuration'];
@@ -132,3 +158,8 @@ test.describe('Zeppelin App Component', () => {
     await waitForZeppelinReady(page);
   });
 });
+
+const getIsolatedLogoutUser = (credentials: Record<string, TestCredentials>): 
TestCredentials | undefined =>
+  Object.values(credentials).find(
+    credential => credential.username && credential.password && 
credential.username !== 'user1'
+  );
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 cac761ae85..66a7e9bd52 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
@@ -12,7 +12,7 @@
 
 import { expect, test } from '@playwright/test';
 import { HomePage } from '../../models/home-page';
-import { addPageAnnotationBeforeEach, performLoginIfRequired, 
waitForZeppelinReady, PAGES } from '../../utils';
+import { addPageAnnotationBeforeEach, waitForZeppelinReady, PAGES } from 
'../../utils';
 
 test.describe('Home Page - Core Elements', () => {
   addPageAnnotationBeforeEach(PAGES.WORKSPACE.HOME);
@@ -23,7 +23,6 @@ test.describe('Home Page - Core Elements', () => {
     homePage = new HomePage(page);
     await page.goto('/#/');
     await waitForZeppelinReady(page);
-    await performLoginIfRequired(page);
   });
 
   test.describe('Welcome Section', () => {
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 97a250d6ab..09cf4ea563 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
@@ -12,7 +12,7 @@
 
 import { expect, test } from '@playwright/test';
 import { HomePage } from '../../models/home-page';
-import { addPageAnnotationBeforeEach, performLoginIfRequired, 
waitForZeppelinReady, PAGES } from '../../utils';
+import { addPageAnnotationBeforeEach, waitForZeppelinReady, PAGES } from 
'../../utils';
 
 test.describe('Home Page - External Links', () => {
   addPageAnnotationBeforeEach(PAGES.WORKSPACE.HOME);
@@ -23,7 +23,6 @@ test.describe('Home Page - External Links', () => {
     homePage = new HomePage(page);
     await page.goto('/#/');
     await waitForZeppelinReady(page);
-    await performLoginIfRequired(page);
   });
 
   test.describe('Documentation Link', () => {
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 5a12f6ea4e..ef3cc36511 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
@@ -12,7 +12,7 @@
 
 import { expect, test } from '@playwright/test';
 import { HomePage } from '../../models/home-page';
-import { addPageAnnotationBeforeEach, performLoginIfRequired, 
waitForZeppelinReady, PAGES } from '../../utils';
+import { addPageAnnotationBeforeEach, waitForZeppelinReady, PAGES } from 
'../../utils';
 
 test.describe('Home Page - Layout and Grid', () => {
   addPageAnnotationBeforeEach(PAGES.WORKSPACE.HOME);
@@ -23,7 +23,6 @@ test.describe('Home Page - Layout and Grid', () => {
     homePage = new HomePage(page);
     await page.goto('/#/');
     await waitForZeppelinReady(page);
-    await performLoginIfRequired(page);
   });
 
   test.describe('Responsive Grid Layout', () => {
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 66be0f6f4d..280ab64a34 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
@@ -12,24 +12,30 @@
 
 import { expect, test } from '@playwright/test';
 import { HomePage } from '../../models/home-page';
-import { addPageAnnotationBeforeEach, performLoginIfRequired, 
waitForZeppelinReady, PAGES } from '../../utils';
+import { addPageAnnotationBeforeEach, createTestNotebookWithName, 
waitForZeppelinReady, PAGES } from '../../utils';
 
 addPageAnnotationBeforeEach(PAGES.WORKSPACE.HOME);
 
 test.describe('Home Page Note Operations', () => {
+  // JUSTIFIED: homePage and testNoteName are describe-scoped; fullyParallel 
can overwrite them.
+  test.describe.configure({ mode: 'default' });
+
   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);
 
-    // Create a test note so all operation tests have a real target
-    await homePage.createNote(testNoteName);
+    // Create the operation target through the REST API so setup is not 
coupled to
+    // the UI create-note modal, which this suite exercises separately below.
+    const testNote = await createTestNotebookWithName(page, {
+      folderPath: null,
+      namePrefix: '_e2e_ops_test'
+    });
+    testNoteName = testNote.notebookName;
+
     await page.goto('/#/');
     await waitForZeppelinReady(page);
 
@@ -161,7 +167,18 @@ test.describe('Home Page Note Operations', () => {
       const maxLengthAttr = await notebookNameInput.getAttribute('maxlength');
       const longName = `_e2e_ml_${'a'.repeat(300)}`;
 
-      await notebookNameInput.fill(longName);
+      await expect(async () => {
+        await notebookNameInput.click();
+        await notebookNameInput.fill(longName);
+        await notebookNameInput.evaluate((el: HTMLInputElement) => {
+          el.dispatchEvent(new Event('input', { bubbles: true }));
+          el.dispatchEvent(new Event('change', { bubbles: true }));
+        });
+        const value = await notebookNameInput.inputValue();
+        if (value.length === 0 || value === 'Untitled Note 1') {
+          throw new Error(`note name fill retry: got "${value}"`);
+        }
+      }).toPass({ timeout: 15000, intervals: [200, 500, 1000, 2000] });
       const actualValue = await notebookNameInput.inputValue();
 
       // Must have content — input did not silently reject the fill
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 c14a5474e2..a92326f32e 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
@@ -12,7 +12,7 @@
 
 import { expect, test } from '@playwright/test';
 import { HomePage } from '../../models/home-page';
-import { addPageAnnotationBeforeEach, performLoginIfRequired, 
waitForZeppelinReady, PAGES } from '../../utils';
+import { addPageAnnotationBeforeEach, waitForZeppelinReady, PAGES } from 
'../../utils';
 
 addPageAnnotationBeforeEach(PAGES.WORKSPACE.HOME);
 
@@ -23,7 +23,6 @@ test.describe('Home Page Notebook Actions', () => {
     homePage = new HomePage(page);
     await page.goto('/#/');
     await waitForZeppelinReady(page);
-    await performLoginIfRequired(page);
   });
 
   test.describe('Given notebook list is displayed', () => {
@@ -54,7 +53,7 @@ test.describe('Home Page Notebook Actions', () => {
 
       // 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);
+        await homePage.fillAndVerifyInput(homePage.nodeList.filterInput, 
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();
@@ -64,7 +63,7 @@ test.describe('Home Page Notebook Actions', () => {
       }
 
       // Clean up: clear the filter so other tests start fresh
-      await homePage.nodeList.filterInput.fill('');
+      await homePage.nodeList.filterInput.clear();
     });
   });
 });
diff --git a/zeppelin-web-angular/e2e/tests/login/login.spec.ts 
b/zeppelin-web-angular/e2e/tests/login/login.spec.ts
index cd9786d82f..e7d07c649e 100644
--- a/zeppelin-web-angular/e2e/tests/login/login.spec.ts
+++ b/zeppelin-web-angular/e2e/tests/login/login.spec.ts
@@ -12,19 +12,30 @@
 
 import { expect, test } from '@playwright/test';
 import { LoginPage } from '../../models/login-page';
-import { LoginTestUtil } from '../../models/login-page.util';
+import { LoginTestUtil, TestCredentials } from '../../models/login-page.util';
 import { addPageAnnotationBeforeEach, PAGES } from '../../utils';
 
 test.describe('Login Page', () => {
+  test.use({ storageState: { cookies: [], origins: [] } });
+
   addPageAnnotationBeforeEach(PAGES.PAGES.LOGIN);
   let loginPage: LoginPage;
-  let testCredentials: Record<string, any>;
+  let testCredentials: Record<string, TestCredentials>;
 
-  test.beforeAll(async () => {
+  test.beforeAll(async ({ request }) => {
     const isShiroEnabled = await LoginTestUtil.isShiroEnabled();
     if (!isShiroEnabled) {
       test.skip(true, 'Skipping all login tests - shiro.ini not found');
     }
+
+    const ticketResponse = await request.get('/api/security/ticket', { 
failOnStatusCode: false });
+    if (ticketResponse.ok()) {
+      const ticket = await ticketResponse.json();
+      if (ticket?.body?.principal === 'anonymous') {
+        test.skip(true, 'Skipping all login tests - Zeppelin server is running 
in anonymous mode');
+      }
+    }
+
     testCredentials = await LoginTestUtil.getTestCredentials();
   });
 
diff --git 
a/zeppelin-web-angular/e2e/tests/notebook/action-bar/action-bar-functionality.spec.ts
 
b/zeppelin-web-angular/e2e/tests/notebook/action-bar/action-bar-functionality.spec.ts
index 4d012b93c4..358e77c65d 100644
--- 
a/zeppelin-web-angular/e2e/tests/notebook/action-bar/action-bar-functionality.spec.ts
+++ 
b/zeppelin-web-angular/e2e/tests/notebook/action-bar/action-bar-functionality.spec.ts
@@ -14,13 +14,16 @@ import { expect, test } from '@playwright/test';
 import { NotebookActionBarPage } from 
'../../../models/notebook-action-bar-page';
 import {
   addPageAnnotationBeforeEach,
-  performLoginIfRequired,
   waitForZeppelinReady,
   PAGES,
-  createTestNotebook
+  createTestNotebook,
+  navigateToNotebookWithFallback
 } from '../../../utils';
 
 test.describe('Notebook Action Bar Functionality', () => {
+  // JUSTIFIED: page objects and notebook ids are stored in describe scope; 
fullyParallel can overwrite them.
+  test.describe.configure({ mode: 'default' });
+
   addPageAnnotationBeforeEach(PAGES.WORKSPACE.NOTEBOOK_ACTION_BAR);
 
   let actionBarPage: NotebookActionBarPage;
@@ -29,13 +32,11 @@ test.describe('Notebook Action Bar Functionality', () => {
   test.beforeEach(async ({ page }) => {
     await page.goto('/#/');
     await waitForZeppelinReady(page);
-    await performLoginIfRequired(page);
 
     testNotebook = await createTestNotebook(page);
     actionBarPage = new NotebookActionBarPage(page);
 
-    await page.goto(`/#/notebook/${testNotebook.noteId}`);
-    await page.waitForLoadState('networkidle');
+    await navigateToNotebookWithFallback(page, testNotebook.noteId);
   });
 
   test('should display and allow title editing with tooltip', async ({ page }) 
=> {
@@ -45,8 +46,7 @@ test.describe('Notebook Action Bar Functionality', () => {
     await actionBarPage.titleEditor.click();
 
     const titleInputField = actionBarPage.titleEditor.locator('input');
-    await expect(titleInputField).toBeVisible();
-    await titleInputField.fill(notebookName);
+    await actionBarPage.fillAndVerifyInput(titleInputField, notebookName);
     await page.keyboard.press('Enter');
 
     await expect(actionBarPage.titleEditor).toHaveText(notebookName, { 
timeout: 10000 });
diff --git 
a/zeppelin-web-angular/e2e/tests/notebook/keyboard/notebook-keyboard-shortcuts.spec.ts
 
b/zeppelin-web-angular/e2e/tests/notebook/keyboard/notebook-keyboard-shortcuts.spec.ts
index e482464364..b0818c9f02 100644
--- 
a/zeppelin-web-angular/e2e/tests/notebook/keyboard/notebook-keyboard-shortcuts.spec.ts
+++ 
b/zeppelin-web-angular/e2e/tests/notebook/keyboard/notebook-keyboard-shortcuts.spec.ts
@@ -14,7 +14,6 @@ import { expect, test } from '@playwright/test';
 import { NotebookKeyboardPage } from 'e2e/models/notebook-keyboard-page';
 import {
   addPageAnnotationBeforeEach,
-  performLoginIfRequired,
   waitForNotebookLinks,
   waitForZeppelinReady,
   PAGES,
@@ -43,7 +42,6 @@ test.describe.serial('Comprehensive Keyboard Shortcuts 
(ShortcutsMap)', () => {
 
     await page.goto('/#/');
     await waitForZeppelinReady(page);
-    await performLoginIfRequired(page);
     await waitForNotebookLinks(page);
 
     // Handle the welcome modal if it appears
@@ -1004,7 +1002,7 @@ test.describe.serial('Comprehensive Keyboard Shortcuts 
(ShortcutsMap)', () => {
       await keyboardPage.setCodeEditorContent('%md\n# Test paragraph');
 
       // Remove focus by clicking on empty area
-      await keyboardPage.page.click('body');
+      await keyboardPage.page.locator('body').click();
       await keyboardPage.page.waitForTimeout(500); // JUSTIFIED: Monaco editor 
internal state settle — cursor/focus state not observable via DOM
 
       const initialCount = await keyboardPage.getParagraphCount();
@@ -1026,13 +1024,14 @@ test.describe.serial('Comprehensive Keyboard Shortcuts 
(ShortcutsMap)', () => {
 
     test('should handle rapid keyboard operations without instability', async 
() => {
       await keyboardPage.tryFocusCodeEditor();
-      await keyboardPage.setCodeEditorContent('%python\nprint("test")');
+      await keyboardPage.setCodeEditorContent('%md\nrapid keyboard test');
 
       // Rapid Shift+Enter operations
       for (let i = 0; i < 3; i++) {
         await keyboardPage.pressRunParagraph();
+        await keyboardPage.waitForParagraphExecution(0, 60000);
         // JUSTIFIED: single-paragraph test notebook; first() is deterministic
-        await expect(keyboardPage.paragraphResult.first()).toBeVisible({ 
timeout: 15000 });
+        await expect(keyboardPage.paragraphResult.first()).toBeVisible({ 
timeout: 60000 });
         await keyboardPage.page.waitForTimeout(500); // JUSTIFIED: brief gap 
between rapid sequential runs to prevent WebSocket message overlap
       }
 
diff --git 
a/zeppelin-web-angular/e2e/tests/notebook/main/notebook-container.spec.ts 
b/zeppelin-web-angular/e2e/tests/notebook/main/notebook-container.spec.ts
index d66df7fa5f..03b956e77d 100644
--- a/zeppelin-web-angular/e2e/tests/notebook/main/notebook-container.spec.ts
+++ b/zeppelin-web-angular/e2e/tests/notebook/main/notebook-container.spec.ts
@@ -14,13 +14,16 @@ import { expect, test } from '@playwright/test';
 import { NotebookPage } from '../../../models/notebook-page';
 import {
   addPageAnnotationBeforeEach,
-  performLoginIfRequired,
   waitForZeppelinReady,
   PAGES,
-  createTestNotebook
+  createTestNotebook,
+  navigateToNotebookWithFallback
 } from '../../../utils';
 
 test.describe('Notebook Container Component', () => {
+  // JUSTIFIED: page objects and notebook ids are stored in describe scope; 
fullyParallel can overwrite them.
+  test.describe.configure({ mode: 'default' });
+
   addPageAnnotationBeforeEach(PAGES.WORKSPACE.NOTEBOOK);
 
   let notebookPage: NotebookPage;
@@ -29,13 +32,11 @@ test.describe('Notebook Container Component', () => {
   test.beforeEach(async ({ page }) => {
     await page.goto('/#/');
     await waitForZeppelinReady(page);
-    await performLoginIfRequired(page);
 
     testNotebook = await createTestNotebook(page);
     notebookPage = new NotebookPage(page);
 
-    await page.goto(`/#/notebook/${testNotebook.noteId}`);
-    await page.waitForLoadState('networkidle');
+    await navigateToNotebookWithFallback(page, testNotebook.noteId);
   });
 
   test('should display notebook container with proper structure', async () => {
diff --git 
a/zeppelin-web-angular/e2e/tests/notebook/main/notebook-navigation.spec.ts 
b/zeppelin-web-angular/e2e/tests/notebook/main/notebook-navigation.spec.ts
index db259de834..877a191569 100644
--- a/zeppelin-web-angular/e2e/tests/notebook/main/notebook-navigation.spec.ts
+++ b/zeppelin-web-angular/e2e/tests/notebook/main/notebook-navigation.spec.ts
@@ -13,7 +13,7 @@
 import { expect, Page, test } from '@playwright/test';
 import { HeaderPage } from '../../../models/header-page';
 import { HomePage } from '../../../models/home-page';
-import { addPageAnnotationBeforeEach, PAGES, performLoginIfRequired, 
waitForZeppelinReady } from '../../../utils';
+import { addPageAnnotationBeforeEach, PAGES, waitForZeppelinReady } from 
'../../../utils';
 
 const noteIdFromUrl = (url: string): string => {
   const match = url.match(/\/notebook\/([^/?]+)/);
@@ -37,7 +37,6 @@ test.describe('Notebook Navigation', () => {
   test.beforeEach(async ({ page }) => {
     await page.goto('/#/');
     await waitForZeppelinReady(page);
-    await performLoginIfRequired(page);
   });
 
   // Regression: ZEPPELIN-6387 moved the note fetch onto the WebSocket 
connectedStatus$
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 475da43f3b..099212243d 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
@@ -15,7 +15,6 @@ import { PublishedParagraphPage } from 
'e2e/models/published-paragraph-page';
 import { PublishedParagraphTestUtil } from 
'../../../models/published-paragraph-page.util';
 import {
   addPageAnnotationBeforeEach,
-  performLoginIfRequired,
   waitForNotebookLinks,
   waitForZeppelinReady,
   PAGES,
@@ -23,6 +22,9 @@ import {
 } from '../../../utils';
 
 test.describe('Published Paragraph', () => {
+  // JUSTIFIED: page objects and notebook ids are stored in describe scope; 
fullyParallel can overwrite them.
+  test.describe.configure({ mode: 'default' });
+
   addPageAnnotationBeforeEach(PAGES.WORKSPACE.PUBLISHED_PARAGRAPH);
 
   let publishedParagraphPage: PublishedParagraphPage;
@@ -33,7 +35,6 @@ test.describe('Published Paragraph', () => {
     publishedParagraphPage = new PublishedParagraphPage(page);
     await page.goto('/#/');
     await waitForZeppelinReady(page);
-    await performLoginIfRequired(page);
     await waitForNotebookLinks(page);
 
     if ((await publishedParagraphPage.cancelButton.count()) > 0) {
@@ -91,8 +92,7 @@ test.describe('Published Paragraph', () => {
     test('should enter published paragraph by clicking link', async ({ page }) 
=> {
       const { noteId, paragraphId } = testNotebook;
 
-      await page.goto(`/#/notebook/${noteId}`);
-      await page.waitForLoadState('networkidle');
+      await publishedParagraphPage.navigateToNotebook(noteId);
 
       // JUSTIFIED: createTestNotebook creates a single paragraph; first() is 
deterministic
       const paragraphElement = 
page.locator('zeppelin-notebook-paragraph').first();
diff --git 
a/zeppelin-web-angular/e2e/tests/notebook/sidebar/sidebar-functionality.spec.ts 
b/zeppelin-web-angular/e2e/tests/notebook/sidebar/sidebar-functionality.spec.ts
index 1cc2b2b260..996ac0d24c 100644
--- 
a/zeppelin-web-angular/e2e/tests/notebook/sidebar/sidebar-functionality.spec.ts
+++ 
b/zeppelin-web-angular/e2e/tests/notebook/sidebar/sidebar-functionality.spec.ts
@@ -14,13 +14,16 @@ import { expect, test } from '@playwright/test';
 import { NotebookSidebarPage } from '../../../models/notebook-sidebar-page';
 import {
   addPageAnnotationBeforeEach,
-  performLoginIfRequired,
   waitForZeppelinReady,
   PAGES,
-  createTestNotebook
+  createTestNotebook,
+  navigateToNotebookWithFallback
 } from '../../../utils';
 
 test.describe('Notebook Sidebar Functionality', () => {
+  // JUSTIFIED: page objects and notebook ids are stored in describe scope; 
fullyParallel can overwrite them.
+  test.describe.configure({ mode: 'default' });
+
   addPageAnnotationBeforeEach(PAGES.WORKSPACE.NOTEBOOK_SIDEBAR);
 
   let sidebar: NotebookSidebarPage;
@@ -29,13 +32,11 @@ test.describe('Notebook Sidebar Functionality', () => {
   test.beforeEach(async ({ page }) => {
     await page.goto('/', { waitUntil: 'load', timeout: 60000 });
     await waitForZeppelinReady(page);
-    await performLoginIfRequired(page);
 
     sidebar = new NotebookSidebarPage(page);
     testNotebook = await createTestNotebook(page);
 
-    await page.goto(`/#/notebook/${testNotebook.noteId}`);
-    await page.waitForLoadState('networkidle');
+    await navigateToNotebookWithFallback(page, testNotebook.noteId);
   });
 
   test('should display navigation buttons', async ({ page }) => {
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 1f2d6fed08..c162293d48 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
@@ -13,7 +13,7 @@
 import { test, expect } from '@playwright/test';
 import { HeaderPage } from '../../../models/header-page';
 import { AboutZeppelinModal } from '../../../models/about-zeppelin-modal';
-import { addPageAnnotationBeforeEach, PAGES, performLoginIfRequired, 
waitForZeppelinReady } from '../../../utils';
+import { addPageAnnotationBeforeEach, PAGES, waitForZeppelinReady } from 
'../../../utils';
 
 test.describe('About Zeppelin Modal', () => {
   let headerPage: HeaderPage;
@@ -27,7 +27,6 @@ test.describe('About Zeppelin Modal', () => {
 
     await page.goto('/');
     await waitForZeppelinReady(page);
-    await performLoginIfRequired(page);
 
     await headerPage.clickUserDropdown();
     await headerPage.clickAboutZeppelin();
diff --git 
a/zeppelin-web-angular/e2e/tests/share/folder-rename/folder-rename.spec.ts 
b/zeppelin-web-angular/e2e/tests/share/folder-rename/folder-rename.spec.ts
index a364a20bb5..1bbc8d090d 100644
--- a/zeppelin-web-angular/e2e/tests/share/folder-rename/folder-rename.spec.ts
+++ b/zeppelin-web-angular/e2e/tests/share/folder-rename/folder-rename.spec.ts
@@ -10,16 +10,16 @@
  * limitations under the License.
  */
 
-import { test, expect } from '@playwright/test';
+import { test, expect, Page } from '@playwright/test';
 import { FolderRenamePage } from '../../../models/folder-rename-page';
 import { FolderRenamePageUtil } from '../../../models/folder-rename-page.util';
-import {
-  addPageAnnotationBeforeEach,
-  PAGES,
-  performLoginIfRequired,
-  waitForZeppelinReady,
-  createTestNotebook
-} from '../../../utils';
+import { addPageAnnotationBeforeEach, PAGES, waitForZeppelinReady, 
createTestNotebook } from '../../../utils';
+
+const refreshHomeAndWaitForFolder = async (page: Page, folderName: string): 
Promise<void> => {
+  await page.reload({ waitUntil: 'domcontentloaded' });
+  await waitForZeppelinReady(page);
+  await expect(page.getByTestId(`folder-${folderName}`)).toBeVisible({ 
timeout: 60000 });
+};
 
 // JUSTIFIED: rename/delete ops mutate shared state; parallel runs cause 
folder-not-found races
 test.describe.serial('Folder Rename', () => {
@@ -35,19 +35,18 @@ test.describe.serial('Folder Rename', () => {
 
     await page.goto('/#/');
     await waitForZeppelinReady(page);
-    await performLoginIfRequired(page);
 
     // Create a test notebook with folder structure
     testFolderName = `TestFolder_${Date.now()}`;
     await createTestNotebook(page, testFolderName);
-    await page.goto('/#/');
+    await refreshHomeAndWaitForFolder(page, testFolderName);
   });
 
   test('Given folder exists in notebook list, When hovering over folder, Then 
context menu should appear with Rename option', async () => {
     await folderRenamePage.hoverOverFolder(testFolderName);
     const folderNode = folderRenamePage.page
       .locator('.node')
-      .filter({ has: folderRenamePage.page.locator('.folder .name', { hasText: 
testFolderName }) })
+      .filter({ has: 
folderRenamePage.page.getByTestId(`folder-${testFolderName}`) })
       // JUSTIFIED: filter already narrows to target folder; first() handles 
nested .node structure
       .first();
     const renameButton = folderNode.locator('.folder .operation 
a[nz-tooltip][nztooltiptitle="Rename folder"]');
@@ -79,13 +78,13 @@ test.describe.serial('Folder Rename', () => {
     await folderRenamePage.clickConfirm();
 
     await expect(folderRenamePage.renameModal).not.toBeVisible({ timeout: 
10000 });
-    await expect(page.locator('.folder .name', { hasText: testFolderName 
})).not.toBeVisible({ timeout: 10000 });
+    await 
expect(page.getByTestId(`folder-${testFolderName}`)).not.toBeVisible({ timeout: 
10000 });
 
     await page.reload();
     await page.waitForLoadState('domcontentloaded', { timeout: 15000 });
 
     const baseNewName = renamedFolderName.split('/').pop() ?? 
renamedFolderName;
-    await expect(page.locator('.folder .name', { hasText: baseNewName 
})).toBeVisible({ timeout: 30000 });
+    await expect(page.getByTestId(`folder-${baseNewName}`)).toBeVisible({ 
timeout: 30000 });
   });
 
   test('Given rename modal is open, When submitting empty name, Then empty 
name should not be allowed', async () => {
@@ -97,7 +96,7 @@ test.describe.serial('Folder Rename', () => {
 
     await folderRenamePage.clickCancel();
     await expect(folderRenamePage.renameModal).not.toBeVisible({ timeout: 5000 
});
-    await expect(folderRenamePage.page.locator('.folder .name', { hasText: 
testFolderName })).toBeVisible({
+    await 
expect(folderRenamePage.page.getByTestId(`folder-${testFolderName}`)).toBeVisible({
       timeout: 5000
     });
   });
@@ -106,7 +105,7 @@ test.describe.serial('Folder Rename', () => {
     await folderRenamePage.hoverOverFolder(testFolderName);
     const folderNode = folderRenamePage.page
       .locator('.node')
-      .filter({ has: folderRenamePage.page.locator('.folder .name', { hasText: 
testFolderName }) })
+      .filter({ has: 
folderRenamePage.page.getByTestId(`folder-${testFolderName}`) })
       // JUSTIFIED: filter already narrows to target folder; first() handles 
nested .node structure
       .first();
     await expect(folderNode.locator('.folder .operation 
a[nztooltiptitle*="Move folder to Trash"]')).toBeVisible();
@@ -129,7 +128,7 @@ test.describe.serial('Folder Rename', () => {
     // Create a second folder to use as a name collision target
     const existingFolderName = `ExistingFolder_${Date.now()}`;
     await createTestNotebook(page, existingFolderName);
-    await page.goto('/#/'); // Refresh to see the new folder
+    await refreshHomeAndWaitForFolder(page, existingFolderName);
 
     // Attempt to rename the first folder to the name of the second folder
     await folderRenamePage.hoverOverFolder(testFolderName);
@@ -139,8 +138,8 @@ test.describe.serial('Folder Rename', () => {
     await folderRenamePage.clickConfirm();
 
     // Wait for the source folder to disappear (as it's merged into target)
-    await expect(page.locator('.folder .name', { hasText: testFolderName 
})).toHaveCount(0, { timeout: 10000 });
+    await expect(page.getByTestId(`folder-${testFolderName}`)).toHaveCount(0, 
{ timeout: 10000 });
     // Wait for the target folder to remain visible
-    await expect(page.locator('.folder .name', { hasText: existingFolderName 
})).toBeVisible({ timeout: 10000 });
+    await 
expect(page.getByTestId(`folder-${existingFolderName}`)).toBeVisible({ timeout: 
10000 });
   });
 });
diff --git 
a/zeppelin-web-angular/e2e/tests/share/header/header-navigation.spec.ts 
b/zeppelin-web-angular/e2e/tests/share/header/header-navigation.spec.ts
index aae38d544a..a37d7c4171 100644
--- a/zeppelin-web-angular/e2e/tests/share/header/header-navigation.spec.ts
+++ b/zeppelin-web-angular/e2e/tests/share/header/header-navigation.spec.ts
@@ -13,7 +13,7 @@
 import { test, expect } from '@playwright/test';
 import { HeaderPage } from '../../../models/header-page';
 import { NodeListPage } from '../../../models/node-list-page';
-import { addPageAnnotationBeforeEach, PAGES, performLoginIfRequired, 
waitForZeppelinReady } from '../../../utils';
+import { addPageAnnotationBeforeEach, PAGES, waitForZeppelinReady } from 
'../../../utils';
 
 test.describe('Header Navigation', () => {
   let headerPage: HeaderPage;
@@ -25,7 +25,6 @@ test.describe('Header Navigation', () => {
 
     await page.goto('/');
     await waitForZeppelinReady(page);
-    await performLoginIfRequired(page);
   });
 
   test('Given user is on any page, When viewing the header, Then all header 
elements should be visible', async () => {
@@ -42,16 +41,14 @@ test.describe('Header Navigation', () => {
     page
   }) => {
     await headerPage.clickBrandLogo();
-    await page.waitForURL(/\/(#\/)?$/);
-    expect(page.url()).toMatch(/\/(#\/)?$/);
+    await expect(page).toHaveURL(/\/(#\/)?$/);
   });
 
   test('Given user is on home page, When clicking the Job menu item, Then user 
should navigate to Job Manager page', async ({
     page
   }) => {
     await headerPage.clickJobMenu();
-    await page.waitForURL(/jobmanager/);
-    expect(page.url()).toContain('jobmanager');
+    await expect(page).toHaveURL(/jobmanager/);
   });
 
   test('Given user is on home page, When clicking the Notebook dropdown, Then 
dropdown with node list should open', async ({
@@ -89,8 +86,7 @@ test.describe('Header Navigation', () => {
   }) => {
     await headerPage.clickUserDropdown();
     await headerPage.clickInterpreter();
-    await page.waitForURL(/interpreter/);
-    expect(page.url()).toContain('interpreter');
+    await expect(page).toHaveURL(/interpreter/);
   });
 
   test('Given user opens user dropdown, When clicking Notebook Repos menu 
item, Then user should navigate to Notebook Repos page', async ({
@@ -98,8 +94,7 @@ test.describe('Header Navigation', () => {
   }) => {
     await headerPage.clickUserDropdown();
     await headerPage.clickNotebookRepos();
-    await page.waitForURL(/notebook-repos/);
-    expect(page.url()).toContain('notebook-repos');
+    await expect(page).toHaveURL(/notebook-repos/);
   });
 
   test('Given user opens user dropdown, When clicking Credential menu item, 
Then user should navigate to Credential page', async ({
@@ -107,8 +102,7 @@ test.describe('Header Navigation', () => {
   }) => {
     await headerPage.clickUserDropdown();
     await headerPage.clickCredential();
-    await page.waitForURL(/credential/);
-    expect(page.url()).toContain('credential');
+    await expect(page).toHaveURL(/credential/);
   });
 
   test('Given user opens user dropdown, When clicking Configuration menu item, 
Then user should navigate to Configuration page', async ({
@@ -116,7 +110,6 @@ test.describe('Header Navigation', () => {
   }) => {
     await headerPage.clickUserDropdown();
     await headerPage.clickConfiguration();
-    await page.waitForURL(/configuration/);
-    expect(page.url()).toContain('configuration');
+    await expect(page).toHaveURL(/configuration/);
   });
 });
diff --git a/zeppelin-web-angular/e2e/tests/share/header/header-search.spec.ts 
b/zeppelin-web-angular/e2e/tests/share/header/header-search.spec.ts
index f6960142a4..17f0c4bfc2 100644
--- a/zeppelin-web-angular/e2e/tests/share/header/header-search.spec.ts
+++ b/zeppelin-web-angular/e2e/tests/share/header/header-search.spec.ts
@@ -12,7 +12,7 @@
 
 import { test, expect } from '@playwright/test';
 import { HeaderPage } from '../../../models/header-page';
-import { addPageAnnotationBeforeEach, PAGES, performLoginIfRequired, 
waitForZeppelinReady } from '../../../utils';
+import { addPageAnnotationBeforeEach, PAGES, waitForZeppelinReady } from 
'../../../utils';
 
 test.describe('Header Search Functionality', () => {
   let headerPage: HeaderPage;
@@ -24,7 +24,6 @@ test.describe('Header Search Functionality', () => {
 
     await page.goto('/');
     await waitForZeppelinReady(page);
-    await performLoginIfRequired(page);
   });
 
   test('Given user is on home page, When entering search query and pressing 
Enter, Then user should navigate to search results page', async ({
@@ -32,9 +31,9 @@ test.describe('Header Search Functionality', () => {
   }) => {
     const searchQuery = 'test';
     await headerPage.searchNote(searchQuery);
-    await page.waitForURL(/search/);
-    expect(page.url()).toContain('search');
-    expect(page.url()).toContain(searchQuery);
+    await expect(page).toHaveURL(/search/);
+    // searchQuery is alphanumeric test data ('test'); safe for new RegExp 
without escaping.
+    await expect(page).toHaveURL(new RegExp(searchQuery));
   });
 
   test('Given user is on home page, When viewing search input, Then search 
input should be visible and accessible', async () => {
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 2ef30aa82f..0bef3a74bb 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
@@ -13,9 +13,12 @@
 import { test, expect } from '@playwright/test';
 import { HomePage } from '../../../models/home-page';
 import { NodeListPage } from '../../../models/node-list-page';
-import { addPageAnnotationBeforeEach, PAGES, performLoginIfRequired, 
waitForZeppelinReady } from '../../../utils';
+import { addPageAnnotationBeforeEach, PAGES, waitForZeppelinReady } from 
'../../../utils';
 
 test.describe('Node List Functionality', () => {
+  // JUSTIFIED: page objects are stored in describe scope; fullyParallel can 
overwrite them.
+  test.describe.configure({ mode: 'default' });
+
   let nodeListPage: NodeListPage;
 
   addPageAnnotationBeforeEach(PAGES.SHARE.NODE_LIST);
@@ -25,7 +28,6 @@ test.describe('Node List Functionality', () => {
 
     await page.goto('/');
     await waitForZeppelinReady(page);
-    await performLoginIfRequired(page);
   });
 
   test('Given user is on home page, When viewing node list, Then node list 
should display tree structure', async () => {
@@ -81,22 +83,17 @@ test.describe('Node List Functionality', () => {
     page
   }) => {
     const homePage = new HomePage(page);
+    const noteName = 
`_e2e_nav_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
 
-    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();
-    }
+    // Seed a unique note so the click target is deterministic even when other
+    // parallel specs leave many notes/folders in the shared test workspace.
+    await homePage.createNote(noteName);
+    await page.goto('/');
+    await waitForZeppelinReady(page);
 
-    const noteName = notes[0].trim();
+    await expect(nodeListPage.noteLinkByName(noteName)).toBeVisible({ timeout: 
15000 });
     await nodeListPage.clickNote(noteName);
-    await page.waitForURL(/notebook\//);
-    expect(page.url()).toContain('notebook/');
+    await expect(page).toHaveURL(/notebook\//, { timeout: 45000 });
   });
 
   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 dbc27205b3..239f874073 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
@@ -13,7 +13,7 @@
 import { test, expect } from '@playwright/test';
 import { HomePage } from '../../../models/home-page';
 import { NoteCreateModal } from '../../../models/note-create-modal';
-import { addPageAnnotationBeforeEach, PAGES, performLoginIfRequired, 
waitForZeppelinReady } from '../../../utils';
+import { addPageAnnotationBeforeEach, PAGES, waitForZeppelinReady } from 
'../../../utils';
 
 test.describe('Note Create Modal', () => {
   let homePage: HomePage;
@@ -27,7 +27,6 @@ test.describe('Note Create Modal', () => {
 
     await page.goto('/');
     await waitForZeppelinReady(page);
-    await performLoginIfRequired(page);
 
     await homePage.clickCreateNewNote();
     await page.waitForSelector('input[name="noteName"]');
@@ -39,7 +38,7 @@ test.describe('Note Create Modal', () => {
     await expect(noteCreateModal.createButton).toBeVisible();
     await expect(noteCreateModal.interpreterDropdown).toBeVisible();
     await expect(noteCreateModal.folderInfoAlert).toBeVisible();
-    expect(await noteCreateModal.folderInfoAlert.textContent()).toContain('/');
+    await expect(noteCreateModal.folderInfoAlert).toContainText('/');
   });
 
   test('Given Create Note modal is open, When checking default note name, Then 
auto-generated name should follow pattern', async () => {
@@ -57,8 +56,7 @@ test.describe('Note Create Modal', () => {
     // Wait for modal to disappear
     await expect(noteCreateModal.modal).not.toBeVisible();
 
-    await page.waitForURL(/notebook\//);
-    expect(page.url()).toContain('notebook/');
+    await expect(page).toHaveURL(/notebook\//);
 
     // Verify the note was created with the correct name
     const notebookTitle = page.locator('[data-testid="notebook-title"]');
@@ -85,8 +83,7 @@ test.describe('Note Create Modal', () => {
     // Wait for modal to disappear
     await expect(noteCreateModal.modal).not.toBeVisible();
 
-    await page.waitForURL(/notebook\//);
-    expect(page.url()).toContain('notebook/');
+    await expect(page).toHaveURL(/notebook\//);
 
     // Verify the note was created with the correct name (without folder path)
     const notebookTitle = page.locator('[data-testid="notebook-title"]');
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 2100d56a39..9fe75cbae3 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
@@ -13,7 +13,7 @@
 import { test, expect } from '@playwright/test';
 import { HomePage } from '../../../models/home-page';
 import { NoteImportModal } from '../../../models/note-import-modal';
-import { addPageAnnotationBeforeEach, PAGES, performLoginIfRequired, 
waitForZeppelinReady } from '../../../utils';
+import { addPageAnnotationBeforeEach, PAGES, waitForZeppelinReady } from 
'../../../utils';
 
 test.describe('Note Import Modal', () => {
   let homePage: HomePage;
@@ -27,7 +27,6 @@ test.describe('Note Import Modal', () => {
 
     await page.goto('/');
     await waitForZeppelinReady(page);
-    await performLoginIfRequired(page);
 
     await homePage.clickImportNote();
     await page.waitForSelector('input[name="noteImportName"]');
diff --git 
a/zeppelin-web-angular/e2e/tests/share/note-rename/note-rename.spec.ts 
b/zeppelin-web-angular/e2e/tests/share/note-rename/note-rename.spec.ts
index a5a6f1c13f..34bdac8a17 100644
--- a/zeppelin-web-angular/e2e/tests/share/note-rename/note-rename.spec.ts
+++ b/zeppelin-web-angular/e2e/tests/share/note-rename/note-rename.spec.ts
@@ -15,13 +15,16 @@ import { NoteRenamePage } from 
'../../../models/note-rename-page';
 import { NoteRenamePageUtil } from '../../../models/note-rename-page.util';
 import {
   addPageAnnotationBeforeEach,
+  createTestNotebook,
+  navigateToNotebookWithFallback,
   PAGES,
-  performLoginIfRequired,
-  waitForZeppelinReady,
-  createTestNotebook
+  waitForZeppelinReady
 } from '../../../utils';
 
 test.describe('Note Rename', () => {
+  // JUSTIFIED: page objects and notebook ids are stored in describe scope; 
fullyParallel can overwrite them.
+  test.describe.configure({ mode: 'default' });
+
   let noteRenamePage: NoteRenamePage;
   let noteRenameUtil: NoteRenamePageUtil;
   let testNotebook: { noteId: string; paragraphId: string };
@@ -34,14 +37,14 @@ test.describe('Note Rename', () => {
 
     await page.goto('/#/');
     await waitForZeppelinReady(page);
-    await performLoginIfRequired(page);
 
     // Create a test notebook for each test
     testNotebook = await createTestNotebook(page);
 
-    // Navigate to the test notebook
-    await page.goto(`/#/notebook/${testNotebook.noteId}`);
-    await page.waitForLoadState('networkidle');
+    // Navigate to the test notebook and wait for the notebook component to 
bind
+    // to backend data. Hash-route navigation can leave the home shell visible
+    // for a short time in auth mode, which makes the title locator race.
+    await navigateToNotebookWithFallback(page, testNotebook.noteId);
   });
 
   test('Given notebook page is loaded, When checking note title, Then title 
should be displayed', async () => {
diff --git a/zeppelin-web-angular/e2e/tests/share/note-toc/note-toc.spec.ts 
b/zeppelin-web-angular/e2e/tests/share/note-toc/note-toc.spec.ts
index 6b6527842e..355bb28770 100644
--- a/zeppelin-web-angular/e2e/tests/share/note-toc/note-toc.spec.ts
+++ b/zeppelin-web-angular/e2e/tests/share/note-toc/note-toc.spec.ts
@@ -13,15 +13,12 @@
 import { test, expect } from '@playwright/test';
 import { NoteTocPage } from '../../../models/note-toc-page';
 import { NoteTocPageUtil } from '../../../models/note-toc-page.util';
-import {
-  addPageAnnotationBeforeEach,
-  PAGES,
-  performLoginIfRequired,
-  waitForZeppelinReady,
-  createTestNotebook
-} from '../../../utils';
+import { addPageAnnotationBeforeEach, PAGES, waitForZeppelinReady, 
createTestNotebook } from '../../../utils';
 
 test.describe('Note Table of Contents', () => {
+  // JUSTIFIED: page objects and notebook ids are stored in describe scope; 
fullyParallel can overwrite them.
+  test.describe.configure({ mode: 'default' });
+
   let noteTocPage: NoteTocPage;
   let noteTocUtil: NoteTocPageUtil;
   let testNotebook: { noteId: string; paragraphId: string };
@@ -34,7 +31,6 @@ test.describe('Note Table of Contents', () => {
 
     await page.goto('/#/');
     await waitForZeppelinReady(page);
-    await performLoginIfRequired(page);
 
     testNotebook = await createTestNotebook(page);
 
@@ -56,7 +52,7 @@ test.describe('Note Table of Contents', () => {
   test('Given TOC panel is open, When checking panel title, Then title should 
display "Table of Contents"', async () => {
     await noteTocUtil.verifyTocPanelOpens();
     await expect(noteTocPage.tocTitle).toBeVisible();
-    expect(await noteTocPage.tocTitle.textContent()).toBe('Table of Contents');
+    await expect(noteTocPage.tocTitle).toHaveText('Table of Contents');
   });
 
   test('Given TOC panel is open with no headings, When checking content, Then 
empty message should be displayed', async () => {
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 13b03fbdd3..49bbdf92cb 100644
--- a/zeppelin-web-angular/e2e/tests/theme/dark-mode.spec.ts
+++ b/zeppelin-web-angular/e2e/tests/theme/dark-mode.spec.ts
@@ -12,7 +12,7 @@
 
 import { expect, test } from '@playwright/test';
 import { DarkModePage } from '../../models/dark-mode-page';
-import { addPageAnnotationBeforeEach, performLoginIfRequired, 
waitForZeppelinReady, PAGES } from '../../utils';
+import { addPageAnnotationBeforeEach, waitForZeppelinReady, PAGES } from 
'../../utils';
 
 test.describe('Dark Mode Theme Switching', () => {
   addPageAnnotationBeforeEach(PAGES.SHARE.THEME_TOGGLE);
@@ -28,7 +28,6 @@ test.describe('Dark Mode Theme Switching', () => {
     await waitForZeppelinReady(page);
 
     // Handle authentication if shiro.ini exists
-    await performLoginIfRequired(page);
 
     // Ensure a clean localStorage for each test
     await darkModePage.clearLocalStorage();
@@ -100,8 +99,8 @@ test.describe('Dark Mode Theme Switching', () => {
       await waitForZeppelinReady(page);
       // When no explicit theme is set, it defaults to 'system' mode
       // Even in system mode with light preference, the icon should be robot
-      await expect(darkModePage.rootElement).toHaveClass(/light/);
-      await expect(darkModePage.rootElement).toHaveAttribute('data-theme', 
'light');
+      await expect(darkModePage.rootElement).toHaveClass(/light/, { timeout: 
15000 });
+      await expect(darkModePage.rootElement).toHaveAttribute('data-theme', 
'light', { timeout: 15000 });
       await darkModePage.assertSystemTheme(); // Should show robot icon
     });
 
@@ -125,8 +124,8 @@ test.describe('Dark Mode Theme Switching', () => {
       await page.emulateMedia({ colorScheme: 'light' });
       await page.goto('/');
       await waitForZeppelinReady(page);
-      await expect(darkModePage.rootElement).toHaveClass(/light/);
-      await expect(darkModePage.rootElement).toHaveAttribute('data-theme', 
'light');
+      await expect(darkModePage.rootElement).toHaveClass(/light/, { timeout: 
15000 });
+      await expect(darkModePage.rootElement).toHaveAttribute('data-theme', 
'light', { timeout: 15000 });
       await darkModePage.assertSystemTheme(); // Robot icon for system theme
     });
 
@@ -135,8 +134,8 @@ test.describe('Dark Mode Theme Switching', () => {
       await page.emulateMedia({ colorScheme: 'dark' });
       await page.goto('/');
       await waitForZeppelinReady(page);
-      await expect(darkModePage.rootElement).toHaveClass(/dark/);
-      await expect(darkModePage.rootElement).toHaveAttribute('data-theme', 
'dark');
+      await expect(darkModePage.rootElement).toHaveClass(/dark/, { timeout: 
15000 });
+      await expect(darkModePage.rootElement).toHaveAttribute('data-theme', 
'dark', { timeout: 15000 });
       await darkModePage.assertSystemTheme(); // Robot icon for system theme
     });
   });
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 1796e1578a..3cdc499f68 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
@@ -12,7 +12,7 @@
 
 import { expect, test } from '@playwright/test';
 import { NotebookReposPage, NotebookRepoItemPage } from 
'../../../models/notebook-repos-page';
-import { addPageAnnotationBeforeEach, performLoginIfRequired, 
waitForZeppelinReady, PAGES } from '../../../utils';
+import { addPageAnnotationBeforeEach, waitForZeppelinReady, PAGES } from 
'../../../utils';
 
 test.describe('Notebook Repository Item - Display Mode', () => {
   addPageAnnotationBeforeEach(PAGES.WORKSPACE.NOTEBOOK_REPOS_ITEM);
@@ -24,7 +24,6 @@ test.describe('Notebook Repository Item - Display Mode', () 
=> {
   test.beforeEach(async ({ page }) => {
     await page.goto('/#/');
     await waitForZeppelinReady(page);
-    await performLoginIfRequired(page);
     notebookReposPage = new NotebookReposPage(page);
     await notebookReposPage.navigate();
 
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 5fd6af53b9..d941829bc0 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
@@ -13,7 +13,7 @@
 import { expect, test } from '@playwright/test';
 import { NotebookReposPage, NotebookRepoItemPage } from 
'../../../models/notebook-repos-page';
 import { NotebookRepoItemUtil } from '../../../models/notebook-repo-item.util';
-import { addPageAnnotationBeforeEach, performLoginIfRequired, 
waitForZeppelinReady, PAGES } from '../../../utils';
+import { addPageAnnotationBeforeEach, waitForZeppelinReady, PAGES } from 
'../../../utils';
 
 test.describe('Notebook Repository Item - Edit Mode', () => {
   addPageAnnotationBeforeEach(PAGES.WORKSPACE.NOTEBOOK_REPOS_ITEM);
@@ -26,7 +26,6 @@ test.describe('Notebook Repository Item - Edit Mode', () => {
   test.beforeEach(async ({ page }) => {
     await page.goto('/#/');
     await waitForZeppelinReady(page);
-    await performLoginIfRequired(page);
     notebookReposPage = new NotebookReposPage(page);
     await notebookReposPage.navigate();
 
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 e4fc940322..29cbd419cb 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
@@ -12,7 +12,7 @@
 
 import { expect, test } from '@playwright/test';
 import { NotebookReposPage, NotebookRepoItemPage } from 
'../../../models/notebook-repos-page';
-import { addPageAnnotationBeforeEach, performLoginIfRequired, 
waitForZeppelinReady, PAGES } from '../../../utils';
+import { addPageAnnotationBeforeEach, waitForZeppelinReady, PAGES } from 
'../../../utils';
 
 test.describe('Notebook Repository Item - Form Validation', () => {
   addPageAnnotationBeforeEach(PAGES.WORKSPACE.NOTEBOOK_REPOS_ITEM);
@@ -24,7 +24,6 @@ test.describe('Notebook Repository Item - Form Validation', 
() => {
   test.beforeEach(async ({ page }) => {
     await page.goto('/#/');
     await waitForZeppelinReady(page);
-    await performLoginIfRequired(page);
     notebookReposPage = new NotebookReposPage(page);
     await notebookReposPage.navigate();
 
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 db65b97063..e8f6f30695 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
@@ -12,7 +12,7 @@
 
 import { expect, test } from '@playwright/test';
 import { NotebookReposPage, NotebookRepoItemPage } from 
'../../../models/notebook-repos-page';
-import { addPageAnnotationBeforeEach, performLoginIfRequired, 
waitForZeppelinReady, PAGES } from '../../../utils';
+import { addPageAnnotationBeforeEach, waitForZeppelinReady, PAGES } from 
'../../../utils';
 
 test.describe('Notebook Repository Item - Settings', () => {
   addPageAnnotationBeforeEach(PAGES.WORKSPACE.NOTEBOOK_REPOS_ITEM);
@@ -24,7 +24,6 @@ test.describe('Notebook Repository Item - Settings', () => {
   test.beforeEach(async ({ page }) => {
     await page.goto('/#/');
     await waitForZeppelinReady(page);
-    await performLoginIfRequired(page);
     notebookReposPage = new NotebookReposPage(page);
     await notebookReposPage.navigate();
 
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 0fd368e493..f39f277303 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
@@ -13,7 +13,7 @@
 import { expect, test } from '@playwright/test';
 import { NotebookReposPage, NotebookRepoItemPage } from 
'../../../models/notebook-repos-page';
 import { NotebookRepoItemUtil } from '../../../models/notebook-repo-item.util';
-import { addPageAnnotationBeforeEach, performLoginIfRequired, 
waitForZeppelinReady, PAGES } from '../../../utils';
+import { addPageAnnotationBeforeEach, waitForZeppelinReady, PAGES } from 
'../../../utils';
 
 test.describe('Notebook Repository Item - Edit Workflow', () => {
   addPageAnnotationBeforeEach(PAGES.WORKSPACE.NOTEBOOK_REPOS_ITEM);
@@ -26,7 +26,6 @@ test.describe('Notebook Repository Item - Edit Workflow', () 
=> {
   test.beforeEach(async ({ page }) => {
     await page.goto('/#/');
     await waitForZeppelinReady(page);
-    await performLoginIfRequired(page);
     notebookReposPage = new NotebookReposPage(page);
     await notebookReposPage.navigate();
 
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 39cccf2c58..40c3fa7ae3 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
@@ -12,7 +12,7 @@
 
 import { expect, test } from '@playwright/test';
 import { NotebookReposPage } from '../../../models/notebook-repos-page';
-import { addPageAnnotationBeforeEach, performLoginIfRequired, 
waitForZeppelinReady, PAGES } from '../../../utils';
+import { addPageAnnotationBeforeEach, waitForZeppelinReady, PAGES } from 
'../../../utils';
 
 test.describe('Notebook Repository Page - Structure', () => {
   addPageAnnotationBeforeEach(PAGES.WORKSPACE.NOTEBOOK_REPOS);
@@ -22,7 +22,6 @@ test.describe('Notebook Repository Page - Structure', () => {
   test.beforeEach(async ({ page }) => {
     await page.goto('/#/');
     await waitForZeppelinReady(page);
-    await performLoginIfRequired(page);
     notebookReposPage = new NotebookReposPage(page);
     await notebookReposPage.navigate();
   });
diff --git 
a/zeppelin-web-angular/e2e/tests/workspace/user-menu-navigation.spec.ts 
b/zeppelin-web-angular/e2e/tests/workspace/user-menu-navigation.spec.ts
index 55adf20cf3..c4ee882cf1 100644
--- a/zeppelin-web-angular/e2e/tests/workspace/user-menu-navigation.spec.ts
+++ b/zeppelin-web-angular/e2e/tests/workspace/user-menu-navigation.spec.ts
@@ -12,7 +12,7 @@
 
 import { expect, test } from '@playwright/test';
 import { HeaderPage } from '../../models/header-page';
-import { performLoginIfRequired, waitForZeppelinReady } from '../../utils';
+import { waitForZeppelinReady } from '../../utils';
 
 /**
  * Regression guard for the header user-menu navigation.
@@ -41,7 +41,6 @@ test.describe('Header user menu - full-row navigation', () => 
{
     header = new HeaderPage(page);
     await page.goto('/#/');
     await waitForZeppelinReady(page);
-    await performLoginIfRequired(page);
   });
 
   for (const item of MENU_ITEMS) {
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 106345fd2e..86095206d7 100644
--- a/zeppelin-web-angular/e2e/tests/workspace/workspace-main.spec.ts
+++ b/zeppelin-web-angular/e2e/tests/workspace/workspace-main.spec.ts
@@ -12,7 +12,7 @@
 
 import { expect, test } from '@playwright/test';
 import { BasePage } from 'e2e/models/base-page';
-import { addPageAnnotationBeforeEach, PAGES, performLoginIfRequired, 
waitForZeppelinReady } from '../../utils';
+import { addPageAnnotationBeforeEach, PAGES, waitForZeppelinReady } from 
'../../utils';
 
 addPageAnnotationBeforeEach(PAGES.WORKSPACE.MAIN);
 
@@ -22,7 +22,6 @@ test.describe('Workspace Main Component', () => {
   test.beforeEach(async ({ page }) => {
     await page.goto('/#/');
     await waitForZeppelinReady(page);
-    await performLoginIfRequired(page);
 
     basePage = new BasePage(page);
   });
diff --git a/zeppelin-web-angular/e2e/utils.ts 
b/zeppelin-web-angular/e2e/utils.ts
index 18a66a0ee6..b8be01c99a 100644
--- a/zeppelin-web-angular/e2e/utils.ts
+++ b/zeppelin-web-angular/e2e/utils.ts
@@ -10,14 +10,13 @@
  * limitations under the License.
  */
 
-import { test, Page, TestInfo } from '@playwright/test';
+import { test, expect, Page, TestInfo } from '@playwright/test';
 import { LoginTestUtil } from './models/login-page.util';
 import { E2E_TEST_FOLDER } from './models/base-page';
-import { NotebookUtil } from './models/notebook.util';
+import { LoginPage } from './models/login-page';
 
 export const NOTEBOOK_PATTERNS = {
   URL_REGEX: /\/notebook\/[^\/\?]+/,
-  URL_EXTRACT_NOTEBOOK_ID_REGEX: /\/notebook\/([^\/\?]+)/,
   LINK_SELECTOR: 'a[href*="/notebook/"]'
 } as const;
 
@@ -161,7 +160,91 @@ export const getBasicPageMetadata = async (
   path: getCurrentPath(page)
 });
 
-import { LoginPage } from './models/login-page';
+interface WaitForZeppelinReadyOptions {
+  allowLoginPage?: boolean;
+}
+
+const isLoginPageVisible = async (page: Page): Promise<boolean> =>
+  page
+    .locator('zeppelin-login')
+    .isVisible()
+    .catch(() => false);
+
+const waitForLoginPageReady = async (page: Page): Promise<void> => {
+  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 =
+        document.querySelector('zeppelin-login') !== null ||
+        document.querySelector('input[placeholder*="User"], 
input[placeholder*="user"], input[type="text"]') !== null;
+      return hasAngular && hasLoginElements;
+    },
+    { timeout: 30000 }
+  );
+};
+
+const waitForWorkspaceOrLogin = async (page: Page): Promise<'workspace' | 
'login' | undefined> =>
+  new Promise(resolve => {
+    let pending = 3;
+    let resolved = false;
+
+    const finish = (state?: 'workspace' | 'login') => {
+      if (resolved) {
+        return;
+      }
+      if (state) {
+        resolved = true;
+        resolve(state);
+        return;
+      }
+      pending -= 1;
+      if (pending === 0) {
+        resolved = true;
+        resolve(undefined);
+      }
+    };
+
+    page
+      .locator('zeppelin-workspace')
+      .waitFor({ state: 'attached', timeout: 45000 })
+      .then(() => finish('workspace'))
+      .catch(() => finish());
+    page
+      .locator('zeppelin-login')
+      .waitFor({ state: 'visible', timeout: 45000 })
+      .then(() => finish('login'))
+      .catch(() => finish());
+    page
+      .waitForURL(url => url.toString().includes('#/login'), { timeout: 45000 
})
+      .then(() => finish('login'))
+      .catch(() => finish());
+  });
+
+const handleLoginPageIfNeeded = async (page: Page, options: 
WaitForZeppelinReadyOptions): Promise<boolean> => {
+  const isOnLoginPage = page.url().includes('#/login') || (await 
isLoginPageVisible(page));
+  if (!isOnLoginPage) {
+    return false;
+  }
+
+  await waitForLoginPageReady(page);
+
+  if (options.allowLoginPage) {
+    return true;
+  }
+
+  if (await LoginTestUtil.isShiroEnabled()) {
+    const loggedIn = await performLoginIfRequired(page);
+    if (loggedIn) {
+      return true;
+    }
+
+    throw new Error('Authentication is required, but the test page remained on 
the login screen');
+  }
+
+  return true;
+};
+
 export const performLoginIfRequired = async (page: Page): Promise<boolean> => {
   const isShiroEnabled = await LoginTestUtil.isShiroEnabled();
   if (!isShiroEnabled) {
@@ -169,9 +252,7 @@ export const performLoginIfRequired = async (page: Page): 
Promise<boolean> => {
   }
 
   const credentials = await LoginTestUtil.getTestCredentials();
-  const validUsers = Object.values(credentials).filter(
-    cred => cred.username && cred.password && cred.username !== 'INVALID_USER' 
&& cred.username !== 'EMPTY_CREDENTIALS'
-  );
+  const validUsers = Object.values(credentials).filter(cred => cred.username 
&& cred.password);
 
   if (validUsers.length === 0) {
     return false;
@@ -205,31 +286,12 @@ export const performLoginIfRequired = async (page: Page): 
Promise<boolean> => {
   return false;
 };
 
-export const waitForZeppelinReady = async (page: Page): Promise<void> => {
+export const waitForZeppelinReady = async (page: Page, options: 
WaitForZeppelinReadyOptions = {}): Promise<void> => {
   try {
     // Enhanced wait for network idle with longer timeout for CI environments
     await page.waitForLoadState('domcontentloaded', { timeout: 45000 });
 
-    // Check if we're on login page and authentication is required
-    const isOnLoginPage = page.url().includes('#/login');
-    if (isOnLoginPage) {
-      console.log('On login page - checking if authentication is enabled');
-
-      // 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 =
-            document.querySelector('zeppelin-login') !== null ||
-            document.querySelector('input[placeholder*="User"], 
input[placeholder*="user"], input[type="text"]') !==
-              null;
-          return hasAngular && hasLoginElements;
-        },
-        { timeout: 30000 }
-      );
-      console.log('Login page is ready');
+    if (await handleLoginPageIfNeeded(page, options)) {
       return;
     }
 
@@ -259,8 +321,10 @@ export const waitForZeppelinReady = async (page: Page): 
Promise<void> => {
       { timeout: 90000 }
     );
 
-    // Additional stability check - wait for DOM to be stable
-    await page.waitForLoadState('domcontentloaded');
+    const settledState = await waitForWorkspaceOrLogin(page);
+    if (settledState === 'login' || (await handleLoginPageIfNeeded(page, 
options))) {
+      return;
+    }
   } catch (error) {
     throw new Error(`Zeppelin loading failed: ${String(error)}`);
   }
@@ -278,6 +342,21 @@ export const waitForNotebookLinks = async (page: Page, 
timeout: number = 30000)
   await locator.first().waitFor({ state: 'visible', timeout });
 };
 
+const waitForNotebookParagraphVisible = async (page: Page, noteId: string): 
Promise<void> => {
+  const waitOnce = async () => {
+    await page.waitForURL(new RegExp(`/notebook/${noteId}`), { timeout: 15000 
});
+    await page.locator('zeppelin-notebook-paragraph').first().waitFor({ state: 
'visible', timeout: 30000 });
+  };
+
+  try {
+    await waitOnce();
+  } catch {
+    await page.reload({ waitUntil: 'domcontentloaded', timeout: 30000 });
+    await waitForZeppelinReady(page);
+    await waitOnce();
+  }
+};
+
 export const navigateToNotebookWithFallback = async (
   page: Page,
   noteId: string,
@@ -290,8 +369,6 @@ export const navigateToNotebookWithFallback = async (
     await page.goto(`/#/notebook/${noteId}`, { waitUntil: 'networkidle', 
timeout: 30000 });
     navigationSuccessful = true;
   } catch (error) {
-    console.log('Direct navigation failed, trying fallback strategies...');
-
     // Strategy 2: Wait for loading completion and check URL
     await page.waitForFunction(
       () => {
@@ -327,121 +404,125 @@ export const navigateToNotebookWithFallback = async (
     throw new Error(`Failed to navigate to notebook ${noteId}`);
   }
 
-  // Wait for notebook to be ready
+  // Wait for notebook to be ready. Hash navigation can occasionally reach the
+  // target URL before the notebook component has subscribed to the backend 
data;
+  // a single reload keeps the same route while forcing Angular to fetch the 
note.
   await waitForZeppelinReady(page);
+  await waitForNotebookParagraphVisible(page, noteId);
 };
 
-const extractNoteIdFromUrl = async (page: Page): Promise<string | null> => {
-  const url = page.url();
-  const match = url.match(NOTEBOOK_PATTERNS.URL_EXTRACT_NOTEBOOK_ID_REGEX);
-  return match ? match[1] : null;
-};
+interface ZeppelinJsonResponse<T> {
+  status: string;
+  message?: string;
+  body: T;
+}
 
-const waitForNotebookNavigation = async (page: Page): Promise<string | null> 
=> {
-  await page.waitForURL(NOTEBOOK_PATTERNS.URL_REGEX, { timeout: 30000 });
-  return await extractNoteIdFromUrl(page);
-};
+interface InterpreterSettingSummary {
+  name?: string;
+}
 
-const navigateViaHomePageFallback = async (page: Page, baseNotebookName: 
string): Promise<string> => {
-  await page.goto('/#/');
-  await page.waitForLoadState('networkidle', { timeout: 15000 });
-  await page.waitForSelector('zeppelin-node-list', { timeout: 15000 });
+interface NoteSummary {
+  paragraphs?: Array<{ id?: string }>;
+}
 
-  await page.locator(NOTEBOOK_PATTERNS.LINK_SELECTOR).first().waitFor({ state: 
'attached', timeout: 15000 });
-  await page.waitForLoadState('domcontentloaded', { timeout: 15000 });
+const getDefaultInterpreterGroup = async (page: Page): Promise<string | 
undefined> => {
+  const response = await page.request.get('/api/interpreter/setting', { 
failOnStatusCode: false });
+  if (!response.ok()) {
+    return undefined;
+  }
 
-  const notebookLink = page.locator(NOTEBOOK_PATTERNS.LINK_SELECTOR).filter({ 
hasText: baseNotebookName });
+  const json = (await response.json()) as 
ZeppelinJsonResponse<InterpreterSettingSummary[]>;
+  return json.body?.find(setting => !!setting.name)?.name;
+};
 
-  const browserName = page.context().browser()?.browserType().name();
-  if (browserName === 'firefox') {
-    await 
page.waitForSelector(`${NOTEBOOK_PATTERNS.LINK_SELECTOR}:has-text("${baseNotebookName}")`,
 {
-      state: 'visible',
-      timeout: 90000
-    });
-  } else {
-    await notebookLink.waitFor({ state: 'visible', timeout: 60000 });
+const createNotebookViaRest = async (
+  page: Page,
+  notebookName: string
+): Promise<{ noteId: string; paragraphId: string }> => {
+  const defaultInterpreterGroup = await getDefaultInterpreterGroup(page);
+  const payload: Record<string, unknown> = {
+    notePath: notebookName,
+    addingEmptyParagraph: true
+  };
+
+  if (defaultInterpreterGroup) {
+    payload.defaultInterpreterGroup = defaultInterpreterGroup;
   }
 
-  await notebookLink.click({ timeout: 15000 });
-  await page.waitForURL(NOTEBOOK_PATTERNS.URL_REGEX, { timeout: 20000 });
+  const createResponse = await page.request.post('/api/notebook', {
+    data: payload,
+    failOnStatusCode: false
+  });
+  if (!createResponse.ok()) {
+    throw new Error(`Create notebook REST request failed: 
${createResponse.status()} ${await createResponse.text()}`);
+  }
 
-  const noteId = await extractNoteIdFromUrl(page);
+  const createJson = (await createResponse.json()) as 
ZeppelinJsonResponse<string>;
+  const noteId = createJson.body;
   if (!noteId) {
-    throw new Error('Failed to extract notebook ID after home page 
navigation');
+    throw new Error(`Create notebook REST response did not include note id: 
${JSON.stringify(createJson)}`);
   }
 
-  return noteId;
-};
-
-const extractFirstParagraphId = async (page: Page): Promise<string> => {
-  await page.locator('zeppelin-notebook-paragraph').first().waitFor({ state: 
'visible', timeout: 20000 });
+  let noteJson!: ZeppelinJsonResponse<NoteSummary>;
+  await expect(async () => {
+    const response = await page.request.get(`/api/notebook/${noteId}`, { 
failOnStatusCode: false });
+    if (!response.ok()) {
+      throw new Error(`Fetch notebook REST request failed: 
${response.status()} ${await response.text()}`);
+    }
+    noteJson = (await response.json()) as ZeppelinJsonResponse<NoteSummary>;
+  }).toPass({ timeout: 7500, intervals: [500, 1000, 1500, 2000, 2500] });
 
-  const paragraphContainer = 
page.locator('zeppelin-notebook-paragraph').first();
-  const dropdownTrigger = paragraphContainer.locator('a[nz-dropdown]');
-  await dropdownTrigger.click();
+  const paragraphId = noteJson.body?.paragraphs?.[0]?.id;
+  if (!paragraphId || !paragraphId.startsWith('paragraph_')) {
+    throw new Error(`Create notebook REST response did not include paragraph 
id: ${JSON.stringify(noteJson.body)}`);
+  }
 
-  const paragraphLink = page.locator('li.paragraph-id a').first();
-  await paragraphLink.waitFor({ state: 'attached', timeout: 15000 });
+  return { noteId, paragraphId };
+};
 
-  const paragraphId = await paragraphLink.textContent();
+interface CreateTestNotebookWithNameOptions {
+  folderPath?: string | null;
+  namePrefix?: string;
+}
 
-  // Close the dropdown before returning — leaving it open leaks state into 
subsequent tests
-  await page.keyboard.press('Escape');
+export const createTestNotebookWithName = async (
+  page: Page,
+  options: CreateTestNotebookWithNameOptions = {}
+): Promise<{ noteId: string; paragraphId: string; notebookName: string; 
notebookPath: string }> => {
+  const isRetryableError = (message: string): boolean =>
+    /REST request failed: (404|409|500)\b/.test(message) || 
message.includes('Fetch notebook REST request failed');
+
+  const tryCreate = async () => {
+    const prefix = options.namePrefix ?? 'TestNotebook';
+    const notebookName = 
`${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
+    const notebookPath =
+      options.folderPath === null ? notebookName : `${options.folderPath || 
E2E_TEST_FOLDER}/${notebookName}`;
+    const { noteId, paragraphId } = await createNotebookViaRest(page, 
notebookPath);
+    await page.goto('/#/');
+    await waitForZeppelinReady(page);
+    return { noteId, paragraphId, notebookName, notebookPath };
+  };
 
-  if (!paragraphId || !paragraphId.startsWith('paragraph_')) {
-    throw new Error(`Invalid paragraph ID found: ${paragraphId}`);
+  for (let attempt = 1; attempt <= 3; attempt++) {
+    try {
+      return await tryCreate();
+    } catch (error) {
+      const message = error instanceof Error ? error.message : String(error);
+      if (attempt === 3 || !isRetryableError(message)) {
+        throw new Error(`Failed to create test notebook: ${message}. Current 
URL: ${page.url()}`);
+      }
+      await page.waitForTimeout(1000 * attempt);
+    }
   }
 
-  return paragraphId;
+  // Unreachable: loop returns on success or throws on final attempt.
+  throw new Error('createTestNotebookWithName: exhausted retries without 
resolution');
 };
 
 export const createTestNotebook = async (
   page: Page,
   folderPath?: string
 ): Promise<{ noteId: string; paragraphId: string }> => {
-  const notebookUtil = new NotebookUtil(page);
-  const baseNotebookName = `TestNotebook_${Date.now()}`;
-  const notebookName = folderPath ? `${folderPath}/${baseNotebookName}` : 
`${E2E_TEST_FOLDER}/${baseNotebookName}`;
-
-  try {
-    // Create notebook
-    await notebookUtil.createNotebook(notebookName);
-
-    let noteId: string | null = null;
-
-    // Try direct navigation first
-    noteId = await waitForNotebookNavigation(page);
-
-    if (!noteId) {
-      console.log('Direct navigation failed, trying fallback strategies...');
-
-      // Check if we're already on a notebook page
-      noteId = await extractNoteIdFromUrl(page);
-
-      if (noteId) {
-        // Use existing fallback navigation
-        await navigateToNotebookWithFallback(page, noteId, notebookName);
-      } else {
-        // Navigate via home page as last resort
-        noteId = await navigateViaHomePageFallback(page, baseNotebookName);
-      }
-    }
-
-    if (!noteId) {
-      throw new Error(`Failed to extract notebook ID from URL: ${page.url()}`);
-    }
-
-    // Extract paragraph ID
-    const paragraphId = await extractFirstParagraphId(page);
-
-    // Navigate back to home
-    await page.goto('/#/');
-    await waitForZeppelinReady(page);
-
-    return { noteId, paragraphId };
-  } catch (error) {
-    const errorMessage = error instanceof Error ? error.message : 
String(error);
-    const currentUrl = page.url();
-    throw new Error(`Failed to create test notebook: ${errorMessage}. Current 
URL: ${currentUrl}`);
-  }
+  const { noteId, paragraphId } = await createTestNotebookWithName(page, { 
folderPath });
+  return { noteId, paragraphId };
 };
diff --git a/zeppelin-web-angular/playwright.config.js 
b/zeppelin-web-angular/playwright.config.js
index 06e9270385..bc1bd46ebd 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 : 5,
+  workers: 5,
   timeout: 300000,
   expect: {
     timeout: 60000
@@ -43,17 +43,39 @@ module.exports = defineConfig({
     navigationTimeout: 180000
   },
   projects: [
+    // Auth setup runs once and writes playwright/.auth/user.json, which the 
browser
+    // projects consume via storageState — replaces the per-test login that 
raced
+    // under parallel workers.
+    {
+      name: 'setup',
+      testMatch: /global\.setup\.ts/
+    },
     {
       name: 'chromium',
-      use: { ...devices['Desktop Chrome'], permissions: ['clipboard-read', 
'clipboard-write'] }
+      use: {
+        ...devices['Desktop Chrome'],
+        permissions: ['clipboard-read', 'clipboard-write'],
+        storageState: 'playwright/.auth/user.json'
+      },
+      dependencies: ['setup']
     },
     {
       name: 'Google Chrome',
-      use: { ...devices['Desktop Chrome'], channel: 'chrome', permissions: 
['clipboard-read', 'clipboard-write'] }
+      use: {
+        ...devices['Desktop Chrome'],
+        channel: 'chrome',
+        permissions: ['clipboard-read', 'clipboard-write'],
+        storageState: 'playwright/.auth/user.json'
+      },
+      dependencies: ['setup']
     },
     {
       name: 'firefox',
-      use: { ...devices['Desktop Firefox'] }
+      use: {
+        ...devices['Desktop Firefox'],
+        storageState: 'playwright/.auth/user.json'
+      },
+      dependencies: ['setup']
     },
     {
       name: 'webkit',
@@ -61,12 +83,20 @@ module.exports = defineConfig({
         ...devices['Desktop Safari'],
         launchOptions: {
           slowMo: 200
-        }
-      }
+        },
+        storageState: 'playwright/.auth/user.json'
+      },
+      dependencies: ['setup']
     },
     {
       name: 'Microsoft Edge',
-      use: { ...devices['Desktop Edge'], channel: 'msedge', permissions: 
['clipboard-read', 'clipboard-write'] }
+      use: {
+        ...devices['Desktop Edge'],
+        channel: 'msedge',
+        permissions: ['clipboard-read', 'clipboard-write'],
+        storageState: 'playwright/.auth/user.json'
+      },
+      dependencies: ['setup']
     }
   ],
   webServer: process.env.CI
diff --git a/zeppelin-web-angular/src/app/share/header/header.component.html 
b/zeppelin-web-angular/src/app/share/header/header.component.html
index a2b378870d..bb0c706bc4 100644
--- a/zeppelin-web-angular/src/app/share/header/header.component.html
+++ b/zeppelin-web-angular/src/app/share/header/header.component.html
@@ -25,6 +25,7 @@
           class="node-list-trigger"
           [nzDropdownMenu]="list"
           [nzTrigger]="'click'"
+          nzOverlayClassName="zeppelin-notebook-dropdown"
           [(nzVisible)]="noteListVisible"
         >
           Notebook
diff --git a/zeppelin-web-angular/src/app/share/header/header.component.less 
b/zeppelin-web-angular/src/app/share/header/header.component.less
index 116d84034f..faec20b367 100644
--- a/zeppelin-web-angular/src/app/share/header/header.component.less
+++ b/zeppelin-web-angular/src/app/share/header/header.component.less
@@ -140,3 +140,14 @@
     }
   }
 }
+
+// Cap the dropdown so workspaces with many notes don't overflow the viewport.
+// ::ng-deep escapes view encapsulation since the overlay renders in a 
body-level CDK overlay.
+// Scoped via nzOverlayClassName so no other dropdown is affected.
+::ng-deep .zeppelin-notebook-dropdown {
+  zeppelin-node-list {
+    display: block;
+    max-height: calc(100vh - 100px);
+    overflow-y: auto;
+  }
+}

Reply via email to