Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions zeppelin-web-angular/e2e/models/about-zeppelin-modal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { Locator, Page } from '@playwright/test';
import { BasePage } from './base-page';

export class AboutZeppelinModal extends BasePage {
readonly modal: Locator;
readonly modalTitle: Locator;
readonly closeButton: Locator;
readonly logo: Locator;
readonly heading: Locator;
readonly versionText: Locator;
readonly getInvolvedLink: Locator;
readonly licenseLink: Locator;

constructor(page: Page) {
super(page);
this.modal = page.locator('[role="dialog"]').filter({ has: page.getByText('About Zeppelin') });
this.modalTitle = page.locator('.ant-modal-title', { hasText: 'About Zeppelin' });
this.closeButton = page.getByRole('button', { name: 'Close' });
this.logo = page.locator('img[alt="Apache Zeppelin"]');
this.heading = page.locator('h3', { hasText: 'Apache Zeppelin' });
this.versionText = page.locator('.about-version');
this.getInvolvedLink = page.getByRole('link', { name: 'Get involved!' });
this.licenseLink = page.getByRole('link', { name: 'Licensed under the Apache License, Version 2.0' });
}

async close(): Promise<void> {
await this.closeButton.click();
}

async getVersionText(): Promise<string> {
return (await this.versionText.textContent()) || '';
}

async isLogoVisible(): Promise<boolean> {
return this.logo.isVisible();
}

async getGetInvolvedHref(): Promise<string | null> {
return this.getInvolvedLink.getAttribute('href');
}

async getLicenseHref(): Promise<string | null> {
return this.licenseLink.getAttribute('href');
}
}
113 changes: 113 additions & 0 deletions zeppelin-web-angular/e2e/models/header-page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { Locator, Page } from '@playwright/test';
import { BasePage } from './base-page';

export class HeaderPage extends BasePage {
readonly header: Locator;
readonly brandLogo: Locator;
readonly brandLink: Locator;
readonly notebookMenuItem: Locator;
readonly notebookDropdownTrigger: Locator;
readonly notebookDropdown: Locator;
readonly jobMenuItem: Locator;
readonly userDropdownTrigger: Locator;
readonly userBadge: Locator;
readonly searchInput: Locator;
readonly themeToggleButton: Locator;

readonly userMenuItems: {
aboutZeppelin: Locator;
interpreter: Locator;
notebookRepos: Locator;
credential: Locator;
configuration: Locator;
logout: Locator;
switchToClassicUI: Locator;
};

constructor(page: Page) {
super(page);
this.header = page.locator('.header');
this.brandLogo = page.locator('.header .brand .logo');
this.brandLink = page.locator('.header .brand');
this.notebookMenuItem = page.locator('[nz-menu-item]').filter({ hasText: 'Notebook' });
this.notebookDropdownTrigger = page.locator('.node-list-trigger');
this.notebookDropdown = page.locator('zeppelin-node-list.ant-dropdown-menu');
this.jobMenuItem = page.getByRole('link', { name: 'Job' });
this.userDropdownTrigger = page.locator('.header .user .status');
this.userBadge = page.locator('.header .user nz-badge');
this.searchInput = page.locator('.header .search input[type="text"]');
this.themeToggleButton = page.locator('zeppelin-theme-toggle button');

this.userMenuItems = {
aboutZeppelin: page.getByText('About Zeppelin', { exact: true }),
interpreter: page.getByRole('link', { name: 'Interpreter' }),
notebookRepos: page.getByRole('link', { name: 'Notebook Repos' }),
credential: page.getByRole('link', { name: 'Credential' }),
configuration: page.getByRole('link', { name: 'Configuration' }),
logout: page.getByText('Logout', { exact: true }),
switchToClassicUI: page.getByRole('link', { name: 'Switch to Classic UI' })
};
}

async clickBrandLogo(): Promise<void> {
await this.brandLink.waitFor({ state: 'visible', timeout: 10000 });
await this.brandLink.click();
}

async clickNotebookMenu(): Promise<void> {
await this.notebookDropdownTrigger.waitFor({ state: 'visible', timeout: 10000 });
await this.notebookDropdownTrigger.click();
}

async clickJobMenu(): Promise<void> {
await this.jobMenuItem.waitFor({ state: 'visible', timeout: 10000 });
await this.jobMenuItem.click();
}

async clickUserDropdown(): Promise<void> {
await this.userDropdownTrigger.waitFor({ state: 'visible', timeout: 10000 });
await this.userDropdownTrigger.click();
}

async clickAboutZeppelin(): Promise<void> {
await this.userMenuItems.aboutZeppelin.click();
}

async clickInterpreter(): Promise<void> {
await this.userMenuItems.interpreter.click();
}

async clickNotebookRepos(): Promise<void> {
await this.userMenuItems.notebookRepos.click();
}

async clickCredential(): Promise<void> {
await this.userMenuItems.credential.click();
}

async clickConfiguration(): Promise<void> {
await this.userMenuItems.configuration.click();
}

async getUsernameText(): Promise<string> {
return (await this.userBadge.textContent()) || '';
}

async searchNote(query: string): Promise<void> {
await this.searchInput.waitFor({ state: 'visible', timeout: 10000 });
await this.searchInput.fill(query);
await this.page.keyboard.press('Enter');
}
}
109 changes: 109 additions & 0 deletions zeppelin-web-angular/e2e/models/header-page.util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { expect, Page } from '@playwright/test';
import { HeaderPage } from './header-page';
import { NodeListPage } from './node-list-page';

