This is an automated email from the ASF dual-hosted git repository.
voidmatcha 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 0fe444ec82 [ZEPPELIN-6536] Stabilize notebook keyboard shortcut e2e
editor seeding
0fe444ec82 is described below
commit 0fe444ec82673ed3741db387512b0c2afdb3c8f6
Author: YONGJAE LEE (이용재) <[email protected]>
AuthorDate: Sat Jul 18 00:19:45 2026 +0900
[ZEPPELIN-6536] Stabilize notebook keyboard shortcut e2e editor seeding
### What is this PR for?
This PR stabilizes the notebook keyboard-shortcut e2e suite by avoiding
keyboard-driven fixture seeding in the Monaco editor.
In the Firefox Playwright job, setup could trigger Monaco suggestions while
typing fixture content. That let Enter accept a suggestion before the shortcut
assertion ran, corrupting expected text, for example `line` becoming `license`.
The helper now seeds fixture text through the editor textarea without
shortcut-style key events. The shortcut assertions still use real keyboard
events.
This also re-enables the clone-content shortcut coverage now that
https://github.com/apache/zeppelin/pull/5254 has been merged. The skip is no
longer needed, and keeping it would leave that shortcut path without regression
coverage.
### What type of PR is it?
Bug Fix
### What is the Jira issue?
https://issues.apache.org/jira/browse/ZEPPELIN-6536
### How should this be tested?
### Screenshots (if appropriate)
N/A
### Questions:
* Does the license files need to update? No
* Is there breaking changes for older versions? No
* Does this needs documentation? No
Closes #5304 from voidmatcha/fix/stabilize-keyboard-shortcut-e2e.
Signed-off-by: YONGJAE LEE <[email protected]>
---
.../e2e/models/notebook-keyboard-page.ts | 152 ++++++--------
.../keyboard/notebook-keyboard-shortcuts.spec.ts | 232 +++++++++------------
zeppelin-web-angular/e2e/utils.ts | 7 +-
3 files changed, 165 insertions(+), 226 deletions(-)
diff --git a/zeppelin-web-angular/e2e/models/notebook-keyboard-page.ts
b/zeppelin-web-angular/e2e/models/notebook-keyboard-page.ts
index 7c2d8b875c..44e5c3118c 100644
--- a/zeppelin-web-angular/e2e/models/notebook-keyboard-page.ts
+++ b/zeppelin-web-angular/e2e/models/notebook-keyboard-page.ts
@@ -58,7 +58,7 @@ export class NotebookKeyboardPage extends BasePage {
this.settingsButton = page.locator('a[nz-dropdown]');
this.clearOutputOption = page.locator('li.list-item:has-text("Clear
output")');
this.deleteButton = page.locator('button:has-text("Delete"),
.delete-paragraph-button');
- this.addParagraphComponent =
page.locator('zeppelin-notebook-add-paragraph').last(); // last() — the
add-paragraph strip at the bottom of the notebook; the first() is the top strip
and is less reliable for insertions
+ this.addParagraphComponent =
page.locator('zeppelin-notebook-add-paragraph').last(); // last() — bottom
add-paragraph strip; first() is the top strip
this.searchDialog = page.locator(
'.dropdown-menu.search-code, .search-widget, .find-widget,
[role="dialog"]:has-text("Find")'
);
@@ -100,15 +100,34 @@ export class NotebookKeyboardPage extends BasePage {
// Wait for any loading/rendering to complete
await this.page.waitForLoadState('domcontentloaded');
- const browserName = this.page.context().browser()?.browserType().name();
- if (browserName === 'firefox' || browserName === 'chromium') {
- // Additional wait for Firefox to ensure editor is fully ready
- await this.page.waitForTimeout(200); // JUSTIFIED: Monaco editor
requires extra settle time in Firefox before focus dispatch
- }
-
await this.focusEditorElement(paragraph, paragraphIndex);
}
+ // Focus the paragraph host: it carries the shortcut bindings (tabindex=-1)
and works even when the editor is hidden (a %md paragraph collapses its editor
after running).
+ async focusParagraphHost(paragraphIndex: number = 0): Promise<void> {
+ const paragraph = this.getParagraphByIndex(paragraphIndex);
+ // Retry: toggling output re-renders the paragraph, so a single focus()
can miss.
+ await expect(async () => {
+ await paragraph.evaluate((el: HTMLElement) => el.focus());
+ await expect(paragraph).toBeFocused({ timeout: 1000 });
+ }).toPass({ timeout: 10000 });
+ }
+
+ // Dispatch a paragraph-scoped shortcut and retry only until its effect is
observed.
+ async pressShortcutFromHostUntil(
+ paragraphIndex: number,
+ press: () => Promise<void>,
+ isSettled: () => Promise<boolean>
+ ): Promise<void> {
+ await expect(async () => {
+ if (!(await isSettled())) {
+ await this.focusParagraphHost(paragraphIndex);
+ await press();
+ }
+ expect(await isSettled()).toBe(true);
+ }).toPass({ timeout: 15000 });
+ }
+
async typeInEditor(text: string): Promise<void> {
await this.page.keyboard.type(text);
}
@@ -209,7 +228,7 @@ export class NotebookKeyboardPage extends BasePage {
// Wait for paragraph count to increase
await this.page.waitForFunction(
- expectedCount =>
document.querySelectorAll('zeppelin-notebook-paragraph').length >
expectedCount, // JUSTIFIED: waitForFunction polls DOM count — Playwright
toHaveCount() requires exact match, not minimum
+ expectedCount =>
document.querySelectorAll('zeppelin-notebook-paragraph').length >
expectedCount, // JUSTIFIED: waitForFunction polls DOM count; Playwright
toHaveCount() requires exact match, not minimum
currentCount,
{ timeout: 10000 }
);
@@ -347,40 +366,29 @@ export class NotebookKeyboardPage extends BasePage {
}
async getCodeEditorContent(): Promise<string> {
- // Fallback to Angular scope
- const angularContent = await this.page.evaluate(() => {
- const paragraphElement =
document.querySelector('zeppelin-notebook-paragraph'); // JUSTIFIED: accesses
AngularJS $scope via window.angular — not accessible via Playwright locator API
- if (paragraphElement) {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const angular = (window as any).angular;
- if (angular) {
- const scope = angular.element(paragraphElement).scope();
- if (scope && scope.$ctrl && scope.$ctrl.paragraph) {
- return scope.$ctrl.paragraph.text || '';
- }
- }
+ return this.readEditorText(this.paragraphContainer.first());
+ }
+
+ // Reconstruct editor text from Monaco's absolutely-positioned `.view-line`
divs sorted by top (DOM order need not match line order), via textContent;
innerText is "" for off-layout lines in headless Chromium.
+ private async readEditorText(paragraph: Locator): Promise<string> {
+ const monaco = paragraph.locator('.monaco-editor').first();
+ if ((await monaco.count()) > 0) {
+ const text = await monaco.evaluate((el: Element) => {
+ const lines = Array.from(el.querySelectorAll('.view-line')) as
HTMLElement[];
+ lines.sort((a, b) => parseInt(a.style.top || '0', 10) -
parseInt(b.style.top || '0', 10));
+ return lines.map(l => (l.textContent || '').replace(/\u00a0/g, '
')).join('\n');
+ });
+ if (text.trim().length > 0) {
+ return text;
}
- return null;
- });
-
- if (angularContent !== null) {
- return angularContent;
}
-
- // Fallback to DOM-based approaches
- const selectors = ['.monaco-editor .view-lines', '.CodeMirror-line',
'.ace_line', 'textarea'];
-
- for (const selector of selectors) {
- const element = this.page.locator(selector).first();
- if (await element.isVisible({ timeout: 1000 })) {
- if (selector === 'textarea') {
- return await element.inputValue();
- } else {
- return (await element.textContent()) || '';
- }
+ const textarea = paragraph.locator('.monaco-editor textarea').first();
+ if ((await textarea.count()) > 0) {
+ const value = await textarea.inputValue().catch(() => '');
+ if (value) {
+ return value;
}
}
-
return '';
}
@@ -419,19 +427,26 @@ export class NotebookKeyboardPage extends BasePage {
for (let i = 0; i < contentLength; i++) {
await this.page.keyboard.press('Backspace');
}
- await this.page.waitForTimeout(100); // JUSTIFIED: Monaco content state
settle between backspaces and new input
-
- await this.page.keyboard.type(content);
- await this.page.waitForTimeout(300); // JUSTIFIED: Monaco content state
settle after keystroke sequence
+ // JUSTIFIED: Monaco textarea can be covered by editor overlays during
fixture setup.
+ await editorInput.fill(content, { force: true });
} else {
// Standard clearing for other browsers
await this.pressSelectAll();
await this.page.keyboard.press('Delete');
- await editorInput.fill(content, { force: true }); // JUSTIFIED: Monaco
textarea may be overlaid by editor decorations after select+delete; force
required for programmatic fill
+ // JUSTIFIED: Monaco textarea can be overlaid after select+delete during
fixture setup.
+ await editorInput.fill(content, { force: true });
}
- await this.page.waitForTimeout(200); // JUSTIFIED: Monaco content state
settle after fill completes
+ // Wait for the full normalized editor content to avoid stale Monaco
renders.
+ const expected = content.replace(/\s+/g, '');
+ if (expected.length === 0) {
+ await expect.poll(async () => (await
this.readEditorText(paragraph)).trim(), { timeout: 10000 }).toBe('');
+ } else {
+ await expect
+ .poll(async () => (await
this.readEditorText(paragraph)).replace(/\s+/g, ''), { timeout: 10000 })
+ .toContain(expected);
+ }
}
// Helper methods for verifying shortcut effects
@@ -496,41 +511,7 @@ export class NotebookKeyboardPage extends BasePage {
}
async getCodeEditorContentByIndex(paragraphIndex: number): Promise<string> {
- const paragraph = this.getParagraphByIndex(paragraphIndex);
-
- const editorTextarea = paragraph.locator('.monaco-editor textarea');
- if (await editorTextarea.isVisible()) {
- const textContent = await editorTextarea.inputValue();
- if (textContent) {
- return textContent;
- }
- }
-
- const viewLines = paragraph.locator('.monaco-editor .view-lines');
- if (await viewLines.isVisible()) {
- const text = await viewLines.evaluate((el: Element) => (el as
HTMLElement).innerText || '');
- if (text && text.trim().length > 0) {
- return text;
- }
- }
-
- const scopeContent = await paragraph.evaluate(el => {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const angular = (window as any).angular;
- if (angular) {
- const scope = angular.element(el).scope();
- if (scope && scope.$ctrl && scope.$ctrl.paragraph) {
- return scope.$ctrl.paragraph.text || '';
- }
- }
- return '';
- });
-
- if (scopeContent) {
- return scopeContent;
- }
-
- return '';
+ return this.readEditorText(this.getParagraphByIndex(paragraphIndex));
}
async waitForParagraphCountChange(expectedCount: number, timeout: number =
30000): Promise<void> {
@@ -650,9 +631,9 @@ export class NotebookKeyboardPage extends BasePage {
const editor = paragraph.locator('.monaco-editor, .CodeMirror,
.ace_editor, textarea').first();
- await editor.waitFor({ state: 'visible', timeout: 5000 }).catch(() => {});
// JUSTIFIED: UI stabilization — editor may not be visible yet; click attempt
follows
- await editor.click({ force: true, trial: true }).catch(async () => {
- // JUSTIFIED: UI stabilization — falls back to textarea focus if click
fails
+ await editor.waitFor({ state: 'visible', timeout: 5000 }).catch(() => {});
// editor may not be visible yet; the click/focus below retries
+ // A `trial` click only runs actionability checks and never focuses; do a
real click, falling back to focusing the textarea if it is intercepted.
+ await editor.click({ force: true }).catch(async () => {
const textArea = editor.locator('textarea').first();
if ((await textArea.count()) > 0) {
await textArea.focus({ timeout: 1000 });
@@ -669,6 +650,8 @@ export class NotebookKeyboardPage extends BasePage {
if (hasTextArea) {
await textArea.focus();
await expect(textArea).toBeFocused({ timeout: 3000 });
+ // Monaco sets the `focused` class only after processing the focus
event; activeElement can be set earlier, dropping the shortcut. Gate on the
class so focus is real.
+ await expect(editor).toHaveClass(/\bfocused\b/, { timeout: 10000 });
} else {
await expect(editor).toHaveClass(/focused|focus|active/, { timeout:
30000 });
}
@@ -676,9 +659,8 @@ export class NotebookKeyboardPage extends BasePage {
private async executePlatformShortcut(shortcut: string | string[]):
Promise<void> {
const shortcuts = Array.isArray(shortcut) ? shortcut : [shortcut];
- const isMac = process.platform === 'darwin';
- const selected = isMac && shortcuts.length > 1 ? shortcuts[1] :
shortcuts[0];
- await this.page.keyboard.press(this.formatKey(selected));
+ // Playwright presses by physical key code, so the primary variant works
on every platform; the macOS special-character variant (e.g. control.alt.∂) is
unpressable via keyboard.press.
+ await this.page.keyboard.press(this.formatKey(shortcuts[0]));
}
private formatKey(shortcut: string): string {
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 b0818c9f02..2f43bb6f33 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
@@ -22,14 +22,10 @@ import {
/**
* Comprehensive keyboard shortcuts test suite based on ShortcutsMap
- * Tests all keyboard shortcuts defined in src/app/key-binding/shortcuts-map.ts
- *
- * Note: This spec uses waitForTimeout in several places because Monaco editor
cursor
- * state and editor focus are not observable via DOM events that Playwright
can detect.
- * These are justified timing gaps to allow Monaco's internal state to settle
between
- * keystroke sequences. See:
https://github.com/microsoft/monaco-editor/issues/2688
+ * (src/app/key-binding/shortcuts-map.ts). The page object gates on Monaco's
`focused`
+ * class before dispatching shortcuts; effects are asserted with web-first
expectations.
*/
-// JUSTIFIED: Monaco editor focus state is not observable via DOM events;
serial ordering prevents cross-test editor state corruption
+// Serial ordering prevents cross-test editor state corruption within the
shared notebook.
test.describe.serial('Comprehensive Keyboard Shortcuts (ShortcutsMap)', () => {
addPageAnnotationBeforeEach(PAGES.WORKSPACE.NOTEBOOK);
addPageAnnotationBeforeEach(PAGES.SHARE.SHORTCUT);
@@ -76,14 +72,13 @@ test.describe.serial('Comprehensive Keyboard Shortcuts
(ShortcutsMap)', () => {
await keyboardPage.tryFocusCodeEditor();
await keyboardPage.setCodeEditorContent('%md\n# Test Heading\n\nThis is
**bold** text.');
- // Verify content was set
- const content = await keyboardPage.getCodeEditorContent();
- expect(content.replace(/\s+/g, '')).toContain('#TestHeading');
+ // Verify content was set (setCodeEditorContent already gated on the
rendered text)
+ await expect(keyboardPage.editorLines.first()).toContainText('Test
Heading');
// When: User presses Shift+Enter
await keyboardPage.pressRunParagraph();
- // Then: Paragraph should execute (reach a terminal state — interpreter
availability varies by env)
+ // Then: Paragraph should execute (reach a terminal state; interpreter
availability varies by env)
await keyboardPage.waitForParagraphExecution(0);
// JUSTIFIED: single-paragraph test notebook; first() is deterministic
const statusEl =
keyboardPage.paragraphContainer.first().locator('.status');
@@ -161,7 +156,7 @@ test.describe.serial('Comprehensive Keyboard Shortcuts
(ShortcutsMap)', () => {
test.describe('ParagraphActions.Cancel: Control+Alt+C', () => {
test('should cancel running paragraph with Control+Alt+C', async () => {
- test.skip(!!process.env.CI, 'Requires Python interpreter with running
indicator — not available in CI');
+ test.skip(!!process.env.CI, 'Requires Python interpreter with running
indicator; not available in CI');
// Given: A long-running paragraph
await keyboardPage.tryFocusCodeEditor();
await keyboardPage.setCodeEditorContent('%python\nimport
time;time.sleep(3)\nprint("Should be cancelled")');
@@ -197,21 +192,17 @@ test.describe.serial('Comprehensive Keyboard Shortcuts
(ShortcutsMap)', () => {
// Position cursor at end of last line using more reliable cross-browser
method
await keyboardPage.pressSelectAll(); // Select all content
await keyboardPage.pressKey('ArrowRight'); // Move to end
- await keyboardPage.page.waitForTimeout(500); // Wait for cursor to
position // JUSTIFIED: Monaco editor internal state settle — cursor/focus state
not observable via DOM
// When: User presses Control+P (should move cursor up one line)
await keyboardPage.pressMoveCursorUp();
- await keyboardPage.page.waitForTimeout(500); // Wait for cursor movement
// JUSTIFIED: Monaco editor internal state settle — cursor/focus state not
observable via DOM
// Then: Verify cursor movement by checking if we can type at the
current position
// Type a marker and check where it appears in the content
await keyboardPage.pressKey('End'); // Move to end of current line
await keyboardPage.page.keyboard.type('MARKER');
- const content = await keyboardPage.getCodeEditorContent();
- // If cursor moved up correctly, marker should be on line2
- expect(content).toContain('line2MARKER');
- expect(content).not.toContain('line3MARKER');
+ await expect.poll(() =>
keyboardPage.getCodeEditorContent()).toContain('line2MARKER');
+ expect(await
keyboardPage.getCodeEditorContent()).not.toContain('line3MARKER');
});
});
@@ -225,20 +216,16 @@ test.describe.serial('Comprehensive Keyboard Shortcuts
(ShortcutsMap)', () => {
await keyboardPage.pressSelectAll(); // Select all content
await keyboardPage.pressKey('ArrowLeft'); // Move to beginning
await keyboardPage.pressKey('ArrowDown'); // Move to line1
- await keyboardPage.page.waitForTimeout(500); // Wait for cursor to
position // JUSTIFIED: Monaco editor internal state settle — cursor/focus state
not observable via DOM
// When: User presses Control+N (should move cursor down one line)
await keyboardPage.pressMoveCursorDown();
- await keyboardPage.page.waitForTimeout(500); // Wait for cursor movement
// JUSTIFIED: Monaco editor internal state settle — cursor/focus state not
observable via DOM
// Then: Verify cursor movement by checking if we can type at the
current position
// Type a marker and check where it appears in the content
await keyboardPage.page.keyboard.type('MARKER');
- const content = await keyboardPage.getCodeEditorContent();
- // If cursor moved down correctly, marker should be on line2
- expect(content).toContain('MARKERline2');
- expect(content).not.toContain('MARKERline1');
+ await expect.poll(() =>
keyboardPage.getCodeEditorContent()).toContain('MARKERline2');
+ expect(await
keyboardPage.getCodeEditorContent()).not.toContain('MARKERline1');
});
});
@@ -265,7 +252,6 @@ test.describe.serial('Comprehensive Keyboard Shortcuts
(ShortcutsMap)', () => {
// Focus first paragraph
await firstParagraph.click();
await keyboardPage.tryFocusCodeEditor(0);
- await keyboardPage.page.waitForTimeout(1000); // JUSTIFIED: Monaco
editor requires time to register focus before keyboard shortcut dispatch
// When: User presses Control+Alt+D
await keyboardPage.pressDeleteParagraph();
@@ -285,7 +271,6 @@ test.describe.serial('Comprehensive Keyboard Shortcuts
(ShortcutsMap)', () => {
expect(initialCount).toBe(1);
await keyboardPage.tryFocusCodeEditor(0);
- await keyboardPage.page.waitForTimeout(500); // JUSTIFIED: Monaco editor
requires time to register focus before keyboard shortcut dispatch
// When: User presses Control+Alt+D on the only paragraph
await keyboardPage.pressDeleteParagraph();
@@ -324,17 +309,14 @@ test.describe.serial('Comprehensive Keyboard Shortcuts
(ShortcutsMap)', () => {
const finalCount = await keyboardPage.getParagraphCount();
expect(finalCount).toBe(initialCount + 1);
- // And: The new paragraph should be at index 0 (above the original)
- const newParagraphContent = await
keyboardPage.getCodeEditorContentByIndex(0);
- const originalParagraphContent = await
keyboardPage.getCodeEditorContentByIndex(1);
+ // And: the new paragraph at index 0 holds no user content; empty or
just an interpreter directive (poll so the async insert/render settles).
+ await expect.poll(() =>
keyboardPage.getCodeEditorContentByIndex(0).then(c =>
c.trim())).toMatch(/^(%\w+)?$/);
- // New paragraph may have default interpreter (%python) or be empty
- expect(newParagraphContent === '' || newParagraphContent ===
'%python').toBe(true);
-
- // Normalize whitespace for comparison since Monaco editor may format
differently
+ // And the original content moved to index 1 (normalize whitespace;
Monaco reflows).
const normalizedOriginalContent = originalContent.replace(/\s+/g, '
').trim();
- const normalizedReceivedContent =
originalParagraphContent.replace(/\s+/g, ' ').trim();
- expect(normalizedReceivedContent).toContain(normalizedOriginalContent);
// Original content should be at index 1
+ await expect
+ .poll(() => keyboardPage.getCodeEditorContentByIndex(1).then(c =>
c.replace(/\s+/g, ' ').trim()))
+ .toContain(normalizedOriginalContent);
});
});
@@ -355,49 +337,33 @@ test.describe.serial('Comprehensive Keyboard Shortcuts
(ShortcutsMap)', () => {
const finalCount = await keyboardPage.getParagraphCount();
expect(finalCount).toBe(initialCount + 1);
- // And: The new paragraph should be at index 1 (below the original)
+ // And: the original content stays at index 0 (poll so the async
insert/render settles).
+ await expect.poll(() =>
keyboardPage.getCodeEditorContentByIndex(0)).toMatch(/Original\s+Paragraph/);
const originalParagraphContent = await
keyboardPage.getCodeEditorContentByIndex(0);
- const newParagraphContent = await
keyboardPage.getCodeEditorContentByIndex(1);
-
- // Compare content - use regex to handle potential encoding issues
- expect(originalParagraphContent).toMatch(/Original\s+Paragraph/);
expect(originalParagraphContent).toMatch(/Content\s+for\s+insert\s+below\s+test/);
- expect(newParagraphContent).toBeDefined(); // New paragraph just needs
to exist
+
+ // And: a new paragraph exists at index 1 holding no user content.
+ await expect.poll(() =>
keyboardPage.getCodeEditorContentByIndex(1).then(c =>
c.trim())).toMatch(/^(%\w+)?$/);
});
});
- // Note (ZEPPELIN-6294):
- // This test appears to be related to ZEPPELIN-6294.
- // A proper fix or verification should be added based on the issue details.
- // In the New UI, the cloned paragraph’s text is empty on PARAGRAPH_ADDED,
- // while the Classic UI receives the correct text. This discrepancy should
be addressed
- // when applying the proper fix for the issue.
test.describe('ParagraphActions.InsertCopyOfParagraphBelow:
Control+Shift+C', () => {
test('should insert copy of paragraph below with Control+Shift+C', async
() => {
- test.skip();
// Given: A paragraph with content
await keyboardPage.tryFocusCodeEditor();
await keyboardPage.setCodeEditorContent('%md\n# Copy Test\nContent to be
copied below');
const initialCount = await keyboardPage.getParagraphCount();
-
- // Capture the original paragraph content to verify the copy
const originalContent = await
keyboardPage.getCodeEditorContentByIndex(0);
// When: User presses Control+Shift+C
await keyboardPage.pressInsertCopy();
- // Then: A copy of the paragraph should be inserted below
+ // Then: a copy is inserted below carrying the same text, and the
original is unchanged
await keyboardPage.waitForParagraphCountChange(initialCount + 1);
- const finalCount = await keyboardPage.getParagraphCount();
- expect(finalCount).toBe(initialCount + 1);
-
- // And: The copied content should be identical to the original
- const originalParagraphContent = await
keyboardPage.getCodeEditorContentByIndex(0);
- const copiedParagraphContent = await
keyboardPage.getCodeEditorContentByIndex(1);
-
- expect(originalParagraphContent).toBe(originalContent); // Original
should remain unchanged
- expect(copiedParagraphContent).toBe(originalContent); // Copied content
should match original exactly
+ expect(await keyboardPage.getParagraphCount()).toBe(initialCount + 1);
+ await expect.poll(() =>
keyboardPage.getCodeEditorContentByIndex(0)).toBe(originalContent);
+ await expect.poll(() =>
keyboardPage.getCodeEditorContentByIndex(1)).toBe(originalContent);
});
});
@@ -407,10 +373,9 @@ test.describe.serial('Comprehensive Keyboard Shortcuts
(ShortcutsMap)', () => {
const firstContent = '%python\nprint("First Paragraph - Content for move
up test")';
const secondContent = '%python\nprint("Second Paragraph - This should
move up")';
- // Set first paragraph content
+ // Set first paragraph content (setCodeEditorContent gates on the
rendered text)
await keyboardPage.tryFocusCodeEditor(0);
await keyboardPage.setCodeEditorContent(firstContent, 0);
- await keyboardPage.page.waitForTimeout(300); // JUSTIFIED: Monaco editor
internal state settle — cursor/focus state not observable via DOM
// Create second paragraph using InsertBelow shortcut (Control+Alt+B)
await keyboardPage.pressInsertBelow();
@@ -419,7 +384,6 @@ test.describe.serial('Comprehensive Keyboard Shortcuts
(ShortcutsMap)', () => {
// Set second paragraph content
await keyboardPage.tryFocusCodeEditor(1);
await keyboardPage.setCodeEditorContent(secondContent, 1);
- await keyboardPage.page.waitForTimeout(300); // JUSTIFIED: Monaco
content state settle before read
// Verify we have 2 paragraphs
const paragraphCount = await keyboardPage.getParagraphCount();
@@ -431,24 +395,17 @@ test.describe.serial('Comprehensive Keyboard Shortcuts
(ShortcutsMap)', () => {
// Focus on second paragraph for move operation
await keyboardPage.tryFocusCodeEditor(1);
- await keyboardPage.page.waitForTimeout(200); // JUSTIFIED: Monaco editor
internal state settle — cursor/focus state not observable via DOM
// When: User presses Control+Alt+K from second paragraph
await keyboardPage.pressMoveParagraphUp();
- // Wait for move operation to complete
- await keyboardPage.page.waitForTimeout(1000); // JUSTIFIED: Monaco
editor internal state settle — cursor/focus state not observable via DOM
-
// Then: Paragraph count should remain the same
const finalParagraphCount = await keyboardPage.getParagraphCount();
expect(finalParagraphCount).toBe(2);
- // And: Paragraph positions should be swapped
- const newFirstParagraph = await
keyboardPage.getCodeEditorContentByIndex(0);
- const newSecondParagraph = await
keyboardPage.getCodeEditorContentByIndex(1);
-
- expect(newFirstParagraph).toBe(initialSecond); // Second paragraph moved
to first position
- expect(newSecondParagraph).toBe(initialFirst); // First paragraph moved
to second position
+ // And: Paragraph positions should be swapped (poll until the move lands
in the DOM)
+ await expect.poll(() =>
keyboardPage.getCodeEditorContentByIndex(0)).toBe(initialSecond);
+ await expect.poll(() =>
keyboardPage.getCodeEditorContentByIndex(1)).toBe(initialFirst);
});
});
@@ -458,10 +415,9 @@ test.describe.serial('Comprehensive Keyboard Shortcuts
(ShortcutsMap)', () => {
const firstContent = '%python\nprint("First Paragraph - This should move
down")';
const secondContent = '%python\nprint("Second Paragraph - Content for
second paragraph")';
- // Set first paragraph content
+ // Set first paragraph content (setCodeEditorContent gates on the
rendered text)
await keyboardPage.tryFocusCodeEditor(0);
await keyboardPage.setCodeEditorContent(firstContent, 0);
- await keyboardPage.page.waitForTimeout(300); // JUSTIFIED: Monaco editor
internal state settle — cursor/focus state not observable via DOM
// Create second paragraph using InsertBelow shortcut (Control+Alt+B)
await keyboardPage.pressInsertBelow();
@@ -470,7 +426,6 @@ test.describe.serial('Comprehensive Keyboard Shortcuts
(ShortcutsMap)', () => {
// Set second paragraph content
await keyboardPage.tryFocusCodeEditor(1);
await keyboardPage.setCodeEditorContent(secondContent, 1);
- await keyboardPage.page.waitForTimeout(300); // JUSTIFIED: Monaco
content state settle before read
// Verify we have 2 paragraphs
const paragraphCount = await keyboardPage.getParagraphCount();
@@ -482,24 +437,17 @@ test.describe.serial('Comprehensive Keyboard Shortcuts
(ShortcutsMap)', () => {
// Focus first paragraph for move operation
await keyboardPage.tryFocusCodeEditor(0);
- await keyboardPage.page.waitForTimeout(200); // JUSTIFIED: Monaco editor
internal state settle — cursor/focus state not observable via DOM
// When: User presses Control+Alt+J from first paragraph
await keyboardPage.pressMoveParagraphDown();
- // Wait for move operation to complete
- await keyboardPage.page.waitForTimeout(1000); // JUSTIFIED: Monaco
editor internal state settle — cursor/focus state not observable via DOM
-
// Then: Paragraph count should remain the same
const finalParagraphCount = await keyboardPage.getParagraphCount();
expect(finalParagraphCount).toBe(2);
- // And: Paragraph positions should be swapped
- const newFirstParagraph = await
keyboardPage.getCodeEditorContentByIndex(0);
- const newSecondParagraph = await
keyboardPage.getCodeEditorContentByIndex(1);
-
- expect(newFirstParagraph).toBe(initialSecond); // Second paragraph moved
to first position
- expect(newSecondParagraph).toBe(initialFirst); // First paragraph moved
to second position
+ // And: Paragraph positions should be swapped (poll until the move lands
in the DOM)
+ await expect.poll(() =>
keyboardPage.getCodeEditorContentByIndex(0)).toBe(initialSecond);
+ await expect.poll(() =>
keyboardPage.getCodeEditorContentByIndex(1)).toBe(initialFirst);
});
});
@@ -516,10 +464,8 @@ test.describe.serial('Comprehensive Keyboard Shortcuts
(ShortcutsMap)', () => {
// When: User presses Control+Alt+E
await keyboardPage.pressSwitchEditor();
- // Then: Editor visibility should toggle
- await keyboardPage.page.waitForTimeout(500); // JUSTIFIED: Monaco editor
internal state settle — cursor/focus state not observable via DOM
- const finalEditorVisibility = await keyboardPage.isEditorVisible(0);
- expect(finalEditorVisibility).not.toBe(initialEditorVisibility);
+ // Then: editor visibility toggles
+ await expect.poll(() => keyboardPage.isEditorVisible(0), { timeout:
10000 }).toBe(!initialEditorVisibility);
});
});
@@ -534,10 +480,8 @@ test.describe.serial('Comprehensive Keyboard Shortcuts
(ShortcutsMap)', () => {
// When: User presses Control+Alt+R
await keyboardPage.pressSwitchEnable();
- // Then: Paragraph enabled state should toggle
- await keyboardPage.page.waitForTimeout(1000); // JUSTIFIED: Monaco
editor internal state settle — cursor/focus state not observable via DOM
- const finalEnabledState = await keyboardPage.isParagraphEnabled(0);
- expect(finalEnabledState).not.toBe(initialEnabledState);
+ // Then: paragraph enabled state toggles
+ await expect.poll(() => keyboardPage.isParagraphEnabled(0), { timeout:
10000 }).toBe(!initialEnabledState);
});
});
@@ -552,14 +496,19 @@ test.describe.serial('Comprehensive Keyboard Shortcuts
(ShortcutsMap)', () => {
const resultLocator =
keyboardPage.getParagraphByIndex(0).locator('[data-testid="paragraph-result"]');
await expect(resultLocator).toBeVisible();
- const initialOutputVisibility = await keyboardPage.isOutputVisible(0);
+ // When: User presses Control+Alt+O from the paragraph host
+ await keyboardPage.pressShortcutFromHostUntil(
+ 0,
+ () => keyboardPage.pressSwitchOutputShow(),
+ () => resultLocator.isHidden()
+ );
- // When: User presses Control+Alt+O
- await keyboardPage.tryFocusCodeEditor(0);
- await keyboardPage.pressSwitchOutputShow();
-
- const finalOutputVisibility = await keyboardPage.isOutputVisible(0);
- expect(finalOutputVisibility).not.toBe(initialOutputVisibility);
+ // And toggling again restores it
+ await keyboardPage.pressShortcutFromHostUntil(
+ 0,
+ () => keyboardPage.pressSwitchOutputShow(),
+ () => resultLocator.isVisible()
+ );
});
});
@@ -574,10 +523,10 @@ test.describe.serial('Comprehensive Keyboard Shortcuts
(ShortcutsMap)', () => {
// When: User presses Control+Alt+M
await keyboardPage.pressSwitchLineNumber();
- // Then: Line numbers visibility should toggle
- await keyboardPage.page.waitForTimeout(500); // JUSTIFIED: Monaco editor
internal state settle — cursor/focus state not observable via DOM
- const finalLineNumbersVisibility = await
keyboardPage.areLineNumbersVisible(0);
-
expect(finalLineNumbersVisibility).not.toBe(initialLineNumbersVisibility);
+ // Then: line numbers visibility toggles
+ await expect
+ .poll(() => keyboardPage.areLineNumbersVisible(0), { timeout: 10000 })
+ .toBe(!initialLineNumbersVisibility);
});
});
@@ -592,9 +541,8 @@ test.describe.serial('Comprehensive Keyboard Shortcuts
(ShortcutsMap)', () => {
// When: User presses Control+Alt+T
await keyboardPage.pressSwitchTitleShow();
- // Then: Title visibility should toggle
- const finalTitleVisibility = await keyboardPage.isTitleVisible(0);
- expect(finalTitleVisibility).not.toBe(initialTitleVisibility);
+ // Then: title visibility toggles (poll; the DOM updates asynchronously)
+ await expect.poll(() => keyboardPage.isTitleVisible(0), { timeout: 10000
}).toBe(!initialTitleVisibility);
});
});
@@ -611,12 +559,15 @@ test.describe.serial('Comprehensive Keyboard Shortcuts
(ShortcutsMap)', () => {
const statusElBefore =
keyboardPage.paragraphContainer.first().locator('.status');
await
expect(statusElBefore).toHaveText(/FINISHED|ERROR|PENDING|RUNNING/);
- // When: User presses Control+Alt+L
- await keyboardPage.tryFocusCodeEditor(0);
- await keyboardPage.pressClearOutput();
+ // When: User presses Control+Alt+L (editor hidden after %md run;
dispatch from the host)
+ const resultLocator =
keyboardPage.getParagraphByIndex(0).locator('[data-testid="paragraph-result"]');
+ await keyboardPage.pressShortcutFromHostUntil(
+ 0,
+ () => keyboardPage.pressClearOutput(),
+ () => resultLocator.isHidden()
+ );
// Then: Output should be cleared
- const resultLocator =
keyboardPage.getParagraphByIndex(0).locator('[data-testid="paragraph-result"]');
await expect(resultLocator).not.toBeVisible();
});
});
@@ -666,9 +617,8 @@ test.describe.serial('Comprehensive Keyboard Shortcuts
(ShortcutsMap)', () => {
// When: User presses Control+Shift+-
await keyboardPage.pressReduceWidth();
- // Then: Paragraph width should be reduced
- const finalWidth = await keyboardPage.getParagraphWidth(0);
- expect(finalWidth).toBeLessThan(initialWidth);
+ // Then: paragraph width reduces (poll; the layout updates
asynchronously)
+ await expect.poll(() => keyboardPage.getParagraphWidth(0), { timeout:
10000 }).toBeLessThan(initialWidth);
});
});
@@ -679,17 +629,18 @@ test.describe.serial('Comprehensive Keyboard Shortcuts
(ShortcutsMap)', () => {
await keyboardPage.setCodeEditorContent('%python\nprint("Test width
increase")');
// First, reduce width to ensure there's room to increase
+ const fullWidth = await keyboardPage.getParagraphWidth(0);
await keyboardPage.pressReduceWidth();
- await keyboardPage.page.waitForTimeout(500); // Give UI a moment to
update after reduction // JUSTIFIED: Monaco editor internal state settle —
cursor/focus state not observable via DOM
+ // Poll until the reduction is reflected in the layout instead of a
fixed settle
+ await expect.poll(() => keyboardPage.getParagraphWidth(0), { timeout:
10000 }).toBeLessThan(fullWidth);
const initialWidth = await keyboardPage.getParagraphWidth(0);
// When: User presses Control+Shift+=
await keyboardPage.pressIncreaseWidth();
- // Then: Paragraph width should be increased
- const finalWidth = await keyboardPage.getParagraphWidth(0);
- expect(finalWidth).toBeGreaterThan(initialWidth);
+ // Then: paragraph width increases (poll; the layout updates
asynchronously)
+ await expect.poll(() => keyboardPage.getParagraphWidth(0), { timeout:
10000 }).toBeGreaterThan(initialWidth);
});
});
@@ -709,20 +660,23 @@ test.describe.serial('Comprehensive Keyboard Shortcuts
(ShortcutsMap)', () => {
// Additional wait and focus for Firefox compatibility
const browserName = test.info().project.name;
if (browserName === 'firefox') {
- await keyboardPage.page.waitForTimeout(200); // JUSTIFIED: Monaco
editor internal state settle — cursor/focus state not observable via DOM
+ // JUSTIFIED: wait for Monaco focus/cursor state after editor shortcut
+ await keyboardPage.page.waitForTimeout(200); // JUSTIFIED: Monaco
editor internal state settle; cursor/focus state not observable via DOM
// Ensure Monaco editor is properly focused
// JUSTIFIED: single Monaco editor per paragraph; first() picks the
active textarea
const editorTextarea = keyboardPage.page.locator('.monaco-editor
textarea').first();
await editorTextarea.click();
await editorTextarea.focus();
- await keyboardPage.page.waitForTimeout(200); // JUSTIFIED: Monaco
editor internal state settle — cursor/focus state not observable via DOM
+ // JUSTIFIED: wait for Monaco focus/cursor state after editor shortcut
+ await keyboardPage.page.waitForTimeout(200); // JUSTIFIED: Monaco
editor internal state settle; cursor/focus state not observable via DOM
}
// When: User presses Control+K (cut to end of line)
await keyboardPage.pressCutLine();
// Then: First line content should be cut (cut from cursor position to
end of line)
- await keyboardPage.page.waitForTimeout(500); // JUSTIFIED: Monaco editor
internal state settle — cursor/focus state not observable via DOM
+ // JUSTIFIED: wait for Monaco focus/cursor state after editor shortcut
+ await keyboardPage.page.waitForTimeout(500); // JUSTIFIED: Monaco editor
internal state settle; cursor/focus state not observable via DOM
const finalContent = await keyboardPage.getCodeEditorContent();
expect(finalContent).toBeDefined();
expect(typeof finalContent).toBe('string');
@@ -744,13 +698,15 @@ test.describe.serial('Comprehensive Keyboard Shortcuts
(ShortcutsMap)', () => {
await keyboardPage.setCodeEditorContent(originalContent);
// Wait for content to be properly set and verify it
- await keyboardPage.page.waitForTimeout(500); // JUSTIFIED: Monaco editor
internal state settle — cursor/focus state not observable via DOM
+ // JUSTIFIED: wait for Monaco focus/cursor state after editor shortcut
+ await keyboardPage.page.waitForTimeout(500); // JUSTIFIED: Monaco editor
internal state settle; cursor/focus state not observable via DOM
const initialContent = await keyboardPage.getCodeEditorContent();
expect(initialContent.replace(/\s+/g, '
').trim()).toContain(originalContent);
// When: User presses Control+K to cut the line
await keyboardPage.pressCutLine();
- await keyboardPage.page.waitForTimeout(500); // JUSTIFIED: Monaco editor
internal state settle — cursor/focus state not observable via DOM
+ // JUSTIFIED: wait for Monaco focus/cursor state after editor shortcut
+ await keyboardPage.page.waitForTimeout(500); // JUSTIFIED: Monaco editor
internal state settle; cursor/focus state not observable via DOM
// Then: Content should be reduced (line was cut)
const afterCutContent = await keyboardPage.getCodeEditorContent();
@@ -758,13 +714,15 @@ test.describe.serial('Comprehensive Keyboard Shortcuts
(ShortcutsMap)', () => {
// Clear the editor to verify paste works from clipboard
await keyboardPage.setCodeEditorContent('');
- await keyboardPage.page.waitForTimeout(200); // JUSTIFIED: Monaco editor
internal state settle — cursor/focus state not observable via DOM
+ // JUSTIFIED: wait for Monaco focus/cursor state after editor shortcut
+ await keyboardPage.page.waitForTimeout(200); // JUSTIFIED: Monaco editor
internal state settle; cursor/focus state not observable via DOM
const emptyContent = await keyboardPage.getCodeEditorContent();
expect(emptyContent.trim()).toBe('');
// When: User presses Control+Y to paste
await keyboardPage.pressPasteLine();
- await keyboardPage.page.waitForTimeout(500); // JUSTIFIED: Monaco editor
internal state settle — cursor/focus state not observable via DOM
+ // JUSTIFIED: wait for Monaco focus/cursor state after editor shortcut
+ await keyboardPage.page.waitForTimeout(500); // JUSTIFIED: Monaco editor
internal state settle; cursor/focus state not observable via DOM
// Then: Original content should be restored from clipboard
const finalContent = await keyboardPage.getCodeEditorContent();
@@ -797,8 +755,7 @@ test.describe.serial('Comprehensive Keyboard Shortcuts
(ShortcutsMap)', () => {
// When: User presses Control+Alt+F
await keyboardPage.pressFindInCode();
- // Then: Find functionality should be triggered
- await keyboardPage.page.waitForTimeout(1000); // JUSTIFIED: Monaco
editor internal state settle — cursor/focus state not observable via DOM
+ // Then: Find functionality should be triggered (toBeVisible
auto-retries)
await expect(keyboardPage.searchDialog).toBeVisible();
// Close search dialog
@@ -817,9 +774,9 @@ test.describe.serial('Comprehensive Keyboard Shortcuts
(ShortcutsMap)', () => {
// When: User presses Control+Space to trigger autocomplete
await keyboardPage.pressControlSpace();
- await keyboardPage.page.waitForTimeout(1000); // JUSTIFIED: Monaco
editor internal state settle — cursor/focus state not observable via DOM
+ await keyboardPage.autocompletePopup.waitFor({ state: 'visible',
timeout: 3000 }).catch(() => {});
- // Then: Editor must remain functional after shortcut (baseline — always
asserts)
+ // Then: Editor must remain functional after shortcut (baseline; always
asserts)
// JUSTIFIED: single-paragraph test notebook; first() is deterministic
await expect(keyboardPage.codeEditor.first()).toBeVisible();
@@ -844,7 +801,7 @@ test.describe.serial('Comprehensive Keyboard Shortcuts
(ShortcutsMap)', () => {
// When: User triggers autocomplete and selects an option
await keyboardPage.pressControlSpace();
- await keyboardPage.page.waitForTimeout(1000); // JUSTIFIED: Monaco
editor internal state settle — cursor/focus state not observable via DOM
+ await keyboardPage.autocompletePopup.waitFor({ state: 'visible',
timeout: 3000 }).catch(() => {});
const isAutocompleteVisible = await keyboardPage.isAutocompleteVisible();
if (isAutocompleteVisible) {
@@ -882,7 +839,7 @@ test.describe.serial('Comprehensive Keyboard Shortcuts
(ShortcutsMap)', () => {
// When: User presses Tab for indentation
await keyboardPage.pressTab();
- // Then: Content should be longer (indentation added) — poll until
CodeMirror processes the Tab asynchronously
+ // Then: Content should be longer (indentation added); poll until
CodeMirror processes the Tab asynchronously
let contentAfterTab = '';
await expect(async () => {
contentAfterTab = await keyboardPage.getCodeEditorContent();
@@ -988,8 +945,7 @@ test.describe.serial('Comprehensive Keyboard Shortcuts
(ShortcutsMap)', () => {
await keyboardPage.setCodeEditorContent('%md\n# Recovery Test\nShortcuts
work after error', newParagraphIndex);
await keyboardPage.pressRunParagraph();
- // Then: New paragraph should execute (FINISHED or ERROR is acceptable —
the key assertion is
- // that execution completed, proving shortcuts are functional after an
error occurred)
+ // Then: Shortcut execution still reaches a terminal state
await keyboardPage.waitForParagraphExecution(newParagraphIndex);
// 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');
@@ -1001,23 +957,22 @@ test.describe.serial('Comprehensive Keyboard Shortcuts
(ShortcutsMap)', () => {
await keyboardPage.tryFocusCodeEditor();
await keyboardPage.setCodeEditorContent('%md\n# Test paragraph');
- // Remove focus by clicking on empty area
+ // Remove focus by clicking on empty area, then confirm no editor holds
focus
await keyboardPage.page.locator('body').click();
- await keyboardPage.page.waitForTimeout(500); // JUSTIFIED: Monaco editor
internal state settle — cursor/focus state not observable via DOM
+ await
expect(keyboardPage.page.locator('.monaco-editor.focused')).toHaveCount(0, {
timeout: 5000 });
const initialCount = await keyboardPage.getParagraphCount();
// When: User tries keyboard shortcuts that require paragraph focus
// These should either not work or gracefully handle the lack of focus
await keyboardPage.pressInsertBelow(); // This may not work without focus
- await keyboardPage.page.waitForTimeout(1000); // JUSTIFIED: Monaco
editor internal state settle — cursor/focus state not observable via DOM
const afterShortcut = await keyboardPage.getParagraphCount();
// Then: Either the shortcut works (creates new paragraph) or is
gracefully ignored
expect(afterShortcut === initialCount || afterShortcut === initialCount
+ 1).toBe(true);
- // System must remain stable — editor still accessible
+ // Editor remains usable after error recovery.
// JUSTIFIED: single-paragraph test notebook; first() is deterministic
await expect(keyboardPage.codeEditor.first()).toBeVisible();
});
@@ -1032,7 +987,6 @@ test.describe.serial('Comprehensive Keyboard Shortcuts
(ShortcutsMap)', () => {
await keyboardPage.waitForParagraphExecution(0, 60000);
// JUSTIFIED: single-paragraph test notebook; first() is deterministic
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
}
// Then: System should remain stable
diff --git a/zeppelin-web-angular/e2e/utils.ts
b/zeppelin-web-angular/e2e/utils.ts
index 8305759647..93aaf67f70 100644
--- a/zeppelin-web-angular/e2e/utils.ts
+++ b/zeppelin-web-angular/e2e/utils.ts
@@ -363,7 +363,7 @@ export const navigateToNotebookWithFallback = async (
// Strategy 1: Direct navigation
await page.goto(`/#/notebook/${noteId}`, { waitUntil: 'networkidle',
timeout: 30000 });
navigationSuccessful = true;
- } catch (error) {
+ } catch {
// Strategy 2: Wait for loading completion and check URL
await page.waitForFunction(
() => {
@@ -485,7 +485,10 @@ export const createTestNotebookWithName = async (
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');
+ /REST request failed: (404|409|500)\b/.test(message) ||
+ message.includes('Fetch notebook REST request failed') ||
+ // TODO: transient WebKit-on-Linux crash (microsoft/playwright#34450);
drop once fixed upstream.
+ /WebKit encountered an internal error|Target crashed/.test(message);
const tryCreate = async () => {
const prefix = options.namePrefix ?? 'TestNotebook';