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 710edf257c [ZEPPELIN-6517] Add E2E tests for notebook paragraph
components
710edf257c is described below
commit 710edf257c43826ffabc47cefc2617e828e3363e
Author: Minho Jang <[email protected]>
AuthorDate: Wed Sep 16 22:24:21 2026 +0900
[ZEPPELIN-6517] Add E2E tests for notebook paragraph components
### What is this PR for?
This PR improves the behavior-focused E2E coverage for the notebook
paragraph sub-components.
It replaces shallow visibility checks and interpreter-error fallbacks with
scenarios that verify user-observable paragraph behavior:
- Add paragraphs above and below the current paragraph
- Edit Monaco content and verify focused, dirty, and rendered line states
- Inspect the paragraph settings available for single and multiple
paragraphs
- Clone, insert, move, and remove paragraphs
- Toggle the editor, output, line numbers, and run state
- Edit paragraph titles and verify Enter, Escape, and blur behavior
- Verify running, finished, outdated, and cancelled paragraph states
- Verify progress and footer timing information
- Verify that paragraph insertion is disabled while execution is in progress
The paragraph page object is also updated to:
- Scope component locators to the primary paragraph
- Provide helpers for add-paragraph and settings-menu interactions
- Replace individual settings-menu locators with a typed
`settingsMenuItem()` accessor
- Support interactions with settings menus belonging to different paragraphs
- Use stable, component-specific locators instead of broad fallback
selectors
### What type of PR is it?
Improvement
### What is the Jira issue?
* https://issues.apache.org/jira/browse/ZEPPELIN-6517
### How should this be tested?
From `zeppelin-web-angular/`, run:
```bash
npm run e2e:fast -- tests/notebook/paragraph/paragraph-functionality.spec.ts
```
### Questions:
* Does the license files need to update? No
* Is there breaking changes for older versions? No
* Does this needs documentation? No
Closes #5476 from miinhho/test/notebook-paragraph-sub-component.
Signed-off-by: YONGJAE LEE <[email protected]>
---
.../e2e/models/notebook-paragraph-page.ts | 126 ++++++--
.../paragraph/paragraph-functionality.spec.ts | 357 +++++++++++++++------
2 files changed, 353 insertions(+), 130 deletions(-)
diff --git a/zeppelin-web-angular/e2e/models/notebook-paragraph-page.ts
b/zeppelin-web-angular/e2e/models/notebook-paragraph-page.ts
index f50359a780..f0083edb26 100644
--- a/zeppelin-web-angular/e2e/models/notebook-paragraph-page.ts
+++ b/zeppelin-web-angular/e2e/models/notebook-paragraph-page.ts
@@ -13,46 +13,102 @@
import { expect, Locator, Page } from '@playwright/test';
import { BasePage } from './base-page';
+type ParagraphSettingsMenuItem =
+ | 'Width'
+ | 'Font size'
+ | 'Insert new'
+ | 'Clone paragraph'
+ | 'Remove'
+ | 'Move paragraph up'
+ | 'Move paragraph down'
+ | 'Clear output'
+ | 'Show Title'
+ | 'Show line numbers'
+ | 'Hide line numbers'
+ | 'Disable run'
+ | 'Enable run';
+
export class NotebookParagraphPage extends BasePage {
+ readonly paragraphContainers: Locator;
readonly paragraphContainer: Locator;
- readonly addParagraphAbove: Locator;
- readonly addParagraphBelow: Locator;
+ readonly addParagraphAboveLink: Locator;
+ readonly addParagraphBelowLink: Locator;
readonly controlPanel: Locator;
readonly codeEditor: Locator;
+ readonly codeEditorHost: Locator;
+ readonly editorInput: Locator;
+ readonly editorViewLinesAll: Locator;
+ readonly editorViewLines: Locator;
+ readonly editorLines: Locator;
readonly dynamicForms: Locator;
readonly resultDisplay: Locator;
- readonly footerInfo: Locator;
+ readonly executionTime: Locator;
+ readonly elapsedTime: Locator;
+ readonly progressIndicator: Locator;
+ readonly progressBar: Locator;
+ readonly paragraphTitleText: Locator;
+ readonly paragraphTitleInput: Locator;
+ readonly lineNumbers: Locator;
readonly runButton: Locator;
- readonly settingsDropdown: Locator;
+ readonly toggleEditorButton: Locator;
+ readonly toggleOutputButton: Locator;
+ readonly settingsDropdowns: Locator;
+ readonly settingsMenu: Locator;
+ readonly paragraphIdMenuItem: Locator;
+ readonly confirmButton: Locator;
+ readonly runAllButton: Locator;
readonly status: Locator;
readonly cancelButton: Locator;
readonly exportDropdownTrigger: Locator;
readonly exportMenu: Locator;
+ private readonly settingsDropdown: Locator;
+ private readonly addParagraphAbove: Locator;
+ private readonly addParagraphBelow: Locator;
constructor(page: Page) {
super(page);
- this.paragraphContainer = page.locator('.paragraph-container').first();
- this.addParagraphAbove =
page.locator('zeppelin-notebook-add-paragraph').first();
- this.addParagraphBelow =
page.locator('zeppelin-notebook-add-paragraph').last();
- this.controlPanel =
page.locator('zeppelin-notebook-paragraph-control').first();
- this.codeEditor =
page.locator('zeppelin-notebook-paragraph-code-editor').first();
- this.dynamicForms =
page.locator('zeppelin-notebook-paragraph-dynamic-forms').first();
- this.resultDisplay =
page.locator('zeppelin-notebook-paragraph-result').first();
- this.footerInfo =
page.locator('zeppelin-notebook-paragraph-footer').first();
- this.runButton = page
- .locator('.paragraph-container')
- .first()
- .locator(
- 'button[nzTooltipTitle*="Run"], button[title*="Run"],
button:has-text("Run"), .run-button, [aria-label*="Run"],
i[nzType="play-circle"]:visible, button:has(i[nzType="play-circle"])'
- )
- .first();
- this.settingsDropdown = page
- .locator('.paragraph-container')
- .first()
- .locator('zeppelin-notebook-paragraph-control a[nz-dropdown]')
- .first();
+ this.paragraphContainers = page.locator('.paragraph-container');
+ // JUSTIFIED: fresh test notebooks contain one paragraph; its container is
the primary target.
+ this.paragraphContainer = this.paragraphContainers.first();
+ const addParagraphControls =
page.locator('zeppelin-notebook-add-paragraph');
+ // JUSTIFIED: the first add control inserts above the first paragraph.
+ this.addParagraphAbove = addParagraphControls.first();
+ // JUSTIFIED: the last add control trails the final paragraph and inserts
below it.
+ this.addParagraphBelow = addParagraphControls.last();
+ this.addParagraphAboveLink = this.addParagraphAbove.getByText('Add
Paragraph', { exact: true });
+ this.addParagraphBelowLink = this.addParagraphBelow.getByText('Add
Paragraph', { exact: true });
+ const controlPanels = page.locator('zeppelin-notebook-paragraph-control');
+ this.controlPanel =
this.paragraphContainer.locator('zeppelin-notebook-paragraph-control');
+ const codeEditors =
page.locator('zeppelin-notebook-paragraph-code-editor');
+ this.codeEditor =
this.paragraphContainer.locator('zeppelin-notebook-paragraph-code-editor');
+ this.codeEditorHost = this.codeEditor.locator('zeppelin-code-editor');
+ this.editorInput = this.codeEditor.getByRole('textbox', { name: 'Editor
content', exact: true });
+ this.editorViewLines = this.codeEditor.locator('.view-lines');
+ this.editorViewLinesAll = codeEditors.locator('.view-lines');
+ this.editorLines = this.codeEditor.locator('.view-line');
+ this.dynamicForms =
this.paragraphContainer.locator('zeppelin-notebook-paragraph-dynamic-forms');
+ this.resultDisplay =
this.paragraphContainer.locator('zeppelin-notebook-paragraph-result');
+ const footerInfo =
this.paragraphContainer.getByTestId('angular-paragraph-footer');
+ this.executionTime = footerInfo.locator('.execution-time');
+ this.elapsedTime = footerInfo.locator('.elapsed-time');
+ // JUSTIFIED: only the running paragraph renders a progress component.
+ this.progressIndicator =
page.locator('zeppelin-notebook-paragraph-progress').first();
+ this.progressBar = this.progressIndicator.locator('.ant-progress');
+ const paragraphTitle =
this.paragraphContainer.locator('zeppelin-elastic-input');
+ this.paragraphTitleText = paragraphTitle.locator('p');
+ this.paragraphTitleInput = paragraphTitle.getByRole('textbox');
+ // Monaco renders gutter line numbers inside its margin overlay.
+ this.lineNumbers = this.codeEditor.locator('.margin-view-overlays
.line-numbers');
+ this.runButton = this.controlPanel.locator('.run-para');
+ this.toggleEditorButton =
this.controlPanel.locator('a[nzTooltipTitle="Show/hide the code"]');
+ this.toggleOutputButton =
this.controlPanel.locator('a[nzTooltipTitle="Show/hide the output"]');
+ this.settingsDropdowns = controlPanels.locator('a[nz-dropdown]');
+ this.settingsDropdown = this.controlPanel.locator('a[nz-dropdown]');
+ this.settingsMenu = page.locator('ul.setting-menu');
+ this.paragraphIdMenuItem = this.settingsMenu.locator('.paragraph-id');
+ this.confirmButton = page.getByRole('button', { name: 'OK', exact: true });
+ this.runAllButton = page.locator('button[nzTooltipTitle="Run all
paragraphs"]');
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');
@@ -69,11 +125,27 @@ export class NotebookParagraphPage extends BasePage {
await this.paragraphContainer.dblclick();
}
+ async clickAddParagraphAbove(): Promise<void> {
+ await this.addParagraphAbove.hover();
+ await this.addParagraphAboveLink.click();
+ }
+
+ async clickAddParagraphBelow(): Promise<void> {
+ await this.addParagraphBelow.hover();
+ await this.addParagraphBelowLink.click();
+ }
+
async runParagraph(): Promise<void> {
await this.runButton.click();
}
- async openSettingsDropdown(): Promise<void> {
- await this.settingsDropdown.click();
+ settingsMenuItem(label: ParagraphSettingsMenuItem): Locator {
+ return this.settingsMenu.locator('li.list-item').filter({ hasText: label
});
+ }
+
+ async openSettingsDropdown(settingsDropdown: Locator =
this.settingsDropdown): Promise<void> {
+ await expect(this.settingsMenu).toBeHidden();
+ await settingsDropdown.click();
+ await expect(this.settingsMenu).toBeVisible();
}
}
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 6d01796326..21bd0c6c8c 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
@@ -14,6 +14,7 @@ import { expect, test } from '@playwright/test';
import { NotebookParagraphPage } from 'e2e/models/notebook-paragraph-page';
import { NotebookKeyboardPage } from 'e2e/models/notebook-keyboard-page';
import {
+ addPageAnnotation,
addPageAnnotationBeforeEach,
createTestNotebook,
performLoginIfRequired,
@@ -23,8 +24,9 @@ import {
} from '../../../utils';
test.describe('Notebook Paragraph Functionality', () => {
+ // JUSTIFIED: this legacy spec stores its page object and notebook id in
describe scope.
+ test.describe.configure({ mode: 'default' });
addPageAnnotationBeforeEach(PAGES.WORKSPACE.NOTEBOOK_PARAGRAPH);
- addPageAnnotationBeforeEach(PAGES.SHARE.CODE_EDITOR);
let paragraphPage: NotebookParagraphPage;
let testNotebook: { noteId: string; paragraphId: string };
@@ -43,57 +45,80 @@ test.describe('Notebook Paragraph Functionality', () => {
await expect(paragraphPage.paragraphContainer).toBeVisible({ timeout:
30000 });
});
- test('should display paragraph container with proper structure', async () =>
{
+ test('should display the paragraph container and control panel', async () =>
{
await expect(paragraphPage.paragraphContainer).toBeVisible();
await expect(paragraphPage.controlPanel).toBeVisible();
});
- test('should support double-click editing functionality', { tag:
'@NB-PARITY-003' }, async () => {
- await expect(paragraphPage.paragraphContainer).toBeVisible();
- await paragraphPage.doubleClickToEdit();
- await expect(paragraphPage.codeEditor).toBeVisible();
- });
+ test(
+ 'should reflect user edits in the code editor state and rendered lines',
+ { tag: '@NB-PARITY-003' },
+ async ({ page }, testInfo) => {
+ addPageAnnotation(PAGES.WORKSPACE.NOTEBOOK_PARAGRAPH_CODE_EDITOR,
testInfo);
+ const keyboard = new NotebookKeyboardPage(page);
+
+ await test.step('Given the paragraph is in edit mode', async () => {
+ await paragraphPage.doubleClickToEdit();
+ await expect(paragraphPage.codeEditor).toBeVisible();
+ await paragraphPage.editorInput.focus();
+ await expect(paragraphPage.editorInput).toBeFocused();
+ });
- test('should display add paragraph buttons', { tag: '@NB-PARITY-004' },
async () => {
- await expect(paragraphPage.addParagraphAbove).toBeVisible();
- await expect(paragraphPage.addParagraphAbove).toHaveCount(1);
- await expect(paragraphPage.addParagraphBelow).toBeVisible();
- await expect(paragraphPage.addParagraphBelow).toHaveCount(1);
- });
+ await test.step('When the user replaces the paragraph with five lines',
async () => {
+ await keyboard.pressSelectAll();
+ await page.keyboard.type('%md\nline one\nline two\nline three\nline
four');
+ });
- test('should display comprehensive control interface', async () => {
- await expect(paragraphPage.controlPanel).toBeVisible();
- await expect(paragraphPage.runButton).toBeVisible();
- await expect(paragraphPage.runButton).toBeEnabled();
- });
+ await test.step('Then Monaco renders the text and marks the editor
focused and dirty', async () => {
+ await expect(paragraphPage.editorViewLines).toContainText('line four');
+ await expect(paragraphPage.editorLines).toHaveCount(5);
+ await expect(paragraphPage.codeEditorHost).toHaveClass(/\bfocused\b/);
+ await expect(paragraphPage.codeEditorHost).toHaveClass(/\bdirty\b/);
- test('should display result system properly', { tag: '@NB-PARITY-021' },
async ({ page }) => {
- await expect(page).toHaveURL(/\/notebook\/[^\/]+/, { timeout: 10000 });
- await page.waitForLoadState('domcontentloaded');
- await expect(paragraphPage.paragraphContainer).toBeVisible({ timeout:
15000 });
- // JUSTIFIED: codeEditor is visibility:hidden before double-click;
toBeAttached confirms it's in the DOM
- await expect(paragraphPage.codeEditor).toBeAttached({ timeout: 10000 });
+ await page.keyboard.press('Escape');
+ await
expect(paragraphPage.codeEditorHost).not.toHaveClass(/\bfocused\b/);
+ });
+ }
+ );
- await paragraphPage.doubleClickToEdit();
- await expect(paragraphPage.codeEditor).toBeVisible();
+ test(
+ 'should insert default paragraphs above and below the original paragraph',
+ { tag: '@NB-PARITY-004' },
+ async ({ page }, testInfo) => {
+ addPageAnnotation(PAGES.WORKSPACE.NOTEBOOK_ADD_PARAGRAPH, testInfo);
+ const keyboard = new NotebookKeyboardPage(page);
+ const originalText = 'Original paragraph marker';
+
+ await test.step('Given one paragraph with distinctive text', async () =>
{
+ await keyboard.setCodeEditorContent(`%md\n${originalText}`);
+ await expect(paragraphPage.paragraphContainers).toHaveCount(1);
+ });
- // JUSTIFIED: compound selector; first() picks primary Monaco input
- const codeEditor = paragraphPage.codeEditor.locator('textarea,
.monaco-editor .input-area').first();
- // JUSTIFIED: Monaco textarea may be visibility:hidden before focus;
toBeAttached confirms DOM presence
- await expect(codeEditor).toBeAttached({ timeout: 10000 });
- await expect(codeEditor).toBeEnabled({ timeout: 10000 });
+ await test.step('When the trailing Add Paragraph control is clicked',
async () => {
+ await paragraphPage.clickAddParagraphBelow();
+ });
- await codeEditor.focus();
- await expect(codeEditor).toBeFocused({ timeout: 5000 });
+ await test.step('Then a new paragraph is inserted below the original',
async () => {
+ await expect(paragraphPage.paragraphContainers).toHaveCount(2);
+ // JUSTIFIED: the first rendered editor is the original paragraph
after inserting below.
+ await
expect(paragraphPage.editorViewLinesAll.first()).toContainText(originalText);
+ // JUSTIFIED: the last rendered editor is the newly inserted default
paragraph.
+ await
expect(paragraphPage.editorViewLinesAll.last()).toHaveText('%md');
+ });
- const notebookKeyboardPage = new NotebookKeyboardPage(page);
- await notebookKeyboardPage.pressSelectAll();
- await page.keyboard.type('%python\nprint("Hello World")');
+ await test.step('When the leading Add Paragraph control is clicked',
async () => {
+ await paragraphPage.clickAddParagraphAbove();
+ });
- await paragraphPage.runParagraph();
- await expect(paragraphPage.resultDisplay).toBeVisible({ timeout: 15000 });
- await expect(paragraphPage.resultDisplay).not.toBeEmpty();
- });
+ await test.step('Then a default paragraph is inserted above the
original', async () => {
+ await expect(paragraphPage.paragraphContainers).toHaveCount(3);
+ // JUSTIFIED: inserting above moves the original paragraph to the
second position.
+ await
expect(paragraphPage.editorViewLinesAll.nth(1)).toContainText(originalText);
+ // JUSTIFIED: the first rendered editor is the newly inserted default
paragraph.
+ await
expect(paragraphPage.editorViewLinesAll.first()).toHaveText('%md');
+ });
+ }
+ );
test(
'should accumulate interpreter output while the paragraph is running',
@@ -126,88 +151,214 @@ test.describe('Notebook Paragraph Functionality', () => {
}
);
- test('should display dynamic forms', async ({ page }) => {
- test.skip(!!process.env.CI, 'Dynamic form tests require a Spark
interpreter — skipped on CI');
+ test('should expose the settings available for a single paragraph', async
({}, testInfo) => {
+ addPageAnnotation(PAGES.WORKSPACE.NOTEBOOK_PARAGRAPH_CONTROL, testInfo);
- await paragraphPage.doubleClickToEdit();
- await expect(paragraphPage.codeEditor).toBeVisible();
+ await paragraphPage.openSettingsDropdown();
- // JUSTIFIED: compound selector; first() picks primary Monaco input
- const codeEditor = paragraphPage.codeEditor.locator('textarea,
.monaco-editor .input-area').first();
- await expect(codeEditor).toBeAttached({ timeout: 10000 });
- await expect(codeEditor).toBeEnabled({ timeout: 10000 });
-
- await codeEditor.focus();
- await expect(codeEditor).toBeFocused({ timeout: 5000 });
-
- const notebookKeyboardPage = new NotebookKeyboardPage(page);
- await notebookKeyboardPage.pressSelectAll();
- await page.keyboard.type(`%spark
-println("Name: " + z.input("name", "World"))
-println("Age: " + z.select("age", Seq(("1","Under 18"), ("2","18-65"),
("3","Over 65"))))
-`);
-
- await paragraphPage.runParagraph();
- await expect(paragraphPage.resultDisplay).toBeVisible({ timeout: 15000 });
-
- // Handles error cases gracefully — Spark may not be available
- // JUSTIFIED: result display may not exist when interpreter is
unavailable; null triggers graceful fallback below
- const resultText = await
paragraphPage.resultDisplay.textContent().catch(() => null);
- const hasInterpreterError =
- resultText &&
- ((resultText.toLowerCase().includes('interpreter') &&
resultText.toLowerCase().includes('not found')) ||
- resultText.toLowerCase().includes('error'));
-
- if (hasInterpreterError) {
- await expect(paragraphPage.resultDisplay).toBeVisible();
- } else {
- await expect(paragraphPage.dynamicForms).toBeVisible();
- }
+ await expect(paragraphPage.settingsMenu).toBeVisible();
+ await
expect(paragraphPage.paragraphIdMenuItem).toHaveText(/paragraph_\d+_\d+/);
+ await expect(paragraphPage.settingsMenuItem('Width')).toBeVisible();
+ await expect(paragraphPage.settingsMenuItem('Font size')).toBeVisible();
+ await expect(paragraphPage.settingsMenuItem('Insert new')).toBeVisible();
+ await expect(paragraphPage.settingsMenuItem('Clone
paragraph')).toBeVisible();
+ await expect(paragraphPage.settingsMenuItem('Clear output')).toBeVisible();
+ await expect(paragraphPage.settingsMenuItem('Show Title')).toBeVisible();
+ await expect(paragraphPage.settingsMenuItem('Show line
numbers')).toBeVisible();
+ await expect(paragraphPage.settingsMenuItem('Disable run')).toBeVisible();
+ await expect(paragraphPage.settingsMenuItem('Remove')).toHaveCount(0);
+ await expect(paragraphPage.settingsMenuItem('Move paragraph
up')).toHaveCount(0);
+ await expect(paragraphPage.settingsMenuItem('Move paragraph
down')).toHaveCount(0);
});
- test('should render footer element in paragraph DOM', async () => {
- // JUSTIFIED: footer is visibility:hidden by default (hover-only);
toBeAttached confirms it's rendered in DOM
- await expect(paragraphPage.footerInfo).toBeAttached();
+ test('should report running and finished execution state', { tag:
'@NB-PARITY-021' }, async ({ page }, testInfo) => {
+ addPageAnnotation(PAGES.WORKSPACE.NOTEBOOK_PARAGRAPH_CONTROL, testInfo);
+ addPageAnnotation(PAGES.WORKSPACE.NOTEBOOK_PARAGRAPH_FOOTER, testInfo);
+ addPageAnnotation(PAGES.WORKSPACE.NOTEBOOK_PARAGRAPH_PROGRESS, testInfo);
+ const keyboard = new NotebookKeyboardPage(page);
+
+ await test.step('Given a paragraph that runs long enough to observe',
async () => {
+ await keyboard.setCodeEditorContent('%python\nimport
time\ntime.sleep(2)\nprint("lifecycle complete")');
+ await expect(paragraphPage.status).toHaveText('READY');
+ });
+
+ await test.step('When the paragraph starts', async () => {
+ await paragraphPage.runParagraph();
+ });
+
+ await test.step('Then running controls, progress, and elapsed time are
shown', async () => {
+ await expect(paragraphPage.status).toHaveText('RUNNING', { timeout:
60000 });
+ await expect(paragraphPage.cancelButton).toBeVisible();
+ await expect(paragraphPage.progressBar).toBeVisible();
+ await expect(paragraphPage.elapsedTime).toHaveText(/^Started .+ ago\.$/,
{ timeout: 15000 });
+ });
+
+ await test.step('Then completion removes progress and shows result
timing', async () => {
+ await expect(paragraphPage.status).toHaveText('FINISHED', { timeout:
60000 });
+ await expect(paragraphPage.resultDisplay).toContainText('lifecycle
complete');
+ await expect(paragraphPage.progressIndicator).toHaveCount(0);
+ await paragraphPage.paragraphContainer.hover();
+ await expect(paragraphPage.executionTime).toHaveText(/^Took .+\. Last
updated by .+ at .+\./);
+ });
+
+ await test.step('When the finished paragraph is edited', async () => {
+ await paragraphPage.editorInput.focus();
+ await page.keyboard.type(' ');
+ await page.keyboard.press('Escape');
+ });
+
+ await test.step('Then the previous result is marked outdated and can be
cleared', async () => {
+ await expect(paragraphPage.executionTime).toContainText('(outdated)');
+ await paragraphPage.openSettingsDropdown();
+ await paragraphPage.settingsMenuItem('Clear output').click();
+ await expect(paragraphPage.resultDisplay).toHaveCount(0);
+ });
});
- test('should provide paragraph control actions', async ({ page }) => {
- await expect(page).toHaveURL(/\/notebook\/[^\/]+/, { timeout: 10000 });
- await expect(paragraphPage.paragraphContainer).toBeVisible({ timeout:
15000 });
+ test('should edit a paragraph title through the elastic input', async ({},
testInfo) => {
+ addPageAnnotation(PAGES.WORKSPACE.NOTEBOOK_PARAGRAPH_CONTROL, testInfo);
+ addPageAnnotation(PAGES.WORKSPACE.NOTEBOOK_ELASTIC_INPUT, testInfo);
+
+ await test.step('Given paragraph titles are enabled', async () => {
+ await paragraphPage.openSettingsDropdown();
+ await paragraphPage.settingsMenuItem('Show Title').click();
+ await expect(paragraphPage.paragraphTitleText).toHaveText('Untitled');
+ });
+
+ await test.step('When a title is entered and committed with Enter', async
() => {
+ await paragraphPage.paragraphTitleText.click();
+ await paragraphPage.paragraphTitleInput.fill('My paragraph');
+ await paragraphPage.paragraphTitleInput.press('Enter');
+ });
+
+ await test.step('Then the committed title replaces the input', async () =>
{
+ await expect(paragraphPage.paragraphTitleInput).toHaveCount(0);
+ await expect(paragraphPage.paragraphTitleText).toHaveText('My
paragraph');
+ });
+
+ await test.step('When a different title is cancelled with Escape', async
() => {
+ await paragraphPage.paragraphTitleText.click();
+ await paragraphPage.paragraphTitleInput.fill('Temporary title');
+ await paragraphPage.paragraphTitleInput.press('Escape');
+ });
+
+ await test.step('Then the committed title remains unchanged', async () => {
+ await expect(paragraphPage.paragraphTitleText).toHaveText('My
paragraph');
+ await expect(paragraphPage.paragraphTitleInput).toHaveCount(0);
+ });
+
+ await test.step('When a longer title is entered and the input loses
focus', async () => {
+ await paragraphPage.paragraphTitleText.click();
+ const longTitle = 'A much longer paragraph title that commits on blur';
+ await paragraphPage.paragraphTitleInput.fill(longTitle);
+ await paragraphPage.paragraphTitleInput.blur();
+ await expect(paragraphPage.paragraphTitleText).toHaveText(longTitle);
+ await expect(paragraphPage.paragraphTitleInput).toHaveCount(0);
+ });
+ });
+
+ test('should clone a paragraph with its editor content', async ({ page },
testInfo) => {
+ addPageAnnotation(PAGES.WORKSPACE.NOTEBOOK_PARAGRAPH_CONTROL, testInfo);
+ const keyboard = new NotebookKeyboardPage(page);
+ const cloneMarker = 'Distinctive clone marker';
+ await keyboard.setCodeEditorContent(`%md\n${cloneMarker}`);
await paragraphPage.openSettingsDropdown();
+ await paragraphPage.settingsMenuItem('Clone paragraph').click();
- const dropdownMenu = page.locator('ul.ant-dropdown-menu, .dropdown-menu');
- await expect(dropdownMenu).toBeVisible({ timeout: 5000 });
- await expect(page.locator('li:has-text("Insert")')).toBeVisible();
- await expect(page.locator('li:has-text("Clone")')).toBeVisible();
+ await expect(paragraphPage.paragraphContainers).toHaveCount(2);
+ await expect(paragraphPage.editorViewLinesAll).toHaveText([
+ /Distinctive\s+clone\s+marker/,
+ /Distinctive\s+clone\s+marker/
+ ]);
+ });
- await page.keyboard.press('Escape');
+ test('should insert, move, and remove paragraphs through the settings menu',
async ({ page }, testInfo) => {
+ addPageAnnotation(PAGES.WORKSPACE.NOTEBOOK_PARAGRAPH_CONTROL, testInfo);
+ const keyboard = new NotebookKeyboardPage(page);
+ const originalMarker = 'Paragraph to move';
+
+ await keyboard.setCodeEditorContent(`%md\n${originalMarker}`);
+ await paragraphPage.openSettingsDropdown();
+ await paragraphPage.settingsMenuItem('Insert new').click();
+ await expect(paragraphPage.paragraphContainers).toHaveCount(2);
+
+ await paragraphPage.openSettingsDropdown();
+ await expect(paragraphPage.settingsMenuItem('Remove')).toBeVisible();
+ await expect(paragraphPage.settingsMenuItem('Move paragraph
down')).toBeVisible();
+ await expect(paragraphPage.settingsMenuItem('Move paragraph
up')).toHaveCount(0);
+ await paragraphPage.settingsMenuItem('Move paragraph down').click();
+
+ // JUSTIFIED: after moving the original first paragraph down, its editor
is last.
+ await
expect(paragraphPage.editorViewLinesAll.last()).toContainText(originalMarker);
+
+ // JUSTIFIED: remove the moved paragraph through its last settings trigger.
+ await
paragraphPage.openSettingsDropdown(paragraphPage.settingsDropdowns.last());
+ await paragraphPage.settingsMenuItem('Remove').click();
+ await expect(paragraphPage.confirmButton).toBeVisible();
+ await paragraphPage.confirmButton.click();
+ await expect(paragraphPage.paragraphContainers).toHaveCount(1);
+ // JUSTIFIED: removing the moved original leaves the inserted default
paragraph first.
+ await expect(paragraphPage.editorViewLinesAll.first()).toHaveText('%md');
});
- test('should show cancel button during execution', async ({ page }) => {
- await expect(page).toHaveURL(/\/notebook\/[^\/]+/, { timeout: 10000 });
- await expect(paragraphPage.paragraphContainer).toBeVisible({ timeout:
15000 });
+ test('should toggle paragraph code, output, line numbers, and run state',
async ({ page }, testInfo) => {
+ addPageAnnotation(PAGES.WORKSPACE.NOTEBOOK_PARAGRAPH_CONTROL, testInfo);
+ addPageAnnotation(PAGES.WORKSPACE.NOTEBOOK_PARAGRAPH_CODE_EDITOR,
testInfo);
+
+ await paragraphPage.toggleEditorButton.click();
+ await expect(paragraphPage.codeEditor).toHaveCount(0);
+ await paragraphPage.toggleEditorButton.click();
+ await expect(paragraphPage.codeEditor).toBeVisible();
+
+ await paragraphPage.toggleOutputButton.click();
+ await expect(paragraphPage.dynamicForms).toHaveCount(0);
+ await paragraphPage.toggleOutputButton.click();
+ await expect(paragraphPage.dynamicForms).toHaveCount(1);
+
+ await paragraphPage.openSettingsDropdown();
+ await paragraphPage.settingsMenuItem('Show line numbers').click();
+ await expect(paragraphPage.lineNumbers).toBeVisible();
+ await expect(paragraphPage.settingsMenuItem('Hide line
numbers')).toBeVisible();
+ await paragraphPage.settingsMenuItem('Hide line numbers').click();
+ await expect(paragraphPage.lineNumbers).toHaveCount(0);
+ await page.keyboard.press('Escape');
+
+ await paragraphPage.openSettingsDropdown();
+ await paragraphPage.settingsMenuItem('Disable run').click();
+ await expect(paragraphPage.runButton).toHaveCount(0);
+ await expect(paragraphPage.settingsMenuItem('Enable run')).toBeVisible();
+ await paragraphPage.settingsMenuItem('Enable run').click();
await expect(paragraphPage.runButton).toBeVisible();
- await expect(paragraphPage.runButton).toBeEnabled();
+ });
- const notebookKeyboardPage = new NotebookKeyboardPage(page);
- const code = '%python\nimport time;time.sleep(10)\nprint("Done")';
- // Seed the run prerequisite without Monaco's per-keystroke auto-closing
edits.
- await notebookKeyboardPage.setCodeEditorContent(code);
- await expect.poll(() =>
notebookKeyboardPage.getParagraphTextByIndex(0)).toBe(code);
+ test('should disable insertion and expose cancellation state while running',
async ({ page }, testInfo) => {
+ addPageAnnotation(PAGES.WORKSPACE.NOTEBOOK_ADD_PARAGRAPH, testInfo);
+ addPageAnnotation(PAGES.WORKSPACE.NOTEBOOK_PARAGRAPH_CONTROL, testInfo);
+ addPageAnnotation(PAGES.WORKSPACE.NOTEBOOK_PARAGRAPH_FOOTER, testInfo);
+ addPageAnnotation(PAGES.WORKSPACE.NOTEBOOK_PARAGRAPH_PROGRESS, testInfo);
+ const keyboard = new NotebookKeyboardPage(page);
- await paragraphPage.runParagraph();
+ const code = '%python\nimport time\ntime.sleep(10)\nprint("Done")';
+ await keyboard.setCodeEditorContent(code);
+ await expect.poll(() => keyboard.getParagraphTextByIndex(0)).toBe(code);
+ await paragraphPage.runAllButton.click();
+ await expect(paragraphPage.confirmButton).toBeVisible();
+ await paragraphPage.confirmButton.click();
- // 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 expect(paragraphPage.status).toHaveText('RUNNING', { timeout: 60000
});
+ await expect(paragraphPage.progressBar).toBeVisible();
+ await expect(paragraphPage.elapsedTime).toHaveText(/^Started .+ ago\.$/, {
timeout: 15000 });
+ await
expect(paragraphPage.addParagraphAboveLink).toHaveClass(/\bdisabled\b/);
+ await
expect(paragraphPage.addParagraphBelowLink).toHaveClass(/\bdisabled\b/);
- await paragraphPage.cancelButton.click();
+ await paragraphPage.clickAddParagraphBelow();
+ await expect(paragraphPage.paragraphContainers).toHaveCount(1);
- // 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 paragraphPage.cancelButton.click();
await expect(paragraphPage.status).toHaveText('ABORT', { timeout: 30000 });
+ await expect(paragraphPage.progressIndicator).toHaveCount(0);
+ await expect(paragraphPage.elapsedTime).toHaveCount(0);
});
});