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 cc5c38aafb [ZEPPELIN-6562] Fix specs that silently pass or flake 
because they target DOM the new UI no longer renders
cc5c38aafb is described below

commit cc5c38aafb2aabefc8182ef9139bb48c333329ed
Author: YONGJAE LEE (이용재) <[email protected]>
AuthorDate: Wed Jul 29 23:29:41 2026 +0900

    [ZEPPELIN-6562] Fix specs that silently pass or flake because they target 
DOM the new UI no longer renders
    
    ### What is this PR for?
    
    Several new UI specs target DOM that the previous UI rendered. Those 
selectors match no element, and the result splits two ways: some checks pass 
without proving anything, and some waits never resolve and time out. Both 
symptoms come from the same cause.
    
    The silent passes are the heavier half. `waitForParagraphExecution` in 
`notebook-keyboard-page.ts` waits on `.paragraph-control .fa-spin, 
.running-indicator, .paragraph-status-running`. None of those classes appear in 
any new UI template, and `.fa-spin` is only a class definition in the vendored 
FontAwesome stylesheet that is never applied. The wait resolves immediately, so 
callers move on while the paragraph still reads `READY`. The keyboard suite 
uses this helper throughout.
    
    The same selector is used as an assertion. 
`paragraph-functionality.spec.ts:184` is meant to confirm that a cancelled 
paragraph stopped running, but the locator matches nothing and 
`not.toBeVisible` passes whether or not execution stopped. This PR asserts that 
`.status` reaches `ABORT` instead. Asserting that the cancel control disappears 
is not enough, because that control only renders while the paragraph is 
`PENDING` or `RUNNING`, so it also disappears on natural completion. For the 
[...]
    
    The flaky half comes from three places. The trash folder's "Empty" anchor 
collapses when the row loses hover: its bounding box is `{width:0, height:0}` 
unhovered and `{width:12, height:17}` hovered, so splitting reveal and click 
clicks empty space. A click that lands while a dialog is still running its open 
animation is not registered at all. And paragraphs arrive over the WebSocket, 
so `waitForLoadState('networkidle')` can resolve before any paragraph has 
rendered.
    
    Strict-mode collisions sit on top of that. `getByRole('link', { name: 'Job' 
})` matches the accessible name as a substring, so it also selects note links 
whose title contains "Job". The header and user-menu locators now pass `exact: 
true`, checked against the names in the templates.
    
    Fallback selector chains move into the Page Object and narrow to the 
element that exists. The cancel button was `.cancel-para, 
[nz-tooltip*="Cancel"], [title*="Cancel"], button:has-text("Cancel"), 
i[nz-icon="pause-circle"], .anticon-pause-circle`; only `.cancel-para` is in 
the new UI. The export dropdown is declared without `nzTrigger`, so ng-zorro 
opens it on hover rather than click, which is now encapsulated as 
`openExportMenu()`. The clipboard spec produced a TEXT result because `% [...]
    
    ### What type of PR is it?
    Bug Fix
    
    ### Todos
    * [x] Confirm the removed selectors appear in no Angular template and in no 
React component
    * [x] Confirm each replacement exists and means what the test assumes
    * [x] Check that every changed assertion still fails when the feature is 
broken
    * [x] Run the cancel test, measure the status transitions, and set the 
timeout from the measurement
    * [x] Lint with the rules added in ZEPPELIN-6560 and confirm no new 
violations
    
    ### What is the Jira issue?
    ZEPPELIN-6562
    
    ### How should this be tested?
    
    ```bash
    cd zeppelin-web-angular
    npm run e2e:fast -- tests/notebook/ tests/home/ tests/share/ 
tests/workspace/
    ```
    
    The clipboard spec needs a shell interpreter and skips on CI. The cancel 
test runs on CI against the Python interpreter; measured locally, the status 
goes `PENDING` to `RUNNING` and reaches `ABORT` about ten seconds after the 
click, which is what the assertion timeout is set from.
    
    The dead selectors can be confirmed directly: `grep -rn 
