diff --git a/zeppelin-web-angular/e2e/models/job-card.ts b/zeppelin-web-angular/e2e/models/job-card.ts new file mode 100644 index 00000000000..529c2c97f47 --- /dev/null +++ b/zeppelin-web-angular/e2e/models/job-card.ts @@ -0,0 +1,117 @@ +/* + * 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 } from '@playwright/test'; + +export class JobCard { + readonly container: Locator; + readonly noteIcon: Locator; + readonly noteName: Locator; + readonly interpreter: Locator; + readonly relativeTime: Locator; + readonly statusText: Locator; + readonly progressPercentage: Locator; + readonly controlButton: Locator; + readonly paragraphStatusBadges: Locator; + readonly progressBar: Locator; + + constructor(container: Locator) { + this.container = container; + this.noteIcon = container.locator('.note-icon'); + this.noteName = container.locator('a[routerLink]').first(); + this.interpreter = container.locator('.interpreter'); + this.relativeTime = container.locator('.right-tools small'); + this.statusText = container.locator('.right-tools > span').nth(0); + this.progressPercentage = container.locator('.right-tools > span').nth(1); + this.controlButton = container.locator('.job-control-btn'); + this.paragraphStatusBadges = container.locator('zeppelin-job-manager-job-status'); + this.progressBar = container.locator('nz-progress'); + } + + async getNoteName(): Promise { + return await this.noteName.textContent(); + } + + async getInterpreter(): Promise { + return await this.interpreter.textContent(); + } + + async getStatus(): Promise { + return await this.statusText.textContent(); + } + + async isRunning(): Promise { + const status = await this.getStatus(); + return status === 'RUNNING'; + } + + async getProgress(): Promise { + if (await this.isRunning()) { + return await this.progressPercentage.textContent(); + } + return null; + } + + async clickControlButton(): Promise { + await this.controlButton.click(); + } + + async getControlButtonTooltip(): Promise { + return await this.controlButton.getAttribute('nz-tooltip'); + } + + async clickNoteName(): Promise { + await this.noteName.click(); + } + + async getParagraphCount(): Promise { + return await this.paragraphStatusBadges.count(); + } + + async isProgressBarVisible(): Promise { + try { + return await this.progressBar.isVisible({ timeout: 1000 }); + } catch { + return false; + } + } + + getParagraphBadge(index: number): Locator { + return this.paragraphStatusBadges.nth(index); + } + + async getParagraphStatuses(): Promise { + const statuses: string[] = []; + const count = await this.getParagraphCount(); + for (let i = 0; i < count; i++) { + const badge = this.getParagraphBadge(i); + const status = await badge.textContent(); + if (status) { + statuses.push(status.trim()); + } + } + return statuses; + } + + async getNoteIconType(): Promise { + return await this.noteIcon.getAttribute('nzType'); + } + + async getRelativeTime(): Promise { + return await this.relativeTime.textContent(); + } + + async hasStatus(status: string): Promise { + const currentStatus = await this.getStatus(); + return currentStatus === status; + } +} diff --git a/zeppelin-web-angular/e2e/models/job-manager-page.ts b/zeppelin-web-angular/e2e/models/job-manager-page.ts new file mode 100644 index 00000000000..ff2ab37e6fc --- /dev/null +++ b/zeppelin-web-angular/e2e/models/job-manager-page.ts @@ -0,0 +1,150 @@ +/* + * 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 JobManagerPage extends BasePage { + readonly pageHeader: Locator; + readonly pageTitle: Locator; + readonly pageDescription: Locator; + readonly searchInput: Locator; + readonly interpreterSelect: Locator; + readonly sortSelect: Locator; + readonly totalCounter: Locator; + readonly statusLegend: Locator; + readonly loadingSkeleton: Locator; + readonly disabledAlert: Locator; + readonly emptyState: Locator; + readonly jobCards: Locator; + readonly jobStatusBadges: Locator; + + constructor(page: Page) { + super(page); + this.pageHeader = page.locator('zeppelin-page-header'); + this.pageTitle = this.pageHeader.getByText('Job'); + this.pageDescription = this.pageHeader.locator('p'); + this.searchInput = page.getByPlaceholder('Search jobs...'); + this.interpreterSelect = page + .locator('nz-form-label') + .filter({ hasText: 'Interpreter' }) + .locator('..') + .locator('nz-select'); + this.sortSelect = page + .locator('nz-form-label') + .filter({ hasText: 'Sort' }) + .locator('..') + .locator('nz-select'); + this.totalCounter = page.locator('nz-form-text'); + this.statusLegend = page.locator('.status-legend'); + this.loadingSkeleton = page.locator('nz-card nz-skeleton'); + this.disabledAlert = page.locator('nz-alert[nzType="info"]'); + this.emptyState = page.locator('nz-empty'); + this.jobCards = page.locator('zeppelin-job-manager-job'); + this.jobStatusBadges = page.locator('zeppelin-job-manager-job-status'); + } + + async navigate(): Promise { + await this.page.goto('/#/jobmanager'); + await this.waitForPageLoad(); + } + + async waitForPageLoad(): Promise { + await super.waitForPageLoad(); + await this.page.waitForFunction( + () => { + const skeleton = document.querySelector('nz-card nz-skeleton'); + return !skeleton || skeleton.getAttribute('nzActive') !== 'true'; + }, + { timeout: 10000 } + ); + } + + async searchJobs(query: string): Promise { + await this.searchInput.fill(query); + await this.page.waitForTimeout(500); + } + + async selectInterpreter(interpreter: string): Promise { + await this.interpreterSelect.click(); + await this.page + .locator('nz-option-item') + .filter({ hasText: interpreter }) + .click(); + await this.page.waitForTimeout(500); + } + + async selectSort(sortOption: string): Promise { + await this.sortSelect.click(); + await this.page + .locator('nz-option-item') + .filter({ hasText: sortOption }) + .click(); + await this.page.waitForTimeout(500); + } + + async getTotalCount(): Promise { + const text = await this.totalCounter.textContent(); + return parseInt(text || '0', 10); + } + + async isLoading(): Promise { + return await this.loadingSkeleton.isVisible(); + } + + async isDisabled(): Promise { + return await this.disabledAlert.isVisible(); + } + + async getDisabledMessage(): Promise { + return await this.disabledAlert.textContent(); + } + + async isEmpty(): Promise { + return await this.emptyState.isVisible(); + } + + async getJobCardCount(): Promise { + return await this.jobCards.count(); + } + + async getStatusBadgeCount(): Promise { + return await this.jobStatusBadges.count(); + } + + getJobCard(index: number): Locator { + return this.jobCards.nth(index); + } + + getJobCardByNoteName(noteName: string): Locator { + return this.jobCards.filter({ hasText: noteName }); + } + + async clearFilters(): Promise { + await this.searchInput.clear(); + await this.selectInterpreter('All'); + } + + async getJobNoteNames(): Promise { + const names: string[] = []; + const count = await this.getJobCardCount(); + for (let i = 0; i < count; i++) { + const card = this.getJobCard(i); + const nameElement = card.locator('a[routerLink]').first(); + const name = await nameElement.textContent(); + if (name) { + names.push(name.trim()); + } + } + return names; + } +} diff --git a/zeppelin-web-angular/e2e/models/job-manager-page.util.ts b/zeppelin-web-angular/e2e/models/job-manager-page.util.ts new file mode 100644 index 00000000000..72571ddc1ce --- /dev/null +++ b/zeppelin-web-angular/e2e/models/job-manager-page.util.ts @@ -0,0 +1,220 @@ +/* + * 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 { JobCard } from './job-card'; +import { JobManagerPage } from './job-manager-page'; + +export class JobManagerUtil { + private page: Page; + private jobManagerPage: JobManagerPage; + + constructor(page: Page) { + this.page = page; + this.jobManagerPage = new JobManagerPage(page); + } + + async verifyPageHeader(): Promise { + await expect(this.jobManagerPage.pageTitle).toBeVisible(); + await expect(this.jobManagerPage.pageDescription).toContainText( + 'You can monitor the status of notebook and navigate to note or paragraph' + ); + } + + async verifyDisabledState(): Promise { + await expect(this.jobManagerPage.disabledAlert).toBeVisible(); + await expect(this.jobManagerPage.disabledAlert).toContainText( + 'Job Manager is disabled in the current configuration' + ); + } + + async verifyLoadingState(): Promise { + await expect(this.jobManagerPage.loadingSkeleton).toBeVisible(); + } + + async verifyStatusLegend(): Promise { + const statusBadges = this.jobManagerPage.statusLegend.locator('zeppelin-job-manager-job-status'); + const expectedStatuses = ['READY', 'RUNNING', 'FINISHED', 'ERROR', 'ABORT', 'PENDING']; + await expect(statusBadges).toHaveCount(expectedStatuses.length); + for (const status of expectedStatuses) { + await expect(statusBadges.filter({ hasText: status })).toBeVisible(); + } + } + + async verifyStatusBadgeColors(): Promise { + const statusLegend = this.jobManagerPage.statusLegend; + const badgeMap = { + READY: 'success', + FINISHED: 'success', + ERROR: 'error', + ABORT: 'warning', + RUNNING: 'processing', + PENDING: 'default' + }; + for (const [status, badgeType] of Object.entries(badgeMap)) { + const badge = statusLegend.locator('zeppelin-job-manager-job-status').filter({ hasText: status }); + await expect(badge.locator(`nz-badge[ng-reflect-nz-status="${badgeType}"]`)).toBeVisible(); + } + } + + async searchAndVerifyFiltering(searchTerm: string, expectedMinCount: number = 0): Promise { + await this.jobManagerPage.searchJobs(searchTerm); + const jobCount = await this.jobManagerPage.getJobCardCount(); + expect(jobCount).toBeGreaterThanOrEqual(expectedMinCount); + const totalCount = await this.jobManagerPage.getTotalCount(); + expect(totalCount).toBe(jobCount); + } + + async verifySearchHighlight(searchTerm: string): Promise { + const highlightedText = this.page.locator('mark.mark-highlight'); + if ((await highlightedText.count()) > 0) { + await expect(highlightedText.first()).toContainText(searchTerm); + } + } + + async selectInterpreterAndVerify(interpreter: string): Promise { + await this.jobManagerPage.selectInterpreter(interpreter); + const jobCount = await this.jobManagerPage.getJobCardCount(); + const totalCount = await this.jobManagerPage.getTotalCount(); + expect(totalCount).toBe(jobCount); + if (interpreter !== 'All' && jobCount > 0) { + const firstJobCard = new JobCard(this.jobManagerPage.getJobCard(0)); + const interpreterText = await firstJobCard.getInterpreter(); + expect(interpreterText).toContain(interpreter); + } + } + + async verifySortOrder(sortOption: string): Promise { + await this.jobManagerPage.selectSort(sortOption); + const jobCount = await this.jobManagerPage.getJobCardCount(); + if (jobCount < 2) { + return; + } + const firstJobCard = new JobCard(this.jobManagerPage.getJobCard(0)); + const secondJobCard = new JobCard(this.jobManagerPage.getJobCard(1)); + const firstTime = await firstJobCard.getRelativeTime(); + const secondTime = await secondJobCard.getRelativeTime(); + expect(firstTime).toBeTruthy(); + expect(secondTime).toBeTruthy(); + } + + async verifyJobCard(index: number): Promise { + const jobCardLocator = this.jobManagerPage.getJobCard(index); + await expect(jobCardLocator).toBeVisible(); + const jobCard = new JobCard(jobCardLocator); + await expect(jobCard.noteIcon).toBeVisible(); + await expect(jobCard.noteName).toBeVisible(); + await expect(jobCard.interpreter).toBeVisible(); + await expect(jobCard.relativeTime).toBeVisible(); + await expect(jobCard.statusText).toBeVisible(); + await expect(jobCard.controlButton).toBeVisible(); + const paragraphCount = await jobCard.getParagraphCount(); + expect(paragraphCount).toBeGreaterThan(0); + } + + async verifyJobControlButton(index: number): Promise { + const jobCardLocator = this.jobManagerPage.getJobCard(index); + const jobCard = new JobCard(jobCardLocator); + const isRunning = await jobCard.isRunning(); + const tooltip = await jobCard.getControlButtonTooltip(); + if (isRunning) { + expect(tooltip).toContain('Stop All Paragraphs'); + const progressBarVisible = await jobCard.isProgressBarVisible(); + if (progressBarVisible) { + await expect(jobCard.progressBar).toBeVisible(); + } + } else { + expect(tooltip).toContain('Start All Paragraphs'); + } + } + + async verifyEmptyState(): Promise { + await expect(this.jobManagerPage.emptyState).toBeVisible(); + await expect(this.jobManagerPage.emptyState).toContainText('No Job found'); + } + + async verifyTotalCountMatchesJobCards(): Promise { + const totalCount = await this.jobManagerPage.getTotalCount(); + const jobCardCount = await this.jobManagerPage.getJobCardCount(); + expect(totalCount).toBe(jobCardCount); + } + + async confirmJobAction(action: 'start' | 'stop'): Promise { + const modal = this.page.locator('.ant-modal'); + await expect(modal).toBeVisible(); + const expectedContent = action === 'start' ? 'Run all paragraphs?' : 'Stop all paragraphs?'; + await expect(modal).toContainText(expectedContent); + const okButton = modal.getByRole('button', { name: 'OK' }); + await okButton.click(); + await expect(modal).toBeHidden(); + } + + async cancelJobAction(): Promise { + const modal = this.page.locator('.ant-modal'); + await expect(modal).toBeVisible(); + const cancelButton = modal.getByRole('button', { name: 'Cancel' }); + await cancelButton.click(); + await expect(modal).toBeHidden(); + } + + async navigateToNotebook(jobIndex: number): Promise { + const jobCardLocator = this.jobManagerPage.getJobCard(jobIndex); + const jobCard = new JobCard(jobCardLocator); + await jobCard.clickNoteName(); + await this.page.waitForURL(/.*\/notebook\/.*/); + } + + async verifyParagraphStatusBadges(jobIndex: number): Promise { + const jobCardLocator = this.jobManagerPage.getJobCard(jobIndex); + const jobCard = new JobCard(jobCardLocator); + const paragraphCount = await jobCard.getParagraphCount(); + expect(paragraphCount).toBeGreaterThan(0); + for (let i = 0; i < paragraphCount; i++) { + const badge = jobCard.getParagraphBadge(i); + try { + const badgeVisible = await badge.isVisible({ timeout: 1000 }); + if (badgeVisible) { + await expect(badge).toBeVisible(); + } + } catch { + continue; + } + } + } + + async setupTest(): Promise<{ isDisabled: boolean; jobCount: number }> { + const isDisabled = await this.jobManagerPage.isDisabled(); + const jobCount = await this.jobManagerPage.getJobCardCount(); + console.log('Test Context:', { isDisabled, jobCount }); + return { isDisabled, jobCount }; + } + + async verifyJobCardDataIntegrity(jobIndex: number): Promise { + const jobCard = new JobCard(this.jobManagerPage.getJobCard(jobIndex)); + const noteName = await jobCard.getNoteName(); + const interpreter = await jobCard.getInterpreter(); + const status = await jobCard.getStatus(); + const relativeTime = await jobCard.getRelativeTime(); + expect(noteName).toBeTruthy(); + expect(noteName?.length).toBeGreaterThan(0); + expect(interpreter).toBeTruthy(); + expect(status).toBeTruthy(); + expect(relativeTime).toBeTruthy(); + } + + async verifyAllJobCardsDataIntegrity(): Promise { + const jobCount = await this.jobManagerPage.getJobCardCount(); + for (let i = 0; i < jobCount; i++) { + await this.verifyJobCardDataIntegrity(i); + } + } +} diff --git a/zeppelin-web-angular/e2e/tests/jobmanager/filter-controls.spec.ts b/zeppelin-web-angular/e2e/tests/jobmanager/filter-controls.spec.ts new file mode 100644 index 00000000000..a4b7af23802 --- /dev/null +++ b/zeppelin-web-angular/e2e/tests/jobmanager/filter-controls.spec.ts @@ -0,0 +1,192 @@ +/* + * 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 { JobManagerPage } from '../../models/job-manager-page'; +import { JobManagerUtil } from '../../models/job-manager-page.util'; +import { addPageAnnotationBeforeEach, performLoginIfRequired, PAGES } from '../../utils'; + +test.describe('Job Manager - Filter Controls', () => { + addPageAnnotationBeforeEach(PAGES.WORKSPACE.JOB_MANAGER); + + test.beforeEach(async ({ page }) => { + await performLoginIfRequired(page); + const jobManagerPage = new JobManagerPage(page); + await jobManagerPage.navigate(); + }); + + test('should filter jobs by search term', async ({ page }) => { + const jobManagerPage = new JobManagerPage(page); + const jobManagerUtil = new JobManagerUtil(page); + + const { isDisabled, jobCount } = await jobManagerUtil.setupTest(); + if (isDisabled || jobCount === 0) { + test.skip(true, 'Job Manager is disabled or no jobs available'); + } + + await jobManagerUtil.searchAndVerifyFiltering('note', 0); + }); + + test('should handle search with special characters', async ({ page }) => { + const jobManagerPage = new JobManagerPage(page); + const jobManagerUtil = new JobManagerUtil(page); + + const { isDisabled } = await jobManagerUtil.setupTest(); + if (isDisabled) { + test.skip(true, 'Job Manager is disabled'); + } + + await jobManagerPage.searchJobs('*.*'); + const totalCount = await jobManagerPage.getTotalCount(); + expect(totalCount).toBeGreaterThanOrEqual(0); + }); + + test('should perform case-insensitive search', async ({ page }) => { + const jobManagerPage = new JobManagerPage(page); + const jobManagerUtil = new JobManagerUtil(page); + + const { isDisabled, jobCount } = await jobManagerUtil.setupTest(); + if (isDisabled || jobCount === 0) { + test.skip(true, 'Job Manager is disabled or no jobs available'); + } + + await jobManagerPage.searchJobs('NOTE'); + const uppercaseCount = await jobManagerPage.getTotalCount(); + + await jobManagerPage.searchJobs('note'); + const lowercaseCount = await jobManagerPage.getTotalCount(); + + expect(uppercaseCount).toBe(lowercaseCount); + }); + + test('should highlight search terms in job names', async ({ page }) => { + const jobManagerPage = new JobManagerPage(page); + const jobManagerUtil = new JobManagerUtil(page); + + const { isDisabled, jobCount } = await jobManagerUtil.setupTest(); + if (isDisabled || jobCount === 0) { + test.skip(true, 'Job Manager is disabled or no jobs available'); + } + + await jobManagerPage.searchJobs('note'); + const filteredCount = await jobManagerPage.getJobCardCount(); + if (filteredCount > 0) { + await jobManagerUtil.verifySearchHighlight('note'); + } + }); + + test('should filter jobs by interpreter', async ({ page }) => { + const jobManagerPage = new JobManagerPage(page); + const jobManagerUtil = new JobManagerUtil(page); + + const { isDisabled, jobCount } = await jobManagerUtil.setupTest(); + if (isDisabled || jobCount === 0) { + test.skip(true, 'Job Manager is disabled or no jobs available'); + } + + await jobManagerUtil.selectInterpreterAndVerify('All'); + }); + + test('should sort jobs by Recently Update', async ({ page }) => { + const jobManagerPage = new JobManagerPage(page); + const jobManagerUtil = new JobManagerUtil(page); + + const { isDisabled, jobCount } = await jobManagerUtil.setupTest(); + if (isDisabled || jobCount < 2) { + test.skip(true, 'Job Manager is disabled or insufficient jobs'); + } + + await jobManagerUtil.verifySortOrder('Recently Update'); + }); + + test('should sort jobs by Oldest Updated', async ({ page }) => { + const jobManagerPage = new JobManagerPage(page); + const jobManagerUtil = new JobManagerUtil(page); + + const { isDisabled, jobCount } = await jobManagerUtil.setupTest(); + if (isDisabled || jobCount < 2) { + test.skip(true, 'Job Manager is disabled or insufficient jobs'); + } + + await jobManagerUtil.verifySortOrder('Oldest Updated'); + }); + + test('should update total counter when filtering', async ({ page }) => { + const jobManagerPage = new JobManagerPage(page); + const jobManagerUtil = new JobManagerUtil(page); + + const { isDisabled, jobCount } = await jobManagerUtil.setupTest(); + if (isDisabled || jobCount === 0) { + test.skip(true, 'Job Manager is disabled or no jobs available'); + } + + await jobManagerUtil.verifyTotalCountMatchesJobCards(); + + await jobManagerPage.searchJobs('nonexistent_job_xyz_123456'); + await jobManagerUtil.verifyTotalCountMatchesJobCards(); + }); + + test('should display empty state when no jobs match filter', async ({ page }) => { + const jobManagerPage = new JobManagerPage(page); + const jobManagerUtil = new JobManagerUtil(page); + + const { isDisabled } = await jobManagerUtil.setupTest(); + if (isDisabled) { + test.skip(true, 'Job Manager is disabled'); + } + + await jobManagerPage.searchJobs('nonexistent_job_xyz_123456'); + const isEmpty = await jobManagerPage.isEmpty(); + if (isEmpty) { + await jobManagerUtil.verifyEmptyState(); + } + }); + + test('should clear filters correctly', async ({ page }) => { + const jobManagerPage = new JobManagerPage(page); + const jobManagerUtil = new JobManagerUtil(page); + + const { isDisabled, jobCount } = await jobManagerUtil.setupTest(); + if (isDisabled || jobCount === 0) { + test.skip(true, 'Job Manager is disabled or no jobs available'); + } + + const initialCount = jobCount; + + await jobManagerPage.searchJobs('note'); + const filteredCount = await jobManagerPage.getJobCardCount(); + + await jobManagerPage.clearFilters(); + const clearedCount = await jobManagerPage.getJobCardCount(); + + expect(clearedCount).toBe(initialCount); + expect(clearedCount).toBeGreaterThanOrEqual(filteredCount); + }); + + test('should maintain filter state during page interactions', async ({ page }) => { + const jobManagerPage = new JobManagerPage(page); + const jobManagerUtil = new JobManagerUtil(page); + + const { isDisabled, jobCount } = await jobManagerUtil.setupTest(); + if (isDisabled || jobCount === 0) { + test.skip(true, 'Job Manager is disabled or no jobs available'); + } + + await jobManagerPage.searchJobs('note'); + const searchCount = await jobManagerPage.getJobCardCount(); + + await jobManagerPage.selectInterpreter('All'); + const combinedCount = await jobManagerPage.getJobCardCount(); + + expect(combinedCount).toBeLessThanOrEqual(searchCount); + }); +}); diff --git a/zeppelin-web-angular/e2e/tests/jobmanager/job-cards.spec.ts b/zeppelin-web-angular/e2e/tests/jobmanager/job-cards.spec.ts new file mode 100644 index 00000000000..cde1e10f6f7 --- /dev/null +++ b/zeppelin-web-angular/e2e/tests/jobmanager/job-cards.spec.ts @@ -0,0 +1,257 @@ +/* + * 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 { JobCard } from '../../models/job-card'; +import { JobManagerPage } from '../../models/job-manager-page'; +import { JobManagerUtil } from '../../models/job-manager-page.util'; +import { addPageAnnotationBeforeEach, performLoginIfRequired, PAGES } from '../../utils'; + +test.describe('Job Manager - Job Cards', () => { + addPageAnnotationBeforeEach(PAGES.WORKSPACE.JOB); + + test.beforeEach(async ({ page }) => { + await performLoginIfRequired(page); + const jobManagerPage = new JobManagerPage(page); + await jobManagerPage.navigate(); + }); + + test('should display job card with all required elements', async ({ page }) => { + const jobManagerPage = new JobManagerPage(page); + const jobManagerUtil = new JobManagerUtil(page); + + const { isDisabled, jobCount } = await jobManagerUtil.setupTest(); + if (isDisabled || jobCount === 0) { + test.skip(true, 'Job Manager is disabled or no jobs available'); + } + + await jobManagerUtil.verifyJobCard(0); + }); + + test('should display correct note icon based on note type', async ({ page }) => { + const jobManagerPage = new JobManagerPage(page); + const jobManagerUtil = new JobManagerUtil(page); + + const { isDisabled, jobCount } = await jobManagerUtil.setupTest(); + if (isDisabled || jobCount === 0) { + test.skip(true, 'Job Manager is disabled or no jobs available'); + } + + const jobCard = new JobCard(jobManagerPage.getJobCard(0)); + await expect(jobCard.noteIcon).toBeVisible(); + + const iconType = await jobCard.getNoteIconType(); + const validIconTypes = ['file', 'close-circle', 'file-unknown']; + expect(validIconTypes).toContain(iconType); + }); + + test('should display interpreter name or unset message', async ({ page }) => { + const jobManagerPage = new JobManagerPage(page); + const jobManagerUtil = new JobManagerUtil(page); + + const { isDisabled, jobCount } = await jobManagerUtil.setupTest(); + if (isDisabled || jobCount === 0) { + test.skip(true, 'Job Manager is disabled or no jobs available'); + } + + const jobCard = new JobCard(jobManagerPage.getJobCard(0)); + const interpreter = await jobCard.getInterpreter(); + + expect(interpreter).toBeTruthy(); + expect(interpreter?.length).toBeGreaterThan(0); + }); + + test('should display relative time for last run', async ({ page }) => { + const jobManagerPage = new JobManagerPage(page); + const jobManagerUtil = new JobManagerUtil(page); + + const { isDisabled, jobCount } = await jobManagerUtil.setupTest(); + if (isDisabled || jobCount === 0) { + test.skip(true, 'Job Manager is disabled or no jobs available'); + } + + const jobCard = new JobCard(jobManagerPage.getJobCard(0)); + await expect(jobCard.relativeTime).toBeVisible(); + + const timeText = await jobCard.getRelativeTime(); + expect(timeText).toBeTruthy(); + }); + + test('should display job status', async ({ page }) => { + const jobManagerPage = new JobManagerPage(page); + const jobManagerUtil = new JobManagerUtil(page); + + const { isDisabled, jobCount } = await jobManagerUtil.setupTest(); + if (isDisabled || jobCount === 0) { + test.skip(true, 'Job Manager is disabled or no jobs available'); + } + + const jobCard = new JobCard(jobManagerPage.getJobCard(0)); + const status = await jobCard.getStatus(); + + const validStatuses = ['READY', 'RUNNING', 'FINISHED', 'ERROR', 'ABORT', 'PENDING']; + expect(validStatuses).toContain(status); + }); + + test('should display control button with correct tooltip', async ({ page }) => { + const jobManagerPage = new JobManagerPage(page); + const jobManagerUtil = new JobManagerUtil(page); + + const { isDisabled, jobCount } = await jobManagerUtil.setupTest(); + if (isDisabled || jobCount === 0) { + test.skip(true, 'Job Manager is disabled or no jobs available'); + } + + await jobManagerUtil.verifyJobControlButton(0); + }); + + test('should display progress bar for running jobs', async ({ page }) => { + const jobManagerPage = new JobManagerPage(page); + const jobManagerUtil = new JobManagerUtil(page); + + const { isDisabled, jobCount } = await jobManagerUtil.setupTest(); + if (isDisabled || jobCount === 0) { + test.skip(true, 'Job Manager is disabled or no jobs available'); + } + + const jobCard = new JobCard(jobManagerPage.getJobCard(0)); + const isRunning = await jobCard.isRunning(); + + if (isRunning) { + await expect(jobCard.progressBar).toBeVisible(); + await expect(jobCard.progressPercentage).toBeVisible(); + + const progress = await jobCard.getProgress(); + expect(progress).toBeTruthy(); + } + }); + + test('should display paragraph status badges', async ({ page }) => { + const jobManagerPage = new JobManagerPage(page); + const jobManagerUtil = new JobManagerUtil(page); + + const { isDisabled, jobCount } = await jobManagerUtil.setupTest(); + if (isDisabled || jobCount === 0) { + test.skip(true, 'Job Manager is disabled or no jobs available'); + } + + await jobManagerUtil.verifyParagraphStatusBadges(0); + }); + + test('should navigate to notebook when clicking note name', async ({ page }) => { + const jobManagerPage = new JobManagerPage(page); + const jobManagerUtil = new JobManagerUtil(page); + + const { isDisabled, jobCount } = await jobManagerUtil.setupTest(); + if (isDisabled || jobCount === 0) { + test.skip(true, 'Job Manager is disabled or no jobs available'); + } + + const jobCard = new JobCard(jobManagerPage.getJobCard(0)); + await jobCard.clickNoteName(); + + await page.waitForURL(/.*\/notebook\/.*/); + }); + + test('should show confirmation modal when starting job', async ({ page }) => { + const jobManagerPage = new JobManagerPage(page); + const jobManagerUtil = new JobManagerUtil(page); + + const { isDisabled, jobCount } = await jobManagerUtil.setupTest(); + if (isDisabled || jobCount === 0) { + test.skip(true, 'Job Manager is disabled or no jobs available'); + } + + const jobCard = new JobCard(jobManagerPage.getJobCard(0)); + const isRunning = await jobCard.isRunning(); + + if (!isRunning) { + await jobCard.clickControlButton(); + + const modal = page.locator('.ant-modal'); + await expect(modal).toBeVisible(); + await expect(modal).toContainText('Run all paragraphs?'); + + const cancelButton = modal.getByRole('button', { name: 'Cancel' }); + await cancelButton.click(); + } + }); + + test('should show confirmation modal when stopping job', async ({ page }) => { + const jobManagerPage = new JobManagerPage(page); + const jobManagerUtil = new JobManagerUtil(page); + + const { isDisabled, jobCount } = await jobManagerUtil.setupTest(); + if (isDisabled || jobCount === 0) { + test.skip(true, 'Job Manager is disabled or no jobs available'); + } + + const jobCard = new JobCard(jobManagerPage.getJobCard(0)); + const isRunning = await jobCard.isRunning(); + + if (isRunning) { + await jobCard.clickControlButton(); + + const modal = page.locator('.ant-modal'); + await expect(modal).toBeVisible(); + await expect(modal).toContainText('Stop all paragraphs?'); + + const cancelButton = modal.getByRole('button', { name: 'Cancel' }); + await cancelButton.click(); + } + }); + + test('should navigate to specific paragraph when clicking paragraph badge', async ({ page }) => { + const jobManagerPage = new JobManagerPage(page); + const jobManagerUtil = new JobManagerUtil(page); + + const { isDisabled, jobCount } = await jobManagerUtil.setupTest(); + if (isDisabled || jobCount === 0) { + test.skip(true, 'Job Manager is disabled or no jobs available'); + } + + const jobCard = new JobCard(jobManagerPage.getJobCard(0)); + const paragraphCount = await jobCard.getParagraphCount(); + + if (paragraphCount > 0) { + const badge = jobCard.getParagraphBadge(0); + await badge.click(); + + await page.waitForURL(/.*\/notebook\/.*/); + expect(page.url()).toContain('paragraph='); + } + }); + + test('should verify job card data integrity', async ({ page }) => { + const jobManagerPage = new JobManagerPage(page); + const jobManagerUtil = new JobManagerUtil(page); + + const { isDisabled, jobCount } = await jobManagerUtil.setupTest(); + if (isDisabled || jobCount === 0) { + test.skip(true, 'Job Manager is disabled or no jobs available'); + } + + await jobManagerUtil.verifyJobCardDataIntegrity(0); + }); + + test('should verify all job cards have valid data', async ({ page }) => { + const jobManagerPage = new JobManagerPage(page); + const jobManagerUtil = new JobManagerUtil(page); + + const { isDisabled, jobCount } = await jobManagerUtil.setupTest(); + if (isDisabled || jobCount === 0) { + test.skip(true, 'Job Manager is disabled or no jobs available'); + } + + await jobManagerUtil.verifyAllJobCardsDataIntegrity(); + }); +}); diff --git a/zeppelin-web-angular/e2e/tests/jobmanager/status-legend.spec.ts b/zeppelin-web-angular/e2e/tests/jobmanager/status-legend.spec.ts new file mode 100644 index 00000000000..4cbab13b871 --- /dev/null +++ b/zeppelin-web-angular/e2e/tests/jobmanager/status-legend.spec.ts @@ -0,0 +1,145 @@ +/* + * 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 { JobManagerPage } from '../../models/job-manager-page'; +import { JobManagerUtil } from '../../models/job-manager-page.util'; +import { addPageAnnotationBeforeEach, performLoginIfRequired, PAGES } from '../../utils'; + +test.describe('Job Manager - Status Legend', () => { + addPageAnnotationBeforeEach(PAGES.WORKSPACE.JOB_STATUS); + + test.beforeEach(async ({ page }) => { + await performLoginIfRequired(page); + const jobManagerPage = new JobManagerPage(page); + await jobManagerPage.navigate(); + }); + + test('should display all job status badges in legend', async ({ page }) => { + const jobManagerPage = new JobManagerPage(page); + const jobManagerUtil = new JobManagerUtil(page); + + const { isDisabled } = await jobManagerUtil.setupTest(); + if (isDisabled) { + test.skip(true, 'Job Manager is disabled'); + } + + await jobManagerUtil.verifyStatusLegend(); + }); + + test('should render status badges with correct colors', async ({ page }) => { + const jobManagerPage = new JobManagerPage(page); + const jobManagerUtil = new JobManagerUtil(page); + + const { isDisabled } = await jobManagerUtil.setupTest(); + if (isDisabled) { + test.skip(true, 'Job Manager is disabled'); + } + + const statusLegendExists = await jobManagerPage.statusLegend.isVisible(); + if (!statusLegendExists) { + test.skip(true, 'Status legend not available'); + } + + await jobManagerUtil.verifyStatusBadgeColors(); + }); + + test('should display READY status with success badge', async ({ page }) => { + const jobManagerPage = new JobManagerPage(page); + const jobManagerUtil = new JobManagerUtil(page); + + const { isDisabled } = await jobManagerUtil.setupTest(); + if (isDisabled) { + test.skip(true, 'Job Manager is disabled'); + } + + const readyBadge = jobManagerPage.statusLegend + .locator('zeppelin-job-manager-job-status') + .filter({ hasText: 'READY' }); + await expect(readyBadge).toBeVisible(); + }); + + test('should display FINISHED status with success badge', async ({ page }) => { + const jobManagerPage = new JobManagerPage(page); + const jobManagerUtil = new JobManagerUtil(page); + + const { isDisabled } = await jobManagerUtil.setupTest(); + if (isDisabled) { + test.skip(true, 'Job Manager is disabled'); + } + + const finishedBadge = jobManagerPage.statusLegend + .locator('zeppelin-job-manager-job-status') + .filter({ hasText: 'FINISHED' }); + await expect(finishedBadge).toBeVisible(); + }); + + test('should display ERROR status with error badge', async ({ page }) => { + const jobManagerPage = new JobManagerPage(page); + const jobManagerUtil = new JobManagerUtil(page); + + const { isDisabled } = await jobManagerUtil.setupTest(); + if (isDisabled) { + test.skip(true, 'Job Manager is disabled'); + } + + const errorBadge = jobManagerPage.statusLegend + .locator('zeppelin-job-manager-job-status') + .filter({ hasText: 'ERROR' }); + await expect(errorBadge).toBeVisible(); + }); + + test('should display ABORT status with warning badge', async ({ page }) => { + const jobManagerPage = new JobManagerPage(page); + const jobManagerUtil = new JobManagerUtil(page); + + const { isDisabled } = await jobManagerUtil.setupTest(); + if (isDisabled) { + test.skip(true, 'Job Manager is disabled'); + } + + const abortBadge = jobManagerPage.statusLegend + .locator('zeppelin-job-manager-job-status') + .filter({ hasText: 'ABORT' }); + await expect(abortBadge).toBeVisible(); + }); + + test('should display RUNNING status with processing badge', async ({ page }) => { + const jobManagerPage = new JobManagerPage(page); + const jobManagerUtil = new JobManagerUtil(page); + + const { isDisabled } = await jobManagerUtil.setupTest(); + if (isDisabled) { + test.skip(true, 'Job Manager is disabled'); + } + + const runningBadge = jobManagerPage.statusLegend + .locator('zeppelin-job-manager-job-status') + .filter({ hasText: 'RUNNING' }); + await expect(runningBadge).toBeVisible(); + }); + + test('should display PENDING status with default badge', async ({ page }) => { + const jobManagerPage = new JobManagerPage(page); + const jobManagerUtil = new JobManagerUtil(page); + + const { isDisabled } = await jobManagerUtil.setupTest(); + if (isDisabled) { + test.skip(true, 'Job Manager is disabled'); + } + + const pendingBadge = jobManagerPage.statusLegend + .locator('zeppelin-job-manager-job-status') + .filter({ hasText: 'PENDING' }); + await expect(pendingBadge).toBeVisible(); + }); +}); diff --git a/zeppelin-web-angular/e2e/tests/jobmanager/system-states.spec.ts b/zeppelin-web-angular/e2e/tests/jobmanager/system-states.spec.ts new file mode 100644 index 00000000000..2ce77287c1f --- /dev/null +++ b/zeppelin-web-angular/e2e/tests/jobmanager/system-states.spec.ts @@ -0,0 +1,101 @@ +/* + * 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 { JobManagerPage } from '../../models/job-manager-page'; +import { JobManagerUtil } from '../../models/job-manager-page.util'; +import { addPageAnnotationBeforeEach, performLoginIfRequired, PAGES } from '../../utils'; + +test.describe('Job Manager - System States', () => { + addPageAnnotationBeforeEach(PAGES.WORKSPACE.JOB_MANAGER); + + test.beforeEach(async ({ page }) => { + await performLoginIfRequired(page); + }); + + test('should display page header correctly', async ({ page }) => { + const jobManagerPage = new JobManagerPage(page); + const jobManagerUtil = new JobManagerUtil(page); + + await jobManagerPage.navigate(); + await jobManagerUtil.verifyPageHeader(); + }); + + test('should handle disabled state when Job Manager is disabled', async ({ page }) => { + const jobManagerPage = new JobManagerPage(page); + const jobManagerUtil = new JobManagerUtil(page); + + await jobManagerPage.navigate(); + + const { isDisabled } = await jobManagerUtil.setupTest(); + if (isDisabled) { + await jobManagerUtil.verifyDisabledState(); + const message = await jobManagerPage.getDisabledMessage(); + expect(message).toContain('Job Manager is disabled in the current configuration'); + } + }); + + test('should display loading state during initialization', async ({ page }) => { + const jobManagerPage = new JobManagerPage(page); + + await page.goto('/#/jobmanager'); + + const skeletonExists = await jobManagerPage.loadingSkeleton.isVisible().catch(() => false); + if (skeletonExists) { + await expect(jobManagerPage.loadingSkeleton).toHaveAttribute('ng-reflect-nz-active', 'true'); + } + + await jobManagerPage.waitForPageLoad(); + }); + + test('should display success state with job list when enabled', async ({ page }) => { + const jobManagerPage = new JobManagerPage(page); + const jobManagerUtil = new JobManagerUtil(page); + + await jobManagerPage.navigate(); + + const { isDisabled } = await jobManagerUtil.setupTest(); + if (!isDisabled) { + await expect(jobManagerPage.pageHeader).toBeVisible(); + await expect(jobManagerPage.searchInput).toBeVisible(); + await expect(jobManagerPage.interpreterSelect).toBeVisible(); + await expect(jobManagerPage.sortSelect).toBeVisible(); + await expect(jobManagerPage.totalCounter).toBeVisible(); + await expect(jobManagerPage.statusLegend).toBeVisible(); + } else { + test.skip(isDisabled, 'Job Manager is disabled'); + } + }); + + test('should handle page refresh correctly', async ({ page }) => { + const jobManagerPage = new JobManagerPage(page); + const jobManagerUtil = new JobManagerUtil(page); + + await jobManagerPage.navigate(); + await jobManagerUtil.setupTest(); + + await page.reload(); + await jobManagerPage.waitForPageLoad(); + + await expect(jobManagerPage.pageHeader).toBeVisible(); + }); + + test('should handle navigation from other pages', async ({ page }) => { + const jobManagerPage = new JobManagerPage(page); + + await page.goto('/#/'); + await page.waitForLoadState('networkidle'); + + await jobManagerPage.navigate(); + await expect(jobManagerPage.pageHeader).toBeVisible(); + }); +});