Skip to content

Commit b90b277

Browse files
kmularisetbonelee
authored andcommitted
[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 <chanholee@apache.org> (cherry picked from commit b279255) Signed-off-by: ChanHo Lee <chanholee@apache.org>
1 parent b776fd6 commit b90b277

11 files changed

Lines changed: 901 additions & 63 deletions

.github/workflows/frontend.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ jobs:
100100
export ZEPPELIN_CONF_DIR=./conf
101101
if [ "${{ matrix.mode }}" != "anonymous" ]; then
102102
cp conf/shiro.ini.template conf/shiro.ini
103+
sed -i 's/user1 = password2, role1, role2/user1 = password2, role1, role2, admin/' conf/shiro.ini
103104
fi
104105
- name: Run headless E2E test with Maven
105106
run: xvfb-run --auto-servernum --server-args="-screen 0 1024x768x24" ./mvnw verify -pl zeppelin-web-angular -Pweb-e2e ${MAVEN_ARGS}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
* http://www.apache.org/licenses/LICENSE-2.0
6+
* Unless required by applicable law or agreed to in writing, software
7+
* distributed under the License is distributed on an "AS IS" BASIS,
8+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9+
* See the License for the specific language governing permissions and
10+
* limitations under the License.
11+
*/
12+
13+
import { Locator, Page } from '@playwright/test';
14+
import { waitForZeppelinReady } from '../utils';
15+
import { BasePage } from './base-page';
16+
17+
export class NotebookReposPage extends BasePage {
18+
readonly pageHeader: Locator;
19+
readonly pageDescription: Locator;
20+
readonly repositoryItems: Locator;
21+
22+
constructor(page: Page) {
23+
super(page);
24+
this.pageHeader = page.locator('zeppelin-page-header[title="Notebook Repository"]');
25+
this.pageDescription = page.locator("text=Manage your Notebook Repositories' settings.");
26+
this.repositoryItems = page.locator('zeppelin-notebook-repo-item');
27+
}
28+
29+
async navigate(): Promise<void> {
30+
await this.page.goto('/#/notebook-repos', { waitUntil: 'load' });
31+
await this.page.waitForURL('**/#/notebook-repos', { timeout: 15000 });
32+
await waitForZeppelinReady(this.page);
33+
await this.page.waitForLoadState('networkidle', { timeout: 15000 });
34+
await this.page.waitForSelector('zeppelin-notebook-repo-item, zeppelin-page-header[title="Notebook Repository"]', {
35+
state: 'visible',
36+
timeout: 20000
37+
});
38+
}
39+
40+
async getRepositoryItemCount(): Promise<number> {
41+
return await this.repositoryItems.count();
42+
}
43+
}
44+
45+
export class NotebookRepoItemPage {
46+
readonly page: Page;
47+
readonly repositoryCard: Locator;
48+
readonly repositoryName: Locator;
49+
readonly editButton: Locator;
50+
readonly saveButton: Locator;
51+
readonly cancelButton: Locator;
52+
readonly settingTable: Locator;
53+
readonly settingRows: Locator;
54+
55+
constructor(page: Page, repoName: string) {
56+
this.page = page;
57+
this.repositoryCard = page.locator('nz-card').filter({ hasText: repoName });
58+
this.repositoryName = this.repositoryCard.locator('.ant-card-head-title');
59+
this.editButton = this.repositoryCard.locator('button:has-text("Edit")');
60+
this.saveButton = this.repositoryCard.locator('button:has-text("Save")');
61+
this.cancelButton = this.repositoryCard.locator('button:has-text("Cancel")');
62+
this.settingTable = this.repositoryCard.locator('nz-table');
63+
this.settingRows = this.repositoryCard.locator('tbody tr');
64+
}
65+
66+
async clickEdit(): Promise<void> {
67+
await this.editButton.click();
68+
}
69+
70+
async clickSave(): Promise<void> {
71+
await this.saveButton.click();
72+
}
73+
74+
async clickCancel(): Promise<void> {
75+
await this.cancelButton.click();
76+
}
77+
78+
async isEditMode(): Promise<boolean> {
79+
return await this.repositoryCard.evaluate(el => el.classList.contains('edit'));
80+
}
81+
82+
async isSaveButtonEnabled(): Promise<boolean> {
83+
return await this.saveButton.isEnabled();
84+
}
85+
86+
async getSettingValue(settingName: string): Promise<string> {
87+
const row = this.repositoryCard.locator('tbody tr').filter({ hasText: settingName });
88+
const valueCell = row.locator('td').nth(1);
89+
return (await valueCell.textContent()) || '';
90+
}
91+
92+
async fillSettingInput(settingName: string, value: string): Promise<void> {
93+
const row = this.repositoryCard.locator('tbody tr').filter({ hasText: settingName });
94+
const input = row.locator('input[nz-input]');
95+
await input.clear();
96+
await input.fill(value);
97+
}
98+
99+
async selectSettingDropdown(settingName: string, optionValue: string): Promise<void> {
100+
const row = this.repositoryCard.locator('tbody tr').filter({ hasText: settingName });
101+
const select = row.locator('nz-select');
102+
await select.click();
103+
await this.page.locator(`nz-option[nzvalue="${optionValue}"]`).click();
104+
}
105+
106+
async getSettingInputValue(settingName: string): Promise<string> {
107+
const row = this.repositoryCard.locator('tbody tr').filter({ hasText: settingName });
108+
const input = row.locator('input[nz-input]');
109+
return await input.inputValue();
110+
}
111+
112+
async isInputVisible(settingName: string): Promise<boolean> {
113+
const row = this.repositoryCard.locator('tbody tr').filter({ hasText: settingName });
114+
const input = row.locator('input[nz-input]');
115+
return await input.isVisible();
116+
}
117+
118+
async isDropdownVisible(settingName: string): Promise<boolean> {
119+
const row = this.repositoryCard.locator('tbody tr').filter({ hasText: settingName });
120+
const select = row.locator('nz-select');
121+
return await select.isVisible();
122+
}
123+
124+
async getSettingCount(): Promise<number> {
125+
return await this.settingRows.count();
126+
}
127+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
* http://www.apache.org/licenses/LICENSE-2.0
6+
* Unless required by applicable law or agreed to in writing, software
7+
* distributed under the License is distributed on an "AS IS" BASIS,
8+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9+
* See the License for the specific language governing permissions and
10+
* limitations under the License.
11+
*/
12+
13+
import { expect, Page } from '@playwright/test';
14+
import { NotebookReposPage, NotebookRepoItemPage } from './notebook-repos-page';
15+
16+
export class NotebookReposPageUtil {
17+
private notebookReposPage: NotebookReposPage;
18+
private page: Page;
19+
20+
constructor(page: Page) {
21+
this.page = page;
22+
this.notebookReposPage = new NotebookReposPage(page);
23+
}
24+
25+
async verifyPageStructure(): Promise<void> {
26+
await expect(this.notebookReposPage.pageHeader).toBeVisible();
27+
await expect(this.notebookReposPage.pageDescription).toBeVisible();
28+
}
29+
30+
async verifyRepositoryListDisplayed(): Promise<void> {
31+
const count = await this.notebookReposPage.getRepositoryItemCount();
32+
expect(count).toBeGreaterThan(0);
33+
}
34+
35+
async verifyAllRepositoriesRendered(): Promise<number> {
36+
const count = await this.notebookReposPage.getRepositoryItemCount();
37+
expect(count).toBeGreaterThan(0);
38+
return count;
39+
}
40+
41+
async getRepositoryItem(repoName: string): Promise<NotebookRepoItemPage> {
42+
return new NotebookRepoItemPage(this.page, repoName);
43+
}
44+
45+
async verifyRepositoryCardDisplayed(repoName: string): Promise<void> {
46+
const repoItem = await this.getRepositoryItem(repoName);
47+
await expect(repoItem.repositoryCard).toBeVisible();
48+
await expect(repoItem.repositoryName).toContainText(repoName);
49+
}
50+
}
51+
52+
export class NotebookRepoItemUtil {
53+
private repoItemPage: NotebookRepoItemPage;
54+
55+
constructor(page: Page, repoName: string) {
56+
this.repoItemPage = new NotebookRepoItemPage(page, repoName);
57+
}
58+
59+
async verifyDisplayMode(): Promise<void> {
60+
await expect(this.repoItemPage.editButton).toBeVisible();
61+
const isEditMode = await this.repoItemPage.isEditMode();
62+
expect(isEditMode).toBe(false);
63+
}
64+
65+
async verifyEditMode(): Promise<void> {
66+
await expect(this.repoItemPage.saveButton).toBeVisible();
67+
await expect(this.repoItemPage.cancelButton).toBeVisible();
68+
const isEditMode = await this.repoItemPage.isEditMode();
69+
expect(isEditMode).toBe(true);
70+
}
71+
72+
async enterEditMode(): Promise<void> {
73+
await this.repoItemPage.clickEdit();
74+
await this.verifyEditMode();
75+
}
76+
77+
async exitEditModeByCancel(): Promise<void> {
78+
await this.repoItemPage.clickCancel();
79+
await this.verifyDisplayMode();
80+
}
81+
82+
async exitEditModeBySave(): Promise<void> {
83+
await this.repoItemPage.clickSave();
84+
await this.verifyDisplayMode();
85+
}
86+
87+
async verifySettingsDisplayed(): Promise<void> {
88+
const settingCount = await this.repoItemPage.getSettingCount();
89+
expect(settingCount).toBeGreaterThan(0);
90+
}
91+
92+
async verifyInputTypeSettingInEditMode(settingName: string): Promise<void> {
93+
const isVisible = await this.repoItemPage.isInputVisible(settingName);
94+
expect(isVisible).toBe(true);
95+
}
96+
97+
async verifyDropdownTypeSettingInEditMode(settingName: string): Promise<void> {
98+
const isVisible = await this.repoItemPage.isDropdownVisible(settingName);
99+
expect(isVisible).toBe(true);
100+
}
101+
102+
async updateInputSetting(settingName: string, value: string): Promise<void> {
103+
await this.repoItemPage.fillSettingInput(settingName, value);
104+
const inputValue = await this.repoItemPage.getSettingInputValue(settingName);
105+
expect(inputValue).toBe(value);
106+
}
107+
108+
async updateDropdownSetting(settingName: string, optionValue: string): Promise<void> {
109+
await this.repoItemPage.selectSettingDropdown(settingName, optionValue);
110+
}
111+
112+
async verifySaveButtonDisabled(): Promise<void> {
113+
const isEnabled = await this.repoItemPage.isSaveButtonEnabled();
114+
expect(isEnabled).toBe(false);
115+
}
116+
117+
async verifySaveButtonEnabled(): Promise<void> {
118+
const isEnabled = await this.repoItemPage.isSaveButtonEnabled();
119+
expect(isEnabled).toBe(true);
120+
}
121+
122+
async verifyFormReset(settingName: string, originalValue: string): Promise<void> {
123+
const currentValue = await this.repoItemPage.getSettingValue(settingName);
124+
expect(currentValue.trim()).toBe(originalValue.trim());
125+
}
126+
127+
async performCompleteEditWorkflow(settingName: string, newValue: string, isInput: boolean = true): Promise<void> {
128+
await this.enterEditMode();
129+
if (isInput) {
130+
await this.updateInputSetting(settingName, newValue);
131+
} else {
132+
await this.updateDropdownSetting(settingName, newValue);
133+
}
134+
await this.verifySaveButtonEnabled();
135+
await this.exitEditModeBySave();
136+
}
137+
}

zeppelin-web-angular/e2e/models/published-paragraph-page.ts

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,6 @@ export class PublishedParagraphPage extends BasePage {
4949
await this.waitForPageLoad();
5050
}
5151

52-
async isPublishedParagraphVisible(): Promise<boolean> {
53-
return await this.publishedParagraphContainer.isVisible();
54-
}
55-
56-
async getErrorModalTitle(): Promise<string> {
57-
return (await this.errorModalTitle.textContent()) || '';
58-
}
59-
6052
async getErrorModalContent(): Promise<string> {
6153
return (await this.errorModalContent.textContent()) || '';
6254
}
@@ -65,14 +57,6 @@ export class PublishedParagraphPage extends BasePage {
6557
await this.errorModalOkButton.click();
6658
}
6759

68-
async isDynamicFormsVisible(): Promise<boolean> {
69-
return await this.dynamicForms.isVisible();
70-
}
71-
72-
async isResultVisible(): Promise<boolean> {
73-
return await this.paragraphResult.isVisible();
74-
}
75-
7660
async getCurrentUrl(): Promise<string> {
7761
return this.page.url();
7862
}

zeppelin-web-angular/e2e/models/published-paragraph-page.util.ts

Lines changed: 0 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -25,53 +25,6 @@ export class PublishedParagraphTestUtil {
2525
this.notebookUtil = new NotebookUtil(page);
2626
}
2727

28-
async testConfirmationModalForNoResultParagraph({
29-
noteId,
30-
paragraphId
31-
}: {
32-
noteId: string;
33-
paragraphId: string;
34-
}): Promise<void> {
35-
await this.publishedParagraphPage.navigateToNotebook(noteId);
36-
37-
const paragraphElement = this.page.locator('zeppelin-notebook-paragraph').first();
38-
39-
const settingsButton = paragraphElement.locator('a[nz-dropdown]');
40-
await settingsButton.click();
41-
42-
const clearOutputButton = this.page.locator('li.list-item:has-text("Clear output")');
43-
await clearOutputButton.click();
44-
await expect(paragraphElement.locator('zeppelin-notebook-paragraph-result')).toBeHidden();
45-
46-
await this.publishedParagraphPage.navigateToPublishedParagraph(noteId, paragraphId);
47-
48-
const modal = this.publishedParagraphPage.confirmationModal;
49-
await expect(modal).toBeVisible();
50-
51-
// Check for the new enhanced modal content
52-
const modalTitle = this.page.locator('.ant-modal-confirm-title, .ant-modal-title');
53-
await expect(modalTitle).toContainText('Run Paragraph?');
54-
55-
// Check that code preview is shown
56-
const modalContent = this.page.locator('.ant-modal-confirm-content, .ant-modal-body');
57-
await expect(modalContent).toContainText('This paragraph contains the following code:');
58-
await expect(modalContent).toContainText('Would you like to execute this code?');
59-
60-
// Verify that the code preview area exists with proper styling
61-
const codePreview = modalContent.locator('div[style*="background-color: #f5f5f5"]');
62-
await expect(codePreview).toBeVisible();
63-
64-
// Check for Run and Cancel buttons
65-
const runButton = this.page.locator('.ant-modal button:has-text("Run"), .ant-btn:has-text("Run")');
66-
const cancelButton = this.page.locator('.ant-modal button:has-text("Cancel"), .ant-btn:has-text("Cancel")');
67-
await expect(runButton).toBeVisible();
68-
await expect(cancelButton).toBeVisible();
69-
70-
// Click the Run button in the modal
71-
await runButton.click();
72-
await expect(modal).toBeHidden();
73-
}
74-
7528
async verifyNonExistentParagraphError(validNoteId: string, invalidParagraphId: string): Promise<void> {
7629
await this.publishedParagraphPage.navigateToPublishedParagraph(validNoteId, invalidParagraphId);
7730

0 commit comments

Comments
 (0)