"fa-spin\|running-indicator\|paragraph-status-running" 
zeppelin-web-angular/src` matches only the vendored FontAwesome stylesheet and 
no template.
    
    ### 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 #5350 from voidmatcha/fix/e2e-flaky-stabilization.
    
    Signed-off-by: ChanHo Lee <[email protected]>
---
 zeppelin-web-angular/e2e/models/header-page.ts     | 22 ++++---
 .../e2e/models/notebook-keyboard-page.ts           | 38 +++++------
 .../e2e/models/notebook-paragraph-page.ts          | 18 +++++-
 .../e2e/models/notebook-repos-page.ts              |  1 -
 .../tests/home/home-page-note-operations.spec.ts   | 16 +++--
 .../tests/home/home-page-notebook-actions.spec.ts  |  2 +-
 .../action-bar/action-bar-functionality.spec.ts    |  1 -
 .../keyboard/notebook-keyboard-shortcuts.spec.ts   | 53 +++++-----------
 .../tests/notebook/main/notebook-container.spec.ts |  2 +-
 .../notebook/paragraph/copy-to-clipboard.spec.ts   | 74 ++++++----------------
 .../paragraph/paragraph-functionality.spec.ts      | 20 +++---
 .../notebook/sidebar/sidebar-functionality.spec.ts |  1 -
 .../share/note-import/note-import-modal.spec.ts    |  4 +-
 .../e2e/tests/share/note-toc/note-toc.spec.ts      |  3 -
 .../tests/workspace/user-menu-navigation.spec.ts   |  3 +-
 zeppelin-web-angular/e2e/utils.ts                  |  5 +-
 16 files changed, 114 insertions(+), 149 deletions(-)

diff --git a/zeppelin-web-angular/e2e/models/header-page.ts 
b/zeppelin-web-angular/e2e/models/header-page.ts
index 0a17045eec..ae30cb3ed7 100644
--- a/zeppelin-web-angular/e2e/models/header-page.ts
+++ b/zeppelin-web-angular/e2e/models/header-page.ts
@@ -44,20 +44,26 @@ export class HeaderPage extends BasePage {
     this.notebookMenuItem = page.locator('[nz-menu-item]').filter({ hasText: 
'Notebook' });
     this.notebookDropdownTrigger = page.locator('.node-list-trigger');
     this.notebookDropdown = 
page.locator('zeppelin-node-list.ant-dropdown-menu');
-    this.jobMenuItem = page.getByRole('link', { name: 'Job' });
+    // A global getByRole('link', { name: 'Job' }) matches by substring.
+    // It also picks up note-list links (e.g. a note named "...Job..."), which 
is a strict-mode violation.
+    // Scoping to the menu item and matching exactly selects the single header 
entry.
+    this.jobMenuItem = page.locator('[nz-menu-item]').getByRole('link', { 
name: 'Job', exact: true });
     this.userDropdownTrigger = page.locator('.header .user .status');
     this.userBadge = page.locator('.header .user nz-badge');
     this.searchInput = page.locator('.header .search input[type="text"]');
     this.themeToggleButton = page.locator('zeppelin-theme-toggle button');
 
+    // Same substring and strict-mode risk as jobMenuItem above.
+    // Scope these links to the dropdown overlay and match exactly.
+    const userMenu = page.locator('.zeppelin-user-menu');
     this.userMenuItems = {
-      aboutZeppelin: page.getByText('About Zeppelin', { exact: true }),
-      interpreter: page.getByRole('link', { name: 'Interpreter' }),
-      notebookRepos: page.getByRole('link', { name: 'Notebook Repos' }),
-      credential: page.getByRole('link', { name: 'Credential' }),
-      configuration: page.getByRole('link', { name: 'Configuration' }),
-      logout: page.getByText('Logout', { exact: true }),
-      switchToClassicUI: page.getByRole('link', { name: 'Switch to Classic UI' 
})
+      aboutZeppelin: userMenu.getByText('About Zeppelin', { exact: true }),
+      interpreter: userMenu.getByRole('link', { name: 'Interpreter', exact: 
true }),
+      notebookRepos: userMenu.getByRole('link', { name: 'Notebook Repos', 
exact: true }),
+      credential: userMenu.getByRole('link', { name: 'Credential', exact: true 
}),
+      configuration: userMenu.getByRole('link', { name: 'Configuration', 
exact: true }),
+      logout: userMenu.getByText('Logout', { exact: true }),
+      switchToClassicUI: userMenu.getByRole('link', { name: 'Switch to Classic 
UI', exact: true })
     };
   }
 
diff --git a/zeppelin-web-angular/e2e/models/notebook-keyboard-page.ts 
b/zeppelin-web-angular/e2e/models/notebook-keyboard-page.ts
index 2ffedd0b64..1b402b0f03 100644
--- a/zeppelin-web-angular/e2e/models/notebook-keyboard-page.ts
+++ b/zeppelin-web-angular/e2e/models/notebook-keyboard-page.ts
@@ -330,6 +330,10 @@ export class NotebookKeyboardPage extends BasePage {
     return this.paragraphContainer.nth(index);
   }
 
+  getParagraphStatus(index: number): Locator {
+    return this.getParagraphByIndex(index).locator('.status');
+  }
+
   async isAutocompleteVisible(): Promise<boolean> {
     return await this.autocompletePopup.isVisible();
   }
@@ -482,16 +486,14 @@ export class NotebookKeyboardPage extends BasePage {
 
     const paragraph = this.getParagraphByIndex(paragraphIndex);
 
-    // Step 1: Wait for execution to start
     await this.waitForExecutionStart(paragraphIndex);
 
-    // Step 2: Wait for execution to complete
-    const runningIndicator = paragraph.locator(
-      '.paragraph-control .fa-spin, .running-indicator, 
.paragraph-status-running'
-    );
-    await this.waitForExecutionComplete(runningIndicator, paragraphIndex, 
timeout);
+    // The spinner classes this used to wait on are not rendered by the new 
UI, so the wait
+    // resolved immediately. The status text is the observable signal.
+    // On a fast re-run that text can still read the previous terminal state, 
so tight re-run
+    // loops get a stability gate here, not proof that this particular run 
finished.
+    await 
expect(paragraph.locator('.status')).toHaveText(/FINISHED|ERROR|ABORT/, { 
timeout });
 
-    // Step 3: Wait for result to be visible
     await this.waitForResultVisible(paragraphIndex, timeout);
   }
 
@@ -562,7 +564,12 @@ export class NotebookKeyboardPage extends BasePage {
     for (let i = 0; i < count; i++) {
       const button = this.okButtons.nth(i);
       await button.waitFor({ state: 'visible', timeout });
-      await button.click({ delay: 100 });
+      // A click landed during the dialog's open animation can fail to 
register at all,
+      // so retry until the button actually goes away.
+      await expect(async () => {
+        await button.click({ delay: 100 });
+        await expect(button).toBeHidden({ timeout: 2000 });
+      }).toPass({ timeout: 15000 });
       await this.modal.waitFor({ state: 'hidden', timeout: 2000 }).catch(() => 
{}); // JUSTIFIED: UI stabilization — next iteration or detach check handles 
remaining modals
     }
 
@@ -582,7 +589,8 @@ export class NotebookKeyboardPage extends BasePage {
             return false;
           }
 
-          const hasRunning = targetParagraph.querySelector('.fa-spin, 
.running-indicator, .paragraph-status-running');
+          const status = targetParagraph.querySelector('.status');
+          const hasRunning = !!status && 
/PENDING|RUNNING/.test(status.textContent || '');
           const hasResult = targetParagraph.querySelector(selector);
 
           return hasRunning || hasResult;
@@ -601,18 +609,6 @@ export class NotebookKeyboardPage extends BasePage {
     }
   }
 
-  private async waitForExecutionComplete(
-    runningIndicator: Locator,
-    paragraphIndex: number,
-    timeout: number
-  ): Promise<void> {
-    if (this.page.isClosed()) {
-      return;
-    }
-
-    await runningIndicator.waitFor({ state: 'detached', timeout: timeout / 2 
}).catch(() => {}); // JUSTIFIED: UI stabilization — paragraph may have 
completed before indicator appeared
-  }
-
   private async waitForResultVisible(paragraphIndex: number, timeout: number): 
Promise<void> {
     if (this.page.isClosed()) {
       return;
diff --git a/zeppelin-web-angular/e2e/models/notebook-paragraph-page.ts 
b/zeppelin-web-angular/e2e/models/notebook-paragraph-page.ts
index 674dcffd14..f50359a780 100644
--- a/zeppelin-web-angular/e2e/models/notebook-paragraph-page.ts
+++ b/zeppelin-web-angular/e2e/models/notebook-paragraph-page.ts
@@ -10,7 +10,7 @@
  * limitations under the License.
  */
 
-import { Locator, Page } from '@playwright/test';
+import { expect, Locator, Page } from '@playwright/test';
 import { BasePage } from './base-page';
 
 export class NotebookParagraphPage extends BasePage {
@@ -24,6 +24,10 @@ export class NotebookParagraphPage extends BasePage {
   readonly footerInfo: Locator;
   readonly runButton: Locator;
   readonly settingsDropdown: Locator;
+  readonly status: Locator;
+  readonly cancelButton: Locator;
+  readonly exportDropdownTrigger: Locator;
+  readonly exportMenu: Locator;
 
   constructor(page: Page) {
     super(page);
@@ -47,6 +51,18 @@ export class NotebookParagraphPage extends BasePage {
       .first()
       .locator('zeppelin-notebook-paragraph-control a[nz-dropdown]')
       .first();
+    this.status = this.controlPanel.locator('.status');
+    // The control renders the cancel icon only while the paragraph is PENDING 
or RUNNING.
+    this.cancelButton = this.controlPanel.locator('.cancel-para');
+    // The export controls render only for a TABLE result.
+    this.exportDropdownTrigger = 
this.resultDisplay.locator('.export-dropdown-icon-btn');
+    this.exportMenu = page.locator('.ant-dropdown-menu');
+  }
+
+  // The export dropdown is declared without nzTrigger, so ng-zorro opens it 
on hover, not click.
+  async openExportMenu(): Promise<void> {
+    await this.exportDropdownTrigger.hover();
+    await expect(this.exportMenu).toBeVisible();
   }
 
   async doubleClickToEdit(): 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 76cbf00f7b..f047b44b2e 100644
--- a/zeppelin-web-angular/e2e/models/notebook-repos-page.ts
+++ b/zeppelin-web-angular/e2e/models/notebook-repos-page.ts
@@ -28,7 +28,6 @@ export class NotebookReposPage extends BasePage {
     await this.navigateToRoute('/notebook-repos', { timeout: 60000 });
     await this.page.waitForURL('**/#/notebook-repos', { timeout: 60000 });
     await waitForZeppelinReady(this.page);
-    await this.page.waitForLoadState('networkidle', { timeout: 15000 });
     await Promise.race([
       this.zeppelinPageHeader.filter({ hasText: 'Notebook Repository' 
}).waitFor({ state: 'visible' }),
       this.page.waitForSelector('zeppelin-notebook-repo-item', { state: 
'visible' })
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 280ab64a34..7a1cf66ce0 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
@@ -85,10 +85,14 @@ test.describe('Home Page Note Operations', () => {
   test.describe('Given rename note functionality', () => {
     test('When rename button is clicked Then should open rename dialog', async 
({ page }) => {
       const testNote = page.locator('.node .file').filter({ hasText: 
testNoteName });
+      // WebSocket note updates re-render the list and can detach a hovered 
row mid-interaction.
+      // Re-confirm the row and re-hover right before revealing the action.
+      await expect(testNote).toBeVisible({ timeout: 15000 });
       await testNote.hover();
 
       const renameButton = testNote.locator('.operation 
a[nztooltiptitle="Rename note"]');
       await expect(renameButton).toBeVisible();
+      await testNote.hover();
       await renameButton.click();
 
       // JUSTIFIED: compound selector targets rename dialog; first() picks the 
visible modal instance
@@ -240,13 +244,13 @@ test.describe('Home Page Note Operations', () => {
 
     test('When empty trash is clicked Then should show permanent deletion 
warning', async ({ page }) => {
       const trashFolder = page.locator('.node .folder').filter({ hasText: 
'Trash' });
-      await trashFolder.hover();
-      await trashFolder.locator('.operation').waitFor({ state: 'visible' });
-
       const emptyButton = trashFolder.locator('.operation 
a[nztooltiptitle*="Empty all"]');
-      await expect(emptyButton).toBeVisible();
-      await emptyButton.hover();
-      await emptyButton.click();
+
+      // The anchor collapses to zero size when the row loses hover, so reveal 
and click must be one step.
+      await expect(async () => {
+        await trashFolder.hover();
+        await emptyButton.click({ timeout: 5000 });
+      }).toPass({ timeout: 30000 });
 
       await expect(page.locator('text=This cannot be undone. Are you 
sure?')).toBeVisible();
     });
diff --git 
a/zeppelin-web-angular/e2e/tests/home/home-page-notebook-actions.spec.ts 
b/zeppelin-web-angular/e2e/tests/home/home-page-notebook-actions.spec.ts
index a92326f32e..5911232c85 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
@@ -42,7 +42,7 @@ test.describe('Home Page Notebook Actions', () => {
     test('When filter is used Then should filter notebook list', async ({ page 
}) => {
       test.skip(true, 'ZEPPELIN-6386: Notebook search filter in the New UI is 
too slow — re-enable when fixed');
       await homePage.filterNotes('test');
-      await page.waitForLoadState('networkidle', { timeout: 15000 });
+      await expect(page.locator('nz-tree .node').filter({ hasText: 'test' 
})).not.toHaveCount(0, { timeout: 15000 });
       const filteredResults = await page.locator('nz-tree .node').count();
       expect(filteredResults).toBeGreaterThan(0);
     });
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 f8d158838b..5825e1d31b 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
@@ -117,7 +117,6 @@ test.describe('Notebook Action Bar Functionality', () => {
     }
 
     await expect(actionBarPage.clearOutputButton).toBeEnabled();
-    await page.waitForLoadState('networkidle');
 
     const paragraphResults = 
page.locator('zeppelin-notebook-paragraph-result');
     const resultCount = await paragraphResults.count();
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 f4e584d215..ee2fa5f481 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
@@ -78,12 +78,8 @@ test.describe.serial('Comprehensive Keyboard Shortcuts 
(ShortcutsMap)', () => {
       // When: User presses Shift+Enter
       await keyboardPage.pressRunParagraph();
 
-      // Then: Paragraph should execute (reach a terminal state; interpreter 
availability varies by env)
+      // waitForParagraphExecution gates on the status text, so it is the 
assertion and throws if the run never settles.
       await keyboardPage.waitForParagraphExecution(0);
-      // JUSTIFIED: single-paragraph test notebook; first() is deterministic
-      const statusEl = 
keyboardPage.paragraphContainer.first().locator('.status');
-      const statusText = (await statusEl.textContent({ timeout: 30000 
}))?.trim();
-      expect(statusText === 'FINISHED' || statusText === 'ERROR' || statusText 
=== 'ABORT').toBe(true);
     });
   });
 
@@ -104,10 +100,8 @@ test.describe.serial('Comprehensive Keyboard Shortcuts 
(ShortcutsMap)', () => {
       await keyboardPage.setCodeEditorContent('%md\n# Second Paragraph\nTest 
content for second paragraph', 1);
       await keyboardPage.tryFocusCodeEditor(1); // Ensure focus on the second 
paragraph
 
-      // Add an explicit wait for the page to be completely stable and the 
notebook UI to be interactive
-      await keyboardPage.page.waitForLoadState('networkidle', { timeout: 30000 
}); // Wait for network to be idle
       // JUSTIFIED: single-paragraph test notebook; first() is deterministic
-      await expect(keyboardPage.paragraphContainer.first()).toBeVisible({ 
timeout: 15000 }); // Ensure a paragraph is visible
+      await expect(keyboardPage.paragraphContainer.first()).toBeVisible({ 
timeout: 15000 });
 
       // When: User presses Control+Shift+ArrowUp from second paragraph
       await keyboardPage.pressRunAbove();
@@ -168,20 +162,14 @@ test.describe.serial('Comprehensive Keyboard Shortcuts 
(ShortcutsMap)', () => {
       // Start execution
       await keyboardPage.pressRunParagraph();
 
-      // Wait for execution to start by checking if paragraph is running
-      // JUSTIFIED: compound selector; first() picks any visible running 
indicator
-      const runningIndicator = keyboardPage.page
-        .locator('zeppelin-notebook-paragraph .fa-spin, .running-indicator')
-        .first();
-      await expect(runningIndicator).toBeVisible({ timeout: 30000 });
+      const paragraphStatus = keyboardPage.getParagraphStatus(0);
+      await expect(paragraphStatus).toHaveText(/PENDING|RUNNING/, { timeout: 
30000 });
 
       // When: User presses Control+Alt+C quickly
       await keyboardPage.pressCancel();
 
       // Then: The execution should be cancelled or completed
-      await expect(
-        keyboardPage.getParagraphByIndex(0).locator('.paragraph-control 
.fa-spin, .running-indicator')
-      ).not.toBeVisible();
+      await expect(paragraphStatus).toHaveText(/ABORT|FINISHED|ERROR/, { 
timeout: 30000 });
     });
   });
 
@@ -573,11 +561,6 @@ test.describe.serial('Comprehensive Keyboard Shortcuts 
(ShortcutsMap)', () => {
       await keyboardPage.pressRunParagraph();
       await keyboardPage.waitForParagraphExecution(0);
 
-      // Verify there is output to clear
-      // JUSTIFIED: single-paragraph test notebook; first() is deterministic
-      const statusElBefore = 
keyboardPage.paragraphContainer.first().locator('.status');
-      await 
expect(statusElBefore).toHaveText(/FINISHED|ERROR|PENDING|RUNNING/);
-
       // Gate: without visible output, isSettled starts true and the helper 
would skip the press entirely.
       const resultLocator = 
keyboardPage.getParagraphByIndex(0).locator('[data-testid="paragraph-result"]');
       await expect(resultLocator).toBeVisible();
@@ -614,7 +597,7 @@ test.describe.serial('Comprehensive Keyboard Shortcuts 
(ShortcutsMap)', () => {
 
       // Then: A new tab should be opened with paragraph link
       const newPage = await newPagePromise;
-      await newPage.waitForLoadState('networkidle');
+      await newPage.waitForURL(/\/paragraph\/paragraph_\d+_\d+/);
 
       // Verify the new tab URL contains the notebook ID and paragraph 
reference
       const newUrl = newPage.url();
@@ -800,7 +783,7 @@ test.describe.serial('Comprehensive Keyboard Shortcuts 
(ShortcutsMap)', () => {
 
       // When: User presses Control+Space to trigger autocomplete
       await keyboardPage.pressControlSpace();
-      await keyboardPage.autocompletePopup.waitFor({ state: 'visible', 
timeout: 3000 }).catch(() => {});
+      await expect(keyboardPage.autocompletePopup).toBeVisible({ timeout: 3000 
});
 
       // Then: Editor must remain functional after shortcut (baseline; always 
asserts)
       // JUSTIFIED: single-paragraph test notebook; first() is deterministic
@@ -827,7 +810,7 @@ test.describe.serial('Comprehensive Keyboard Shortcuts 
(ShortcutsMap)', () => {
 
       // When: User triggers autocomplete and selects an option
       await keyboardPage.pressControlSpace();
-      await keyboardPage.autocompletePopup.waitFor({ state: 'visible', 
timeout: 3000 }).catch(() => {});
+      await expect(keyboardPage.autocompletePopup).toBeVisible({ timeout: 3000 
});
 
       const isAutocompleteVisible = await keyboardPage.isAutocompleteVisible();
       if (isAutocompleteVisible) {
@@ -953,7 +936,8 @@ test.describe.serial('Comprehensive Keyboard Shortcuts 
(ShortcutsMap)', () => {
       await keyboardPage.tryFocusCodeEditor();
       await keyboardPage.setCodeEditorContent('invalid python syntax here');
       await keyboardPage.pressRunParagraph();
-      await keyboardPage.waitForParagraphExecution(0);
+      // Real interpreter run: a cold interpreter under CI load can stay 
RUNNING past the 30s default.
+      await keyboardPage.waitForParagraphExecution(0, 60000);
 
       // Verify error result exists (invalid syntax produces a final ERROR or 
FINISHED with error output)
       // JUSTIFIED: single-paragraph test notebook; first() is deterministic
@@ -971,12 +955,8 @@ test.describe.serial('Comprehensive Keyboard Shortcuts 
(ShortcutsMap)', () => {
       await keyboardPage.setCodeEditorContent('%md\n# Recovery Test\nShortcuts 
work after error', newParagraphIndex);
       await keyboardPage.pressRunParagraph();
 
-      // Then: Shortcut execution still reaches a terminal state (real 
interpreter run;
-      // allow extra time as a cold interpreter under CI load can stay RUNNING 
past 30s)
+      // Allow extra time; a cold interpreter under CI load can stay RUNNING 
past the 30s default.
       await keyboardPage.waitForParagraphExecution(newParagraphIndex, 60000);
-      // JUSTIFIED: newParagraphIndex is dynamically computed from 
getParagraphCount(); nth() is the only way to address this specific paragraph
-      const statusElNew = 
keyboardPage.paragraphContainer.nth(newParagraphIndex).locator('.status');
-      await expect(statusElNew).toHaveText(/FINISHED|ERROR/, { timeout: 60000 
});
     });
 
     test('should gracefully handle shortcuts when no paragraph is focused', 
async () => {
@@ -1008,17 +988,18 @@ test.describe.serial('Comprehensive Keyboard Shortcuts 
(ShortcutsMap)', () => {
       await keyboardPage.tryFocusCodeEditor();
       await keyboardPage.setCodeEditorContent('%md\nrapid keyboard test');
 
-      // Rapid Shift+Enter operations
+      // On a fast re-run the status can still read the previous FINISHED, so 
this checks stability, not each run.
+      // The result element re-renders and briefly detaches on every %md 
re-run, so do not assert on it.
       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: 60000 });
       }
 
-      // Then: System should remain stable
+      // Running a %md paragraph collapses it to rendered mode and detaches 
the Monaco editor,
+      // so assert on the rendered output instead. It survives the re-render 
and proves the runs
+      // produced a result, which the container being visible does not.
       // JUSTIFIED: single-paragraph test notebook; first() is deterministic
-      await expect(keyboardPage.codeEditor.first()).toBeVisible();
+      await expect(keyboardPage.paragraphResult.first()).toContainText('rapid 
keyboard test', { timeout: 30000 });
     });
   });
 });
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 03b956e77d..a8656cc8f3 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
@@ -60,7 +60,7 @@ test.describe('Notebook Container Component', () => {
 
   test('should display paragraph container with grid layout', async () => {
     await expect(notebookPage.paragraphInner).toBeVisible();
-    expect(await 
notebookPage.paragraphInner.getAttribute('class')).toContain('paragraph-inner');
+    await expect(notebookPage.paragraphInner).toHaveClass(/paragraph-inner/);
     await expect(notebookPage.paragraphInner).toHaveAttribute('nz-row');
   });
 
diff --git 
a/zeppelin-web-angular/e2e/tests/notebook/paragraph/copy-to-clipboard.spec.ts 
b/zeppelin-web-angular/e2e/tests/notebook/paragraph/copy-to-clipboard.spec.ts
index 93a9b7029e..3816c6a594 100644
--- 
a/zeppelin-web-angular/e2e/tests/notebook/paragraph/copy-to-clipboard.spec.ts
+++ 
b/zeppelin-web-angular/e2e/tests/notebook/paragraph/copy-to-clipboard.spec.ts
@@ -25,6 +25,7 @@ test.describe('Copy table result to clipboard', () => {
   addPageAnnotationBeforeEach(PAGES.WORKSPACE.SHARE_RESULT);
 
   let paragraphPage: NotebookParagraphPage;
+  let keyboard: NotebookKeyboardPage;
   let testNotebook: { noteId: string; paragraphId: string };
 
   test.beforeEach(async ({ page, context }, testInfo) => {
@@ -40,34 +41,19 @@ test.describe('Copy table result to clipboard', () => {
     paragraphPage = new NotebookParagraphPage(page);
 
     await page.goto(`/#/notebook/${testNotebook.noteId}`);
