blob: 105c4226202857cebb4184a5753a6277756468cf [file]
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { expect, test } from '@playwright/test';
import { NotebookParagraphPage } from 'e2e/models/notebook-paragraph-page';
import { NotebookKeyboardPage } from 'e2e/models/notebook-keyboard-page';
import {
addPageAnnotationBeforeEach,
performLoginIfRequired,
waitForZeppelinReady,
PAGES,
createTestNotebook
} from '../../../utils';
test.describe('Notebook Paragraph Functionality', () => {
addPageAnnotationBeforeEach(PAGES.WORKSPACE.NOTEBOOK_PARAGRAPH);
addPageAnnotationBeforeEach(PAGES.SHARE.CODE_EDITOR);
let paragraphPage: NotebookParagraphPage;
let testNotebook: { noteId: string; paragraphId: string };
test.beforeEach(async ({ page }) => {
await page.goto('/#/');
await waitForZeppelinReady(page);
await performLoginIfRequired(page);
testNotebook = await createTestNotebook(page);
paragraphPage = new NotebookParagraphPage(page);
await page.goto(`/#/notebook/${testNotebook.noteId}`);
// 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 () => {
await expect(paragraphPage.paragraphContainer).toBeVisible();
await expect(paragraphPage.controlPanel).toBeVisible();
});
test('should support double-click editing functionality', async () => {
await expect(paragraphPage.paragraphContainer).toBeVisible();
await paragraphPage.doubleClickToEdit();
await expect(paragraphPage.codeEditor).toBeVisible();
});
test('should display add paragraph buttons', async () => {
await expect(paragraphPage.addParagraphAbove).toBeVisible();
await expect(paragraphPage.addParagraphAbove).toHaveCount(1);
await expect(paragraphPage.addParagraphBelow).toBeVisible();
await expect(paragraphPage.addParagraphBelow).toHaveCount(1);
});
test('should display comprehensive control interface', async () => {
await expect(paragraphPage.controlPanel).toBeVisible();
await expect(paragraphPage.runButton).toBeVisible();
await expect(paragraphPage.runButton).toBeEnabled();
});
test('should display result system properly', 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 paragraphPage.doubleClickToEdit();
await expect(paragraphPage.codeEditor).toBeVisible();
// 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 codeEditor.focus();
await expect(codeEditor).toBeFocused({ timeout: 5000 });
const notebookKeyboardPage = new NotebookKeyboardPage(page);
await notebookKeyboardPage.pressSelectAll();
await page.keyboard.type('%python\nprint("Hello World")');
await paragraphPage.runParagraph();
await expect(paragraphPage.resultDisplay).toBeVisible({ timeout: 15000 });
await expect(paragraphPage.resultDisplay).not.toBeEmpty();
});
test('should display dynamic forms', async ({ page }) => {
test.skip(!!process.env.CI, 'Dynamic form tests require a Spark interpreter — skipped on CI');
await paragraphPage.doubleClickToEdit();
await expect(paragraphPage.codeEditor).toBeVisible();
// 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();
}
});
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 provide paragraph control actions', async ({ page }) => {
await expect(page).toHaveURL(/\/notebook\/[^\/]+/, { timeout: 10000 });
await expect(paragraphPage.paragraphContainer).toBeVisible({ timeout: 15000 });
await paragraphPage.openSettingsDropdown();
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 page.keyboard.press('Escape');
});
test('should show cancel button during execution', async ({ page }) => {
await expect(page).toHaveURL(/\/notebook\/[^\/]+/, { timeout: 10000 });
await expect(paragraphPage.paragraphContainer).toBeVisible({ timeout: 15000 });
await expect(paragraphPage.runButton).toBeVisible();
await expect(paragraphPage.runButton).toBeEnabled();
await paragraphPage.doubleClickToEdit();
await expect(paragraphPage.codeEditor).toBeVisible();
// 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('%python\nimport time;time.sleep(10)\nprint("Done")');
await paragraphPage.runParagraph();
// 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 paragraphPage.cancelButton.click();
// 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 });
});
});