This is an automated email from the ASF dual-hosted git repository.
chanholee pushed a commit to branch branch-0.12
in repository https://gitbox.apache.org/repos/asf/zeppelin.git
The following commit(s) were added to refs/heads/branch-0.12 by this push:
new 2affc612bd [ZEPPELIN-6357] Fixed missing login prerequisite in each
test
2affc612bd is described below
commit 2affc612bd79364a45a631fb151aed5346fc9c1c
Author: YONGJAE LEE(이용재) <[email protected]>
AuthorDate: Mon Oct 6 10:44:58 2025 +0900
[ZEPPELIN-6357] Fixed missing login prerequisite in each test
### What is this PR for?
https://github.com/apache/zeppelin/actions/runs/18239525409/job/51939323284?pr=5095
I recently found that the `run-playwright-e2e-tests (auth)` step wasn’t
running properly during the New UI's E2E tests.
The root cause was that the tests required a setup (tear-up) process.
Since handling login in `globalSetup` within `playwright.config` made it
difficult to account for all browser contexts,
I added a
method([performLoginIfRequired](https://github.com/apache/zeppelin/pull/5096/files#diff-10918824923e3beebfd460fdab4db4ee063a0f1ee10c49bb5829e459e3af31f3R164-R196))
in `utils` to perform the login when a `shiro.ini` file exists, and made each
test execute it in `beforeEach`.
During this process, I also merged the `helper` into `utils`, as they
essentially served the same purpose.
### What type of PR is it?
Bug Fix
Improvement
### Todos
### What is the Jira issue?
ZEPPELIN-6357
### 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 #5096 from dididy/fix/login-tearup.
Signed-off-by: ChanHo Lee <[email protected]>
(cherry picked from commit 89aecde0ed381d5ff6c68ed299c1562e675445e5)
Signed-off-by: ChanHo Lee <[email protected]>
---
zeppelin-web-angular/e2e/helper.ts | 37 --------------
zeppelin-web-angular/e2e/models/login-page.util.ts | 4 +-
zeppelin-web-angular/e2e/tests/app.spec.ts | 19 +++-----
.../anonymous-login-redirect.spec.ts | 55 ++++++++++++++-------
.../e2e/tests/theme/dark-mode.spec.ts | 27 +++++-----
zeppelin-web-angular/e2e/utils.ts | 57 +++++++++++++++++++++-
6 files changed, 118 insertions(+), 81 deletions(-)
diff --git a/zeppelin-web-angular/e2e/helper.ts
b/zeppelin-web-angular/e2e/helper.ts
deleted file mode 100644
index ef971e7a92..0000000000
--- a/zeppelin-web-angular/e2e/helper.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * 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 { Page } from '@playwright/test';
-
-export class ZeppelinHelper {
- constructor(private page: Page) {}
-
- async waitForZeppelinReady(): Promise<void> {
- try {
- await this.page.waitForLoadState('networkidle', { timeout: 30000 });
- await this.page.waitForFunction(
- () => {
- const hasAngular = document.querySelector('[ng-version]') !== null;
- const hasZeppelinContent =
- document.body.textContent?.includes('Zeppelin') ||
- document.body.textContent?.includes('Notebook') ||
- document.body.textContent?.includes('Welcome');
- const hasZeppelinRoot = document.querySelector('zeppelin-root') !==
null;
- return hasAngular && (hasZeppelinContent || hasZeppelinRoot);
- },
- { timeout: 60 * 1000 }
- );
- } catch (error) {
- throw error instanceof Error ? error : new Error(`Zeppelin loading
failed: ${String(error)}`);
- }
- }
-}
diff --git a/zeppelin-web-angular/e2e/models/login-page.util.ts
b/zeppelin-web-angular/e2e/models/login-page.util.ts
index 48ce3053d3..e5cce7fee5 100644
--- a/zeppelin-web-angular/e2e/models/login-page.util.ts
+++ b/zeppelin-web-angular/e2e/models/login-page.util.ts
@@ -98,7 +98,9 @@ export class LoginTestUtil {
private static _parseUserLine(line: string, users: Record<string,
TestCredentials>): void {
const [userPart, ...roleParts] = line.split('=');
- if (!userPart || roleParts.length === 0) return;
+ if (!userPart || roleParts.length === 0) {
+ return;
+ }
const username = userPart.trim();
const rightSide = roleParts.join('=').trim();
diff --git a/zeppelin-web-angular/e2e/tests/app.spec.ts
b/zeppelin-web-angular/e2e/tests/app.spec.ts
index 8baa40123a..9231eca79e 100644
--- a/zeppelin-web-angular/e2e/tests/app.spec.ts
+++ b/zeppelin-web-angular/e2e/tests/app.spec.ts
@@ -11,18 +11,15 @@
*/
import { expect, test } from '@playwright/test';
-import { ZeppelinHelper } from '../helper';
import { BasePage } from '../models/base-page';
import { LoginTestUtil } from '../models/login-page.util';
-import { addPageAnnotationBeforeEach, PAGES } from '../utils';
+import { addPageAnnotationBeforeEach, waitForZeppelinReady, PAGES } from
'../utils';
test.describe('Zeppelin App Component', () => {
addPageAnnotationBeforeEach(PAGES.APP);
- let zeppelinHelper: ZeppelinHelper;
let basePage: BasePage;
test.beforeEach(async ({ page }) => {
- zeppelinHelper = new ZeppelinHelper(page);
basePage = new BasePage(page);
await page.goto('/', { waitUntil: 'load' });
@@ -56,7 +53,7 @@ test.describe('Zeppelin App Component', () => {
});
test('should display workspace after loading', async ({ page }) => {
- await zeppelinHelper.waitForZeppelinReady();
+ await waitForZeppelinReady(page);
const isShiroEnabled = await LoginTestUtil.isShiroEnabled();
if (isShiroEnabled) {
await expect(page.locator('zeppelin-login')).toBeVisible();
@@ -66,7 +63,7 @@ test.describe('Zeppelin App Component', () => {
});
test('should handle navigation events correctly', async ({ page }) => {
- await zeppelinHelper.waitForZeppelinReady();
+ await waitForZeppelinReady(page);
// Test navigation back to root path
try {
@@ -79,7 +76,7 @@ test.describe('Zeppelin App Component', () => {
const spinnerCount = await loadingSpinner.count();
expect(spinnerCount).toBeGreaterThanOrEqual(0);
- await zeppelinHelper.waitForZeppelinReady();
+ await waitForZeppelinReady(page);
// After ready, loading should be hidden if it was visible
if (await loadingSpinner.isVisible()) {
@@ -108,12 +105,12 @@ test.describe('Zeppelin App Component', () => {
}
// Wait for loading to complete
- await zeppelinHelper.waitForZeppelinReady();
+ await waitForZeppelinReady(page);
await expect(loadingSpinner).toBeHidden();
});
test('should handle logout observable correctly', async ({ page }) => {
- await zeppelinHelper.waitForZeppelinReady();
+ await waitForZeppelinReady(page);
const logoutSpinner = page.locator('zeppelin-spin').filter({ hasText:
'Logging out' });
@@ -142,7 +139,7 @@ test.describe('Zeppelin App Component', () => {
});
test('should maintain component integrity during navigation', async ({ page
}) => {
- await zeppelinHelper.waitForZeppelinReady();
+ await waitForZeppelinReady(page);
const zeppelinRoot = page.locator('zeppelin-root');
@@ -160,7 +157,7 @@ test.describe('Zeppelin App Component', () => {
const routerOutlet = zeppelinRoot.locator('router-outlet');
await expect(routerOutlet).toBeAttached();
- await zeppelinHelper.waitForZeppelinReady();
+ await waitForZeppelinReady(page);
} catch (error) {
// Skip paths that don't exist or are not accessible
console.log(`Skipping path ${path}: ${error}`);
diff --git
a/zeppelin-web-angular/e2e/tests/authentication/anonymous-login-redirect.spec.ts
b/zeppelin-web-angular/e2e/tests/authentication/anonymous-login-redirect.spec.ts
index 34b9f498f0..ce66ce2f74 100644
---
a/zeppelin-web-angular/e2e/tests/authentication/anonymous-login-redirect.spec.ts
+++
b/zeppelin-web-angular/e2e/tests/authentication/anonymous-login-redirect.spec.ts
@@ -11,25 +11,36 @@
*/
import { expect, test } from '@playwright/test';
-import { ZeppelinHelper } from '../../helper';
import { HomePageUtil } from '../../models/home-page.util';
-import { addPageAnnotationBeforeEach, PAGES, waitForUrlNotContaining,
getCurrentPath } from '../../utils';
+import { LoginTestUtil } from '../../models/login-page.util';
+import {
+ addPageAnnotationBeforeEach,
+ getCurrentPath,
+ waitForUrlNotContaining,
+ waitForZeppelinReady,
+ PAGES
+} from '../../utils';
test.describe('Anonymous User Login Redirect', () => {
addPageAnnotationBeforeEach(PAGES.WORKSPACE.HOME);
- let zeppelinHelper: ZeppelinHelper;
let homePageUtil: HomePageUtil;
+ test.beforeAll(async () => {
+ const isShiroEnabled = await LoginTestUtil.isShiroEnabled();
+ if (isShiroEnabled) {
+ test.skip(true, 'Skipping anonymous login redirect tests -
authentication is enabled (shiro.ini found)');
+ }
+ });
+
test.beforeEach(async ({ page }) => {
- zeppelinHelper = new ZeppelinHelper(page);
homePageUtil = new HomePageUtil(page);
});
test.describe('Given an anonymous user is already logged in', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/', { waitUntil: 'load' });
- await zeppelinHelper.waitForZeppelinReady();
+ await waitForZeppelinReady(page);
});
test('When accessing login page directly, Then should redirect to home
with proper URL change', async ({
@@ -48,7 +59,7 @@ test.describe('Anonymous User Login Redirect', () => {
page
}) => {
await page.goto('/#/login', { waitUntil: 'load' });
- await zeppelinHelper.waitForZeppelinReady();
+ await waitForZeppelinReady(page);
await page.waitForURL(url => !url.toString().includes('#/login'));
await homePageUtil.verifyHomePageIntegrity();
@@ -56,7 +67,7 @@ test.describe('Anonymous User Login Redirect', () => {
test('When clicking Zeppelin logo after redirect, Then should maintain
home URL and content', async ({ page }) => {
await page.goto('/#/login', { waitUntil: 'load' });
- await zeppelinHelper.waitForZeppelinReady();
+ await waitForZeppelinReady(page);
await page.waitForURL(url => !url.toString().includes('#/login'));
const navigationResult = await homePageUtil.testNavigationConsistency();
@@ -69,7 +80,7 @@ test.describe('Anonymous User Login Redirect', () => {
test('When accessing login page, Then should redirect and maintain
anonymous user state', async ({ page }) => {
await page.goto('/#/login', { waitUntil: 'load' });
- await zeppelinHelper.waitForZeppelinReady();
+ await waitForZeppelinReady(page);
await page.waitForURL(url => !url.toString().includes('#/login'));
const metadata = await homePageUtil.getPageMetadata();
@@ -82,7 +93,7 @@ test.describe('Anonymous User Login Redirect', () => {
test('When accessing login page, Then should display welcome heading and
main sections', async ({ page }) => {
await page.goto('/#/login', { waitUntil: 'load' });
- await zeppelinHelper.waitForZeppelinReady();
+ await waitForZeppelinReady(page);
await page.waitForURL(url => !url.toString().includes('#/login'));
await expect(page.locator('h1', { hasText: 'Welcome to Zeppelin!'
})).toBeVisible();
@@ -93,7 +104,7 @@ test.describe('Anonymous User Login Redirect', () => {
test('When accessing login page, Then should display notebook
functionalities', async ({ page }) => {
await page.goto('/#/login', { waitUntil: 'load' });
- await zeppelinHelper.waitForZeppelinReady();
+ await waitForZeppelinReady(page);
await page.waitForURL(url => !url.toString().includes('#/login'));
await expect(page.locator('text=Create new Note')).toBeVisible();
@@ -109,7 +120,7 @@ test.describe('Anonymous User Login Redirect', () => {
page
}) => {
await page.goto('/#/login', { waitUntil: 'load' });
- await zeppelinHelper.waitForZeppelinReady();
+ await waitForZeppelinReady(page);
await page.waitForURL(url => !url.toString().includes('#/login'));
const docLinks = page.locator('a[href*="zeppelin.apache.org/docs"]');
@@ -117,24 +128,32 @@ test.describe('Anonymous User Login Redirect', () => {
const issuesLinks = page.locator('a[href*="issues.apache.org"]');
const githubLinks =
page.locator('a[href*="github.com/apache/zeppelin"]');
- if ((await docLinks.count()) > 0) await expect(docLinks).toBeVisible();
- if ((await communityLinks.count()) > 0) await
expect(communityLinks).toBeVisible();
- if ((await issuesLinks.count()) > 0) await
expect(issuesLinks).toBeVisible();
- if ((await githubLinks.count()) > 0) await
expect(githubLinks).toBeVisible();
+ if ((await docLinks.count()) > 0) {
+ await expect(docLinks).toBeVisible();
+ }
+ if ((await communityLinks.count()) > 0) {
+ await expect(communityLinks).toBeVisible();
+ }
+ if ((await issuesLinks.count()) > 0) {
+ await expect(issuesLinks).toBeVisible();
+ }
+ if ((await githubLinks.count()) > 0) {
+ await expect(githubLinks).toBeVisible();
+ }
});
test('When navigating between home and login URLs, Then should maintain
consistent user experience', async ({
page
}) => {
await page.goto('/', { waitUntil: 'load' });
- await zeppelinHelper.waitForZeppelinReady();
+ await waitForZeppelinReady(page);
const homeMetadata = await homePageUtil.getPageMetadata();
expect(homeMetadata.path).toContain('#/');
expect(homeMetadata.isAnonymous).toBe(true);
await page.goto('/#/login', { waitUntil: 'load' });
- await zeppelinHelper.waitForZeppelinReady();
+ await waitForZeppelinReady(page);
await page.waitForURL(url => !url.toString().includes('#/login'));
const loginMetadata = await homePageUtil.getPageMetadata();
@@ -149,7 +168,7 @@ test.describe('Anonymous User Login Redirect', () => {
test('When multiple page loads occur on login URL, Then should
consistently redirect to home', async ({ page }) => {
for (let i = 0; i < 3; i++) {
await page.goto('/#/login', { waitUntil: 'load' });
- await zeppelinHelper.waitForZeppelinReady();
+ await waitForZeppelinReady(page);
await waitForUrlNotContaining(page, '#/login');
await expect(page.locator('h1', { hasText: 'Welcome to Zeppelin!'
})).toBeVisible();
diff --git a/zeppelin-web-angular/e2e/tests/theme/dark-mode.spec.ts
b/zeppelin-web-angular/e2e/tests/theme/dark-mode.spec.ts
index 4dedf66218..9b8423dacd 100644
--- a/zeppelin-web-angular/e2e/tests/theme/dark-mode.spec.ts
+++ b/zeppelin-web-angular/e2e/tests/theme/dark-mode.spec.ts
@@ -11,20 +11,21 @@
*/
import { expect, test } from '@playwright/test';
-import { ZeppelinHelper } from '../../helper';
import { ThemePage } from '../../models/theme.page';
-import { addPageAnnotationBeforeEach, PAGES } from '../../utils';
+import { addPageAnnotationBeforeEach, performLoginIfRequired,
waitForZeppelinReady, PAGES } from '../../utils';
test.describe('Dark Mode Theme Switching', () => {
addPageAnnotationBeforeEach(PAGES.SHARE.THEME_TOGGLE);
- let zeppelinHelper: ZeppelinHelper;
let themePage: ThemePage;
test.beforeEach(async ({ page }) => {
- zeppelinHelper = new ZeppelinHelper(page);
themePage = new ThemePage(page);
await page.goto('/', { waitUntil: 'load' });
- await zeppelinHelper.waitForZeppelinReady();
+ await waitForZeppelinReady(page);
+
+ // Handle authentication if shiro.ini exists
+ await performLoginIfRequired(page);
+
// Ensure a clean localStorage for each test
await themePage.clearLocalStorage();
});
@@ -39,7 +40,7 @@ test.describe('Dark Mode Theme Switching', () => {
await test.step('WHEN the user explicitly sets theme to light mode', async
() => {
await themePage.setThemeInLocalStorage('light');
await page.reload();
- await zeppelinHelper.waitForZeppelinReady();
+ await waitForZeppelinReady(page);
await themePage.assertLightTheme(); // Now it should be light mode with
sun icon
});
@@ -47,7 +48,7 @@ test.describe('Dark Mode Theme Switching', () => {
await test.step('WHEN the user switches to dark mode', async () => {
await themePage.setThemeInLocalStorage('dark');
await page.reload();
- await zeppelinHelper.waitForZeppelinReady();
+ await waitForZeppelinReady(page);
});
// THEN: The theme changes to dark mode.
@@ -58,7 +59,7 @@ test.describe('Dark Mode Theme Switching', () => {
// AND: User refreshes the page.
await test.step('AND the user refreshes the page', async () => {
await page.reload();
- await zeppelinHelper.waitForZeppelinReady();
+ await waitForZeppelinReady(page);
});
// THEN: Dark mode is maintained after refresh.
@@ -84,7 +85,7 @@ test.describe('Dark Mode Theme Switching', () => {
await test.step('GIVEN: No localStorage, System preference is Light',
async () => {
await page.emulateMedia({ colorScheme: 'light' });
await page.goto('/', { waitUntil: 'load' });
- await zeppelinHelper.waitForZeppelinReady();
+ await waitForZeppelinReady(page);
// When no explicit theme is set, it defaults to 'system' mode
// Even in system mode with light preference, the icon should be robot
await expect(themePage.rootElement).toHaveClass(/light/);
@@ -95,7 +96,7 @@ test.describe('Dark Mode Theme Switching', () => {
await test.step('GIVEN: No localStorage, System preference is Dark
(initial system state)', async () => {
await themePage.setThemeInLocalStorage('system');
await page.goto('/', { waitUntil: 'load' });
- await zeppelinHelper.waitForZeppelinReady();
+ await waitForZeppelinReady(page);
await themePage.assertSystemTheme(); // Robot icon for system theme
});
@@ -103,7 +104,7 @@ test.describe('Dark Mode Theme Switching', () => {
await themePage.setThemeInLocalStorage('dark');
await page.emulateMedia({ colorScheme: 'light' });
await page.goto('/', { waitUntil: 'load' });
- await zeppelinHelper.waitForZeppelinReady();
+ await waitForZeppelinReady(page);
await themePage.assertDarkTheme(); // localStorage should override system
});
@@ -111,7 +112,7 @@ test.describe('Dark Mode Theme Switching', () => {
await themePage.setThemeInLocalStorage('system');
await page.emulateMedia({ colorScheme: 'light' });
await page.goto('/', { waitUntil: 'load' });
- await zeppelinHelper.waitForZeppelinReady();
+ await waitForZeppelinReady(page);
await expect(themePage.rootElement).toHaveClass(/light/);
await expect(themePage.rootElement).toHaveAttribute('data-theme',
'light');
await themePage.assertSystemTheme(); // Robot icon for system theme
@@ -121,7 +122,7 @@ test.describe('Dark Mode Theme Switching', () => {
await themePage.setThemeInLocalStorage('system');
await page.emulateMedia({ colorScheme: 'dark' });
await page.goto('/', { waitUntil: 'load' });
- await zeppelinHelper.waitForZeppelinReady();
+ await waitForZeppelinReady(page);
await expect(themePage.rootElement).toHaveClass(/dark/);
await expect(themePage.rootElement).toHaveAttribute('data-theme',
'dark');
await themePage.assertSystemTheme(); // Robot icon for system theme
diff --git a/zeppelin-web-angular/e2e/utils.ts
b/zeppelin-web-angular/e2e/utils.ts
index 652cedc53f..9f6193d083 100644
--- a/zeppelin-web-angular/e2e/utils.ts
+++ b/zeppelin-web-angular/e2e/utils.ts
@@ -10,7 +10,8 @@
* limitations under the License.
*/
-import { test, TestInfo, Page } from '@playwright/test';
+import { test, Page, TestInfo } from '@playwright/test';
+import { LoginTestUtil } from './models/login-page.util';
export const PAGES = {
// Main App
@@ -159,3 +160,57 @@ export async function getBasicPageMetadata(
path: getCurrentPath(page)
};
}
+
+export async function performLoginIfRequired(page: Page): Promise<boolean> {
+ const isShiroEnabled = await LoginTestUtil.isShiroEnabled();
+ if (!isShiroEnabled) {
+ return false;
+ }
+
+ const credentials = await LoginTestUtil.getTestCredentials();
+ const validUsers = Object.values(credentials).filter(
+ cred => cred.username && cred.password && cred.username !== 'INVALID_USER'
&& cred.username !== 'EMPTY_CREDENTIALS'
+ );
+
+ if (validUsers.length === 0) {
+ return false;
+ }
+
+ const testUser = validUsers[0];
+
+ const isLoginVisible = await page.locator('zeppelin-login').isVisible();
+ if (isLoginVisible) {
+ const userNameInput = page.getByRole('textbox', { name: 'User Name' });
+ const passwordInput = page.getByRole('textbox', { name: 'Password' });
+ const loginButton = page.getByRole('button', { name: 'Login' });
+
+ await userNameInput.fill(testUser.username);
+ await passwordInput.fill(testUser.password);
+ await loginButton.click();
+
+ await page.waitForSelector('zeppelin-workspace', { timeout: 10000 });
+ return true;
+ }
+
+ return false;
+}
+
+export async function waitForZeppelinReady(page: Page): Promise<void> {
+ try {
+ await page.waitForLoadState('networkidle', { timeout: 30000 });
+ await page.waitForFunction(
+ () => {
+ const hasAngular = document.querySelector('[ng-version]') !== null;
+ const hasZeppelinContent =
+ document.body.textContent?.includes('Zeppelin') ||
+ document.body.textContent?.includes('Notebook') ||
+ document.body.textContent?.includes('Welcome');
+ const hasZeppelinRoot = document.querySelector('zeppelin-root') !==
null;
+ return hasAngular && (hasZeppelinContent || hasZeppelinRoot);
+ },
+ { timeout: 60 * 1000 }
+ );
+ } catch (error) {
+ throw error instanceof Error ? error : new Error(`Zeppelin loading failed:
${String(error)}`);
+ }
+}