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

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


The following commit(s) were added to refs/heads/master by this push:
     new b27925599d [ZEPPELIN-6365] Add E2E tests about Notebook Repositories 
(/notebook-repos)
b27925599d is described below

commit b27925599deb4045aeca7fa273291a6997354ae5
Author: Yuijin Kim(yuikim) <[email protected]>
AuthorDate: Sun Oct 26 17:19:03 2025 +0900

    [ZEPPELIN-6365] Add E2E tests about Notebook Repositories (/notebook-repos)
    
    ### What is this PR for?
    Addition of Notebook Repositories E2E tests for New UI
    
    ---
    
    PAGES.WORKSPACE.NOTEBOOK_REPOS
    
    → src/app/pages/workspace/notebook-repos/notebook-repos.component
    
    PAGES.WORKSPACE.NOTEBOOK_REPOS_ITEM
    
    → src/app/pages/workspace/notebook-repos/item/item.component
    
    
    ### What type of PR is it?
    Improvement
    
    
    ### Todos
    * [X] - add Notebook Repositories E2E test
    
    ### What is the Jira issue?
    
[ZEPPELIN-6365](https://issues.apache.org/jira/secure/RapidBoard.jspa?rapidView=632&view=detail&selectedIssue=ZEPPELIN-6365)
    
    ### How should this be tested?
    by E2E test CLI
    
    ### 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 #5105 from kmularise/ZEPPELIN-6365.
    
    Signed-off-by: ChanHo Lee <[email protected]>
---
 .github/workflows/frontend.yml                     |   1 +
 .../e2e/models/notebook-repos-page.ts              | 127 +++++++++++++++++
 .../e2e/models/notebook-repos-page.util.ts         | 137 ++++++++++++++++++
 .../e2e/models/published-paragraph-page.ts         |  16 ---
 .../e2e/models/published-paragraph-page.util.ts    |  47 -------
 .../notebook-repo-item-display.spec.ts             |  56 ++++++++
 .../notebook-repos/notebook-repo-item-edit.spec.ts |  95 +++++++++++++
 .../notebook-repo-item-form-validation.spec.ts     | 124 ++++++++++++++++
 .../notebook-repo-item-settings.spec.ts            | 154 ++++++++++++++++++++
 .../notebook-repo-item-workflow.spec.ts            | 156 +++++++++++++++++++++
 .../notebook-repos-page-structure.spec.ts          |  51 +++++++
 11 files changed, 901 insertions(+), 63 deletions(-)

diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml
index 8cdd0f808a..587005fb08 100644
--- a/.github/workflows/frontend.yml
+++ b/.github/workflows/frontend.yml
@@ -100,6 +100,7 @@ jobs:
           export ZEPPELIN_CONF_DIR=./conf
           if [ "${{ matrix.mode }}" != "anonymous" ]; then
             cp conf/shiro.ini.template conf/shiro.ini
+            sed -i 's/user1 = password2, role1, role2/user1 = password2, 
role1, role2, admin/' conf/shiro.ini
           fi
       - name: Run headless E2E test with Maven
         run: xvfb-run --auto-servernum --server-args="-screen 0 1024x768x24" 
./mvnw verify -pl zeppelin-web-angular -Pweb-e2e ${MAVEN_ARGS}
diff --git a/zeppelin-web-angular/e2e/models/notebook-repos-page.ts 
b/zeppelin-web-angular/e2e/models/notebook-repos-page.ts
new file mode 100644
index 0000000000..66befc4d2b
--- /dev/null
+++ b/zeppelin-web-angular/e2e/models/notebook-repos-page.ts
@@ -0,0 +1,127 @@
+/*
+ * 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 { waitForZeppelinReady } from '../utils';
+import { BasePage } from './base-page';
+
+export class NotebookReposPage extends BasePage {
+  readonly pageHeader: Locator;
+  readonly pageDescription: Locator;
+  readonly repositoryItems: Locator;
+
+  constructor(page: Page) {
+    super(page);
+    this.pageHeader = page.locator('zeppelin-page-header[title="Notebook 
Repository"]');
+    this.pageDescription = page.locator("text=Manage your Notebook 
Repositories' settings.");
+    this.repositoryItems = page.locator('zeppelin-notebook-repo-item');
+  }
+
+  async navigate(): Promise<void> {
+    await this.page.goto('/#/notebook-repos', { waitUntil: 'load' });
+    await this.page.waitForURL('**/#/notebook-repos', { timeout: 15000 });
+    await waitForZeppelinReady(this.page);
+    await this.page.waitForLoadState('networkidle', { timeout: 15000 });
+    await this.page.waitForSelector('zeppelin-notebook-repo-item, 
zeppelin-page-header[title="Notebook Repository"]', {
+      state: 'visible',
+      timeout: 20000
+    });
+  }
+
+  async getRepositoryItemCount(): Promise<number> {
+    return await this.repositoryItems.count();
+  }
+}
+
+export class NotebookRepoItemPage {
+  readonly page: Page;
+  readonly repositoryCard: Locator;
+  readonly repositoryName: Locator;
+  readonly editButton: Locator;
+  readonly saveButton: Locator;
+  readonly cancelButton: Locator;
+  readonly settingTable: Locator;
+  readonly settingRows: Locator;
+
+  constructor(page: Page, repoName: string) {
+    this.page = page;
+    this.repositoryCard = page.locator('nz-card').filter({ hasText: repoName 
});
+    this.repositoryName = this.repositoryCard.locator('.ant-card-head-title');
+    this.editButton = this.repositoryCard.locator('button:has-text("Edit")');
+    this.saveButton = this.repositoryCard.locator('button:has-text("Save")');
+    this.cancelButton = 
this.repositoryCard.locator('button:has-text("Cancel")');
+    this.settingTable = this.repositoryCard.locator('nz-table');
+    this.settingRows = this.repositoryCard.locator('tbody tr');
+  }
+
+  async clickEdit(): Promise<void> {
+    await this.editButton.click();
+  }
+
+  async clickSave(): Promise<void> {
+    await this.saveButton.click();
+  }
+
+  async clickCancel(): Promise<void> {
+    await this.cancelButton.click();
+  }
+
+  async isEditMode(): Promise<boolean> {
+    return await this.repositoryCard.evaluate(el => 
el.classList.contains('edit'));
+  }
+
+  async isSaveButtonEnabled(): Promise<boolean> {
+    return await this.saveButton.isEnabled();
+  }
+
+  async getSettingValue(settingName: string): Promise<string> {
+    const row = this.repositoryCard.locator('tbody tr').filter({ hasText: 
settingName });
+    const valueCell = row.locator('td').nth(1);
+    return (await valueCell.textContent()) || '';
+  }
+
+  async fillSettingInput(settingName: string, value: string): Promise<void> {
+    const row = this.repositoryCard.locator('tbody tr').filter({ hasText: 
settingName });
+    const input = row.locator('input[nz-input]');
+    await input.clear();
+    await input.fill(value);
+  }
+
+  async selectSettingDropdown(settingName: string, optionValue: string): 
Promise<void> {
+    const row = this.repositoryCard.locator('tbody tr').filter({ hasText: 
settingName });
+    const select = row.locator('nz-select');
+    await select.click();
+    await this.page.locator(`nz-option[nzvalue="${optionValue}"]`).click();
+  }
+
+  async getSettingInputValue(settingName: string): Promise<string> {
+    const row = this.repositoryCard.locator('tbody tr').filter({ hasText: 
settingName });
+    const input = row.locator('input[nz-input]');
+    return await input.inputValue();
+  }
+
+  async isInputVisible(settingName: string): Promise<boolean> {
+    const row = this.repositoryCard.locator('tbody tr').filter({ hasText: 
settingName });
+    const input = row.locator('input[nz-input]');
+    return await input.isVisible();
+  }
+
+  async isDropdownVisible(settingName: string): Promise<boolean> {
+    const row = this.repositoryCard.locator('tbody tr').filter({ hasText: 
settingName });
+    const select = row.locator('nz-select');
+    return await select.isVisible();
+  }
+
+  async getSettingCount(): Promise<number> {
+    return await this.settingRows.count();
+  }
+}
diff --git a/zeppelin-web-angular/e2e/models/notebook-repos-page.util.ts 
b/zeppelin-web-angular/e2e/models/notebook-repos-page.util.ts
new file mode 100644
index 0000000000..d2b0b1f204
--- /dev/null
+++ b/zeppelin-web-angular/e2e/models/notebook-repos-page.util.ts
@@ -0,0 +1,137 @@
+/*
+ * 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 { NotebookReposPage, NotebookRepoItemPage } from 
'./notebook-repos-page';
+
+export class NotebookReposPageUtil {
+  private notebookReposPage: NotebookReposPage;
+  private page: Page;
+
+  constructor(page: Page) {
+    this.page = page;
+    this.notebookReposPage = new NotebookReposPage(page);
+  }
+
+  async verifyPageStructure(): Promise<void> {
+    await expect(this.notebookReposPage.pageHeader).toBeVisible();
+    await expect(this.notebookReposPage.pageDescription).toBeVisible();
+  }
+
+  async verifyRepositoryListDisplayed(): Promise<void> {
+    const count = await this.notebookReposPage.getRepositoryItemCount();
+    expect(count).toBeGreaterThan(0);
+  }
+
+  async verifyAllRepositoriesRendered(): Promise<number> {
+    const count = await this.notebookReposPage.getRepositoryItemCount();
+    expect(count).toBeGreaterThan(0);
+    return count;
+  }
+
+  async getRepositoryItem(repoName: string): Promise<NotebookRepoItemPage> {
+    return new NotebookRepoItemPage(this.page, repoName);
+  }
+
+  async verifyRepositoryCardDisplayed(repoName: string): Promise<void> {
+    const repoItem = await this.getRepositoryItem(repoName);
+    await expect(repoItem.repositoryCard).toBeVisible();
+    await expect(repoItem.repositoryName).toContainText(repoName);
+  }
+}
+
+export class NotebookRepoItemUtil {
+  private repoItemPage: NotebookRepoItemPage;
+
+  constructor(page: Page, repoName: string) {
+    this.repoItemPage = new NotebookRepoItemPage(page, repoName);
+  }
+
+  async verifyDisplayMode(): Promise<void> {
+    await expect(this.repoItemPage.editButton).toBeVisible();
+    const isEditMode = await this.repoItemPage.isEditMode();
+    expect(isEditMode).toBe(false);
+  }
+
+  async verifyEditMode(): Promise<void> {
+    await expect(this.repoItemPage.saveButton).toBeVisible();
+    await expect(this.repoItemPage.cancelButton).toBeVisible();
+    const isEditMode = await this.repoItemPage.isEditMode();
+    expect(isEditMode).toBe(true);
+  }
+
+  async enterEditMode(): Promise<void> {
+    await this.repoItemPage.clickEdit();
+    await this.verifyEditMode();
+  }
+
+  async exitEditModeByCancel(): Promise<void> {
+    await this.repoItemPage.clickCancel();
+    await this.verifyDisplayMode();
+  }
+
+  async exitEditModeBySave(): Promise<void> {
+    await this.repoItemPage.clickSave();
+    await this.verifyDisplayMode();
+  }
+
+  async verifySettingsDisplayed(): Promise<void> {
+    const settingCount = await this.repoItemPage.getSettingCount();
+    expect(settingCount).toBeGreaterThan(0);
+  }
+
+  async verifyInputTypeSettingInEditMode(settingName: string): Promise<void> {
+    const isVisible = await this.repoItemPage.isInputVisible(settingName);
+    expect(isVisible).toBe(true);
+  }
+
+  async verifyDropdownTypeSettingInEditMode(settingName: string): 
Promise<void> {
+    const isVisible = await this.repoItemPage.isDropdownVisible(settingName);
+    expect(isVisible).toBe(true);
+  }
+
+  async updateInputSetting(settingName: string, value: string): Promise<void> {
+    await this.repoItemPage.fillSettingInput(settingName, value);
+    const inputValue = await 
this.repoItemPage.getSettingInputValue(settingName);
+    expect(inputValue).toBe(value);
+  }
+
+  async updateDropdownSetting(settingName: string, optionValue: string): 
Promise<void> {
+    await this.repoItemPage.selectSettingDropdown(settingName, optionValue);
+  }
+
+  async verifySaveButtonDisabled(): Promise<void> {
+    const isEnabled = await this.repoItemPage.isSaveButtonEnabled();
+    expect(isEnabled).toBe(false);
+  }
+
+  async verifySaveButtonEnabled(): Promise<void> {
+    const isEnabled = await this.repoItemPage.isSaveButtonEnabled();
+    expect(isEnabled).toBe(true);
+  }
+
+  async verifyFormReset(settingName: string, originalValue: string): 
Promise<void> {
+    const currentValue = await this.repoItemPage.getSettingValue(settingName);
+    expect(currentValue.trim()).toBe(originalValue.trim());
+  }
+
+  async performCompleteEditWorkflow(settingName: string, newValue: string, 
isInput: boolean = true): Promise<void> {
+    await this.enterEditMode();
+    if (isInput) {
+      await this.updateInputSetting(settingName, newValue);
+    } else {
+      await this.updateDropdownSetting(settingName, newValue);
+    }
+    await this.verifySaveButtonEnabled();
+    await this.exitEditModeBySave();
+  }
+}
diff --git a/zeppelin-web-angular/e2e/models/published-paragraph-page.ts 
b/zeppelin-web-angular/e2e/models/published-paragraph-page.ts
index 410fd8bfa2..73f37b1798 100644
--- a/zeppelin-web-angular/e2e/models/published-paragraph-page.ts
+++ b/zeppelin-web-angular/e2e/models/published-paragraph-page.ts
@@ -49,14 +49,6 @@ export class PublishedParagraphPage extends BasePage {
     await this.waitForPageLoad();
   }
 
-  async isPublishedParagraphVisible(): Promise<boolean> {
-    return await this.publishedParagraphContainer.isVisible();
-  }
-
-  async getErrorModalTitle(): Promise<string> {
-    return (await this.errorModalTitle.textContent()) || '';
-  }
-
   async getErrorModalContent(): Promise<string> {
     return (await this.errorModalContent.textContent()) || '';
   }
@@ -65,14 +57,6 @@ export class PublishedParagraphPage extends BasePage {
     await this.errorModalOkButton.click();
   }
 
-  async isDynamicFormsVisible(): Promise<boolean> {
-    return await this.dynamicForms.isVisible();
-  }
-
-  async isResultVisible(): Promise<boolean> {
-    return await this.paragraphResult.isVisible();
-  }
-
   async getCurrentUrl(): Promise<string> {
     return this.page.url();
   }
diff --git a/zeppelin-web-angular/e2e/models/published-paragraph-page.util.ts 
b/zeppelin-web-angular/e2e/models/published-paragraph-page.util.ts
index fe86b6c630..8f91c02094 100644
--- a/zeppelin-web-angular/e2e/models/published-paragraph-page.util.ts
+++ b/zeppelin-web-angular/e2e/models/published-paragraph-page.util.ts
@@ -25,53 +25,6 @@ export class PublishedParagraphTestUtil {
     this.notebookUtil = new NotebookUtil(page);
   }
 
-  async testConfirmationModalForNoResultParagraph({
-    noteId,
-    paragraphId
-  }: {
-    noteId: string;
-    paragraphId: string;
-  }): Promise<void> {
-    await this.publishedParagraphPage.navigateToNotebook(noteId);
-
-    const paragraphElement = 
this.page.locator('zeppelin-notebook-paragraph').first();
-
-    const settingsButton = paragraphElement.locator('a[nz-dropdown]');
-    await settingsButton.click();
-
-    const clearOutputButton = this.page.locator('li.list-item:has-text("Clear 
output")');
-    await clearOutputButton.click();
-    await 
expect(paragraphElement.locator('zeppelin-notebook-paragraph-result')).toBeHidden();
-
-    await this.publishedParagraphPage.navigateToPublishedParagraph(noteId, 
paragraphId);
-
-    const modal = this.publishedParagraphPage.confirmationModal;
-    await expect(modal).toBeVisible();
-
-    // Check for the new enhanced modal content
-    const modalTitle = this.page.locator('.ant-modal-confirm-title, 
.ant-modal-title');
-    await expect(modalTitle).toContainText('Run Paragraph?');
-
-    // Check that code preview is shown
-    const modalContent = this.page.locator('.ant-modal-confirm-content, 
.ant-modal-body');
-    await expect(modalContent).toContainText('This paragraph contains the 
following code:');
-    await expect(modalContent).toContainText('Would you like to execute this 
code?');
-
-    // Verify that the code preview area exists with proper styling
-    const codePreview = modalContent.locator('div[style*="background-color: 
#f5f5f5"]');
-    await expect(codePreview).toBeVisible();
-
-    // Check for Run and Cancel buttons
-    const runButton = this.page.locator('.ant-modal button:has-text("Run"), 
.ant-btn:has-text("Run")');
-    const cancelButton = this.page.locator('.ant-modal 
button:has-text("Cancel"), .ant-btn:has-text("Cancel")');
-    await expect(runButton).toBeVisible();
-    await expect(cancelButton).toBeVisible();
-
-    // Click the Run button in the modal
-    await runButton.click();
-    await expect(modal).toBeHidden();
-  }
-
   async verifyNonExistentParagraphError(validNoteId: string, 
invalidParagraphId: string): Promise<void> {
     await 
this.publishedParagraphPage.navigateToPublishedParagraph(validNoteId, 
invalidParagraphId);
 
diff --git 
a/zeppelin-web-angular/e2e/tests/workspace/notebook-repos/notebook-repo-item-display.spec.ts
 
b/zeppelin-web-angular/e2e/tests/workspace/notebook-repos/notebook-repo-item-display.spec.ts
new file mode 100644
index 0000000000..a8cd35ff10
--- /dev/null
+++ 
b/zeppelin-web-angular/e2e/tests/workspace/notebook-repos/notebook-repo-item-display.spec.ts
@@ -0,0 +1,56 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { expect, test } from '@playwright/test';
+import { NotebookReposPage, NotebookRepoItemPage } from 
'../../../models/notebook-repos-page';
+import { NotebookRepoItemUtil } from 
'../../../models/notebook-repos-page.util';
+import { addPageAnnotationBeforeEach, performLoginIfRequired, 
waitForZeppelinReady, PAGES } from '../../../utils';
+
+test.describe('Notebook Repository Item - Display Mode', () => {
+  addPageAnnotationBeforeEach(PAGES.WORKSPACE.NOTEBOOK_REPOS_ITEM);
+
+  let notebookReposPage: NotebookReposPage;
+  let repoItemPage: NotebookRepoItemPage;
+  let repoItemUtil: NotebookRepoItemUtil;
+  let firstRepoName: string;
+
+  test.beforeEach(async ({ page }) => {
+    await page.goto('/');
+    await waitForZeppelinReady(page);
+    await performLoginIfRequired(page);
+    notebookReposPage = new NotebookReposPage(page);
+    await notebookReposPage.navigate();
+
+    const firstCard = notebookReposPage.repositoryItems.first();
+    firstRepoName = (await 
firstCard.locator('.ant-card-head-title').textContent()) || '';
+    repoItemPage = new NotebookRepoItemPage(page, firstRepoName);
+    repoItemUtil = new NotebookRepoItemUtil(page, firstRepoName);
+  });
+
+  test('should display repository card with name', async () => {
+    await expect(repoItemPage.repositoryCard).toBeVisible();
+    await expect(repoItemPage.repositoryName).toContainText(firstRepoName);
+  });
+
+  test('should show edit button in display mode', async () => {
+    await expect(repoItemPage.editButton).toBeVisible();
+  });
+
+  test('should display settings table', async () => {
+    await expect(repoItemPage.settingTable).toBeVisible();
+  });
+
+  test('should show all settings in display mode', async () => {
+    const settingCount = await repoItemPage.getSettingCount();
+    expect(settingCount).toBeGreaterThan(0);
+  });
+});
diff --git 
a/zeppelin-web-angular/e2e/tests/workspace/notebook-repos/notebook-repo-item-edit.spec.ts
 
b/zeppelin-web-angular/e2e/tests/workspace/notebook-repos/notebook-repo-item-edit.spec.ts
new file mode 100644
index 0000000000..597f5ff191
--- /dev/null
+++ 
b/zeppelin-web-angular/e2e/tests/workspace/notebook-repos/notebook-repo-item-edit.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 { expect, test } from '@playwright/test';
+import { NotebookReposPage, NotebookRepoItemPage } from 
'../../../models/notebook-repos-page';
+import { NotebookRepoItemUtil } from 
'../../../models/notebook-repos-page.util';
+import { addPageAnnotationBeforeEach, performLoginIfRequired, 
waitForZeppelinReady, PAGES } from '../../../utils';
+
+test.describe('Notebook Repository Item - Edit Mode', () => {
+  addPageAnnotationBeforeEach(PAGES.WORKSPACE.NOTEBOOK_REPOS_ITEM);
+
+  let notebookReposPage: NotebookReposPage;
+  let repoItemPage: NotebookRepoItemPage;
+  let repoItemUtil: NotebookRepoItemUtil;
+  let firstRepoName: string;
+
+  test.beforeEach(async ({ page }) => {
+    await page.goto('/');
+    await waitForZeppelinReady(page);
+    await performLoginIfRequired(page);
+    notebookReposPage = new NotebookReposPage(page);
+    await notebookReposPage.navigate();
+
+    const firstCard = notebookReposPage.repositoryItems.first();
+    firstRepoName = (await 
firstCard.locator('.ant-card-head-title').textContent()) || '';
+    repoItemPage = new NotebookRepoItemPage(page, firstRepoName);
+    repoItemUtil = new NotebookRepoItemUtil(page, firstRepoName);
+  });
+
+  test('should enter edit mode when edit button is clicked', async () => {
+    await repoItemPage.clickEdit();
+    await repoItemUtil.verifyEditMode();
+  });
+
+  test('should show save and cancel buttons in edit mode', async () => {
+    await repoItemPage.clickEdit();
+    await expect(repoItemPage.saveButton).toBeVisible();
+    await expect(repoItemPage.cancelButton).toBeVisible();
+  });
+
+  test('should hide edit button in edit mode', async () => {
+    await repoItemPage.clickEdit();
+    await expect(repoItemPage.editButton).toBeHidden();
+  });
+
+  test('should apply edit CSS class to card in edit mode', async () => {
+    await repoItemPage.clickEdit();
+    const isEditMode = await repoItemPage.isEditMode();
+    expect(isEditMode).toBe(true);
+  });
+
+  test('should exit edit mode when cancel button is clicked', async () => {
+    await repoItemPage.clickEdit();
+    await repoItemUtil.verifyEditMode();
+    await repoItemPage.clickCancel();
+    await repoItemUtil.verifyDisplayMode();
+  });
+
+  test('should reset form when cancel is clicked', async () => {
+    const settingRows = await repoItemPage.settingRows.count();
+    if (settingRows === 0) {
+      test.skip();
+      return;
+    }
+
+    const firstRow = repoItemPage.settingRows.first();
+    const settingName =
+      (await firstRow
+        .locator('td')
+        .first()
+        .textContent()) || '';
+    const originalValue = await repoItemPage.getSettingValue(settingName);
+
+    await repoItemPage.clickEdit();
+
+    const isInputVisible = await repoItemPage.isInputVisible(settingName);
+    if (isInputVisible) {
+      await repoItemPage.fillSettingInput(settingName, 'temp-value');
+    }
+
+    await repoItemPage.clickCancel();
+
+    const currentValue = await repoItemPage.getSettingValue(settingName);
+    expect(currentValue.trim()).toBe(originalValue.trim());
+  });
+});
diff --git 
a/zeppelin-web-angular/e2e/tests/workspace/notebook-repos/notebook-repo-item-form-validation.spec.ts
 
b/zeppelin-web-angular/e2e/tests/workspace/notebook-repos/notebook-repo-item-form-validation.spec.ts
new file mode 100644
index 0000000000..69f9367e6b
--- /dev/null
+++ 
b/zeppelin-web-angular/e2e/tests/workspace/notebook-repos/notebook-repo-item-form-validation.spec.ts
@@ -0,0 +1,124 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { expect, test } from '@playwright/test';
+import { NotebookReposPage, NotebookRepoItemPage } from 
'../../../models/notebook-repos-page';
+import { NotebookRepoItemUtil } from 
'../../../models/notebook-repos-page.util';
+import { addPageAnnotationBeforeEach, performLoginIfRequired, 
waitForZeppelinReady, PAGES } from '../../../utils';
+
+test.describe('Notebook Repository Item - Form Validation', () => {
+  addPageAnnotationBeforeEach(PAGES.WORKSPACE.NOTEBOOK_REPOS_ITEM);
+
+  let notebookReposPage: NotebookReposPage;
+  let repoItemPage: NotebookRepoItemPage;
+  let repoItemUtil: NotebookRepoItemUtil;
+  let firstRepoName: string;
+
+  test.beforeEach(async ({ page }) => {
+    await page.goto('/');
+    await waitForZeppelinReady(page);
+    await performLoginIfRequired(page);
+    notebookReposPage = new NotebookReposPage(page);
+    await notebookReposPage.navigate();
+
+    const firstCard = notebookReposPage.repositoryItems.first();
+    firstRepoName = (await 
firstCard.locator('.ant-card-head-title').textContent()) || '';
+    repoItemPage = new NotebookRepoItemPage(page, firstRepoName);
+    repoItemUtil = new NotebookRepoItemUtil(page, firstRepoName);
+  });
+
+  test('should disable save button when form is invalid', async () => {
+    const settingRows = await repoItemPage.settingRows.count();
+    if (settingRows === 0) {
+      test.skip();
+      return;
+    }
+
+    await repoItemPage.clickEdit();
+
+    const firstRow = repoItemPage.settingRows.first();
+    const settingName =
+      (await firstRow
+        .locator('td')
+        .first()
+        .textContent()) || '';
+
+    const isInputVisible = await repoItemPage.isInputVisible(settingName);
+    if (isInputVisible) {
+      await repoItemPage.fillSettingInput(settingName, '');
+
+      const isSaveEnabled = await repoItemPage.isSaveButtonEnabled();
+      expect(isSaveEnabled).toBe(false);
+    } else {
+      test.skip();
+    }
+  });
+
+  test('should enable save button when form is valid', async () => {
+    const settingRows = await repoItemPage.settingRows.count();
+    if (settingRows === 0) {
+      test.skip();
+      return;
+    }
+
+    await repoItemPage.clickEdit();
+
+    const firstRow = repoItemPage.settingRows.first();
+    const settingName =
+      (await firstRow
+        .locator('td')
+        .first()
+        .textContent()) || '';
+
+    const isInputVisible = await repoItemPage.isInputVisible(settingName);
+    if (isInputVisible) {
+      const originalValue = await 
repoItemPage.getSettingInputValue(settingName);
+      await repoItemPage.fillSettingInput(settingName, originalValue || 
'valid-value');
+
+      const isSaveEnabled = await repoItemPage.isSaveButtonEnabled();
+      expect(isSaveEnabled).toBe(true);
+    } else {
+      test.skip();
+    }
+  });
+
+  test('should validate required fields on form controls', async () => {
+    const settingRows = await repoItemPage.settingRows.count();
+    if (settingRows === 0) {
+      test.skip();
+      return;
+    }
+
+    await repoItemPage.clickEdit();
+
+    for (let i = 0; i < settingRows; i++) {
+      const row = repoItemPage.settingRows.nth(i);
+      const settingName =
+        (await row
+          .locator('td')
+          .first()
+          .textContent()) || '';
+
+      const isInputVisible = await repoItemPage.isInputVisible(settingName);
+      if (isInputVisible) {
+        const input = row.locator('input[nz-input]');
+        await expect(input).toBeVisible();
+      }
+
+      const isDropdownVisible = await 
repoItemPage.isDropdownVisible(settingName);
+      if (isDropdownVisible) {
+        const select = row.locator('nz-select');
+        await expect(select).toBeVisible();
+      }
+    }
+  });
+});
diff --git 
a/zeppelin-web-angular/e2e/tests/workspace/notebook-repos/notebook-repo-item-settings.spec.ts
 
b/zeppelin-web-angular/e2e/tests/workspace/notebook-repos/notebook-repo-item-settings.spec.ts
new file mode 100644
index 0000000000..905074ba13
--- /dev/null
+++ 
b/zeppelin-web-angular/e2e/tests/workspace/notebook-repos/notebook-repo-item-settings.spec.ts
@@ -0,0 +1,154 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { expect, test } from '@playwright/test';
+import { NotebookReposPage, NotebookRepoItemPage } from 
'../../../models/notebook-repos-page';
+import { NotebookRepoItemUtil } from 
'../../../models/notebook-repos-page.util';
+import { addPageAnnotationBeforeEach, performLoginIfRequired, 
waitForZeppelinReady, PAGES } from '../../../utils';
+
+test.describe('Notebook Repository Item - Settings', () => {
+  addPageAnnotationBeforeEach(PAGES.WORKSPACE.NOTEBOOK_REPOS_ITEM);
+
+  let notebookReposPage: NotebookReposPage;
+  let repoItemPage: NotebookRepoItemPage;
+  let repoItemUtil: NotebookRepoItemUtil;
+  let firstRepoName: string;
+
+  test.beforeEach(async ({ page }) => {
+    await page.goto('/');
+    await waitForZeppelinReady(page);
+    await performLoginIfRequired(page);
+    notebookReposPage = new NotebookReposPage(page);
+    await notebookReposPage.navigate();
+
+    const firstCard = notebookReposPage.repositoryItems.first();
+    firstRepoName = (await 
firstCard.locator('.ant-card-head-title').textContent()) || '';
+    repoItemPage = new NotebookRepoItemPage(page, firstRepoName);
+    repoItemUtil = new NotebookRepoItemUtil(page, firstRepoName);
+  });
+
+  test('should display settings table with headers', async () => {
+    await expect(repoItemPage.settingTable).toBeVisible();
+
+    const headers = repoItemPage.settingTable.locator('thead th');
+    await expect(headers.nth(0)).toContainText('Name');
+    await expect(headers.nth(1)).toContainText('Value');
+  });
+
+  test('should display all setting rows', async () => {
+    const settingCount = await repoItemPage.getSettingCount();
+    expect(settingCount).toBeGreaterThan(0);
+  });
+
+  test('should show input controls for INPUT type settings in edit mode', 
async () => {
+    const settingRows = await repoItemPage.settingRows.count();
+    if (settingRows === 0) {
+      test.skip();
+      return;
+    }
+
+    await repoItemPage.clickEdit();
+
+    for (let i = 0; i < settingRows; i++) {
+      const row = repoItemPage.settingRows.nth(i);
+      const settingName =
+        (await row
+          .locator('td')
+          .first()
+          .textContent()) || '';
+
+      const isInputVisible = await repoItemPage.isInputVisible(settingName);
+      if (isInputVisible) {
+        const input = row.locator('input[nz-input]');
+        await expect(input).toBeVisible();
+        await expect(input).toHaveAttribute('nz-input');
+      }
+    }
+  });
+
+  test('should show dropdown controls for DROPDOWN type settings in edit 
mode', async () => {
+    const settingRows = await repoItemPage.settingRows.count();
+    if (settingRows === 0) {
+      test.skip();
+      return;
+    }
+
+    await repoItemPage.clickEdit();
+
+    for (let i = 0; i < settingRows; i++) {
+      const row = repoItemPage.settingRows.nth(i);
+      const settingName =
+        (await row
+          .locator('td')
+          .first()
+          .textContent()) || '';
+
+      const isDropdownVisible = await 
repoItemPage.isDropdownVisible(settingName);
+      if (isDropdownVisible) {
+        const select = row.locator('nz-select');
+        await expect(select).toBeVisible();
+      }
+    }
+  });
+
+  test('should update input value in edit mode', async () => {
+    const settingRows = await repoItemPage.settingRows.count();
+    if (settingRows === 0) {
+      test.skip();
+      return;
+    }
+
+    await repoItemPage.clickEdit();
+
+    let foundInput = false;
+    for (let i = 0; i < settingRows; i++) {
+      const row = repoItemPage.settingRows.nth(i);
+      const settingName =
+        (await row
+          .locator('td')
+          .first()
+          .textContent()) || '';
+
+      const isInputVisible = await repoItemPage.isInputVisible(settingName);
+      if (isInputVisible) {
+        const testValue = 'test-value';
+        await repoItemPage.fillSettingInput(settingName, testValue);
+        const inputValue = await 
repoItemPage.getSettingInputValue(settingName);
+        expect(inputValue).toBe(testValue);
+        foundInput = true;
+        break;
+      }
+    }
+
+    if (!foundInput) {
+      test.skip();
+    }
+  });
+
+  test('should display setting name and value in display mode', async () => {
+    const settingRows = await repoItemPage.settingRows.count();
+    if (settingRows === 0) {
+      test.skip();
+      return;
+    }
+
+    const firstRow = repoItemPage.settingRows.first();
+    const nameCell = firstRow.locator('td').first();
+    const valueCell = firstRow.locator('td').nth(1);
+
+    await expect(nameCell).toBeVisible();
+    await expect(valueCell).toBeVisible();
+
+    const nameText = await nameCell.textContent();
+    expect(nameText).toBeTruthy();
+  });
+});
diff --git 
a/zeppelin-web-angular/e2e/tests/workspace/notebook-repos/notebook-repo-item-workflow.spec.ts
 
b/zeppelin-web-angular/e2e/tests/workspace/notebook-repos/notebook-repo-item-workflow.spec.ts
new file mode 100644
index 0000000000..c730ea3d2c
--- /dev/null
+++ 
b/zeppelin-web-angular/e2e/tests/workspace/notebook-repos/notebook-repo-item-workflow.spec.ts
@@ -0,0 +1,156 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { expect, test } from '@playwright/test';
+import { NotebookReposPage, NotebookRepoItemPage } from 
'../../../models/notebook-repos-page';
+import { NotebookRepoItemUtil } from 
'../../../models/notebook-repos-page.util';
+import { addPageAnnotationBeforeEach, performLoginIfRequired, 
waitForZeppelinReady, PAGES } from '../../../utils';
+
+test.describe('Notebook Repository Item - Edit Workflow', () => {
+  addPageAnnotationBeforeEach(PAGES.WORKSPACE.NOTEBOOK_REPOS_ITEM);
+
+  let notebookReposPage: NotebookReposPage;
+  let repoItemPage: NotebookRepoItemPage;
+  let repoItemUtil: NotebookRepoItemUtil;
+  let firstRepoName: string;
+
+  test.beforeEach(async ({ page }) => {
+    await page.goto('/');
+    await waitForZeppelinReady(page);
+    await performLoginIfRequired(page);
+    notebookReposPage = new NotebookReposPage(page);
+    await notebookReposPage.navigate();
+
+    const firstCard = notebookReposPage.repositoryItems.first();
+    firstRepoName = (await 
firstCard.locator('.ant-card-head-title').textContent()) || '';
+    repoItemPage = new NotebookRepoItemPage(page, firstRepoName);
+    repoItemUtil = new NotebookRepoItemUtil(page, firstRepoName);
+  });
+
+  test('should complete full edit workflow with save', async ({ page }) => {
+    const settingRows = await repoItemPage.settingRows.count();
+    if (settingRows === 0) {
+      test.skip();
+      return;
+    }
+
+    await repoItemUtil.verifyDisplayMode();
+
+    await repoItemPage.clickEdit();
+    await repoItemUtil.verifyEditMode();
+
+    let foundSetting = false;
+    for (let i = 0; i < settingRows; i++) {
+      const row = repoItemPage.settingRows.nth(i);
+      const settingName =
+        (await row
+          .locator('td')
+          .first()
+          .textContent()) || '';
+
+      const isInputVisible = await repoItemPage.isInputVisible(settingName);
+      if (isInputVisible) {
+        const originalValue = await 
repoItemPage.getSettingInputValue(settingName);
+        await repoItemPage.fillSettingInput(settingName, originalValue || 
'test-value');
+        foundSetting = true;
+        break;
+      }
+    }
+
+    if (!foundSetting) {
+      test.skip();
+      return;
+    }
+
+    const isSaveEnabled = await repoItemPage.isSaveButtonEnabled();
+    expect(isSaveEnabled).toBe(true);
+
+    await repoItemPage.clickSave();
+
+    await page.waitForTimeout(1000);
+
+    await repoItemUtil.verifyDisplayMode();
+  });
+
+  test('should complete full edit workflow with cancel', async () => {
+    const settingRows = await repoItemPage.settingRows.count();
+    if (settingRows === 0) {
+      test.skip();
+      return;
+    }
+
+    await repoItemUtil.verifyDisplayMode();
+
+    const firstRow = repoItemPage.settingRows.first();
+    const settingName =
+      (await firstRow
+        .locator('td')
+        .first()
+        .textContent()) || '';
+    const originalValue = await repoItemPage.getSettingValue(settingName);
+
+    await repoItemPage.clickEdit();
+    await repoItemUtil.verifyEditMode();
+
+    const isInputVisible = await repoItemPage.isInputVisible(settingName);
+    if (isInputVisible) {
+      await repoItemPage.fillSettingInput(settingName, 'temp-modified-value');
+    } else {
+      test.skip();
+      return;
+    }
+
+    await repoItemPage.clickCancel();
+    await repoItemUtil.verifyDisplayMode();
+
+    const currentValue = await repoItemPage.getSettingValue(settingName);
+    expect(currentValue.trim()).toBe(originalValue.trim());
+  });
+
+  test('should toggle between display and edit modes multiple times', async () 
=> {
+    await repoItemUtil.verifyDisplayMode();
+
+    await repoItemPage.clickEdit();
+    await repoItemUtil.verifyEditMode();
+
+    await repoItemPage.clickCancel();
+    await repoItemUtil.verifyDisplayMode();
+
+    await repoItemPage.clickEdit();
+    await repoItemUtil.verifyEditMode();
+
+    await repoItemPage.clickCancel();
+    await repoItemUtil.verifyDisplayMode();
+  });
+
+  test('should preserve card visibility throughout edit workflow', async () => 
{
+    await expect(repoItemPage.repositoryCard).toBeVisible();
+
+    await repoItemPage.clickEdit();
+    await expect(repoItemPage.repositoryCard).toBeVisible();
+
+    await repoItemPage.clickCancel();
+    await expect(repoItemPage.repositoryCard).toBeVisible();
+  });
+
+  test('should maintain settings count during mode transitions', async () => {
+    const initialCount = await repoItemPage.getSettingCount();
+
+    await repoItemPage.clickEdit();
+    const editModeCount = await repoItemPage.getSettingCount();
+    expect(editModeCount).toBe(initialCount);
+
+    await repoItemPage.clickCancel();
+    const finalCount = await repoItemPage.getSettingCount();
+    expect(finalCount).toBe(initialCount);
+  });
+});
diff --git 
a/zeppelin-web-angular/e2e/tests/workspace/notebook-repos/notebook-repos-page-structure.spec.ts
 
b/zeppelin-web-angular/e2e/tests/workspace/notebook-repos/notebook-repos-page-structure.spec.ts
new file mode 100644
index 0000000000..957a7a8a3d
--- /dev/null
+++ 
b/zeppelin-web-angular/e2e/tests/workspace/notebook-repos/notebook-repos-page-structure.spec.ts
@@ -0,0 +1,51 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { expect, test } from '@playwright/test';
+import { NotebookReposPage } from '../../../models/notebook-repos-page';
+import { NotebookReposPageUtil } from 
'../../../models/notebook-repos-page.util';
+import { addPageAnnotationBeforeEach, performLoginIfRequired, 
waitForZeppelinReady, PAGES } from '../../../utils';
+
+test.describe('Notebook Repository Page - Structure', () => {
+  addPageAnnotationBeforeEach(PAGES.WORKSPACE.NOTEBOOK_REPOS);
+
+  let notebookReposPage: NotebookReposPage;
+  let notebookReposUtil: NotebookReposPageUtil;
+
+  test.beforeEach(async ({ page }) => {
+    await page.goto('/');
+    await waitForZeppelinReady(page);
+    await performLoginIfRequired(page);
+    notebookReposPage = new NotebookReposPage(page);
+    notebookReposUtil = new NotebookReposPageUtil(page);
+    await notebookReposPage.navigate();
+  });
+
+  test('should display page header with correct title and description', async 
() => {
+    await expect(notebookReposPage.pageHeader).toBeVisible();
+    await expect(notebookReposPage.pageDescription).toBeVisible();
+  });
+
+  test('should render repository list container', async () => {
+    const count = await notebookReposPage.getRepositoryItemCount();
+    expect(count).toBeGreaterThanOrEqual(0);
+  });
+
+  test('should display all repository items', async () => {
+    const count = await notebookReposPage.getRepositoryItemCount();
+    if (count === 0) {
+      test.skip();
+      return;
+    }
+    await notebookReposUtil.verifyAllRepositoriesRendered();
+  });
+});


Reply via email to