This is an automated email from the ASF dual-hosted git repository.

chanholee pushed a commit to branch branch-0.12
in repository https://gitbox.apache.org/repos/asf/zeppelin.git


The following commit(s) were added to refs/heads/branch-0.12 by this push:
     new cf894ffec4 [ZEPPELIN-6373] Add E2E tests about share area
cf894ffec4 is described below

commit cf894ffec487f2727f3e2ccbd3c6376ba07d205c
Author: YONGJAE LEE(이용재) <[email protected]>
AuthorDate: Thu Feb 26 09:52:16 2026 +0900

    [ZEPPELIN-6373] Add E2E tests about share area
    
    ### What is this PR for?
    ABOUT_ZEPPELIN:'src/app/share/about-zeppelin/about-zeppelin.component',
    CODE_EDITOR:'src/app/share/code-editor/code-editor.component',
    FOLDER_RENAME:'src/app/share/folder-rename/folder-rename.component',
    HEADER:'src/app/share/header/header.component',
    NODE_LIST:'src/app/share/node-list/node-list.component',
    NOTE_CREATE:'src/app/share/note-create/note-create.component',
    NOTE_IMPORT:'src/app/share/note-import/note-import.component',
    NOTE_RENAME:'src/app/share/note-rename/note-rename.component',
    NOTE_TOC:'src/app/share/note-toc/note-toc.component',
    PAGE_HEADER:'src/app/share/page-header/page-header.component',
    RESIZE_HANDLE:'src/app/share/resize-handle/resize-handle.component',
    SHORTCUT:'src/app/share/shortcut/shortcut.component',
    SPIN:'src/app/share/spin/spin.component',
    THEME_TOGGLE:'src/app/share/theme-toggle/theme-toggle.component'
    
    ### What type of PR is it?
    Improvement
    *Please leave your type of PR only*
    
    ### Todos
    
    ### What is the Jira issue?
    ZEPPELIN-6373
    
    ### How should this be tested?
    
    ### Screenshots (if appropriate)
    
    ### Questions:
    * Does the license files need to update? No
    * Is there breaking changes for older versions? No
    * Does this needs documentation? No
    
    Closes #5114 from dididy/e2e/share.
    
    Signed-off-by: ChanHo Lee <[email protected]>
    (cherry picked from commit cf766dbcbc0373be73424b0ce8859d5bb747c948)
    Signed-off-by: ChanHo Lee <[email protected]>
