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();
+ });
+});