export class HeaderPageUtil {
constructor(
private readonly page: Page,
private readonly headerPage: HeaderPage
) {}

async verifyHeaderIsDisplayed(): Promise<void> {
await expect(this.headerPage.header).toBeVisible();
await expect(this.headerPage.brandLogo).toBeVisible();
await expect(this.headerPage.notebookMenuItem).toBeVisible();
await expect(this.headerPage.jobMenuItem).toBeVisible();
await expect(this.headerPage.userDropdownTrigger).toBeVisible();
await expect(this.headerPage.searchInput).toBeVisible();
await expect(this.headerPage.themeToggleButton).toBeVisible();
}

async verifyNavigationToHomePage(): Promise<void> {
await this.headerPage.clickBrandLogo();
await this.page.waitForURL(/\/(#\/)?$/);
const url = this.page.url();
expect(url).toMatch(/\/(#\/)?$/);
}

async verifyNavigationToJobManager(): Promise<void> {
await this.headerPage.clickJobMenu();
await this.page.waitForURL(/jobmanager/);
expect(this.page.url()).toContain('jobmanager');
}

async verifyUserDropdownOpens(): Promise<void> {
await this.headerPage.clickUserDropdown();
await expect(this.headerPage.userMenuItems.aboutZeppelin).toBeVisible();
}

async verifyNotebookDropdownOpens(): Promise<void> {
await this.headerPage.clickNotebookMenu();
await expect(this.headerPage.notebookDropdown).toBeVisible();

const nodeList = new NodeListPage(this.page);
await expect(nodeList.createNewNoteButton).toBeVisible();
}

async verifySearchNavigation(query: string): Promise<void> {
await this.headerPage.searchNote(query);
await this.page.waitForURL(/search/);
expect(this.page.url()).toContain('search');
expect(this.page.url()).toContain(query);
}

async verifyUserMenuItemsVisible(isLoggedIn: boolean): Promise<void> {
await this.headerPage.clickUserDropdown();
await expect(this.headerPage.userMenuItems.aboutZeppelin).toBeVisible();
await expect(this.headerPage.userMenuItems.interpreter).toBeVisible();
await expect(this.headerPage.userMenuItems.notebookRepos).toBeVisible();
await expect(this.headerPage.userMenuItems.credential).toBeVisible();
await expect(this.headerPage.userMenuItems.configuration).toBeVisible();
await expect(this.headerPage.userMenuItems.switchToClassicUI).toBeVisible();

if (isLoggedIn) {
const username = await this.headerPage.getUsernameText();
expect(username).not.toBe('anonymous');
await expect(this.headerPage.userMenuItems.logout).toBeVisible();
}
}

async navigateToInterpreterSettings(): Promise<void> {
await this.headerPage.clickUserDropdown();
await this.headerPage.clickInterpreter();
await this.page.waitForURL(/interpreter/);
expect(this.page.url()).toContain('interpreter');
}

async navigateToNotebookRepos(): Promise<void> {
await this.headerPage.clickUserDropdown();
await this.headerPage.clickNotebookRepos();
await this.page.waitForURL(/notebook-repos/);
expect(this.page.url()).toContain('notebook-repos');
}

async navigateToCredential(): Promise<void> {
await this.headerPage.clickUserDropdown();
await this.headerPage.clickCredential();
await this.page.waitForURL(/credential/);
expect(this.page.url()).toContain('credential');
}

async navigateToConfiguration(): Promise<void> {
await this.headerPage.clickUserDropdown();
await this.headerPage.clickConfiguration();
await this.page.waitForURL(/configuration/);
expect(this.page.url()).toContain('configuration');
}
}
78 changes: 78 additions & 0 deletions zeppelin-web-angular/e2e/models/node-list-page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { Locator, Page } from '@playwright/test';
import { BasePage } from './base-page';

export class NodeListPage extends BasePage {
readonly nodeListContainer: Locator;
readonly importNoteButton: Locator;
readonly createNewNoteButton: Locator;
readonly filterInput: Locator;
readonly treeView: Locator;
readonly notes: Locator;
readonly trashFolder: Locator;

constructor(page: Page) {
super(page);
this.nodeListContainer = page.locator('zeppelin-node-list');
this.importNoteButton = page.getByText('Import Note', { exact: true }).first();
this.createNewNoteButton = page.getByText('Create new Note', { exact: true }).first();
this.filterInput = page.locator('zeppelin-node-list input[placeholder*="Filter"]');
this.treeView = page.locator('zeppelin-node-list nz-tree');
this.notes = page.locator('nz-tree-node').filter({ has: page.locator('.ant-tree-node-content-wrapper .file') });
this.trashFolder = page.locator('nz-tree-node').filter({ hasText: '~Trash' });
}

async clickImportNote(): Promise<void> {
await this.importNoteButton.click();
}

async clickCreateNewNote(): Promise<void> {
await this.createNewNoteButton.click();
}

getFolderByName(folderName: string): Locator {
return this.page.locator('nz-tree-node').filter({ hasText: folderName }).first();
}

getNoteByName(noteName: string): Locator {
return this.page.locator('nz-tree-node').filter({ hasText: noteName }).first();
}

async clickNote(noteName: string): Promise<void> {
const note = this.getNoteByName(noteName);
// Target the specific link that navigates to the notebook (has href with "#/notebook/")
const noteLink = note.locator('a[href*="#/notebook/"]');
await noteLink.click();
}

async isFilterInputVisible(): Promise<boolean> {
return this.filterInput.isVisible();
}

async isTrashFolderVisible(): Promise<boolean> {
return this.trashFolder.isVisible();
}

async getAllVisibleNoteNames(): Promise<string[]> {
const noteElements = await this.notes.all();
const names: string[] = [];
for (const note of noteElements) {
const text = await note.textContent();
if (text) {
names.push(text.trim());
}
}
return names;
}
}
Loading
Loading