-    await page.waitForLoadState('networkidle');
+    await expect(page.locator('zeppelin-notebook-paragraph')).toHaveCount(1, { 
timeout: 15000 });
 
-    // Type a paragraph that outputs a TABLE result using the %sh interpreter
-    await paragraphPage.doubleClickToEdit();
-    await expect(paragraphPage.codeEditor).toBeVisible();
-
-    const codeEditor = paragraphPage.codeEditor.locator('textarea, 
.monaco-editor .input-area').first();
-    await expect(codeEditor).toBeAttached({ timeout: 10000 });
-    await codeEditor.focus();
-
-    const keyboard = new NotebookKeyboardPage(page);
-    await keyboard.pressSelectAll();
-    await page.keyboard.type('%sh\nprintf 
"name\\tcount\\na\\t12\\nb\\t24\\n"');
+    // Without the %table marker the output renders as TEXT and no export 
control exists.
+    keyboard = new NotebookKeyboardPage(page);
+    await keyboard.setCodeEditorContent('%sh\nprintf "%%table 
name\\tcount\\na\\t12\\nb\\t24\\n"');
 
     await paragraphPage.runParagraph();
     await expect(paragraphPage.resultDisplay).toBeVisible({ timeout: 30000 });
   });
 
-  test('export dropdown should contain Copy as TSV and Copy as CSV options', 
async ({ page }) => {
-    // Open the export dropdown (down-arrow button next to the download icon)
-    const exportDropdownTrigger = page
-      .locator('.export-dropdown .export-dropdown-icon-btn, .export-dropdown 
button:last-child')
-      .first();
-    await expect(exportDropdownTrigger).toBeVisible({ timeout: 10000 });
-    await exportDropdownTrigger.click();
-
-    const menu = page.locator('.ant-dropdown-menu');
-    await expect(menu).toBeVisible({ timeout: 5000 });
+  test('export dropdown should contain Copy as TSV and Copy as CSV options', 
async () => {
+    await paragraphPage.openExportMenu();
+    const menu = paragraphPage.exportMenu;
 
     await expect(menu.locator('li:has-text("Download as CSV")')).toBeVisible();
     await expect(menu.locator('li:has-text("Download as TSV")')).toBeVisible();
@@ -76,14 +62,8 @@ test.describe('Copy table result to clipboard', () => {
   });
 
   test('Copy as TSV should write tab-delimited data with headers to 
clipboard', async ({ page }) => {
-    const exportDropdownTrigger = page
-      .locator('.export-dropdown .export-dropdown-icon-btn, .export-dropdown 
button:last-child')
-      .first();
-    await expect(exportDropdownTrigger).toBeVisible({ timeout: 10000 });
-    await exportDropdownTrigger.click();
-
-    const menu = page.locator('.ant-dropdown-menu');
-    await expect(menu).toBeVisible({ timeout: 5000 });
+    await paragraphPage.openExportMenu();
+    const menu = paragraphPage.exportMenu;
     await menu.locator('li:has-text("Copy as TSV")').click();
 
     // Read back what was written to the clipboard
@@ -98,14 +78,8 @@ test.describe('Copy table result to clipboard', () => {
   });
 
   test('Copy as CSV should write comma-delimited data with headers to 
clipboard', async ({ page }) => {
-    const exportDropdownTrigger = page
-      .locator('.export-dropdown .export-dropdown-icon-btn, .export-dropdown 
button:last-child')
-      .first();
-    await expect(exportDropdownTrigger).toBeVisible({ timeout: 10000 });
-    await exportDropdownTrigger.click();
-
-    const menu = page.locator('.ant-dropdown-menu');
-    await expect(menu).toBeVisible({ timeout: 5000 });
+    await paragraphPage.openExportMenu();
+    const menu = paragraphPage.exportMenu;
     await menu.locator('li:has-text("Copy as CSV")').click();
 
     const clipboardText = await page.evaluate(() => 
navigator.clipboard.readText());
@@ -118,22 +92,14 @@ test.describe('Copy table result to clipboard', () => {
 
   test('Copy as CSV should quote cell values that contain double quotes', 
async ({ page }) => {
     // Re-run the paragraph with a value containing a double quote
-    const codeEditor = page.locator('.monaco-editor .input-area, 
textarea').first();
-    await codeEditor.focus();
-    const keyboard = new NotebookKeyboardPage(page);
-    await keyboard.pressSelectAll();
-    await page.keyboard.type('%sh\nprintf "col1\\tcol2\\nsay 
\\"hi\\"\\t1\\n"');
-    await new NotebookParagraphPage(page).runParagraph();
-    await page.waitForLoadState('networkidle');
-
-    const exportDropdownTrigger = page
-      .locator('.export-dropdown .export-dropdown-icon-btn, .export-dropdown 
button:last-child')
-      .first();
-    await expect(exportDropdownTrigger).toBeVisible({ timeout: 10000 });
-    await exportDropdownTrigger.click();
-
-    const menu = page.locator('.ant-dropdown-menu');
-    await expect(menu).toBeVisible({ timeout: 5000 });
+    await keyboard.setCodeEditorContent('%sh\nprintf "%%table 
col1\\tcol2\\nsay \\"hi\\"\\t1\\n"');
+    await paragraphPage.runParagraph();
+    // runParagraph only clicks Run, and the previous table result is still 
rendered, so wait for
+    // the new output before exporting or the clipboard reads the stale table.
+    await expect(paragraphPage.resultDisplay).toContainText('col2', { timeout: 
30000 });
+
+    await paragraphPage.openExportMenu();
+    const menu = paragraphPage.exportMenu;
     await menu.locator('li:has-text("Copy as CSV")').click();
 
     const clipboardText = await page.evaluate(() => 
navigator.clipboard.readText());
diff --git 
a/zeppelin-web-angular/e2e/tests/notebook/paragraph/paragraph-functionality.spec.ts
 
b/zeppelin-web-angular/e2e/tests/notebook/paragraph/paragraph-functionality.spec.ts
index c1d5020309..105c422620 100644
--- 
a/zeppelin-web-angular/e2e/tests/notebook/paragraph/paragraph-functionality.spec.ts
+++ 
b/zeppelin-web-angular/e2e/tests/notebook/paragraph/paragraph-functionality.spec.ts
@@ -37,7 +37,9 @@ test.describe('Notebook Paragraph Functionality', () => {
     paragraphPage = new NotebookParagraphPage(page);
 
     await page.goto(`/#/notebook/${testNotebook.noteId}`);
-    await page.waitForLoadState('networkidle');
+    // Paragraphs arrive over the WebSocket, so 'networkidle' can resolve 
before they render.
+    // Wait for the paragraph to mount so tests do not act on a bare page.
+    await expect(paragraphPage.paragraphContainer).toBeVisible({ timeout: 
30000 });
   });
 
   test('should display paragraph container with proper structure', async () => 
{
@@ -173,14 +175,16 @@ println("Age: " + z.select("age", Seq(("1","Under 18"), 
("2","18-65"), ("3","Ove
 
     await paragraphPage.runParagraph();
 
-    const cancelButton = page.locator(
-      '.cancel-para, [nz-tooltip*="Cancel"], [title*="Cancel"], 
button:has-text("Cancel"), i[nz-icon="pause-circle"], .anticon-pause-circle'
-    );
-    await expect(cancelButton).toBeVisible({ timeout: 5000 });
+    // The control also renders while the paragraph is PENDING, so wait for 
the run to start
+    // before cancelling it.
+    await expect(paragraphPage.cancelButton).toBeVisible({ timeout: 10000 });
+    await expect(paragraphPage.status).toHaveText('RUNNING', { timeout: 30000 
});
 
-    await cancelButton.click();
+    await paragraphPage.cancelButton.click();
 
-    // Then: Execution should stop — running spinner disappears
-    await expect(page.locator('.paragraph-control 
.fa-spin')).not.toBeVisible({ timeout: 15000 });
+    // Waiting for the button to disappear would also pass on natural 
completion, since the
+    // control is hidden once the paragraph leaves PENDING or RUNNING. Only 
cancelling reaches
+    // ABORT. The interpreter finishes the statement it is on first, so allow 
for the sleep.
+    await expect(paragraphPage.status).toHaveText('ABORT', { timeout: 30000 });
   });
 });
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 996ac0d24c..348d56004b 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
@@ -71,7 +71,6 @@ test.describe('Notebook Sidebar Functionality', () => {
   });
 
   test('should close sidebar functionality work properly', async ({ page }) => 
{
-    await page.waitForLoadState('networkidle', { timeout: 15000 });
     await expect(sidebar.sidebarContainer).toBeVisible({ timeout: 10000 });
 
     // Try to open TOC, but accept FILE_TREE if TOC isn't available
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 9fe75cbae3..229967d719 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
@@ -78,8 +78,8 @@ test.describe('Note Import Modal', () => {
   });
 
   test('Given JSON File tab is selected, When viewing file size limit, Then 
limit should be displayed', async () => {
-    const fileSizeLimit = await noteImportModal.getFileSizeLimit();
-    expect(fileSizeLimit).toMatch(/\d+\s*(MB|KB|GB)/i);
+    // The limit is fetched asynchronously and renders as "-" until it arrives.
+    await 
expect(noteImportModal.fileSizeLimit).toHaveText(/\d+\s*(MB|KB|GB)/i);
   });
 
   test('Given Import Note modal is open, When clicking close button, Then 
modal should close', 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 355bb28770..d56b91941b 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
@@ -37,9 +37,6 @@ test.describe('Note Table of Contents', () => {
     // Use the more robust navigation method from parent class
     await noteTocPage.navigateToNotebook(testNotebook.noteId);
 
-    // Wait for notebook to fully load
-    await page.waitForLoadState('networkidle');
-
     // Verify we're actually in a notebook with more specific checks
     await expect(page).toHaveURL(new 
RegExp(`#/notebook/${testNotebook.noteId}`));
     // JUSTIFIED: test notebook always has exactly one paragraph
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 656dcd5c61..a58d108ca4 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
@@ -57,8 +57,7 @@ test.describe('Header user menu - full-row navigation', () => 
{
       });
 
       await test.step(`Then the app navigates to ${item.route}`, async () => {
-        await page.waitForURL(url => url.hash.includes(item.route), { timeout: 
10000 });
-        expect(page.url()).toContain(item.route);
+        await expect(page).toHaveURL(new RegExp(item.route));
       });
     });
   }
diff --git a/zeppelin-web-angular/e2e/utils.ts 
b/zeppelin-web-angular/e2e/utils.ts
index 7ccae56156..cfc3c110e1 100644
--- a/zeppelin-web-angular/e2e/utils.ts
+++ b/zeppelin-web-angular/e2e/utils.ts
@@ -269,7 +269,6 @@ export const performLoginIfRequired = async (page: Page): 
Promise<boolean> => {
     try {
       await page.waitForSelector('zeppelin-login', { state: 'hidden', timeout: 
30000 });
       await page.waitForSelector('text=Welcome to Zeppelin!', { timeout: 30000 
});
-      await page.waitForLoadState('networkidle');
       await page.waitForSelector('zeppelin-node-list', { timeout: 30000 });
       await waitForZeppelinReady(page);
       return true;
@@ -369,7 +368,8 @@ export const navigateToNotebookWithFallback = async (
 
   try {
     // Strategy 1: Direct navigation
-    await page.goto(`/#/notebook/${noteId}`, { waitUntil: 'networkidle', 
timeout: 30000 });
+    await page.goto(`/#/notebook/${noteId}`, { waitUntil: 'domcontentloaded', 
timeout: 30000 });
+    await page.locator('zeppelin-notebook-paragraph').first().waitFor({ state: 
'visible', timeout: 30000 });
     navigationSuccessful = true;
   } catch {
     // Strategy 2: Wait for loading completion and check URL
@@ -389,7 +389,6 @@ export const navigateToNotebookWithFallback = async (
     // Strategy 3: Navigate through home page if notebook name is provided
     if (!navigationSuccessful && notebookName) {
       await page.goto('/#/');
-      await page.waitForLoadState('networkidle', { timeout: 15000 });
       await page.waitForSelector('zeppelin-node-list', { timeout: 15000 });
 
       // The link text in the UI is the base name of the note, not the full 
path.

Reply via email to