| /* |
| * 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 }); |
| }); |
| }); |