Skip to content
Open
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
78 changes: 77 additions & 1 deletion zeppelin-web-angular/e2e/models/base-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* limitations under the License.
*/

import { Locator, Page } from '@playwright/test';
import { expect, Locator, Page } from '@playwright/test';

export const E2E_TEST_FOLDER = 'E2E_TEST_FOLDER';
export const BASE_URL = 'http://localhost:4200';
Expand All @@ -23,12 +23,32 @@ export class BasePage {
readonly zeppelinPageHeader: Locator;
readonly zeppelinHeader: Locator;

readonly modalTitle: Locator;
readonly modalBody: Locator;
readonly modalContent: Locator;

readonly okButton: Locator;
readonly cancelButton: Locator;
readonly runButton: Locator;

readonly welcomeTitle: Locator;

constructor(page: Page) {
this.page = page;
this.zeppelinNodeList = page.locator('zeppelin-node-list');
this.zeppelinWorkspace = page.locator('zeppelin-workspace');
this.zeppelinPageHeader = page.locator('zeppelin-page-header');
this.zeppelinHeader = page.locator('zeppelin-header');

this.modalTitle = page.locator('.ant-modal-confirm-title, .ant-modal-title');
this.modalBody = page.locator('.ant-modal-confirm-content, .ant-modal-body');
this.modalContent = page.locator('.ant-modal-body');

this.okButton = page.locator('button:has-text("OK")');
this.cancelButton = page.locator('button:has-text("Cancel")');
this.runButton = page.locator('button:has-text("Run")');

this.welcomeTitle = page.getByRole('heading', { name: 'Welcome to Zeppelin!' });
}

async waitForPageLoad(): Promise<void> {
Expand Down Expand Up @@ -63,4 +83,60 @@ export class BasePage {
async getElementText(locator: Locator): Promise<string> {
return (await locator.textContent()) || '';
}

async waitForFormLabels(labelTexts: string[], timeout = 10000): Promise<void> {
await this.page.waitForFunction(
texts => {
const labels = Array.from(document.querySelectorAll('nz-form-label'));
return texts.some(text => labels.some(l => l.textContent?.includes(text)));
},
labelTexts,
{ timeout }
);
}

async waitForElementAttribute(
selector: string,
attribute: string,
hasAttribute: boolean = true,
timeout = 10000
): Promise<void> {
await this.page.waitForFunction(
({ sel, attr, has }) => {
const el = document.querySelector(sel);
return el && (has ? el.hasAttribute(attr) : !el.hasAttribute(attr));
},
{ sel: selector, attr: attribute, has: hasAttribute },
{ timeout }
);
}

async waitForRouterOutletChild(timeout = 10000): Promise<void> {
await this.page.waitForFunction(
() => {
const workspace = document.querySelector('zeppelin-workspace');
const outlet = workspace?.querySelector('router-outlet');
return outlet && outlet.nextElementSibling !== null;
},
{ timeout }
);
}

async fillAndVerifyInput(
locator: Locator,
value: string,
options?: { timeout?: number; clearFirst?: boolean }
): Promise<void> {
const { timeout = 10000, clearFirst = true } = options || {};

await expect(locator).toBeVisible({ timeout });
await expect(locator).toBeEnabled({ timeout: 5000 });

if (clearFirst) {
await locator.clear();
}

await locator.fill(value);
await expect(locator).toHaveValue(value);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,36 +11,36 @@
*/

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

export class ThemePage {
readonly page: Page;
export class DarkModePage extends BasePage {
readonly themeToggleButton: Locator;
readonly rootElement: Locator;

constructor(page: Page) {
this.page = page;
super(page);
this.themeToggleButton = page.locator('zeppelin-theme-toggle button');
this.rootElement = page.locator('html');
}

async toggleTheme() {
await this.themeToggleButton.click();
await this.themeToggleButton.click({ timeout: 15000 });
}

async assertDarkTheme() {
await expect(this.rootElement).toHaveClass(/dark/);
await expect(this.rootElement).toHaveClass(/dark/, { timeout: 10000 });
await expect(this.rootElement).toHaveAttribute('data-theme', 'dark');
await expect(this.themeToggleButton).toHaveText('dark_mode');
}

async assertLightTheme() {
await expect(this.rootElement).toHaveClass(/light/);
await expect(this.rootElement).toHaveClass(/light/, { timeout: 10000 });
await expect(this.rootElement).toHaveAttribute('data-theme', 'light');
await expect(this.themeToggleButton).toHaveText('light_mode');
}

async assertSystemTheme() {
await expect(this.themeToggleButton).toHaveText('smart_toy');
await expect(this.themeToggleButton).toHaveText('smart_toy', { timeout: 60000 });
}

async setThemeInLocalStorage(theme: 'light' | 'dark' | 'system') {
Expand Down
144 changes: 36 additions & 108 deletions zeppelin-web-angular/e2e/models/home-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,12 @@
*/

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

export class HomePage extends BasePage {
readonly welcomeHeading: Locator;
readonly notebookSection: Locator;
readonly helpSection: Locator;
readonly communitySection: Locator;
readonly createNewNoteButton: Locator;
readonly importNoteButton: Locator;
readonly searchInput: Locator;
readonly filterInput: Locator;
readonly zeppelinLogo: Locator;
readonly anonymousUserIndicator: Locator;
readonly welcomeSection: Locator;
Expand All @@ -31,11 +25,12 @@ export class HomePage extends BasePage {
readonly helpCommunityColumn: Locator;
readonly welcomeDescription: Locator;
readonly refreshNoteButton: Locator;
readonly refreshIcon: Locator;
readonly notebookList: Locator;
readonly notebookHeading: Locator;
readonly helpHeading: Locator;
readonly communityHeading: Locator;
readonly createNoteModal: Locator;
readonly createNoteButton: Locator;
readonly notebookNameInput: Locator;
readonly externalLinks: {
documentation: Locator;
mailingList: Locator;
Expand All @@ -52,27 +47,13 @@ export class HomePage extends BasePage {
clearOutput: Locator;
moveToTrash: Locator;
};
folderActions: {
createNote: Locator;
renameFolder: Locator;
moveToTrash: Locator;
};
trashActions: {
restoreAll: Locator;
emptyAll: Locator;
};
};

constructor(page: Page) {
super(page);
this.welcomeHeading = page.locator('h1', { hasText: 'Welcome to Zeppelin!' });
this.notebookSection = page.locator('text=Notebook').first();
this.helpSection = page.locator('text=Help').first();
this.communitySection = page.locator('text=Community').first();
this.createNewNoteButton = page.locator('text=Create new Note');
this.importNoteButton = page.locator('text=Import Note');
this.searchInput = page.locator('textbox', { hasText: 'Search' });
this.filterInput = page.locator('input[placeholder*="Filter"]');
this.zeppelinLogo = page.locator('text=Zeppelin').first();
this.anonymousUserIndicator = page.locator('text=anonymous');
this.welcomeSection = page.locator('.welcome');
Expand All @@ -81,11 +62,12 @@ export class HomePage extends BasePage {
this.helpCommunityColumn = page.locator('[nz-col]').last();
this.welcomeDescription = page.locator('.welcome').getByText('Zeppelin is web-based notebook');
this.refreshNoteButton = page.locator('a.refresh-note');
this.refreshIcon = page.locator('a.refresh-note i[nz-icon]');
this.notebookList = page.locator('zeppelin-node-list');
this.notebookHeading = this.notebookColumn.locator('h3');
this.helpHeading = page.locator('h3').filter({ hasText: 'Help' });
this.communityHeading = page.locator('h3').filter({ hasText: 'Community' });
this.createNoteModal = page.locator('div.ant-modal-content');
this.createNoteButton = this.createNoteModal.locator('button', { hasText: 'Create' });
this.notebookNameInput = this.createNoteModal.locator('input[name="noteName"]');

this.externalLinks = {
documentation: page.locator('a[href*="zeppelin.apache.org/docs"]'),
Expand All @@ -103,67 +85,30 @@ export class HomePage extends BasePage {
renameNote: page.locator('.file .operation a[nztooltiptitle*="Rename note"]'),
clearOutput: page.locator('.file .operation a[nztooltiptitle*="Clear output"]'),
moveToTrash: page.locator('.file .operation a[nztooltiptitle*="Move note to Trash"]')
},
folderActions: {
createNote: page.locator('.folder .operation a[nztooltiptitle*="Create new note"]'),
renameFolder: page.locator('.folder .operation a[nztooltiptitle*="Rename folder"]'),
moveToTrash: page.locator('.folder .operation a[nztooltiptitle*="Move folder to Trash"]')
},
trashActions: {
restoreAll: page.locator('.folder .operation a[nztooltiptitle*="Restore all"]'),
emptyAll: page.locator('.folder .operation a[nztooltiptitle*="Empty all"]')
}
};
}

async navigateToHome(): Promise<void> {
await this.page.goto('/', { waitUntil: 'load' });
await this.waitForPageLoad();
}

async navigateToLogin(): Promise<void> {
await this.page.goto('/#/login', { waitUntil: 'load' });
await this.waitForPageLoad();
await this.navigateToRoute('/login');
// Wait for potential redirect to complete by checking URL change
await waitForUrlNotContaining(this.page, '#/login');
await this.waitForUrlNotContaining('#/login');
}

async isHomeContentDisplayed(): Promise<boolean> {
try {
await expect(this.welcomeHeading).toBeVisible();
return true;
} catch {
return false;
}
return this.welcomeTitle.isVisible();
}

async isAnonymousUser(): Promise<boolean> {
try {
await expect(this.anonymousUserIndicator).toBeVisible();
return true;
} catch {
return false;
}
return this.anonymousUserIndicator.isVisible();
}

async clickZeppelinLogo(): Promise<void> {
await this.zeppelinLogo.click();
}

async getCurrentURL(): Promise<string> {
return this.page.url();
}

getCurrentPath(): string {
return getCurrentPath(this.page);
}

async getPageTitle(): Promise<string> {
return this.page.title();
await this.zeppelinLogo.click({ timeout: 15000 });
}

async getWelcomeHeadingText(): Promise<string> {
const text = await this.welcomeHeading.textContent();
const text = await this.welcomeTitle.textContent();
return text || '';
}

Expand All @@ -173,65 +118,48 @@ export class HomePage extends BasePage {
}

async clickRefreshNotes(): Promise<void> {
await this.refreshNoteButton.click();
await this.refreshNoteButton.click({ timeout: 15000 });
}

async isNotebookListVisible(): Promise<boolean> {
return this.notebookList.isVisible();
return this.zeppelinNodeList.isVisible();
}

async clickCreateNewNote(): Promise<void> {
await this.nodeList.createNewNoteLink.click();
await this.nodeList.createNewNoteLink.click({ timeout: 15000 });
await this.createNoteModal.waitFor({ state: 'visible' });
}

async clickImportNote(): Promise<void> {
await this.nodeList.importNoteLink.click();
}
async createNote(notebookName: string): Promise<void> {
await this.clickCreateNewNote();

async filterNotes(searchTerm: string): Promise<void> {
await this.nodeList.filterInput.fill(searchTerm);
}
// Wait for the modal form to be fully rendered with proper labels
await this.page.waitForSelector('nz-form-label', { timeout: 10000 });

async isRefreshIconSpinning(): Promise<boolean> {
const spinAttribute = await this.refreshIcon.getAttribute('nzSpin');
return spinAttribute === 'true' || spinAttribute === '';
}
await this.waitForFormLabels(['Note Name', 'Clone Note']);

async waitForRefreshToComplete(): Promise<void> {
await this.page.waitForFunction(
() => {
const icon = document.querySelector('a.refresh-note i[nz-icon]');
return icon && !icon.hasAttribute('nzSpin');
},
{ timeout: 10000 }
);
// Fill and verify the notebook name input
await this.fillAndVerifyInput(this.notebookNameInput, notebookName);

// Click the 'Create' button in the modal
await expect(this.createNoteButton).toBeEnabled({ timeout: 5000 });
await this.createNoteButton.click({ timeout: 15000 });
await this.waitForPageLoad();
}

async getDocumentationLinkHref(): Promise<string | null> {
return this.externalLinks.documentation.getAttribute('href');
async clickImportNote(): Promise<void> {
await this.nodeList.importNoteLink.click({ timeout: 15000 });
}

async areExternalLinksVisible(): Promise<boolean> {
const links = [
this.externalLinks.documentation,
this.externalLinks.mailingList,
this.externalLinks.issuesTracking,
this.externalLinks.github
];

for (const link of links) {
if (!(await link.isVisible())) {
return false;
}
}
return true;
async filterNotes(searchTerm: string): Promise<void> {
await this.nodeList.filterInput.fill(searchTerm, { timeout: 15000 });
}

async isWelcomeSectionVisible(): Promise<boolean> {
return this.welcomeSection.isVisible();
async waitForRefreshToComplete(): Promise<void> {
await this.waitForElementAttribute('a.refresh-note i[nz-icon]', 'nzSpin', false);
}

async isMoreInfoGridVisible(): Promise<boolean> {
return this.moreInfoGrid.isVisible();
async getDocumentationLinkHref(): Promise<string | null> {
return this.externalLinks.documentation.getAttribute('href');
}
}
Loading
Loading