Skip to content

Commit cf894ff

Browse files
voidmatchatbonelee
authored andcommitted
[ZEPPELIN-6373] Add E2E tests about share area
### What is this PR for? ABOUT_ZEPPELIN:'src/app/share/about-zeppelin/about-zeppelin.component', CODE_EDITOR:'src/app/share/code-editor/code-editor.component', FOLDER_RENAME:'src/app/share/folder-rename/folder-rename.component', HEADER:'src/app/share/header/header.component', NODE_LIST:'src/app/share/node-list/node-list.component', NOTE_CREATE:'src/app/share/note-create/note-create.component', NOTE_IMPORT:'src/app/share/note-import/note-import.component', NOTE_RENAME:'src/app/share/note-rename/note-rename.component', NOTE_TOC:'src/app/share/note-toc/note-toc.component', PAGE_HEADER:'src/app/share/page-header/page-header.component', RESIZE_HANDLE:'src/app/share/resize-handle/resize-handle.component', SHORTCUT:'src/app/share/shortcut/shortcut.component', SPIN:'src/app/share/spin/spin.component', THEME_TOGGLE:'src/app/share/theme-toggle/theme-toggle.component' ### What type of PR is it? Improvement *Please leave your type of PR only* ### Todos ### What is the Jira issue? ZEPPELIN-6373 ### How should this be tested? ### Screenshots (if appropriate) ### Questions: * Does the license files need to update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Closes #5114 from dididy/e2e/share. Signed-off-by: ChanHo Lee <chanholee@apache.org> (cherry picked from commit cf766db) Signed-off-by: ChanHo Lee <chanholee@apache.org>
1 parent 0fa52a7 commit cf894ff

15 files changed

