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.