---
 .../e2e/models/about-zeppelin-modal.ts             |  57 +++++++++++
 zeppelin-web-angular/e2e/models/header-page.ts     | 113 +++++++++++++++++++++
 .../e2e/models/header-page.util.ts                 | 109 ++++++++++++++++++++
 zeppelin-web-angular/e2e/models/node-list-page.ts  |  78 ++++++++++++++
 .../e2e/models/note-create-modal.ts                |  54 ++++++++++
 .../e2e/models/note-create-modal.util.ts           |  40 ++++++++
 .../e2e/models/note-import-modal.ts                |  95 +++++++++++++++++
 .../tests/home/home-page-note-operations.spec.ts   |  12 +--
 .../about-zeppelin/about-zeppelin-modal.spec.ts    |  69 +++++++++++++
 .../tests/share/header/header-navigation.spec.ts   |  73 +++++++++++++
 .../e2e/tests/share/header/header-search.spec.ts   |  42 ++++++++
 .../node-list/node-list-functionality.spec.ts      | 113 +++++++++++++++++++++
 .../share/note-create/note-create-modal.spec.ts    | 108 ++++++++++++++++++++
 .../share/note-import/note-import-modal.spec.ts    | 105 +++++++++++++++++++
 zeppelin-web-angular/e2e/utils.ts                  |   2 +-
 15 files changed, 1063 insertions(+), 7 deletions(-)

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 0000000000..d5a44add77
--- /dev/null
+++ b/zeppelin-web-angular/e2e/models/about-zeppelin-modal.ts
@@ -0,0 +1,57 @@
+/*
+ * 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 close(): Promise<void> {
+    await this.closeButton.click();
+  }
+
+  async getVersionText(): Promise<string> {
+    return (await this.versionText.textContent()) || '';
+  }
+
+  async isLogoVisible(): Promise<boolean> {
+    return this.logo.isVisible();
+  }
+
+  async getGetInvolvedHref(): Promise<string | null> {
+    return this.getInvolvedLink.getAttribute('href');
+  }
+
+  async getLicenseHref(): Promise<string | null> {
+    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 0000000000..2f5c1c496f
--- /dev/null
+++ b/zeppelin-web-angular/e2e/models/header-page.ts
@@ -0,0 +1,113 @@
+/*
+ * 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 searchInput: Locator;
+  readonly themeToggleButton: 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.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');
+    this.searchInput = page.locator('.header .search input[type="text"]');
+    this.themeToggleButton = page.locator('zeppelin-theme-toggle button');
+
+    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<void> {
+    await this.brandLink.waitFor({ state: 'visible', timeout: 10000 });
+    await this.brandLink.click();
+  }
+
+  async clickNotebookMenu(): Promise<void> {
+    await this.notebookDropdownTrigger.waitFor({ state: 'visible', timeout: 
10000 });
+    await this.notebookDropdownTrigger.click();
+  }
+
+  async clickJobMenu(): Promise<void> {
+    await this.jobMenuItem.waitFor({ state: 'visible', timeout: 10000 });
+    await this.jobMenuItem.click();
+  }
+
+  async clickUserDropdown(): Promise<void> {
+    await this.userDropdownTrigger.waitFor({ state: 'visible', timeout: 10000 
});
+    await this.userDropdownTrigger.click();
+  }
+
+  async clickAboutZeppelin(): Promise<void> {
+    await this.userMenuItems.aboutZeppelin.click();
+  }
+
+  async clickInterpreter(): Promise<void> {
+    await this.userMenuItems.interpreter.click();
+  }
+
+  async clickNotebookRepos(): Promise<void> {
+    await this.userMenuItems.notebookRepos.click();
+  }
+
+  async clickCredential(): Promise<void> {
+    await this.userMenuItems.credential.click();
+  }
+
+  async clickConfiguration(): Promise<void> {
+    await this.userMenuItems.configuration.click();
+  }
+
+  async getUsernameText(): Promise<string> {
+    return (await this.userBadge.textContent()) || '';
+  }
+
+  async searchNote(query: string): Promise<void> {
+    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 0000000000..14a369eb0e
--- /dev/null
+++ b/zeppelin-web-angular/e2e/models/header-page.util.ts
@@ -0,0 +1,109 @@
+/*
+ * 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 { NodeListPage } from './node-list-page';
+
+export class HeaderPageUtil {
+  constructor(
+    private readonly page: Page,
+    private readonly headerPage: HeaderPage
+  ) {}
+
+  async verifyHeaderIsDisplayed(): Promise<void> {
+    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<void> {
+    await this.headerPage.clickBrandLogo();
+    await this.page.waitForURL(/\/(#\/)?$/);
+    const url = this.page.url();
+    expect(url).toMatch(/\/(#\/)?$/);
+  }
+
+  async verifyNavigationToJobManager(): Promise<void> {
+    await this.headerPage.clickJobMenu();
+    await this.page.waitForURL(/jobmanager/);
+    expect(this.page.url()).toContain('jobmanager');
+  }
+
+  async verifyUserDropdownOpens(): Promise<void> {
+    await this.headerPage.clickUserDropdown();
+    await expect(this.headerPage.userMenuItems.aboutZeppelin).toBeVisible();
+  }
+
+  async verifyNotebookDropdownOpens(): Promise<void> {
+    await this.headerPage.clickNotebookMenu();
+    await expect(this.headerPage.notebookDropdown).toBeVisible();
+
+    const nodeList = new NodeListPage(this.page);
+    await expect(nodeList.createNewNoteButton).toBeVisible();
+  }
+
+  async verifySearchNavigation(query: string): Promise<void> {
+    await this.headerPage.searchNote(query);
+    await this.page.waitForURL(/search/);
+    expect(this.page.url()).toContain('search');
+    expect(this.page.url()).toContain(query);
+  }
+
+  async verifyUserMenuItemsVisible(isLoggedIn: boolean): Promise<void> {
+    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 navigateToInterpreterSettings(): Promise<void> {
+    await this.headerPage.clickUserDropdown();
+    await this.headerPage.clickInterpreter();
+    await this.page.waitForURL(/interpreter/);
+    expect(this.page.url()).toContain('interpreter');
+  }
+
+  async navigateToNotebookRepos(): Promise<void> {
+    await this.headerPage.clickUserDropdown();
+    await this.headerPage.clickNotebookRepos();
+    await this.page.waitForURL(/notebook-repos/);
+    expect(this.page.url()).toContain('notebook-repos');
+  }
+
+  async navigateToCredential(): Promise<void> {
+    await this.headerPage.clickUserDropdown();
+    await this.headerPage.clickCredential();
+    await this.page.waitForURL(/credential/);
+    expect(this.page.url()).toContain('credential');
+  }
+
+  async navigateToConfiguration(): Promise<void> {
+    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 0000000000..17bd93de33
--- /dev/null
+++ b/zeppelin-web-angular/e2e/models/node-list-page.ts
@@ -0,0 +1,78 @@
+/*
+ * 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 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.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<void> {
+    await this.importNoteButton.click();
+  }
+
+  async clickCreateNewNote(): Promise<void> {
+    await this.createNewNoteButton.click();
+  }
+
+  getFolderByName(folderName: string): Locator {
+    return this.page.locator('nz-tree-node').filter({ hasText: folderName 
}).first();
+  }
+
+  getNoteByName(noteName: string): Locator {
+    return this.page.locator('nz-tree-node').filter({ hasText: noteName 
}).first();
+  }
+
+  async clickNote(noteName: string): Promise<void> {
+    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();
+  }
+
+  async isFilterInputVisible(): Promise<boolean> {
+    return this.filterInput.isVisible();
+  }
+
+  async isTrashFolderVisible(): Promise<boolean> {
+    return this.trashFolder.isVisible();
+  }
+
+  async getAllVisibleNoteNames(): Promise<string[]> {
+    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;
+  }
+}
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 0000000000..1e1a0c4808
--- /dev/null
+++ b/zeppelin-web-angular/e2e/models/note-create-modal.ts
@@ -0,0 +1,54 @@
+/*
+ * 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 closeButton: Locator;
+  readonly noteNameInput: Locator;
+  readonly interpreterDropdown: Locator;
+  readonly folderInfoAlert: Locator;
+  readonly createButton: Locator;
+
+  constructor(page: Page) {
+    super(page);
+    this.modal = page.locator('[role="dialog"]').filter({ has: 
page.locator('input[name="noteName"]') });
+    this.closeButton = page.getByRole('button', { name: 'Close' });
+    this.noteNameInput = page.locator('input[name="noteName"]');
+    this.interpreterDropdown = 
page.locator('nz-select[name="defaultInterpreter"]');
+    this.folderInfoAlert = page.getByText("Use '/' to create folders");
+    this.createButton = page.getByRole('button', { name: 'Create' });
+  }
+
+  async close(): Promise<void> {
+    await this.closeButton.click();
+  }
+
+  async getNoteName(): Promise<string> {
+    return (await this.noteNameInput.inputValue()) || '';
+  }
+
+  async setNoteName(name: string): Promise<void> {
+    await this.noteNameInput.clear();
+    await this.noteNameInput.fill(name);
+  }
+
+  async clickCreate(): Promise<void> {
+    await this.createButton.click();
+  }
+
+  async isFolderInfoVisible(): Promise<boolean> {
+    return this.folderInfoAlert.isVisible();
+  }
+}
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 0000000000..7553325c1e
--- /dev/null
+++ b/zeppelin-web-angular/e2e/models/note-create-modal.util.ts
@@ -0,0 +1,40 @@
+/*
+ * 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 } from '@playwright/test';
+import { NoteCreateModal } from './note-create-modal';
+
+export class NoteCreateModalUtil {
+  constructor(private readonly modal: NoteCreateModal) {}
+
+  async verifyModalIsOpen(): Promise<void> {
+    await expect(this.modal.modal).toBeVisible();
+    await expect(this.modal.noteNameInput).toBeVisible();
+    await expect(this.modal.createButton).toBeVisible();
+  }
+
+  async verifyDefaultNoteName(expectedPattern: RegExp): Promise<void> {
+    const noteName = await this.modal.getNoteName();
+    expect(noteName).toMatch(expectedPattern);
+  }
+
+  async verifyFolderCreationInfo(): Promise<void> {
+    await expect(this.modal.folderInfoAlert).toBeVisible();
+    const text = await this.modal.folderInfoAlert.textContent();
+    expect(text).toContain('/');
+  }
+
+  async verifyModalClose(): Promise<void> {
+    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 0000000000..11db6d5da4
--- /dev/null
+++ b/zeppelin-web-angular/e2e/models/note-import-modal.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 { 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 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.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 close(): Promise<void> {
+    await this.closeButton.click();
+  }
+
+  async setImportAsName(name: string): Promise<void> {
+    await this.importAsInput.fill(name);
+  }
+
+  async getImportAsName(): Promise<string> {
+    return (await this.importAsInput.inputValue()) || '';
+  }
+
+  async switchToUrlTab(): Promise<void> {
+    await this.urlTab.click();
+  }
+
+  async isJsonFileTabSelected(): Promise<boolean> {
+    const ariaSelected = await this.jsonFileTab.getAttribute('aria-selected');
+    return ariaSelected === 'true';
+  }
+
+  async isUrlTabSelected(): Promise<boolean> {
+    const ariaSelected = await this.urlTab.getAttribute('aria-selected');
+    return ariaSelected === 'true';
+  }
+
+  async setImportUrl(url: string): Promise<void> {
+    await this.urlInput.fill(url);
+  }
+
+  async clickImportNote(): Promise<void> {
+    await this.importNoteButton.click();
+  }
+
+  async isImportNoteButtonDisabled(): Promise<boolean> {
+    return this.importNoteButton.isDisabled();
+  }
+
+  async getFileSizeLimit(): Promise<string> {
+    return (await this.fileSizeLimit.textContent()) || '';
+  }
+
+  async isErrorAlertVisible(): Promise<boolean> {
+    return this.errorAlert.isVisible();
+  }
+
+  async getErrorMessage(): Promise<string> {
+    return (await this.errorAlert.textContent()) || '';
+  }
+}
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 018bfbf40e..f385de5f58 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/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 0000000000..2e8ab234a7
--- /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 0000000000..18ae43faba
--- /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 0000000000..171f2d5255
--- /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 0000000000..111d01011f
--- /dev/null
+++ 
b/zeppelin-web-angular/e2e/tests/share/node-list/node-list-functionality.spec.ts
@@ -0,0 +1,113 @@
+/*
+ * 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 nodeListPage: NodeListPage;
+
+  addPageAnnotationBeforeEach(PAGES.SHARE.NODE_LIST);
+
+  test.beforeEach(async ({ 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 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);
+
+    // 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();
+
+    // Click the delete icon (nz-popconfirm is on the <i> 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
+    await expect(nodeListPage.trashFolder).toBeVisible({ timeout: 10000 });
+    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 expect(nodeListPage.treeView).toBeVisible();
+    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 0000000000..a2674b4c4a
--- /dev/null
+++ b/zeppelin-web-angular/e2e/tests/share/note-create/note-create-modal.spec.ts
@@ -0,0 +1,108 @@
+/*
+ * 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(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 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();
+
+    // 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('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.getByRole('link', { name: 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 ({
+    page
+  }) => {
+    const folderPath = `/TestFolder/SubFolder`;
+    const noteName = `Note ${Date.now()}`;
+    const fullPath = `${folderPath}/${noteName}`;
+
+    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('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 () => {
+    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 0000000000..b20bee0902
--- /dev/null
+++ b/zeppelin-web-angular/e2e/tests/share/note-import/note-import-modal.spec.ts
@@ -0,0 +1,105 @@
+/*
+ * 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 () => {
+    await noteImportModal.switchToUrlTab();
+    await noteImportModal.setImportUrl('invalid-url');
+    await noteImportModal.clickImportNote();
+
+    await expect(noteImportModal.errorAlert).toBeVisible();
+    const errorMessage = await noteImportModal.getErrorMessage();
+    expect(errorMessage).toBeTruthy();
+  });
+});
diff --git a/zeppelin-web-angular/e2e/utils.ts 
b/zeppelin-web-angular/e2e/utils.ts
index dab04a1325..d4fd455a97 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<void> => {
     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(
         () => {

Reply via email to