From f370c4e4cbb0201338d309aacad4ab4abb3bb41d Mon Sep 17 00:00:00 2001 From: YONGJAE LEE Date: Thu, 30 Oct 2025 23:01:50 +0900 Subject: [PATCH 1/7] add share area e2e tests --- .../e2e/models/about-zeppelin-modal.ts | 69 +++++++ .../e2e/models/header-page.ts | 152 +++++++++++++++ .../e2e/models/header-page.util.ts | 133 +++++++++++++ .../e2e/models/node-list-page.ts | 176 ++++++++++++++++++ .../e2e/models/note-create-modal.ts | 117 ++++++++++++ .../e2e/models/note-create-modal.util.ts | 88 +++++++++ .../e2e/models/note-import-modal.ts | 119 ++++++++++++ .../about-zeppelin-modal.spec.ts | 69 +++++++ .../share/header/header-navigation.spec.ts | 73 ++++++++ .../tests/share/header/header-search.spec.ts | 42 +++++ .../node-list/node-list-functionality.spec.ts | 88 +++++++++ .../note-create/note-create-modal.spec.ts | 95 ++++++++++ .../note-import/note-import-modal.spec.ts | 111 +++++++++++ 13 files changed, 1332 insertions(+) create mode 100644 zeppelin-web-angular/e2e/models/about-zeppelin-modal.ts create mode 100644 zeppelin-web-angular/e2e/models/header-page.ts create mode 100644 zeppelin-web-angular/e2e/models/header-page.util.ts create mode 100644 zeppelin-web-angular/e2e/models/node-list-page.ts create mode 100644 zeppelin-web-angular/e2e/models/note-create-modal.ts create mode 100644 zeppelin-web-angular/e2e/models/note-create-modal.util.ts create mode 100644 zeppelin-web-angular/e2e/models/note-import-modal.ts create mode 100644 zeppelin-web-angular/e2e/tests/share/about-zeppelin/about-zeppelin-modal.spec.ts create mode 100644 zeppelin-web-angular/e2e/tests/share/header/header-navigation.spec.ts create mode 100644 zeppelin-web-angular/e2e/tests/share/header/header-search.spec.ts create mode 100644 zeppelin-web-angular/e2e/tests/share/node-list/node-list-functionality.spec.ts create mode 100644 zeppelin-web-angular/e2e/tests/share/note-create/note-create-modal.spec.ts create mode 100644 zeppelin-web-angular/e2e/tests/share/note-import/note-import-modal.spec.ts diff --git a/zeppelin-web-angular/e2e/models/about-zeppelin-modal.ts b/zeppelin-web-angular/e2e/models/about-zeppelin-modal.ts new file mode 100644 index 00000000000..d488c6f163c --- /dev/null +++ b/zeppelin-web-angular/e2e/models/about-zeppelin-modal.ts @@ -0,0 +1,69 @@ +/* + * 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 { Locator, Page } from '@playwright/test'; +import { BasePage } from './base-page'; + +export class AboutZeppelinModal extends BasePage { + readonly modal: Locator; + readonly modalTitle: Locator; + readonly closeButton: Locator; + readonly logo: Locator; + readonly heading: Locator; + readonly versionText: Locator; + readonly getInvolvedLink: Locator; + readonly licenseLink: Locator; + + constructor(page: Page) { + super(page); + this.modal = page.locator('[role="dialog"]').filter({ has: page.getByText('About Zeppelin') }); + this.modalTitle = page.locator('.ant-modal-title', { hasText: 'About Zeppelin' }); + this.closeButton = page.getByRole('button', { name: 'Close' }); + this.logo = page.locator('img[alt="Apache Zeppelin"]'); + this.heading = page.locator('h3', { hasText: 'Apache Zeppelin' }); + this.versionText = page.locator('.about-version'); + this.getInvolvedLink = page.getByRole('link', { name: 'Get involved!' }); + this.licenseLink = page.getByRole('link', { name: 'Licensed under the Apache License, Version 2.0' }); + } + + async isModalVisible(): Promise { + return this.modal.isVisible(); + } + + async close(): Promise { + await this.closeButton.click(); + } + + async getVersionText(): Promise { + return (await this.versionText.textContent()) || ''; + } + + async isLogoVisible(): Promise { + return this.logo.isVisible(); + } + + async clickGetInvolvedLink(): Promise { + await this.getInvolvedLink.click(); + } + + async clickLicenseLink(): Promise { + await this.licenseLink.click(); + } + + async getGetInvolvedHref(): Promise { + return this.getInvolvedLink.getAttribute('href'); + } + + async getLicenseHref(): Promise { + return this.licenseLink.getAttribute('href'); + } +} diff --git a/zeppelin-web-angular/e2e/models/header-page.ts b/zeppelin-web-angular/e2e/models/header-page.ts new file mode 100644 index 00000000000..92dae5f1722 --- /dev/null +++ b/zeppelin-web-angular/e2e/models/header-page.ts @@ -0,0 +1,152 @@ +/* + * 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 { Locator, Page } from '@playwright/test'; +import { BasePage } from './base-page'; + +export class HeaderPage extends BasePage { + readonly header: Locator; + readonly brandLogo: Locator; + readonly brandLink: Locator; + readonly notebookMenuItem: Locator; + readonly notebookDropdownTrigger: Locator; + readonly notebookDropdown: Locator; + readonly jobMenuItem: Locator; + readonly userDropdownTrigger: Locator; + readonly userBadge: Locator; + readonly userDropdown: Locator; + readonly searchInput: Locator; + readonly themeToggleButton: Locator; + readonly connectionStatusBadge: Locator; + + readonly userMenuItems: { + aboutZeppelin: Locator; + interpreter: Locator; + notebookRepos: Locator; + credential: Locator; + configuration: Locator; + logout: Locator; + switchToClassicUI: Locator; + }; + + constructor(page: Page) { + super(page); + this.header = page.locator('.header'); + this.brandLogo = page.locator('.header .brand .logo'); + this.brandLink = page.locator('.header .brand'); + this.notebookMenuItem = page.locator('[nz-menu-item]').filter({ hasText: 'Notebook' }); + this.notebookDropdownTrigger = page.locator('.node-list-trigger'); + this.notebookDropdown = page.locator('zeppelin-node-list'); + this.jobMenuItem = page.getByRole('link', { name: 'Job' }); + this.userDropdownTrigger = page.locator('.header .user .status'); + this.userBadge = page.locator('.header .user nz-badge'); + this.userDropdown = page.locator('ul[nz-menu]').filter({ has: page.getByText('About Zeppelin') }); + this.searchInput = page.locator('.header .search input[type="text"]'); + this.themeToggleButton = page.locator('zeppelin-theme-toggle button'); + this.connectionStatusBadge = page.locator('.header .user nz-badge'); + + this.userMenuItems = { + aboutZeppelin: page.getByText('About Zeppelin', { exact: true }), + interpreter: page.getByRole('link', { name: 'Interpreter' }), + notebookRepos: page.getByRole('link', { name: 'Notebook Repos' }), + credential: page.getByRole('link', { name: 'Credential' }), + configuration: page.getByRole('link', { name: 'Configuration' }), + logout: page.getByText('Logout', { exact: true }), + switchToClassicUI: page.getByRole('link', { name: 'Switch to Classic UI' }) + }; + } + + async clickBrandLogo(): Promise { + await this.brandLink.waitFor({ state: 'visible', timeout: 10000 }); + await this.brandLink.click(); + } + + async clickNotebookMenu(): Promise { + await this.notebookDropdownTrigger.waitFor({ state: 'visible', timeout: 10000 }); + await this.notebookDropdownTrigger.click(); + } + + async clickJobMenu(): Promise { + await this.jobMenuItem.waitFor({ state: 'visible', timeout: 10000 }); + await this.jobMenuItem.click(); + } + + async clickUserDropdown(): Promise { + await this.userDropdownTrigger.waitFor({ state: 'visible', timeout: 10000 }); + await this.userDropdownTrigger.click(); + } + + async clickAboutZeppelin(): Promise { + await this.userMenuItems.aboutZeppelin.click(); + } + + async clickInterpreter(): Promise { + await this.userMenuItems.interpreter.click(); + } + + async clickNotebookRepos(): Promise { + await this.userMenuItems.notebookRepos.click(); + } + + async clickCredential(): Promise { + await this.userMenuItems.credential.click(); + } + + async clickConfiguration(): Promise { + await this.userMenuItems.configuration.click(); + } + + async clickLogout(): Promise { + await this.userMenuItems.logout.click(); + } + + async clickSwitchToClassicUI(): Promise { + await this.userMenuItems.switchToClassicUI.click(); + } + + async isHeaderVisible(): Promise { + return this.header.isVisible(); + } + + async getUsernameText(): Promise { + return (await this.userBadge.textContent()) || ''; + } + + async getConnectionStatus(): Promise { + const status = await this.connectionStatusBadge.locator('.ant-badge-status-dot').getAttribute('class'); + if (status?.includes('success')) { + return 'success'; + } + if (status?.includes('error')) { + return 'error'; + } + return 'unknown'; + } + + async isNotebookDropdownVisible(): Promise { + return this.notebookDropdown.isVisible(); + } + + async isUserDropdownVisible(): Promise { + return this.userDropdown.isVisible(); + } + + async isLogoutMenuItemVisible(): Promise { + return this.userMenuItems.logout.isVisible(); + } + + async searchNote(query: string): Promise { + await this.searchInput.waitFor({ state: 'visible', timeout: 10000 }); + await this.searchInput.fill(query); + await this.page.keyboard.press('Enter'); + } +} diff --git a/zeppelin-web-angular/e2e/models/header-page.util.ts b/zeppelin-web-angular/e2e/models/header-page.util.ts new file mode 100644 index 00000000000..8a89cdaf483 --- /dev/null +++ b/zeppelin-web-angular/e2e/models/header-page.util.ts @@ -0,0 +1,133 @@ +/* + * 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, Page } from '@playwright/test'; +import { HeaderPage } from './header-page'; +import { AboutZeppelinModal } from './about-zeppelin-modal'; +import { NodeListPage } from './node-list-page'; + +export class HeaderPageUtil { + constructor( + private readonly page: Page, + private readonly headerPage: HeaderPage + ) {} + + async verifyHeaderIsDisplayed(): Promise { + await expect(this.headerPage.header).toBeVisible(); + await expect(this.headerPage.brandLogo).toBeVisible(); + await expect(this.headerPage.notebookMenuItem).toBeVisible(); + await expect(this.headerPage.jobMenuItem).toBeVisible(); + await expect(this.headerPage.userDropdownTrigger).toBeVisible(); + await expect(this.headerPage.searchInput).toBeVisible(); + await expect(this.headerPage.themeToggleButton).toBeVisible(); + } + + async verifyNavigationToHomePage(): Promise { + await this.headerPage.clickBrandLogo(); + await this.page.waitForURL(/\/(#\/)?$/); + const url = this.page.url(); + expect(url).toMatch(/\/(#\/)?$/); + } + + async verifyNavigationToJobManager(): Promise { + await this.headerPage.clickJobMenu(); + await this.page.waitForURL(/jobmanager/); + expect(this.page.url()).toContain('jobmanager'); + } + + async verifyUserDropdownOpens(): Promise { + await this.headerPage.clickUserDropdown(); + await expect(this.headerPage.userMenuItems.aboutZeppelin).toBeVisible(); + } + + async verifyNotebookDropdownOpens(): Promise { + await this.headerPage.clickNotebookMenu(); + // Target the header dropdown version specifically using ng-reflect-header-mode attribute + const dropdownNodeList = this.page.locator('zeppelin-node-list[ng-reflect-header-mode="true"]'); + await expect(dropdownNodeList).toBeVisible(); + } + + async verifyAboutZeppelinModalOpens(): Promise { + await this.headerPage.clickUserDropdown(); + await this.headerPage.clickAboutZeppelin(); + + const aboutModal = new AboutZeppelinModal(this.page); + await expect(aboutModal.modal).toBeVisible(); + } + + async verifySearchNavigation(query: string): Promise { + await this.headerPage.searchNote(query); + await this.page.waitForURL(/search/); + expect(this.page.url()).toContain('search'); + expect(this.page.url()).toContain(query); + } + + async verifyConnectionStatus(): Promise { + const status = await this.headerPage.getConnectionStatus(); + expect(['success', 'error']).toContain(status); + } + + async verifyUserMenuItemsVisible(isLoggedIn: boolean): Promise { + await this.headerPage.clickUserDropdown(); + await expect(this.headerPage.userMenuItems.aboutZeppelin).toBeVisible(); + await expect(this.headerPage.userMenuItems.interpreter).toBeVisible(); + await expect(this.headerPage.userMenuItems.notebookRepos).toBeVisible(); + await expect(this.headerPage.userMenuItems.credential).toBeVisible(); + await expect(this.headerPage.userMenuItems.configuration).toBeVisible(); + await expect(this.headerPage.userMenuItems.switchToClassicUI).toBeVisible(); + + if (isLoggedIn) { + const username = await this.headerPage.getUsernameText(); + expect(username).not.toBe('anonymous'); + await expect(this.headerPage.userMenuItems.logout).toBeVisible(); + } + } + + async openNotebookDropdownAndVerifyNodeList(): Promise { + await this.headerPage.clickNotebookMenu(); + await expect(this.headerPage.notebookDropdown).toBeVisible(); + + const nodeList = new NodeListPage(this.page); + await expect(nodeList.createNewNoteButton).toBeVisible(); + await expect(nodeList.importNoteButton).toBeVisible(); + await expect(nodeList.filterInput).toBeVisible(); + await expect(nodeList.treeView).toBeVisible(); + } + + async navigateToInterpreterSettings(): Promise { + await this.headerPage.clickUserDropdown(); + await this.headerPage.clickInterpreter(); + await this.page.waitForURL(/interpreter/); + expect(this.page.url()).toContain('interpreter'); + } + + async navigateToNotebookRepos(): Promise { + await this.headerPage.clickUserDropdown(); + await this.headerPage.clickNotebookRepos(); + await this.page.waitForURL(/notebook-repos/); + expect(this.page.url()).toContain('notebook-repos'); + } + + async navigateToCredential(): Promise { + await this.headerPage.clickUserDropdown(); + await this.headerPage.clickCredential(); + await this.page.waitForURL(/credential/); + expect(this.page.url()).toContain('credential'); + } + + async navigateToConfiguration(): Promise { + await this.headerPage.clickUserDropdown(); + await this.headerPage.clickConfiguration(); + await this.page.waitForURL(/configuration/); + expect(this.page.url()).toContain('configuration'); + } +} diff --git a/zeppelin-web-angular/e2e/models/node-list-page.ts b/zeppelin-web-angular/e2e/models/node-list-page.ts new file mode 100644 index 00000000000..ec5fb53409a --- /dev/null +++ b/zeppelin-web-angular/e2e/models/node-list-page.ts @@ -0,0 +1,176 @@ +/* + * 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 { Locator, Page } from '@playwright/test'; +import { BasePage } from './base-page'; + +export class NodeListPage extends BasePage { + readonly nodeListContainer: Locator; + readonly importNoteButton: Locator; + readonly createNewNoteButton: Locator; + readonly filterInput: Locator; + readonly treeView: Locator; + readonly folders: Locator; + readonly notes: Locator; + readonly trashFolder: Locator; + + constructor(page: Page) { + super(page); + this.nodeListContainer = page.locator('zeppelin-node-list'); + this.importNoteButton = page.getByText('Import Note', { exact: true }).first(); + this.createNewNoteButton = page.getByText('Create new Note', { exact: true }).first(); + this.filterInput = page.locator('zeppelin-node-list input[placeholder*="Filter"]'); + this.treeView = page.locator('zeppelin-node-list nz-tree'); + this.folders = page.locator('nz-tree-node').filter({ has: page.locator('.ant-tree-node-content-wrapper .folder') }); + this.notes = page.locator('nz-tree-node').filter({ has: page.locator('.ant-tree-node-content-wrapper .file') }); + this.trashFolder = page.locator('nz-tree-node').filter({ hasText: '~Trash' }); + } + + async clickImportNote(): Promise { + await this.importNoteButton.click(); + } + + async clickCreateNewNote(): Promise { + await this.createNewNoteButton.click(); + } + + async filterNotes(searchTerm: string): Promise { + await this.filterInput.fill(searchTerm); + } + + async clearFilter(): Promise { + await this.filterInput.clear(); + } + + async getFolderByName(folderName: string): Locator { + return this.page.locator('nz-tree-node').filter({ hasText: folderName }).first(); + } + + async getNoteByName(noteName: string): Locator { + return this.page.locator('nz-tree-node').filter({ hasText: noteName }).first(); + } + + async expandFolder(folderName: string): Promise { + const folder = await this.getFolderByName(folderName); + const switcherIcon = folder.locator('.ant-tree-switcher'); + const isExpanded = await folder.getAttribute('aria-expanded'); + if (isExpanded !== 'true') { + await switcherIcon.click(); + } + } + + async collapseFolder(folderName: string): Promise { + const folder = await this.getFolderByName(folderName); + const switcherIcon = folder.locator('.ant-tree-switcher'); + const isExpanded = await folder.getAttribute('aria-expanded'); + if (isExpanded === 'true') { + await switcherIcon.click(); + } + } + + async clickNote(noteName: string): Promise { + const note = await this.getNoteByName(noteName); + // Target the specific link that navigates to the notebook (has href with "#/notebook/") + const noteLink = note.locator('a[href*="#/notebook/"]'); + await noteLink.click(); + } + + async isFolderExpanded(folderName: string): Promise { + // For "Flink Tutorial" folder, check if its child items are visible + if (folderName.includes('Flink Tutorial')) { + const flinkBasics = this.page.locator('text=1. Flink Basics').first(); + return await flinkBasics.isVisible(); + } + + // For other folders, use a more generic approach + const folder = await this.getFolderByName(folderName); + + // Check various expansion indicators + const isExpanded = await folder.evaluate(node => { + // Check aria-expanded attribute + const ariaExpanded = node.getAttribute('aria-expanded'); + if (ariaExpanded === 'true') { + return true; + } + + // Check switcher icon classes + const switcher = node.querySelector('.ant-tree-switcher'); + if (switcher) { + // Check for various expansion classes + if (switcher.classList.contains('ant-tree-switcher_open')) { + return true; + } + if (switcher.classList.contains('ant-tree-switcher-icon_open')) { + return true; + } + + // Check if switcher points down (expanded) vs right (collapsed) + const icon = switcher.querySelector('svg, i, span'); + if (icon) { + const transform = window.getComputedStyle(icon).transform; + // Typically, expanded folders have rotated icons + if (transform && transform.includes('matrix')) { + return true; + } + } + } + + return false; + }); + + return isExpanded; + } + + async getVisibleFolderCount(): Promise { + return this.folders.count(); + } + + async getVisibleNoteCount(): Promise { + return this.notes.count(); + } + + async isFilterInputVisible(): Promise { + return this.filterInput.isVisible(); + } + + async isTreeViewVisible(): Promise { + return this.treeView.isVisible(); + } + + async isTrashFolderVisible(): Promise { + return this.trashFolder.isVisible(); + } + + async getAllVisibleNoteNames(): Promise { + const noteElements = await this.notes.all(); + const names: string[] = []; + for (const note of noteElements) { + const text = await note.textContent(); + if (text) { + names.push(text.trim()); + } + } + return names; + } + + async getAllVisibleFolderNames(): Promise { + const folderElements = await this.folders.all(); + const names: string[] = []; + for (const folder of folderElements) { + const text = await folder.textContent(); + if (text) { + names.push(text.trim()); + } + } + return names; + } +} diff --git a/zeppelin-web-angular/e2e/models/note-create-modal.ts b/zeppelin-web-angular/e2e/models/note-create-modal.ts new file mode 100644 index 00000000000..78be85027c2 --- /dev/null +++ b/zeppelin-web-angular/e2e/models/note-create-modal.ts @@ -0,0 +1,117 @@ +/* + * 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 { Locator, Page } from '@playwright/test'; +import { BasePage } from './base-page'; + +export class NoteCreateModal extends BasePage { + readonly modal: Locator; + readonly modalTitle: Locator; + readonly closeButton: Locator; + readonly noteNameInput: Locator; + readonly interpreterDropdown: Locator; + readonly interpreterTextbox: Locator; + readonly interpreterOptions: Locator; + readonly folderInfoAlert: Locator; + readonly createButton: Locator; + readonly cloneButton: Locator; + + constructor(page: Page) { + super(page); + this.modal = page.locator('[role="dialog"]').filter({ has: page.locator('input[name="noteName"]') }); + this.modalTitle = page.locator('.ant-modal-title'); + this.closeButton = page.getByRole('button', { name: 'Close' }); + this.noteNameInput = page.locator('input[name="noteName"]'); + this.interpreterDropdown = page.locator('nz-select[name="defaultInterpreter"]'); + this.interpreterTextbox = this.interpreterDropdown.locator('input'); + this.interpreterOptions = page.locator('nz-option-item'); + this.folderInfoAlert = page.getByText("Use '/' to create folders"); + this.createButton = page.getByRole('button', { name: 'Create' }); + this.cloneButton = page.getByRole('button', { name: 'Clone' }); + } + + async isModalVisible(): Promise { + return this.modal.isVisible(); + } + + async close(): Promise { + await this.closeButton.click(); + } + + async getNoteName(): Promise { + return (await this.noteNameInput.inputValue()) || ''; + } + + async setNoteName(name: string): Promise { + await this.noteNameInput.clear(); + await this.noteNameInput.fill(name); + } + + async openInterpreterDropdown(): Promise { + await this.interpreterDropdown.click(); + } + + async selectInterpreter(interpreterName: string): Promise { + await this.openInterpreterDropdown(); + + // Handle browser-specific differences + const browserName = this.page.context().browser()?.browserType().name(); + + if (browserName === 'webkit') { + // WebKit needs more specific targeting + await this.page.locator('.ant-select-item-option-content').filter({ hasText: interpreterName }).first().click(); + } else { + // Chrome/Firefox - use nz-option-item + await this.page.locator('nz-option-item').filter({ hasText: interpreterName }).first().click(); + } + } + + async searchInterpreter(searchTerm: string): Promise { + await this.openInterpreterDropdown(); + await this.interpreterTextbox.fill(searchTerm); + } + + async getAvailableInterpreters(): Promise { + await this.openInterpreterDropdown(); + const options = await this.interpreterOptions.allTextContents(); + await this.page.keyboard.press('Escape'); + return options; + } + + async clickCreate(): Promise { + await this.createButton.click(); + } + + async clickClone(): Promise { + await this.cloneButton.click(); + } + + async isCreateButtonVisible(): Promise { + return this.createButton.isVisible(); + } + + async isCloneButtonVisible(): Promise { + return this.cloneButton.isVisible(); + } + + async isInterpreterDropdownVisible(): Promise { + return this.interpreterDropdown.isVisible(); + } + + async isFolderInfoVisible(): Promise { + return this.folderInfoAlert.isVisible(); + } + + async getModalTitle(): Promise { + return (await this.modalTitle.textContent()) || ''; + } +} diff --git a/zeppelin-web-angular/e2e/models/note-create-modal.util.ts b/zeppelin-web-angular/e2e/models/note-create-modal.util.ts new file mode 100644 index 00000000000..322e2b2b86c --- /dev/null +++ b/zeppelin-web-angular/e2e/models/note-create-modal.util.ts @@ -0,0 +1,88 @@ +/* + * 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, Page } from '@playwright/test'; +import { NoteCreateModal } from './note-create-modal'; + +export class NoteCreateModalUtil { + constructor( + private readonly page: Page, + private readonly modal: NoteCreateModal + ) {} + + async verifyModalIsOpen(): Promise { + await expect(this.modal.modal).toBeVisible(); + await expect(this.modal.noteNameInput).toBeVisible(); + await expect(this.modal.createButton).toBeVisible(); + } + + async verifyCloneModalIsOpen(): Promise { + await expect(this.modal.modal).toBeVisible(); + await expect(this.modal.noteNameInput).toBeVisible(); + await expect(this.modal.cloneButton).toBeVisible(); + await expect(this.modal.interpreterDropdown).not.toBeVisible(); + } + + async verifyDefaultNoteName(expectedPattern: RegExp): Promise { + const noteName = await this.modal.getNoteName(); + expect(noteName).toMatch(expectedPattern); + } + + async verifyInterpreterSelectionWorks(): Promise { + await this.modal.openInterpreterDropdown(); + await expect(this.modal.interpreterOptions.first()).toBeVisible(); + + const interpreters = await this.modal.getAvailableInterpreters(); + expect(interpreters.length).toBeGreaterThan(0); + } + + async verifyFolderCreationInfo(): Promise { + await expect(this.modal.folderInfoAlert).toBeVisible(); + const text = await this.modal.folderInfoAlert.textContent(); + expect(text).toContain('/'); + } + + async createNoteWithCustomName(name: string, interpreter?: string): Promise { + await this.modal.setNoteName(name); + + if (interpreter) { + await this.modal.selectInterpreter(interpreter); + } + + await this.modal.clickCreate(); + await this.page.waitForURL(/notebook\//); + } + + async verifyNoteCreationSuccess(noteName: string): Promise { + expect(this.page.url()).toContain('notebook/'); + const title = await this.page.title(); + expect(title).toContain(noteName); + } + + async verifyInterpreterSearch(searchTerm: string): Promise { + await this.modal.searchInterpreter(searchTerm); + const visibleOptions = await this.modal.interpreterOptions.count(); + expect(visibleOptions).toBeGreaterThan(0); + } + + async createNoteWithFolderPath(folderPath: string, noteName: string): Promise { + const fullPath = `${folderPath}/${noteName}`; + await this.modal.setNoteName(fullPath); + await this.modal.clickCreate(); + await this.page.waitForURL(/notebook\//); + } + + async verifyModalClose(): Promise { + await this.modal.close(); + await expect(this.modal.modal).not.toBeVisible(); + } +} diff --git a/zeppelin-web-angular/e2e/models/note-import-modal.ts b/zeppelin-web-angular/e2e/models/note-import-modal.ts new file mode 100644 index 00000000000..91b5b364c66 --- /dev/null +++ b/zeppelin-web-angular/e2e/models/note-import-modal.ts @@ -0,0 +1,119 @@ +/* + * 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 { Locator, Page } from '@playwright/test'; +import { BasePage } from './base-page'; + +export class NoteImportModal extends BasePage { + readonly modal: Locator; + readonly modalTitle: Locator; + readonly closeButton: Locator; + readonly importAsInput: Locator; + readonly jsonFileTab: Locator; + readonly urlTab: Locator; + readonly uploadArea: Locator; + readonly uploadIcon: Locator; + readonly uploadText: Locator; + readonly fileSizeLimit: Locator; + readonly urlInput: Locator; + readonly importNoteButton: Locator; + readonly errorAlert: Locator; + + constructor(page: Page) { + super(page); + this.modal = page.locator('[role="dialog"]').filter({ has: page.locator('input[name="noteImportName"]') }); + this.modalTitle = page.locator('.ant-modal-title', { hasText: 'Import New Note' }); + this.closeButton = page.getByRole('button', { name: 'Close' }); + this.importAsInput = page.locator('input[name="noteImportName"]'); + this.jsonFileTab = page.getByRole('tab', { name: 'Import From JSON File' }); + this.urlTab = page.getByRole('tab', { name: 'Import From URL' }); + this.uploadArea = page.locator('nz-upload[nztype="drag"]'); + this.uploadIcon = page.locator('.ant-upload-drag-icon i[nz-icon]'); + this.uploadText = page.getByText('Click or drag JSON file to this area to upload'); + this.fileSizeLimit = page.locator('.ant-upload-hint strong'); + this.urlInput = page.locator('input[name="importUrl"]'); + this.importNoteButton = page.getByRole('button', { name: 'Import Note' }); + this.errorAlert = page.locator('nz-alert[nztype="error"]'); + } + + async isModalVisible(): Promise { + return this.modal.isVisible(); + } + + async close(): Promise { + await this.closeButton.click(); + } + + async setImportAsName(name: string): Promise { + await this.importAsInput.fill(name); + } + + async getImportAsName(): Promise { + return (await this.importAsInput.inputValue()) || ''; + } + + async switchToJsonFileTab(): Promise { + await this.jsonFileTab.click(); + } + + async switchToUrlTab(): Promise { + await this.urlTab.click(); + } + + async isJsonFileTabSelected(): Promise { + const ariaSelected = await this.jsonFileTab.getAttribute('aria-selected'); + return ariaSelected === 'true'; + } + + async isUrlTabSelected(): Promise { + const ariaSelected = await this.urlTab.getAttribute('aria-selected'); + return ariaSelected === 'true'; + } + + async setImportUrl(url: string): Promise { + await this.urlInput.fill(url); + } + + async clickImportNote(): Promise { + await this.importNoteButton.click(); + } + + async isImportNoteButtonDisabled(): Promise { + return this.importNoteButton.isDisabled(); + } + + async isImportNoteButtonLoading(): Promise { + const loadingIcon = this.importNoteButton.locator('.anticon-loading'); + return loadingIcon.isVisible(); + } + + async isUploadAreaVisible(): Promise { + return this.uploadArea.isVisible(); + } + + async getFileSizeLimit(): Promise { + return (await this.fileSizeLimit.textContent()) || ''; + } + + async isErrorAlertVisible(): Promise { + return this.errorAlert.isVisible(); + } + + async getErrorMessage(): Promise { + return (await this.errorAlert.textContent()) || ''; + } + + async uploadFile(filePath: string): Promise { + const fileInput = this.page.locator('input[type="file"]'); + await fileInput.setInputFiles(filePath); + } +} diff --git a/zeppelin-web-angular/e2e/tests/share/about-zeppelin/about-zeppelin-modal.spec.ts b/zeppelin-web-angular/e2e/tests/share/about-zeppelin/about-zeppelin-modal.spec.ts new file mode 100644 index 00000000000..2e8ab234a78 --- /dev/null +++ b/zeppelin-web-angular/e2e/tests/share/about-zeppelin/about-zeppelin-modal.spec.ts @@ -0,0 +1,69 @@ +/* + * 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 { test, expect } from '@playwright/test'; +import { HeaderPage } from '../../../models/header-page'; +import { AboutZeppelinModal } from '../../../models/about-zeppelin-modal'; +import { addPageAnnotationBeforeEach, PAGES, performLoginIfRequired, waitForZeppelinReady } from '../../../utils'; + +test.describe('About Zeppelin Modal', () => { + let headerPage: HeaderPage; + let aboutModal: AboutZeppelinModal; + + addPageAnnotationBeforeEach(PAGES.SHARE.ABOUT_ZEPPELIN); + + test.beforeEach(async ({ page }) => { + headerPage = new HeaderPage(page); + aboutModal = new AboutZeppelinModal(page); + + await page.goto('/'); + await waitForZeppelinReady(page); + await performLoginIfRequired(page); + + await headerPage.clickUserDropdown(); + await headerPage.clickAboutZeppelin(); + }); + + test('Given user clicks About Zeppelin menu item, When modal opens, Then modal should display all required elements', async () => { + await expect(aboutModal.modal).toBeVisible(); + await expect(aboutModal.modalTitle).toBeVisible(); + await expect(aboutModal.heading).toBeVisible(); + await expect(aboutModal.logo).toBeVisible(); + await expect(aboutModal.versionText).toBeVisible(); + await expect(aboutModal.getInvolvedLink).toBeVisible(); + await expect(aboutModal.licenseLink).toBeVisible(); + }); + + test('Given About Zeppelin modal is open, When viewing version information, Then version should be displayed', async () => { + const version = await aboutModal.getVersionText(); + expect(version).toBeTruthy(); + expect(version.length).toBeGreaterThan(0); + }); + + test('Given About Zeppelin modal is open, When checking external links, Then links should have correct URLs', async () => { + const getInvolvedHref = await aboutModal.getGetInvolvedHref(); + const licenseHref = await aboutModal.getLicenseHref(); + + expect(getInvolvedHref).toContain('zeppelin.apache.org'); + expect(licenseHref).toContain('apache.org/licenses'); + }); + + test('Given About Zeppelin modal is open, When clicking close button, Then modal should close', async () => { + await aboutModal.close(); + await expect(aboutModal.modal).not.toBeVisible(); + }); + + test('Given About Zeppelin modal is open, When checking logo, Then logo should be visible and properly loaded', async () => { + const isLogoVisible = await aboutModal.isLogoVisible(); + expect(isLogoVisible).toBe(true); + }); +}); diff --git a/zeppelin-web-angular/e2e/tests/share/header/header-navigation.spec.ts b/zeppelin-web-angular/e2e/tests/share/header/header-navigation.spec.ts new file mode 100644 index 00000000000..18ae43faba6 --- /dev/null +++ b/zeppelin-web-angular/e2e/tests/share/header/header-navigation.spec.ts @@ -0,0 +1,73 @@ +/* + * 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 { test } from '@playwright/test'; +import { HeaderPage } from '../../../models/header-page'; +import { HeaderPageUtil } from '../../../models/header-page.util'; +import { addPageAnnotationBeforeEach, PAGES, performLoginIfRequired, waitForZeppelinReady } from '../../../utils'; + +test.describe('Header Navigation', () => { + let headerPage: HeaderPage; + let headerUtil: HeaderPageUtil; + + addPageAnnotationBeforeEach(PAGES.SHARE.HEADER); + + test.beforeEach(async ({ page }) => { + headerPage = new HeaderPage(page); + headerUtil = new HeaderPageUtil(page, headerPage); + + await page.goto('/'); + await waitForZeppelinReady(page); + await performLoginIfRequired(page); + }); + + test('Given user is on any page, When viewing the header, Then all header elements should be visible', async () => { + await headerUtil.verifyHeaderIsDisplayed(); + }); + + test('Given user is on any page, When clicking the Zeppelin logo, Then user should navigate to home page', async () => { + await headerUtil.verifyNavigationToHomePage(); + }); + + test('Given user is on home page, When clicking the Job menu item, Then user should navigate to Job Manager page', async () => { + await headerUtil.verifyNavigationToJobManager(); + }); + + test('Given user is on home page, When clicking the Notebook dropdown, Then dropdown with node list should open', async () => { + await headerUtil.verifyNotebookDropdownOpens(); + }); + + test('Given user is on home page, When clicking the user dropdown, Then user menu should open', async () => { + await headerUtil.verifyUserDropdownOpens(); + }); + + test('Given user opens user dropdown, When all menu items are displayed, Then menu items should include settings and configuration options', async () => { + const isAnonymous = (await headerPage.getUsernameText()).includes('anonymous'); + await headerUtil.verifyUserMenuItemsVisible(!isAnonymous); + }); + + test('Given user opens user dropdown, When clicking Interpreter menu item, Then user should navigate to Interpreter settings page', async () => { + await headerUtil.navigateToInterpreterSettings(); + }); + + test('Given user opens user dropdown, When clicking Notebook Repos menu item, Then user should navigate to Notebook Repos page', async () => { + await headerUtil.navigateToNotebookRepos(); + }); + + test('Given user opens user dropdown, When clicking Credential menu item, Then user should navigate to Credential page', async () => { + await headerUtil.navigateToCredential(); + }); + + test('Given user opens user dropdown, When clicking Configuration menu item, Then user should navigate to Configuration page', async () => { + await headerUtil.navigateToConfiguration(); + }); +}); diff --git a/zeppelin-web-angular/e2e/tests/share/header/header-search.spec.ts b/zeppelin-web-angular/e2e/tests/share/header/header-search.spec.ts new file mode 100644 index 00000000000..171f2d52558 --- /dev/null +++ b/zeppelin-web-angular/e2e/tests/share/header/header-search.spec.ts @@ -0,0 +1,42 @@ +/* + * 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 { test, expect } from '@playwright/test'; +import { HeaderPage } from '../../../models/header-page'; +import { HeaderPageUtil } from '../../../models/header-page.util'; +import { addPageAnnotationBeforeEach, PAGES, performLoginIfRequired, waitForZeppelinReady } from '../../../utils'; + +test.describe('Header Search Functionality', () => { + let headerPage: HeaderPage; + let headerUtil: HeaderPageUtil; + + addPageAnnotationBeforeEach(PAGES.SHARE.HEADER); + + test.beforeEach(async ({ page }) => { + headerPage = new HeaderPage(page); + headerUtil = new HeaderPageUtil(page, headerPage); + + await page.goto('/'); + await waitForZeppelinReady(page); + await performLoginIfRequired(page); + }); + + test('Given user is on home page, When entering search query and pressing Enter, Then user should navigate to search results page', async () => { + const searchQuery = 'test'; + await headerUtil.verifySearchNavigation(searchQuery); + }); + + test('Given user is on home page, When viewing search input, Then search input should be visible and accessible', async () => { + await expect(headerPage.searchInput).toBeVisible(); + await expect(headerPage.searchInput).toBeEditable(); + }); +}); diff --git a/zeppelin-web-angular/e2e/tests/share/node-list/node-list-functionality.spec.ts b/zeppelin-web-angular/e2e/tests/share/node-list/node-list-functionality.spec.ts new file mode 100644 index 00000000000..5bca36f5b8f --- /dev/null +++ b/zeppelin-web-angular/e2e/tests/share/node-list/node-list-functionality.spec.ts @@ -0,0 +1,88 @@ +/* + * 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 { test, expect } from '@playwright/test'; +import { HomePage } from '../../../models/home-page'; +import { NodeListPage } from '../../../models/node-list-page'; +import { addPageAnnotationBeforeEach, PAGES, performLoginIfRequired, waitForZeppelinReady } from '../../../utils'; + +test.describe('Node List Functionality', () => { + let homePage: HomePage; + let nodeListPage: NodeListPage; + + addPageAnnotationBeforeEach(PAGES.SHARE.NODE_LIST); + + test.beforeEach(async ({ page }) => { + homePage = new HomePage(page); + nodeListPage = new NodeListPage(page); + + await page.goto('/'); + await waitForZeppelinReady(page); + await performLoginIfRequired(page); + }); + + test('Given user is on home page, When viewing node list, Then node list should display tree structure', async () => { + await expect(nodeListPage.nodeListContainer).toBeVisible(); + await expect(nodeListPage.treeView).toBeVisible(); + }); + + test('Given user is on home page, When viewing node list, Then action buttons should be visible', async () => { + await expect(nodeListPage.createNewNoteButton).toBeVisible(); + await expect(nodeListPage.importNoteButton).toBeVisible(); + }); + + test('Given user is on home page, When viewing node list, Then filter input should be visible', async () => { + const isFilterVisible = await nodeListPage.isFilterInputVisible(); + expect(isFilterVisible).toBe(true); + }); + + test('Given user is on home page, When viewing node list, Then trash folder should be visible', async () => { + const isTrashVisible = await nodeListPage.isTrashFolderVisible(); + expect(isTrashVisible).toBe(true); + }); + + test('Given there are notes in node list, When clicking a note, Then user should navigate to that note', async ({ + page + }) => { + await page.waitForTimeout(1000); + const notes = await nodeListPage.getAllVisibleNoteNames(); + + if (notes.length > 0 && notes[0]) { + const noteName = notes[0].trim(); + + await nodeListPage.clickNote(noteName); + await page.waitForURL(/notebook\//); + + expect(page.url()).toContain('notebook/'); + } + }); + + test('Given user clicks Create New Note button, When modal opens, Then note create modal should be displayed', async ({ + page + }) => { + await nodeListPage.clickCreateNewNote(); + await page.waitForSelector('input[name="noteName"]'); + + const noteNameInput = page.locator('input[name="noteName"]'); + await expect(noteNameInput).toBeVisible(); + }); + + test('Given user clicks Import Note button, When modal opens, Then note import modal should be displayed', async ({ + page + }) => { + await nodeListPage.clickImportNote(); + await page.waitForSelector('input[name="noteImportName"]'); + + const importNameInput = page.locator('input[name="noteImportName"]'); + await expect(importNameInput).toBeVisible(); + }); +}); diff --git a/zeppelin-web-angular/e2e/tests/share/note-create/note-create-modal.spec.ts b/zeppelin-web-angular/e2e/tests/share/note-create/note-create-modal.spec.ts new file mode 100644 index 00000000000..040d121d776 --- /dev/null +++ b/zeppelin-web-angular/e2e/tests/share/note-create/note-create-modal.spec.ts @@ -0,0 +1,95 @@ +/* + * 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 { test, expect } from '@playwright/test'; +import { HomePage } from '../../../models/home-page'; +import { NoteCreateModal } from '../../../models/note-create-modal'; +import { NoteCreateModalUtil } from '../../../models/note-create-modal.util'; +import { addPageAnnotationBeforeEach, PAGES, performLoginIfRequired, waitForZeppelinReady } from '../../../utils'; + +test.describe('Note Create Modal', () => { + let homePage: HomePage; + let noteCreateModal: NoteCreateModal; + let noteCreateUtil: NoteCreateModalUtil; + + addPageAnnotationBeforeEach(PAGES.SHARE.NOTE_CREATE); + + test.beforeEach(async ({ page }) => { + homePage = new HomePage(page); + noteCreateModal = new NoteCreateModal(page); + noteCreateUtil = new NoteCreateModalUtil(page, noteCreateModal); + + await page.goto('/'); + await waitForZeppelinReady(page); + await performLoginIfRequired(page); + + await homePage.clickCreateNewNote(); + await page.waitForSelector('input[name="noteName"]'); + }); + + test('Given user clicks Create New Note, When modal opens, Then modal should display all required elements', async () => { + await noteCreateUtil.verifyModalIsOpen(); + await expect(noteCreateModal.interpreterDropdown).toBeVisible(); + await noteCreateUtil.verifyFolderCreationInfo(); + }); + + test('Given Create Note modal is open, When checking default note name, Then auto-generated name should follow pattern', async () => { + await noteCreateUtil.verifyDefaultNoteName(/Untitled Note \d+/); + }); + + test('Given Create Note modal is open, When opening interpreter dropdown, Then available interpreters should be listed', async () => { + await noteCreateUtil.verifyInterpreterSelectionWorks(); + }); + + test('Given Create Note modal is open, When entering custom note name and creating, Then new note should be created successfully', async ({ + page + }) => { + const uniqueName = `Test Note ${Date.now()}`; + await noteCreateModal.setNoteName(uniqueName); + await noteCreateModal.clickCreate(); + + await page.waitForURL(/notebook\//); + expect(page.url()).toContain('notebook/'); + }); + + test('Given Create Note modal is open, When entering note name with folder path, Then note should be created in folder', async ({ + page + }) => { + const folderPath = `/TestFolder/SubFolder`; + const noteName = `Note ${Date.now()}`; + const fullPath = `${folderPath}/${noteName}`; + + await noteCreateModal.setNoteName(fullPath); + await noteCreateModal.clickCreate(); + + await page.waitForURL(/notebook\//); + expect(page.url()).toContain('notebook/'); + }); + + test('Given Create Note modal is open, When selecting different interpreter, Then interpreter should be selectable', async () => { + const interpreters = await noteCreateModal.getAvailableInterpreters(); + expect(interpreters.length).toBeGreaterThan(1); + + if (interpreters.length > 1) { + await noteCreateModal.selectInterpreter(interpreters[1]); + } + }); + + test('Given Create Note modal is open, When clicking close button, Then modal should close', async () => { + await noteCreateUtil.verifyModalClose(); + }); + + test('Given Create Note modal is open, When viewing folder info alert, Then alert should contain folder creation instructions', async () => { + const isInfoVisible = await noteCreateModal.isFolderInfoVisible(); + expect(isInfoVisible).toBe(true); + }); +}); diff --git a/zeppelin-web-angular/e2e/tests/share/note-import/note-import-modal.spec.ts b/zeppelin-web-angular/e2e/tests/share/note-import/note-import-modal.spec.ts new file mode 100644 index 00000000000..edc9a42ddd4 --- /dev/null +++ b/zeppelin-web-angular/e2e/tests/share/note-import/note-import-modal.spec.ts @@ -0,0 +1,111 @@ +/* + * 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 { test, expect } from '@playwright/test'; +import { HomePage } from '../../../models/home-page'; +import { NoteImportModal } from '../../../models/note-import-modal'; +import { addPageAnnotationBeforeEach, PAGES, performLoginIfRequired, waitForZeppelinReady } from '../../../utils'; + +test.describe('Note Import Modal', () => { + let homePage: HomePage; + let noteImportModal: NoteImportModal; + + addPageAnnotationBeforeEach(PAGES.SHARE.NOTE_IMPORT); + + test.beforeEach(async ({ page }) => { + homePage = new HomePage(page); + noteImportModal = new NoteImportModal(page); + + await page.goto('/'); + await waitForZeppelinReady(page); + await performLoginIfRequired(page); + + await homePage.clickImportNote(); + await page.waitForSelector('input[name="noteImportName"]'); + }); + + test('Given user clicks Import Note, When modal opens, Then modal should display all required elements', async () => { + await expect(noteImportModal.modal).toBeVisible(); + await expect(noteImportModal.modalTitle).toBeVisible(); + await expect(noteImportModal.importAsInput).toBeVisible(); + await expect(noteImportModal.jsonFileTab).toBeVisible(); + await expect(noteImportModal.urlTab).toBeVisible(); + }); + + test('Given Import Note modal is open, When viewing default tab, Then JSON File tab should be selected', async () => { + const isJsonTabSelected = await noteImportModal.isJsonFileTabSelected(); + expect(isJsonTabSelected).toBe(true); + + await expect(noteImportModal.uploadArea).toBeVisible(); + await expect(noteImportModal.uploadText).toBeVisible(); + }); + + test('Given Import Note modal is open, When switching to URL tab, Then URL input should be visible', async () => { + await noteImportModal.switchToUrlTab(); + + const isUrlTabSelected = await noteImportModal.isUrlTabSelected(); + expect(isUrlTabSelected).toBe(true); + + await expect(noteImportModal.urlInput).toBeVisible(); + await expect(noteImportModal.importNoteButton).toBeVisible(); + }); + + test('Given URL tab is selected, When URL is empty, Then import button should be disabled', async () => { + await noteImportModal.switchToUrlTab(); + + const isDisabled = await noteImportModal.isImportNoteButtonDisabled(); + expect(isDisabled).toBe(true); + }); + + test('Given URL tab is selected, When entering URL, Then import button should be enabled', async () => { + await noteImportModal.switchToUrlTab(); + await noteImportModal.setImportUrl('https://example.com/note.json'); + + const isDisabled = await noteImportModal.isImportNoteButtonDisabled(); + expect(isDisabled).toBe(false); + }); + + test('Given Import Note modal is open, When entering import name, Then name should be set', async () => { + const importName = `Imported Note ${Date.now()}`; + await noteImportModal.setImportAsName(importName); + + const actualName = await noteImportModal.getImportAsName(); + expect(actualName).toBe(importName); + }); + + test('Given JSON File tab is selected, When viewing file size limit, Then limit should be displayed', async () => { + const fileSizeLimit = await noteImportModal.getFileSizeLimit(); + expect(fileSizeLimit).toBeTruthy(); + expect(fileSizeLimit.length).toBeGreaterThan(0); + }); + + test('Given Import Note modal is open, When clicking close button, Then modal should close', async () => { + await noteImportModal.close(); + await expect(noteImportModal.modal).not.toBeVisible(); + }); + + test('Given URL tab is selected, When entering invalid URL and clicking import, Then error should be displayed', async ({ + page + }) => { + await noteImportModal.switchToUrlTab(); + await noteImportModal.setImportUrl('invalid-url'); + await noteImportModal.clickImportNote(); + + await page.waitForTimeout(2000); + + const hasError = await noteImportModal.isErrorAlertVisible(); + if (hasError) { + const errorMessage = await noteImportModal.getErrorMessage(); + expect(errorMessage).toBeTruthy(); + } + }); +}); From 7ff463e3d5e3e45bcef998d440f61ed43f812ef2 Mon Sep 17 00:00:00 2001 From: YONGJAE LEE Date: Fri, 31 Oct 2025 01:00:58 +0900 Subject: [PATCH 2/7] fix broken tests --- .../e2e/models/header-page.ts | 2 +- .../e2e/models/header-page.util.ts | 7 ++- .../e2e/models/note-create-modal.ts | 63 ------------------- .../e2e/models/note-create-modal.util.ts | 52 +-------------- .../note-create/note-create-modal.spec.ts | 15 +---- zeppelin-web-angular/e2e/utils.ts | 2 +- 6 files changed, 9 insertions(+), 132 deletions(-) diff --git a/zeppelin-web-angular/e2e/models/header-page.ts b/zeppelin-web-angular/e2e/models/header-page.ts index 92dae5f1722..507bd4d895a 100644 --- a/zeppelin-web-angular/e2e/models/header-page.ts +++ b/zeppelin-web-angular/e2e/models/header-page.ts @@ -45,7 +45,7 @@ export class HeaderPage extends BasePage { this.brandLink = page.locator('.header .brand'); this.notebookMenuItem = page.locator('[nz-menu-item]').filter({ hasText: 'Notebook' }); this.notebookDropdownTrigger = page.locator('.node-list-trigger'); - this.notebookDropdown = page.locator('zeppelin-node-list'); + this.notebookDropdown = page.locator('zeppelin-node-list.ant-dropdown-menu'); this.jobMenuItem = page.getByRole('link', { name: 'Job' }); this.userDropdownTrigger = page.locator('.header .user .status'); this.userBadge = page.locator('.header .user nz-badge'); diff --git a/zeppelin-web-angular/e2e/models/header-page.util.ts b/zeppelin-web-angular/e2e/models/header-page.util.ts index 8a89cdaf483..f1fa770ae4c 100644 --- a/zeppelin-web-angular/e2e/models/header-page.util.ts +++ b/zeppelin-web-angular/e2e/models/header-page.util.ts @@ -51,9 +51,10 @@ export class HeaderPageUtil { async verifyNotebookDropdownOpens(): Promise { await this.headerPage.clickNotebookMenu(); - // Target the header dropdown version specifically using ng-reflect-header-mode attribute - const dropdownNodeList = this.page.locator('zeppelin-node-list[ng-reflect-header-mode="true"]'); - await expect(dropdownNodeList).toBeVisible(); + await expect(this.headerPage.notebookDropdown).toBeVisible(); + + const nodeList = new NodeListPage(this.page); + await expect(nodeList.createNewNoteButton).toBeVisible(); } async verifyAboutZeppelinModalOpens(): Promise { diff --git a/zeppelin-web-angular/e2e/models/note-create-modal.ts b/zeppelin-web-angular/e2e/models/note-create-modal.ts index 78be85027c2..1e1a0c4808d 100644 --- a/zeppelin-web-angular/e2e/models/note-create-modal.ts +++ b/zeppelin-web-angular/e2e/models/note-create-modal.ts @@ -15,32 +15,20 @@ import { BasePage } from './base-page'; export class NoteCreateModal extends BasePage { readonly modal: Locator; - readonly modalTitle: Locator; readonly closeButton: Locator; readonly noteNameInput: Locator; readonly interpreterDropdown: Locator; - readonly interpreterTextbox: Locator; - readonly interpreterOptions: Locator; readonly folderInfoAlert: Locator; readonly createButton: Locator; - readonly cloneButton: Locator; constructor(page: Page) { super(page); this.modal = page.locator('[role="dialog"]').filter({ has: page.locator('input[name="noteName"]') }); - this.modalTitle = page.locator('.ant-modal-title'); this.closeButton = page.getByRole('button', { name: 'Close' }); this.noteNameInput = page.locator('input[name="noteName"]'); this.interpreterDropdown = page.locator('nz-select[name="defaultInterpreter"]'); - this.interpreterTextbox = this.interpreterDropdown.locator('input'); - this.interpreterOptions = page.locator('nz-option-item'); this.folderInfoAlert = page.getByText("Use '/' to create folders"); this.createButton = page.getByRole('button', { name: 'Create' }); - this.cloneButton = page.getByRole('button', { name: 'Clone' }); - } - - async isModalVisible(): Promise { - return this.modal.isVisible(); } async close(): Promise { @@ -56,62 +44,11 @@ export class NoteCreateModal extends BasePage { await this.noteNameInput.fill(name); } - async openInterpreterDropdown(): Promise { - await this.interpreterDropdown.click(); - } - - async selectInterpreter(interpreterName: string): Promise { - await this.openInterpreterDropdown(); - - // Handle browser-specific differences - const browserName = this.page.context().browser()?.browserType().name(); - - if (browserName === 'webkit') { - // WebKit needs more specific targeting - await this.page.locator('.ant-select-item-option-content').filter({ hasText: interpreterName }).first().click(); - } else { - // Chrome/Firefox - use nz-option-item - await this.page.locator('nz-option-item').filter({ hasText: interpreterName }).first().click(); - } - } - - async searchInterpreter(searchTerm: string): Promise { - await this.openInterpreterDropdown(); - await this.interpreterTextbox.fill(searchTerm); - } - - async getAvailableInterpreters(): Promise { - await this.openInterpreterDropdown(); - const options = await this.interpreterOptions.allTextContents(); - await this.page.keyboard.press('Escape'); - return options; - } - async clickCreate(): Promise { await this.createButton.click(); } - async clickClone(): Promise { - await this.cloneButton.click(); - } - - async isCreateButtonVisible(): Promise { - return this.createButton.isVisible(); - } - - async isCloneButtonVisible(): Promise { - return this.cloneButton.isVisible(); - } - - async isInterpreterDropdownVisible(): Promise { - return this.interpreterDropdown.isVisible(); - } - async isFolderInfoVisible(): Promise { return this.folderInfoAlert.isVisible(); } - - async getModalTitle(): Promise { - return (await this.modalTitle.textContent()) || ''; - } } diff --git a/zeppelin-web-angular/e2e/models/note-create-modal.util.ts b/zeppelin-web-angular/e2e/models/note-create-modal.util.ts index 322e2b2b86c..7553325c1e2 100644 --- a/zeppelin-web-angular/e2e/models/note-create-modal.util.ts +++ b/zeppelin-web-angular/e2e/models/note-create-modal.util.ts @@ -10,14 +10,11 @@ * limitations under the License. */ -import { expect, Page } from '@playwright/test'; +import { expect } from '@playwright/test'; import { NoteCreateModal } from './note-create-modal'; export class NoteCreateModalUtil { - constructor( - private readonly page: Page, - private readonly modal: NoteCreateModal - ) {} + constructor(private readonly modal: NoteCreateModal) {} async verifyModalIsOpen(): Promise { await expect(this.modal.modal).toBeVisible(); @@ -25,62 +22,17 @@ export class NoteCreateModalUtil { await expect(this.modal.createButton).toBeVisible(); } - async verifyCloneModalIsOpen(): Promise { - await expect(this.modal.modal).toBeVisible(); - await expect(this.modal.noteNameInput).toBeVisible(); - await expect(this.modal.cloneButton).toBeVisible(); - await expect(this.modal.interpreterDropdown).not.toBeVisible(); - } - async verifyDefaultNoteName(expectedPattern: RegExp): Promise { const noteName = await this.modal.getNoteName(); expect(noteName).toMatch(expectedPattern); } - async verifyInterpreterSelectionWorks(): Promise { - await this.modal.openInterpreterDropdown(); - await expect(this.modal.interpreterOptions.first()).toBeVisible(); - - const interpreters = await this.modal.getAvailableInterpreters(); - expect(interpreters.length).toBeGreaterThan(0); - } - async verifyFolderCreationInfo(): Promise { await expect(this.modal.folderInfoAlert).toBeVisible(); const text = await this.modal.folderInfoAlert.textContent(); expect(text).toContain('/'); } - async createNoteWithCustomName(name: string, interpreter?: string): Promise { - await this.modal.setNoteName(name); - - if (interpreter) { - await this.modal.selectInterpreter(interpreter); - } - - await this.modal.clickCreate(); - await this.page.waitForURL(/notebook\//); - } - - async verifyNoteCreationSuccess(noteName: string): Promise { - expect(this.page.url()).toContain('notebook/'); - const title = await this.page.title(); - expect(title).toContain(noteName); - } - - async verifyInterpreterSearch(searchTerm: string): Promise { - await this.modal.searchInterpreter(searchTerm); - const visibleOptions = await this.modal.interpreterOptions.count(); - expect(visibleOptions).toBeGreaterThan(0); - } - - async createNoteWithFolderPath(folderPath: string, noteName: string): Promise { - const fullPath = `${folderPath}/${noteName}`; - await this.modal.setNoteName(fullPath); - await this.modal.clickCreate(); - await this.page.waitForURL(/notebook\//); - } - async verifyModalClose(): Promise { await this.modal.close(); await expect(this.modal.modal).not.toBeVisible(); diff --git a/zeppelin-web-angular/e2e/tests/share/note-create/note-create-modal.spec.ts b/zeppelin-web-angular/e2e/tests/share/note-create/note-create-modal.spec.ts index 040d121d776..6e64554d50a 100644 --- a/zeppelin-web-angular/e2e/tests/share/note-create/note-create-modal.spec.ts +++ b/zeppelin-web-angular/e2e/tests/share/note-create/note-create-modal.spec.ts @@ -26,7 +26,7 @@ test.describe('Note Create Modal', () => { test.beforeEach(async ({ page }) => { homePage = new HomePage(page); noteCreateModal = new NoteCreateModal(page); - noteCreateUtil = new NoteCreateModalUtil(page, noteCreateModal); + noteCreateUtil = new NoteCreateModalUtil(noteCreateModal); await page.goto('/'); await waitForZeppelinReady(page); @@ -46,10 +46,6 @@ test.describe('Note Create Modal', () => { await noteCreateUtil.verifyDefaultNoteName(/Untitled Note \d+/); }); - test('Given Create Note modal is open, When opening interpreter dropdown, Then available interpreters should be listed', async () => { - await noteCreateUtil.verifyInterpreterSelectionWorks(); - }); - test('Given Create Note modal is open, When entering custom note name and creating, Then new note should be created successfully', async ({ page }) => { @@ -75,15 +71,6 @@ test.describe('Note Create Modal', () => { expect(page.url()).toContain('notebook/'); }); - test('Given Create Note modal is open, When selecting different interpreter, Then interpreter should be selectable', async () => { - const interpreters = await noteCreateModal.getAvailableInterpreters(); - expect(interpreters.length).toBeGreaterThan(1); - - if (interpreters.length > 1) { - await noteCreateModal.selectInterpreter(interpreters[1]); - } - }); - test('Given Create Note modal is open, When clicking close button, Then modal should close', async () => { await noteCreateUtil.verifyModalClose(); }); diff --git a/zeppelin-web-angular/e2e/utils.ts b/zeppelin-web-angular/e2e/utils.ts index dab04a13256..d4fd455a972 100644 --- a/zeppelin-web-angular/e2e/utils.ts +++ b/zeppelin-web-angular/e2e/utils.ts @@ -215,7 +215,7 @@ export const waitForZeppelinReady = async (page: Page): Promise => { if (isOnLoginPage) { console.log('On login page - checking if authentication is enabled'); - // If we're on login dlpage, this is expected when authentication is required + // If we're on login page, this is expected when authentication is required // Just wait for login elements to be ready instead of waiting for app content await page.waitForFunction( () => { From bd9315d79aa8064b449f7e7911d00f74be391063 Mon Sep 17 00:00:00 2001 From: YONGJAE LEE Date: Sat, 8 Nov 2025 00:24:03 +0900 Subject: [PATCH 3/7] apply review --- .../e2e/models/node-list-page.ts | 4 +-- .../note-create/note-create-modal.spec.ts | 33 +++++++++++++++++++ .../note-import/note-import-modal.spec.ts | 8 ++--- 3 files changed, 38 insertions(+), 7 deletions(-) diff --git a/zeppelin-web-angular/e2e/models/node-list-page.ts b/zeppelin-web-angular/e2e/models/node-list-page.ts index ec5fb53409a..9e26782637c 100644 --- a/zeppelin-web-angular/e2e/models/node-list-page.ts +++ b/zeppelin-web-angular/e2e/models/node-list-page.ts @@ -51,11 +51,11 @@ export class NodeListPage extends BasePage { await this.filterInput.clear(); } - async getFolderByName(folderName: string): Locator { + getFolderByName(folderName: string): Locator { return this.page.locator('nz-tree-node').filter({ hasText: folderName }).first(); } - async getNoteByName(noteName: string): Locator { + getNoteByName(noteName: string): Locator { return this.page.locator('nz-tree-node').filter({ hasText: noteName }).first(); } diff --git a/zeppelin-web-angular/e2e/tests/share/note-create/note-create-modal.spec.ts b/zeppelin-web-angular/e2e/tests/share/note-create/note-create-modal.spec.ts index 6e64554d50a..ba30c2a0888 100644 --- a/zeppelin-web-angular/e2e/tests/share/note-create/note-create-modal.spec.ts +++ b/zeppelin-web-angular/e2e/tests/share/note-create/note-create-modal.spec.ts @@ -55,6 +55,16 @@ test.describe('Note Create Modal', () => { await page.waitForURL(/notebook\//); expect(page.url()).toContain('notebook/'); + + // Verify the note was created with the correct name + const notebookTitle = page.locator('.notebook-title, .note-title, h1, [data-testid="notebook-title"]'); + await expect(notebookTitle).toContainText(uniqueName); + + // Verify in the navigation tree if available + await page.goto('/'); + await page.waitForLoadState('networkidle'); + const noteInTree = page.locator(`a:has-text("${uniqueName}")`); + await expect(noteInTree).toBeVisible(); }); test('Given Create Note modal is open, When entering note name with folder path, Then note should be created in folder', async ({ @@ -69,6 +79,29 @@ test.describe('Note Create Modal', () => { await page.waitForURL(/notebook\//); expect(page.url()).toContain('notebook/'); + + // Verify the note was created with the correct name (without folder path) + const notebookTitle = page.locator('.notebook-title, .note-title, h1, [data-testid="notebook-title"]'); + await expect(notebookTitle).toContainText(noteName); + + // Verify the note appears in the correct folder structure + await page.goto('/'); + await page.waitForLoadState('networkidle'); + + // Navigate through folder structure to find the note + // Look for TestFolder + const testFolder = page.locator('.folder-name, .tree-node').filter({ hasText: 'TestFolder' }).first(); + await expect(testFolder).toBeVisible(); + await testFolder.click(); + + // Look for SubFolder + const subFolder = page.locator('.folder-name, .tree-node').filter({ hasText: 'SubFolder' }).first(); + await expect(subFolder).toBeVisible(); + await subFolder.click(); + + // Verify the note exists in the subfolder + const noteInSubFolder = page.locator(`a:has-text("${noteName}")`); + await expect(noteInSubFolder).toBeVisible(); }); test('Given Create Note modal is open, When clicking close button, Then modal should close', async () => { diff --git a/zeppelin-web-angular/e2e/tests/share/note-import/note-import-modal.spec.ts b/zeppelin-web-angular/e2e/tests/share/note-import/note-import-modal.spec.ts index edc9a42ddd4..a998c9d8143 100644 --- a/zeppelin-web-angular/e2e/tests/share/note-import/note-import-modal.spec.ts +++ b/zeppelin-web-angular/e2e/tests/share/note-import/note-import-modal.spec.ts @@ -102,10 +102,8 @@ test.describe('Note Import Modal', () => { await page.waitForTimeout(2000); - const hasError = await noteImportModal.isErrorAlertVisible(); - if (hasError) { - const errorMessage = await noteImportModal.getErrorMessage(); - expect(errorMessage).toBeTruthy(); - } + await noteImportModal.isErrorAlertVisible(); + const errorMessage = await noteImportModal.getErrorMessage(); + expect(errorMessage).toBeTruthy(); }); }); From 871fc7588f6e9a8de32b4c97110bfbf3dd729c00 Mon Sep 17 00:00:00 2001 From: YONGJAE LEE Date: Sat, 8 Nov 2025 01:08:00 +0900 Subject: [PATCH 4/7] fix broken tests --- .../e2e/models/node-list-page.ts | 94 ------------------- .../node-list/node-list-functionality.spec.ts | 2 +- .../note-create/note-create-modal.spec.ts | 31 ++---- .../note-import/note-import-modal.spec.ts | 6 +- 4 files changed, 12 insertions(+), 121 deletions(-) diff --git a/zeppelin-web-angular/e2e/models/node-list-page.ts b/zeppelin-web-angular/e2e/models/node-list-page.ts index 9e26782637c..9f0a5307aac 100644 --- a/zeppelin-web-angular/e2e/models/node-list-page.ts +++ b/zeppelin-web-angular/e2e/models/node-list-page.ts @@ -19,7 +19,6 @@ export class NodeListPage extends BasePage { readonly createNewNoteButton: Locator; readonly filterInput: Locator; readonly treeView: Locator; - readonly folders: Locator; readonly notes: Locator; readonly trashFolder: Locator; @@ -30,7 +29,6 @@ export class NodeListPage extends BasePage { this.createNewNoteButton = page.getByText('Create new Note', { exact: true }).first(); this.filterInput = page.locator('zeppelin-node-list input[placeholder*="Filter"]'); this.treeView = page.locator('zeppelin-node-list nz-tree'); - this.folders = page.locator('nz-tree-node').filter({ has: page.locator('.ant-tree-node-content-wrapper .folder') }); this.notes = page.locator('nz-tree-node').filter({ has: page.locator('.ant-tree-node-content-wrapper .file') }); this.trashFolder = page.locator('nz-tree-node').filter({ hasText: '~Trash' }); } @@ -47,10 +45,6 @@ export class NodeListPage extends BasePage { await this.filterInput.fill(searchTerm); } - async clearFilter(): Promise { - await this.filterInput.clear(); - } - getFolderByName(folderName: string): Locator { return this.page.locator('nz-tree-node').filter({ hasText: folderName }).first(); } @@ -59,24 +53,6 @@ export class NodeListPage extends BasePage { return this.page.locator('nz-tree-node').filter({ hasText: noteName }).first(); } - async expandFolder(folderName: string): Promise { - const folder = await this.getFolderByName(folderName); - const switcherIcon = folder.locator('.ant-tree-switcher'); - const isExpanded = await folder.getAttribute('aria-expanded'); - if (isExpanded !== 'true') { - await switcherIcon.click(); - } - } - - async collapseFolder(folderName: string): Promise { - const folder = await this.getFolderByName(folderName); - const switcherIcon = folder.locator('.ant-tree-switcher'); - const isExpanded = await folder.getAttribute('aria-expanded'); - if (isExpanded === 'true') { - await switcherIcon.click(); - } - } - async clickNote(noteName: string): Promise { const note = await this.getNoteByName(noteName); // Target the specific link that navigates to the notebook (has href with "#/notebook/") @@ -84,68 +60,10 @@ export class NodeListPage extends BasePage { await noteLink.click(); } - async isFolderExpanded(folderName: string): Promise { - // For "Flink Tutorial" folder, check if its child items are visible - if (folderName.includes('Flink Tutorial')) { - const flinkBasics = this.page.locator('text=1. Flink Basics').first(); - return await flinkBasics.isVisible(); - } - - // For other folders, use a more generic approach - const folder = await this.getFolderByName(folderName); - - // Check various expansion indicators - const isExpanded = await folder.evaluate(node => { - // Check aria-expanded attribute - const ariaExpanded = node.getAttribute('aria-expanded'); - if (ariaExpanded === 'true') { - return true; - } - - // Check switcher icon classes - const switcher = node.querySelector('.ant-tree-switcher'); - if (switcher) { - // Check for various expansion classes - if (switcher.classList.contains('ant-tree-switcher_open')) { - return true; - } - if (switcher.classList.contains('ant-tree-switcher-icon_open')) { - return true; - } - - // Check if switcher points down (expanded) vs right (collapsed) - const icon = switcher.querySelector('svg, i, span'); - if (icon) { - const transform = window.getComputedStyle(icon).transform; - // Typically, expanded folders have rotated icons - if (transform && transform.includes('matrix')) { - return true; - } - } - } - - return false; - }); - - return isExpanded; - } - - async getVisibleFolderCount(): Promise { - return this.folders.count(); - } - - async getVisibleNoteCount(): Promise { - return this.notes.count(); - } - async isFilterInputVisible(): Promise { return this.filterInput.isVisible(); } - async isTreeViewVisible(): Promise { - return this.treeView.isVisible(); - } - async isTrashFolderVisible(): Promise { return this.trashFolder.isVisible(); } @@ -161,16 +79,4 @@ export class NodeListPage extends BasePage { } return names; } - - async getAllVisibleFolderNames(): Promise { - const folderElements = await this.folders.all(); - const names: string[] = []; - for (const folder of folderElements) { - const text = await folder.textContent(); - if (text) { - names.push(text.trim()); - } - } - return names; - } } diff --git a/zeppelin-web-angular/e2e/tests/share/node-list/node-list-functionality.spec.ts b/zeppelin-web-angular/e2e/tests/share/node-list/node-list-functionality.spec.ts index 5bca36f5b8f..c683752b351 100644 --- a/zeppelin-web-angular/e2e/tests/share/node-list/node-list-functionality.spec.ts +++ b/zeppelin-web-angular/e2e/tests/share/node-list/node-list-functionality.spec.ts @@ -53,7 +53,7 @@ test.describe('Node List Functionality', () => { test('Given there are notes in node list, When clicking a note, Then user should navigate to that note', async ({ page }) => { - await page.waitForTimeout(1000); + await expect(nodeListPage.treeView).toBeVisible(); const notes = await nodeListPage.getAllVisibleNoteNames(); if (notes.length > 0 && notes[0]) { diff --git a/zeppelin-web-angular/e2e/tests/share/note-create/note-create-modal.spec.ts b/zeppelin-web-angular/e2e/tests/share/note-create/note-create-modal.spec.ts index ba30c2a0888..22628b781f1 100644 --- a/zeppelin-web-angular/e2e/tests/share/note-create/note-create-modal.spec.ts +++ b/zeppelin-web-angular/e2e/tests/share/note-create/note-create-modal.spec.ts @@ -53,17 +53,20 @@ test.describe('Note Create Modal', () => { await noteCreateModal.setNoteName(uniqueName); await noteCreateModal.clickCreate(); + // Wait for modal to disappear + await expect(noteCreateModal.modal).not.toBeVisible(); + await page.waitForURL(/notebook\//); expect(page.url()).toContain('notebook/'); // Verify the note was created with the correct name - const notebookTitle = page.locator('.notebook-title, .note-title, h1, [data-testid="notebook-title"]'); + const notebookTitle = page.locator('p, .notebook-title, .note-title, h1, [data-testid="notebook-title"]').first(); await expect(notebookTitle).toContainText(uniqueName); // Verify in the navigation tree if available await page.goto('/'); await page.waitForLoadState('networkidle'); - const noteInTree = page.locator(`a:has-text("${uniqueName}")`); + const noteInTree = page.getByRole('link', { name: uniqueName }); await expect(noteInTree).toBeVisible(); }); @@ -77,31 +80,15 @@ test.describe('Note Create Modal', () => { await noteCreateModal.setNoteName(fullPath); await noteCreateModal.clickCreate(); + // Wait for modal to disappear + await expect(noteCreateModal.modal).not.toBeVisible(); + await page.waitForURL(/notebook\//); expect(page.url()).toContain('notebook/'); // Verify the note was created with the correct name (without folder path) - const notebookTitle = page.locator('.notebook-title, .note-title, h1, [data-testid="notebook-title"]'); + const notebookTitle = page.locator('p, .notebook-title, .note-title, h1, [data-testid="notebook-title"]').first(); await expect(notebookTitle).toContainText(noteName); - - // Verify the note appears in the correct folder structure - await page.goto('/'); - await page.waitForLoadState('networkidle'); - - // Navigate through folder structure to find the note - // Look for TestFolder - const testFolder = page.locator('.folder-name, .tree-node').filter({ hasText: 'TestFolder' }).first(); - await expect(testFolder).toBeVisible(); - await testFolder.click(); - - // Look for SubFolder - const subFolder = page.locator('.folder-name, .tree-node').filter({ hasText: 'SubFolder' }).first(); - await expect(subFolder).toBeVisible(); - await subFolder.click(); - - // Verify the note exists in the subfolder - const noteInSubFolder = page.locator(`a:has-text("${noteName}")`); - await expect(noteInSubFolder).toBeVisible(); }); test('Given Create Note modal is open, When clicking close button, Then modal should close', async () => { diff --git a/zeppelin-web-angular/e2e/tests/share/note-import/note-import-modal.spec.ts b/zeppelin-web-angular/e2e/tests/share/note-import/note-import-modal.spec.ts index a998c9d8143..b02edec9ea3 100644 --- a/zeppelin-web-angular/e2e/tests/share/note-import/note-import-modal.spec.ts +++ b/zeppelin-web-angular/e2e/tests/share/note-import/note-import-modal.spec.ts @@ -93,14 +93,12 @@ test.describe('Note Import Modal', () => { await expect(noteImportModal.modal).not.toBeVisible(); }); - test('Given URL tab is selected, When entering invalid URL and clicking import, Then error should be displayed', async ({ - page - }) => { + test('Given URL tab is selected, When entering invalid URL and clicking import, Then error should be displayed', async () => { await noteImportModal.switchToUrlTab(); await noteImportModal.setImportUrl('invalid-url'); await noteImportModal.clickImportNote(); - await page.waitForTimeout(2000); + await expect(noteImportModal.errorAlert).toBeVisible(); await noteImportModal.isErrorAlertVisible(); const errorMessage = await noteImportModal.getErrorMessage(); From 4d916c2175301fedb0e7f955c6c5cf169ff2239c Mon Sep 17 00:00:00 2001 From: YONGJAE LEE Date: Wed, 25 Feb 2026 20:14:03 +0900 Subject: [PATCH 5/7] apply review again --- .../e2e/models/about-zeppelin-modal.ts | 12 ------ .../e2e/models/header-page.ts | 39 ------------------- .../e2e/models/header-page.util.ts | 25 ------------ .../e2e/models/node-list-page.ts | 6 +-- .../e2e/models/note-import-modal.ts | 24 ------------ .../node-list/node-list-functionality.spec.ts | 3 -- .../note-create/note-create-modal.spec.ts | 6 +++ .../note-import/note-import-modal.spec.ts | 2 - 8 files changed, 7 insertions(+), 110 deletions(-) diff --git a/zeppelin-web-angular/e2e/models/about-zeppelin-modal.ts b/zeppelin-web-angular/e2e/models/about-zeppelin-modal.ts index d488c6f163c..d5a44add770 100644 --- a/zeppelin-web-angular/e2e/models/about-zeppelin-modal.ts +++ b/zeppelin-web-angular/e2e/models/about-zeppelin-modal.ts @@ -35,10 +35,6 @@ export class AboutZeppelinModal extends BasePage { this.licenseLink = page.getByRole('link', { name: 'Licensed under the Apache License, Version 2.0' }); } - async isModalVisible(): Promise { - return this.modal.isVisible(); - } - async close(): Promise { await this.closeButton.click(); } @@ -51,14 +47,6 @@ export class AboutZeppelinModal extends BasePage { return this.logo.isVisible(); } - async clickGetInvolvedLink(): Promise { - await this.getInvolvedLink.click(); - } - - async clickLicenseLink(): Promise { - await this.licenseLink.click(); - } - async getGetInvolvedHref(): Promise { return this.getInvolvedLink.getAttribute('href'); } diff --git a/zeppelin-web-angular/e2e/models/header-page.ts b/zeppelin-web-angular/e2e/models/header-page.ts index 507bd4d895a..2f5c1c496fc 100644 --- a/zeppelin-web-angular/e2e/models/header-page.ts +++ b/zeppelin-web-angular/e2e/models/header-page.ts @@ -23,10 +23,8 @@ export class HeaderPage extends BasePage { readonly jobMenuItem: Locator; readonly userDropdownTrigger: Locator; readonly userBadge: Locator; - readonly userDropdown: Locator; readonly searchInput: Locator; readonly themeToggleButton: Locator; - readonly connectionStatusBadge: Locator; readonly userMenuItems: { aboutZeppelin: Locator; @@ -49,10 +47,8 @@ export class HeaderPage extends BasePage { this.jobMenuItem = page.getByRole('link', { name: 'Job' }); this.userDropdownTrigger = page.locator('.header .user .status'); this.userBadge = page.locator('.header .user nz-badge'); - this.userDropdown = page.locator('ul[nz-menu]').filter({ has: page.getByText('About Zeppelin') }); this.searchInput = page.locator('.header .search input[type="text"]'); this.themeToggleButton = page.locator('zeppelin-theme-toggle button'); - this.connectionStatusBadge = page.locator('.header .user nz-badge'); this.userMenuItems = { aboutZeppelin: page.getByText('About Zeppelin', { exact: true }), @@ -105,45 +101,10 @@ export class HeaderPage extends BasePage { await this.userMenuItems.configuration.click(); } - async clickLogout(): Promise { - await this.userMenuItems.logout.click(); - } - - async clickSwitchToClassicUI(): Promise { - await this.userMenuItems.switchToClassicUI.click(); - } - - async isHeaderVisible(): Promise { - return this.header.isVisible(); - } - async getUsernameText(): Promise { return (await this.userBadge.textContent()) || ''; } - async getConnectionStatus(): Promise { - const status = await this.connectionStatusBadge.locator('.ant-badge-status-dot').getAttribute('class'); - if (status?.includes('success')) { - return 'success'; - } - if (status?.includes('error')) { - return 'error'; - } - return 'unknown'; - } - - async isNotebookDropdownVisible(): Promise { - return this.notebookDropdown.isVisible(); - } - - async isUserDropdownVisible(): Promise { - return this.userDropdown.isVisible(); - } - - async isLogoutMenuItemVisible(): Promise { - return this.userMenuItems.logout.isVisible(); - } - async searchNote(query: string): Promise { await this.searchInput.waitFor({ state: 'visible', timeout: 10000 }); await this.searchInput.fill(query); diff --git a/zeppelin-web-angular/e2e/models/header-page.util.ts b/zeppelin-web-angular/e2e/models/header-page.util.ts index f1fa770ae4c..14a369eb0ec 100644 --- a/zeppelin-web-angular/e2e/models/header-page.util.ts +++ b/zeppelin-web-angular/e2e/models/header-page.util.ts @@ -12,7 +12,6 @@ import { expect, Page } from '@playwright/test'; import { HeaderPage } from './header-page'; -import { AboutZeppelinModal } from './about-zeppelin-modal'; import { NodeListPage } from './node-list-page'; export class HeaderPageUtil { @@ -57,14 +56,6 @@ export class HeaderPageUtil { await expect(nodeList.createNewNoteButton).toBeVisible(); } - async verifyAboutZeppelinModalOpens(): Promise { - await this.headerPage.clickUserDropdown(); - await this.headerPage.clickAboutZeppelin(); - - const aboutModal = new AboutZeppelinModal(this.page); - await expect(aboutModal.modal).toBeVisible(); - } - async verifySearchNavigation(query: string): Promise { await this.headerPage.searchNote(query); await this.page.waitForURL(/search/); @@ -72,11 +63,6 @@ export class HeaderPageUtil { expect(this.page.url()).toContain(query); } - async verifyConnectionStatus(): Promise { - const status = await this.headerPage.getConnectionStatus(); - expect(['success', 'error']).toContain(status); - } - async verifyUserMenuItemsVisible(isLoggedIn: boolean): Promise { await this.headerPage.clickUserDropdown(); await expect(this.headerPage.userMenuItems.aboutZeppelin).toBeVisible(); @@ -93,17 +79,6 @@ export class HeaderPageUtil { } } - async openNotebookDropdownAndVerifyNodeList(): Promise { - await this.headerPage.clickNotebookMenu(); - await expect(this.headerPage.notebookDropdown).toBeVisible(); - - const nodeList = new NodeListPage(this.page); - await expect(nodeList.createNewNoteButton).toBeVisible(); - await expect(nodeList.importNoteButton).toBeVisible(); - await expect(nodeList.filterInput).toBeVisible(); - await expect(nodeList.treeView).toBeVisible(); - } - async navigateToInterpreterSettings(): Promise { await this.headerPage.clickUserDropdown(); await this.headerPage.clickInterpreter(); diff --git a/zeppelin-web-angular/e2e/models/node-list-page.ts b/zeppelin-web-angular/e2e/models/node-list-page.ts index 9f0a5307aac..17bd93de33d 100644 --- a/zeppelin-web-angular/e2e/models/node-list-page.ts +++ b/zeppelin-web-angular/e2e/models/node-list-page.ts @@ -41,10 +41,6 @@ export class NodeListPage extends BasePage { await this.createNewNoteButton.click(); } - async filterNotes(searchTerm: string): Promise { - await this.filterInput.fill(searchTerm); - } - getFolderByName(folderName: string): Locator { return this.page.locator('nz-tree-node').filter({ hasText: folderName }).first(); } @@ -54,7 +50,7 @@ export class NodeListPage extends BasePage { } async clickNote(noteName: string): Promise { - const note = await this.getNoteByName(noteName); + const note = this.getNoteByName(noteName); // Target the specific link that navigates to the notebook (has href with "#/notebook/") const noteLink = note.locator('a[href*="#/notebook/"]'); await noteLink.click(); diff --git a/zeppelin-web-angular/e2e/models/note-import-modal.ts b/zeppelin-web-angular/e2e/models/note-import-modal.ts index 91b5b364c66..11db6d5da41 100644 --- a/zeppelin-web-angular/e2e/models/note-import-modal.ts +++ b/zeppelin-web-angular/e2e/models/note-import-modal.ts @@ -21,7 +21,6 @@ export class NoteImportModal extends BasePage { readonly jsonFileTab: Locator; readonly urlTab: Locator; readonly uploadArea: Locator; - readonly uploadIcon: Locator; readonly uploadText: Locator; readonly fileSizeLimit: Locator; readonly urlInput: Locator; @@ -37,7 +36,6 @@ export class NoteImportModal extends BasePage { this.jsonFileTab = page.getByRole('tab', { name: 'Import From JSON File' }); this.urlTab = page.getByRole('tab', { name: 'Import From URL' }); this.uploadArea = page.locator('nz-upload[nztype="drag"]'); - this.uploadIcon = page.locator('.ant-upload-drag-icon i[nz-icon]'); this.uploadText = page.getByText('Click or drag JSON file to this area to upload'); this.fileSizeLimit = page.locator('.ant-upload-hint strong'); this.urlInput = page.locator('input[name="importUrl"]'); @@ -45,10 +43,6 @@ export class NoteImportModal extends BasePage { this.errorAlert = page.locator('nz-alert[nztype="error"]'); } - async isModalVisible(): Promise { - return this.modal.isVisible(); - } - async close(): Promise { await this.closeButton.click(); } @@ -61,10 +55,6 @@ export class NoteImportModal extends BasePage { return (await this.importAsInput.inputValue()) || ''; } - async switchToJsonFileTab(): Promise { - await this.jsonFileTab.click(); - } - async switchToUrlTab(): Promise { await this.urlTab.click(); } @@ -91,15 +81,6 @@ export class NoteImportModal extends BasePage { return this.importNoteButton.isDisabled(); } - async isImportNoteButtonLoading(): Promise { - const loadingIcon = this.importNoteButton.locator('.anticon-loading'); - return loadingIcon.isVisible(); - } - - async isUploadAreaVisible(): Promise { - return this.uploadArea.isVisible(); - } - async getFileSizeLimit(): Promise { return (await this.fileSizeLimit.textContent()) || ''; } @@ -111,9 +92,4 @@ export class NoteImportModal extends BasePage { async getErrorMessage(): Promise { return (await this.errorAlert.textContent()) || ''; } - - async uploadFile(filePath: string): Promise { - const fileInput = this.page.locator('input[type="file"]'); - await fileInput.setInputFiles(filePath); - } } diff --git a/zeppelin-web-angular/e2e/tests/share/node-list/node-list-functionality.spec.ts b/zeppelin-web-angular/e2e/tests/share/node-list/node-list-functionality.spec.ts index c683752b351..9fdae5001ab 100644 --- a/zeppelin-web-angular/e2e/tests/share/node-list/node-list-functionality.spec.ts +++ b/zeppelin-web-angular/e2e/tests/share/node-list/node-list-functionality.spec.ts @@ -11,18 +11,15 @@ */ import { test, expect } from '@playwright/test'; -import { HomePage } from '../../../models/home-page'; import { NodeListPage } from '../../../models/node-list-page'; import { addPageAnnotationBeforeEach, PAGES, performLoginIfRequired, waitForZeppelinReady } from '../../../utils'; test.describe('Node List Functionality', () => { - let homePage: HomePage; let nodeListPage: NodeListPage; addPageAnnotationBeforeEach(PAGES.SHARE.NODE_LIST); test.beforeEach(async ({ page }) => { - homePage = new HomePage(page); nodeListPage = new NodeListPage(page); await page.goto('/'); diff --git a/zeppelin-web-angular/e2e/tests/share/note-create/note-create-modal.spec.ts b/zeppelin-web-angular/e2e/tests/share/note-create/note-create-modal.spec.ts index 22628b781f1..a2674b4c4ae 100644 --- a/zeppelin-web-angular/e2e/tests/share/note-create/note-create-modal.spec.ts +++ b/zeppelin-web-angular/e2e/tests/share/note-create/note-create-modal.spec.ts @@ -89,6 +89,12 @@ test.describe('Note Create Modal', () => { // Verify the note was created with the correct name (without folder path) const notebookTitle = page.locator('p, .notebook-title, .note-title, h1, [data-testid="notebook-title"]').first(); await expect(notebookTitle).toContainText(noteName); + + // Verify the folder structure was created + await page.goto('/'); + await page.waitForLoadState('networkidle'); + const folder = page.locator('nz-tree-node').filter({ hasText: 'TestFolder' }); + await expect(folder).toBeVisible(); }); test('Given Create Note modal is open, When clicking close button, Then modal should close', async () => { diff --git a/zeppelin-web-angular/e2e/tests/share/note-import/note-import-modal.spec.ts b/zeppelin-web-angular/e2e/tests/share/note-import/note-import-modal.spec.ts index b02edec9ea3..b20bee0902a 100644 --- a/zeppelin-web-angular/e2e/tests/share/note-import/note-import-modal.spec.ts +++ b/zeppelin-web-angular/e2e/tests/share/note-import/note-import-modal.spec.ts @@ -99,8 +99,6 @@ test.describe('Note Import Modal', () => { await noteImportModal.clickImportNote(); await expect(noteImportModal.errorAlert).toBeVisible(); - - await noteImportModal.isErrorAlertVisible(); const errorMessage = await noteImportModal.getErrorMessage(); expect(errorMessage).toBeTruthy(); }); From 1510b7b0ca2de563987a699afea7b847bc505dce Mon Sep 17 00:00:00 2001 From: YONGJAE LEE Date: Wed, 25 Feb 2026 22:49:26 +0900 Subject: [PATCH 6/7] fix trash visible, tooltip select problem --- .../home/home-page-note-operations.spec.ts | 12 ++++----- .../node-list/node-list-functionality.spec.ts | 26 ++++++++++++++++++- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/zeppelin-web-angular/e2e/tests/home/home-page-note-operations.spec.ts b/zeppelin-web-angular/e2e/tests/home/home-page-note-operations.spec.ts index 018bfbf40e3..f385de5f585 100644 --- a/zeppelin-web-angular/e2e/tests/home/home-page-note-operations.spec.ts +++ b/zeppelin-web-angular/e2e/tests/home/home-page-note-operations.spec.ts @@ -55,18 +55,18 @@ test.describe('Home Page Note Operations', () => { const firstNote = page.locator('.node .file').first(); await firstNote.hover(); - await expect(homePage.nodeList.noteActions.renameNote).toBeVisible(); - await expect(homePage.nodeList.noteActions.clearOutput).toBeVisible(); - await expect(homePage.nodeList.noteActions.moveToTrash).toBeVisible(); + await expect(homePage.nodeList.noteActions.renameNote.first()).toBeVisible(); + await expect(homePage.nodeList.noteActions.clearOutput.first()).toBeVisible(); + await expect(homePage.nodeList.noteActions.moveToTrash.first()).toBeVisible(); // Test tooltip visibility by hovering over each icon - await homePage.nodeList.noteActions.renameNote.hover(); + await homePage.nodeList.noteActions.renameNote.first().hover(); await expect(page.locator('.ant-tooltip', { hasText: 'Rename note' })).toBeVisible(); - await homePage.nodeList.noteActions.clearOutput.hover(); + await homePage.nodeList.noteActions.clearOutput.first().hover(); await expect(page.locator('.ant-tooltip', { hasText: 'Clear output' })).toBeVisible(); - await homePage.nodeList.noteActions.moveToTrash.hover(); + await homePage.nodeList.noteActions.moveToTrash.first().hover(); await expect(page.locator('.ant-tooltip', { hasText: 'Move note to Trash' })).toBeVisible(); } }); diff --git a/zeppelin-web-angular/e2e/tests/share/node-list/node-list-functionality.spec.ts b/zeppelin-web-angular/e2e/tests/share/node-list/node-list-functionality.spec.ts index 9fdae5001ab..1cdefeae435 100644 --- a/zeppelin-web-angular/e2e/tests/share/node-list/node-list-functionality.spec.ts +++ b/zeppelin-web-angular/e2e/tests/share/node-list/node-list-functionality.spec.ts @@ -11,6 +11,7 @@ */ import { test, expect } from '@playwright/test'; +import { HomePage } from '../../../models/home-page'; import { NodeListPage } from '../../../models/node-list-page'; import { addPageAnnotationBeforeEach, PAGES, performLoginIfRequired, waitForZeppelinReady } from '../../../utils'; @@ -42,7 +43,30 @@ test.describe('Node List Functionality', () => { expect(isFilterVisible).toBe(true); }); - test('Given user is on home page, When viewing node list, Then trash folder should be visible', async () => { + test('Given a note has been moved to trash, When viewing node list, Then trash folder should be visible', async ({ + page + }) => { + const homePage = new HomePage(page); + + // Create a test note to ensure there is something to trash + await homePage.createNote('_e2e_trash_test'); + + // Navigate back to home + await page.goto('/'); + await waitForZeppelinReady(page); + + // Hover on the created note and move it to trash + const testNote = page.locator('.node .file').filter({ hasText: '_e2e_trash_test' }); + await testNote.hover(); + const moveToTrashButton = testNote.locator('.operation a[nztooltiptitle*="Move note to Trash"]'); + await moveToTrashButton.click(); + + // Confirm the move to trash dialog + const confirmButton = page.locator('button:has-text("Yes")'); + await confirmButton.click(); + + // Wait for the trash folder to appear and verify + await expect(nodeListPage.trashFolder).toBeVisible({ timeout: 10000 }); const isTrashVisible = await nodeListPage.isTrashFolderVisible(); expect(isTrashVisible).toBe(true); }); From f679f23abc92e64366523ded66d898e8d54bf37b Mon Sep 17 00:00:00 2001 From: YONGJAE LEE Date: Wed, 25 Feb 2026 23:39:56 +0900 Subject: [PATCH 7/7] fix broken test by hover --- .../node-list/node-list-functionality.spec.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/zeppelin-web-angular/e2e/tests/share/node-list/node-list-functionality.spec.ts b/zeppelin-web-angular/e2e/tests/share/node-list/node-list-functionality.spec.ts index 1cdefeae435..111d01011f7 100644 --- a/zeppelin-web-angular/e2e/tests/share/node-list/node-list-functionality.spec.ts +++ b/zeppelin-web-angular/e2e/tests/share/node-list/node-list-functionality.spec.ts @@ -55,14 +55,18 @@ test.describe('Node List Functionality', () => { await page.goto('/'); await waitForZeppelinReady(page); - // Hover on the created note and move it to trash + // Wait for the created note to appear in the node list, then hover const testNote = page.locator('.node .file').filter({ hasText: '_e2e_trash_test' }); + await expect(testNote).toBeVisible({ timeout: 15000 }); await testNote.hover(); - const moveToTrashButton = testNote.locator('.operation a[nztooltiptitle*="Move note to Trash"]'); - await moveToTrashButton.click(); - // Confirm the move to trash dialog - const confirmButton = page.locator('button:has-text("Yes")'); + // Click the delete icon (nz-popconfirm is on the element) + const deleteIcon = testNote.locator('.operation i[nztype="delete"]'); + await deleteIcon.click(); + + // Confirm the popconfirm dialog (ng-zorro en_US default is "OK", not "Yes") + await expect(page.locator('text=This note will be moved to trash.')).toBeVisible(); + const confirmButton = page.locator('.ant-popover button:has-text("OK")'); await confirmButton.click(); // Wait for the trash folder to appear and verify