Lines changed: 1063 additions & 7 deletions
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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 { BasePage } from './base-page';
15+
16+
export class AboutZeppelinModal extends BasePage {
17+
readonly modal: Locator;
18+
readonly modalTitle: Locator;
19+
readonly closeButton: Locator;
20+
readonly logo: Locator;
21+
readonly heading: Locator;
22+
readonly versionText: Locator;
23+
readonly getInvolvedLink: Locator;
24+
readonly licenseLink: Locator;
25+
26+
constructor(page: Page) {
27+
super(page);
28+
this.modal = page.locator('[role="dialog"]').filter({ has: page.getByText('About Zeppelin') });
29+
this.modalTitle = page.locator('.ant-modal-title', { hasText: 'About Zeppelin' });
30+
this.closeButton = page.getByRole('button', { name: 'Close' });
31+
this.logo = page.locator('img[alt="Apache Zeppelin"]');
32+
this.heading = page.locator('h3', { hasText: 'Apache Zeppelin' });
33+
this.versionText = page.locator('.about-version');
34+
this.getInvolvedLink = page.getByRole('link', { name: 'Get involved!' });
35+
this.licenseLink = page.getByRole('link', { name: 'Licensed under the Apache License, Version 2.0' });
36+
}
37+
38+
async close(): Promise<void> {
39+
await this.closeButton.click();
40+
}
41+
42+
async getVersionText(): Promise<string> {
43+
return (await this.versionText.textContent()) || '';
44+
}
45+
46+
async isLogoVisible(): Promise<boolean> {
47+
return this.logo.isVisible();
48+
}
49+
50+
async getGetInvolvedHref(): Promise<string | null> {
51+
return this.getInvolvedLink.getAttribute('href');
52+
}
53+
54+
async getLicenseHref(): Promise<string | null> {
55+
return this.licenseLink.getAttribute('href');
56+
}
57+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
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 { BasePage } from './base-page';
15+
16+
export class HeaderPage extends BasePage {
17+
readonly header: Locator;
18+
readonly brandLogo: Locator;
19+
readonly brandLink: Locator;
20+
readonly notebookMenuItem: Locator;
21+
readonly notebookDropdownTrigger: Locator;
22+
readonly notebookDropdown: Locator;
23+
readonly jobMenuItem: Locator;
24+
readonly userDropdownTrigger: Locator;
25+
readonly userBadge: Locator;
26+
readonly searchInput: Locator;
27+
readonly themeToggleButton: Locator;
28+
29+
readonly userMenuItems: {
30+
aboutZeppelin: Locator;
31+
interpreter: Locator;
32+
notebookRepos: Locator;
33+
credential: Locator;
34+
configuration: Locator;
35+
logout: Locator;
36+
switchToClassicUI: Locator;
37+
};
38+
39+
constructor(page: Page) {
40+
super(page);
41+
this.header = page.locator('.header');
42+
this.brandLogo = page.locator('.header .brand .logo');
43+
this.brandLink = page.locator('.header .brand');
44+
this.notebookMenuItem = page.locator('[nz-menu-item]').filter({ hasText: 'Notebook' });
45+
this.notebookDropdownTrigger = page.locator('.node-list-trigger');
46+
this.notebookDropdown = page.locator('zeppelin-node-list.ant-dropdown-menu');
47+
this.jobMenuItem = page.getByRole('link', { name: 'Job' });
48+
this.userDropdownTrigger = page.locator('.header .user .status');
49+
this.userBadge = page.locator('.header .user nz-badge');
50+
this.searchInput = page.locator('.header .search input[type="text"]');
51+
this.themeToggleButton = page.locator('zeppelin-theme-toggle button');
52+
53+
this.userMenuItems = {
54+
aboutZeppelin: page.getByText('About Zeppelin', { exact: true }),
55+
interpreter: page.getByRole('link', { name: 'Interpreter' }),
56+
notebookRepos: page.getByRole('link', { name: 'Notebook Repos' }),
57+
credential: page.getByRole('link', { name: 'Credential' }),
58+
configuration: page.getByRole('link', { name: 'Configuration' }),
59+
logout: page.getByText('Logout', { exact: true }),
60+
switchToClassicUI: page.getByRole('link', { name: 'Switch to Classic UI' })
61+
};
62+
}
63+
64+
async clickBrandLogo(): Promise<void> {
65+
await this.brandLink.waitFor({ state: 'visible', timeout: 10000 });
66+
await this.brandLink.click();
67+
}
68+
69+
async clickNotebookMenu(): Promise<void> {
70+
await this.notebookDropdownTrigger.waitFor({ state: 'visible', timeout: 10000 });
71+
await this.notebookDropdownTrigger.click();
72+
}
73+
74+
async clickJobMenu(): Promise<void> {
75+
await this.jobMenuItem.waitFor({ state: 'visible', timeout: 10000 });
76+
await this.jobMenuItem.click();
77+
}
78+
79+
async clickUserDropdown(): Promise<void> {
80+
await this.userDropdownTrigger.waitFor({ state: 'visible', timeout: 10000 });
81+
await this.userDropdownTrigger.click();
82+
}
83+
84+
async clickAboutZeppelin(): Promise<void> {
85+
await this.userMenuItems.aboutZeppelin.click();
86+
}
87+
88+
async clickInterpreter(): Promise<void> {
89+
await this.userMenuItems.interpreter.click();
90+
}
91+
92+
async clickNotebookRepos(): Promise<void> {
93+
await this.userMenuItems.notebookRepos.click();
94+
}
95+
96+
async clickCredential(): Promise<void> {
97+
await this.userMenuItems.credential.click();
98+
}
99+
100+
async clickConfiguration(): Promise<void> {
101+
await this.userMenuItems.configuration.click();
102+
}
103+
104+
async getUsernameText(): Promise<string> {
105+
return (await this.userBadge.textContent()) || '';
106+
}
107+
108+
async searchNote(query: string): Promise<void> {
109+
await this.searchInput.waitFor({ state: 'visible', timeout: 10000 });
110+
await this.searchInput.fill(query);
111+
await this.page.keyboard.press('Enter');
112+
}
113+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
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 { HeaderPage } from './header-page';
15+
import { NodeListPage } from './node-list-page';
16+
17+
export class HeaderPageUtil {
18+
constructor(
19+
private readonly page: Page,
20+
private readonly headerPage: HeaderPage
21+
) {}
22+
23+
async verifyHeaderIsDisplayed(): Promise<void> {
24+
await expect(this.headerPage.header).toBeVisible();
25+
await expect(this.headerPage.brandLogo).toBeVisible();
26+
await expect(this.headerPage.notebookMenuItem).toBeVisible();
27+
await expect(this.headerPage.jobMenuItem).toBeVisible();
28+
await expect(this.headerPage.userDropdownTrigger).toBeVisible();
29+
await expect(this.headerPage.searchInput).toBeVisible();
30+
await expect(this.headerPage.themeToggleButton).toBeVisible();
31+
}
32+
33+
async verifyNavigationToHomePage(): Promise<void> {
34+
await this.headerPage.clickBrandLogo();
35+
await this.page.waitForURL(/\/(#\/)?$/);
36+
const url = this.page.url();
37+
expect(url).toMatch(/\/(#\/)?$/);
38+
}
39+
40+
async verifyNavigationToJobManager(): Promise<void> {
41+
await this.headerPage.clickJobMenu();
42+
await this.page.waitForURL(/jobmanager/);
43+
expect(this.page.url()).toContain('jobmanager');
44+
}
45+
46+
async verifyUserDropdownOpens(): Promise<void> {
47+
await this.headerPage.clickUserDropdown();
48+
await expect(this.headerPage.userMenuItems.aboutZeppelin).toBeVisible();
49+
}
50+
51+
async verifyNotebookDropdownOpens(): Promise<void> {
52+
await this.headerPage.clickNotebookMenu();
53+
await expect(this.headerPage.notebookDropdown).toBeVisible();
54+
55+
const nodeList = new NodeListPage(this.page);
56+
await expect(nodeList.createNewNoteButton).toBeVisible();
57+
}
58+
59+
async verifySearchNavigation(query: string): Promise<void> {
60+
await this.headerPage.searchNote(query);
61+
await this.page.waitForURL(/search/);
62+
expect(this.page.url()).toContain('search');
63+
expect(this.page.url()).toContain(query);
64+
}
65+
66+
async verifyUserMenuItemsVisible(isLoggedIn: boolean): Promise<void> {
67+
await this.headerPage.clickUserDropdown();
68+
await expect(this.headerPage.userMenuItems.aboutZeppelin).toBeVisible();
69+
await expect(this.headerPage.userMenuItems.interpreter).toBeVisible();
70+
await expect(this.headerPage.userMenuItems.notebookRepos).toBeVisible();
71+
await expect(this.headerPage.userMenuItems.credential).toBeVisible();
72+
await expect(this.headerPage.userMenuItems.configuration).toBeVisible();
73+
await expect(this.headerPage.userMenuItems.switchToClassicUI).toBeVisible();
74+
75+
if (isLoggedIn) {
76+
const username = await this.headerPage.getUsernameText();
77+
expect(username).not.toBe('anonymous');
78+
await expect(this.headerPage.userMenuItems.logout).toBeVisible();
79+
}
80+
}
81+
82+
async navigateToInterpreterSettings(): Promise<void> {
83+
await this.headerPage.clickUserDropdown();
84+
await this.headerPage.clickInterpreter();
85+
await this.page.waitForURL(/interpreter/);
86+
expect(this.page.url()).toContain('interpreter');
87+
}
88+
89+
async navigateToNotebookRepos(): Promise<void> {
90+
await this.headerPage.clickUserDropdown();
91+
await this.headerPage.clickNotebookRepos();
92+
await this.page.waitForURL(/notebook-repos/);
93+
expect(this.page.url()).toContain('notebook-repos');
94+
}
95+
96+
async navigateToCredential(): Promise<void> {
97+
await this.headerPage.clickUserDropdown();
98+
await this.headerPage.clickCredential();
99+
await this.page.waitForURL(/credential/);
100+
expect(this.page.url()).toContain('credential');
101+
}
102+
103+
async navigateToConfiguration(): Promise<void> {
104+
await this.headerPage.clickUserDropdown();
105+
await this.headerPage.clickConfiguration();
106+
await this.page.waitForURL(/configuration/);
107+
expect(this.page.url()).toContain('configuration');
108+
}
109+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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 { BasePage } from './base-page';
15+
16+
export class NodeListPage extends BasePage {
17+
readonly nodeListContainer: Locator;
18+
readonly importNoteButton: Locator;
19+
readonly createNewNoteButton: Locator;
20+
readonly filterInput: Locator;
21+
readonly treeView: Locator;
22+
readonly notes: Locator;
23+
readonly trashFolder: Locator;
24+
25+
constructor(page: Page) {
26+
super(page);
27+
this.nodeListContainer = page.locator('zeppelin-node-list');
28+
this.importNoteButton = page.getByText('Import Note', { exact: true }).first();
29+
this.createNewNoteButton = page.getByText('Create new Note', { exact: true }).first();
30+
this.filterInput = page.locator('zeppelin-node-list input[placeholder*="Filter"]');
31+
this.treeView = page.locator('zeppelin-node-list nz-tree');
32+
this.notes = page.locator('nz-tree-node').filter({ has: page.locator('.ant-tree-node-content-wrapper .file') });
33+
this.trashFolder = page.locator('nz-tree-node').filter({ hasText: '~Trash' });
34+
}
35+
36+
async clickImportNote(): Promise<void> {
37+
await this.importNoteButton.click();
38+
}
39+
40+
async clickCreateNewNote(): Promise<void> {
41+
await this.createNewNoteButton.click();
42+
}
43+
44+
getFolderByName(folderName: string): Locator {
45+
return this.page.locator('nz-tree-node').filter({ hasText: folderName }).first();
46+
}
47+
48+
getNoteByName(noteName: string): Locator {
49+
return this.page.locator('nz-tree-node').filter({ hasText: noteName }).first();
50+
}
51+
52+
async clickNote(noteName: string): Promise<void> {
53+
const note = this.getNoteByName(noteName);
54+
// Target the specific link that navigates to the notebook (has href with "#/notebook/")
55+
const noteLink = note.locator('a[href*="#/notebook/"]');
56+
await noteLink.click();
57+
}
58+
59+
async isFilterInputVisible(): Promise<boolean> {
60+
return this.filterInput.isVisible();
61+
}
62+
63+
async isTrashFolderVisible(): Promise<boolean> {
64+
return this.trashFolder.isVisible();
65+
}
66+
67+
async getAllVisibleNoteNames(): Promise<string[]> {
68+
const noteElements = await this.notes.all();
69+
const names: string[] = [];
70+
for (const note of noteElements) {
71+
const text = await note.textContent();
72+
if (text) {
73+
names.push(text.trim());
74+
}
75+
}
76+
return names;
77+
}
78+
}

0 commit comments

Comments
 (0)