Skip to content

Commit 451297b

Browse files
committed
Seperate environment config and shared utilities from #5101
1 parent 27be062 commit 451297b

File tree

11 files changed

+560
-67
lines changed

11 files changed

+560
-67
lines changed

.github/workflows/frontend.yml

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ env:
2424
SPARK_LOCAL_IP: 127.0.0.1
2525
ZEPPELIN_LOCAL_IP: 127.0.0.1
2626
INTERPRETERS: '!hbase,!jdbc,!file,!flink,!cassandra,!elasticsearch,!bigquery,!alluxio,!livy,!groovy,!java,!neo4j,!sparql,!mongodb'
27+
ZEPPELIN_E2E_TEST_NOTEBOOK_DIR: '/tmp/zeppelin-e2e-notebooks'
2728

2829
permissions:
2930
contents: read # to fetch code (actions/checkout)
@@ -62,9 +63,13 @@ jobs:
6263

6364
run-playwright-e2e-tests:
6465
runs-on: ubuntu-24.04
66+
env:
67+
# Use VFS storage instead of Git to avoid Git-related issues in CI
68+
ZEPPELIN_NOTEBOOK_STORAGE: org.apache.zeppelin.notebook.repo.VFSNotebookRepo
6569
strategy:
6670
matrix:
6771
mode: [anonymous, auth]
72+
python: [ 3.9 ]
6873
steps:
6974
- name: Checkout
7075
uses: actions/checkout@v4
@@ -93,15 +98,29 @@ jobs:
9398
key: ${{ runner.os }}-zeppelin-${{ hashFiles('**/pom.xml') }}
9499
restore-keys: |
95100
${{ runner.os }}-zeppelin-
101+
- name: Setup conda environment with python ${{ matrix.python }}
102+
uses: conda-incubator/setup-miniconda@v3
103+
with:
104+
activate-environment: python_only
105+
python-version: ${{ matrix.python }}
106+
auto-activate-base: false
107+
use-mamba: true
108+
channels: conda-forge,defaults
109+
channel-priority: strict
96110
- name: Install application
97-
run: ./mvnw clean install -DskipTests -am -pl zeppelin-web-angular ${MAVEN_ARGS}
111+
run: ./mvnw clean install -DskipTests -am -pl python,rlang,zeppelin-jupyter-interpreter,zeppelin-web-angular ${MAVEN_ARGS}
98112
- name: Setup Zeppelin Server (Shiro.ini)
99113
run: |
100114
export ZEPPELIN_CONF_DIR=./conf
101115
if [ "${{ matrix.mode }}" != "anonymous" ]; then
102116
cp conf/shiro.ini.template conf/shiro.ini
103117
sed -i 's/user1 = password2, role1, role2/user1 = password2, role1, role2, admin/' conf/shiro.ini
104118
fi
119+
- name: Setup Test Notebook Directory
120+
run: |
121+
# NOTE: Must match zeppelin.notebook.dir defined in pom.xml
122+
mkdir -p $ZEPPELIN_E2E_TEST_NOTEBOOK_DIR
123+
echo "Created test notebook directory: $ZEPPELIN_E2E_TEST_NOTEBOOK_DIR"
105124
- name: Run headless E2E test with Maven
106125
run: xvfb-run --auto-servernum --server-args="-screen 0 1024x768x24" ./mvnw verify -pl zeppelin-web-angular -Pweb-e2e ${MAVEN_ARGS}
107126
- name: Upload Playwright Report
@@ -110,10 +129,20 @@ jobs:
110129
with:
111130
name: playwright-report-${{ matrix.mode }}
112131
path: zeppelin-web-angular/playwright-report/
113-
retention-days: 30
132+
retention-days: 3
114133
- name: Print Zeppelin logs
115134
if: always()
116135
run: if [ -d "logs" ]; then cat logs/*; fi
136+
- name: Cleanup Test Notebook Directory
137+
if: always()
138+
run: |
139+
if [ -d "$ZEPPELIN_E2E_TEST_NOTEBOOK_DIR" ]; then
140+
echo "Cleaning up test notebook directory: $ZEPPELIN_E2E_TEST_NOTEBOOK_DIR"
141+
rm -rf $ZEPPELIN_E2E_TEST_NOTEBOOK_DIR
142+
echo "Test notebook directory cleaned up"
143+
else
144+
echo "No test notebook directory to clean up"
145+
fi
117146
118147
test-selenium-with-spark-module-for-spark-3-5:
119148
runs-on: ubuntu-24.04
@@ -162,7 +191,7 @@ jobs:
162191
- name: Setup conda environment with python 3.9 and R
163192
uses: conda-incubator/setup-miniconda@v3
164193
with:
165-
activate-environment: python_3_with_R
194+
activate-environment: python_only
166195
environment-file: testing/env_python_3_with_R.yml
167196
python-version: 3.9
168197
channels: conda-forge,defaults

package-lock.json

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

zeppelin-web-angular/.eslintrc.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,17 @@
119119
"yoda": "error"
120120
}
121121
},
122+
{
123+
"files": ["*.js"],
124+
"parserOptions": {
125+
"ecmaVersion": "latest",
126+
"sourceType": "module"
127+
},
128+
"env": {
129+
"node": true,
130+
"es6": true
131+
}
132+
},
122133
{
123134
"files": ["*.html"],
124135
"extends": ["plugin:@angular-eslint/template/recommended"],
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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 { BASE_URL, E2E_TEST_FOLDER } from './models/base-page';
14+
15+
export const cleanupTestNotebooks = async () => {
16+
try {
17+
console.log('Cleaning up test folder via API...');
18+
19+
// Get all notebooks and folders
20+
const response = await fetch(`${BASE_URL}/api/notebook`);
21+
const data = await response.json();
22+
if (!data.body || !Array.isArray(data.body)) {
23+
console.log('No notebooks found or invalid response format');
24+
return;
25+
}
26+
27+
// Find the test folders (E2E_TEST_FOLDER, TestFolder_, and TestFolderRenamed_ patterns)
28+
const testFolders = data.body.filter((item: { path: string }) => {
29+
if (!item.path || item.path.includes(`~Trash`)) {
30+
return false;
31+
}
32+
const folderName = item.path.split('/')[1];
33+
return (
34+
folderName === E2E_TEST_FOLDER ||
35+
folderName?.startsWith('TestFolder_') ||
36+
folderName?.startsWith('TestFolderRenamed_')
37+
);
38+
});
39+
40+
if (testFolders.length === 0) {
41+
console.log('No test folder found to clean up');
42+
return;
43+
}
44+
45+
await Promise.all(
46+
testFolders.map(async (testFolder: { id: string; path: string }) => {
47+
try {
48+
console.log(`Deleting test folder: ${testFolder.id} (${testFolder.path})`);
49+
50+
const deleteResponse = await fetch(`${BASE_URL}/api/notebook/${testFolder.id}`, {
51+
method: 'DELETE'
52+
});
53+
54+
// Although a 500 status code is generally not considered a successful response,
55+
// this API returns 500 even when the operation actually succeeds.
56+
// I'll investigate this further and create an issue.
57+
if (deleteResponse.status === 200 || deleteResponse.status === 500) {
58+
console.log(`Deleted test folder: ${testFolder.path}`);
59+
} else {
60+
console.warn(`Failed to delete test folder ${testFolder.path}: ${deleteResponse.status}`);
61+
}
62+
} catch (error) {
63+
console.error(`Error deleting test folder ${testFolder.path}:`, error);
64+
}
65+
})
66+
);
67+
68+
console.log('Test folder cleanup completed');
69+
} catch (error) {
70+
if (error instanceof Error && error.message.includes('ECONNREFUSED')) {
71+
console.error('Failed to connect to local server. Please start the frontend server first:');
72+
console.error(' npm start');
73+
console.error(` or make sure ${BASE_URL} is running`);
74+
} else {
75+
console.warn('Failed to cleanup test folder:', error);
76+
}
77+
}
78+
};
79+
80+
if (require.main === module) {
81+
cleanupTestNotebooks()
82+
.then(() => {
83+
console.log('Cleanup completed successfully');
84+
process.exit(0);
85+
})
86+
.catch(error => {
87+
console.error('Cleanup failed:', error);
88+
process.exit(1);
89+
});
90+
}

zeppelin-web-angular/e2e/global-setup.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,19 @@
1010
* limitations under the License.
1111
*/
1212

13+
import * as fs from 'fs';
1314
import { LoginTestUtil } from './models/login-page.util';
1415

1516
async function globalSetup() {
16-
console.log('🔧 Global Setup: Checking Shiro configuration...');
17+
console.log('Global Setup: Preparing test environment...');
1718

1819
// Reset cache to ensure fresh check
1920
LoginTestUtil.resetCache();
2021

22+
// Set up test notebook directory if specified
23+
await setupTestNotebookDirectory();
24+
25+
// Check Shiro configuration
2126
const isShiroEnabled = await LoginTestUtil.isShiroEnabled();
2227

2328
if (isShiroEnabled) {
@@ -33,4 +38,23 @@ async function globalSetup() {
3338
}
3439
}
3540

41+
async function setupTestNotebookDirectory(): Promise<void> {
42+
const testNotebookDir = process.env.ZEPPELIN_E2E_TEST_NOTEBOOK_DIR;
43+
44+
if (!testNotebookDir) {
45+
console.log('No custom test notebook directory configured');
46+
return;
47+
}
48+
49+
console.log(`Setting up test notebook directory: ${testNotebookDir}`);
50+
51+
// Remove existing directory if it exists, then create fresh
52+
if (fs.existsSync(testNotebookDir)) {
53+
await fs.promises.rmdir(testNotebookDir, { recursive: true });
54+
}
55+
56+
fs.mkdirSync(testNotebookDir, { recursive: true });
57+
fs.chmodSync(testNotebookDir, 0o777);
58+
}
59+
3660
export default globalSetup;

zeppelin-web-angular/e2e/global-teardown.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,37 @@
1010
* limitations under the License.
1111
*/
1212

13+
import { exec } from 'child_process';
14+
import { promisify } from 'util';
1315
import { LoginTestUtil } from './models/login-page.util';
1416

15-
async function globalTeardown() {
16-
console.log('🧹 Global Teardown: Cleaning up test environment...');
17+
const execAsync = promisify(exec);
18+
19+
const globalTeardown = async () => {
20+
console.log('Global Teardown: Cleaning up test environment...');
1721

1822
LoginTestUtil.resetCache();
19-
console.log('✅ Test cache cleared');
20-
}
23+
console.log('Test cache cleared');
24+
25+
// CI: Uses ZEPPELIN_E2E_TEST_NOTEBOOK_DIR which gets cleaned up by workflow
26+
// Local: Uses API-based cleanup to avoid server restart required for directory changes
27+
if (!process.env.CI) {
28+
console.log('Running cleanup script: npx tsx e2e/cleanup-util.ts');
29+
30+
try {
31+
// The reason for calling it this way instead of using the function directly
32+
// is to maintain compatibility between ESM and CommonJS modules.
33+
const { stdout, stderr } = await execAsync('npx tsx e2e/cleanup-util.ts');
34+
if (stdout) {
35+
console.log(stdout);
36+
}
37+
if (stderr) {
38+
console.error(stderr);
39+
}
40+
} catch (error) {
41+
console.error('Cleanup script failed:', error);
42+
}
43+
}
44+
};
2145

2246
export default globalTeardown;

zeppelin-web-angular/e2e/models/base-page.ts

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,78 @@
1212

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

15+
export const E2E_TEST_FOLDER = 'E2E_TEST_FOLDER';
16+
export const BASE_URL = 'http://localhost:4200';
17+
1518
export class BasePage {
1619
readonly page: Page;
17-
readonly loadingScreen: Locator;
20+
21+
// Common Zeppelin component locators
22+
readonly zeppelinNodeList: Locator;
23+
readonly zeppelinWorkspace: Locator;
24+
readonly zeppelinHeader: Locator;
25+
readonly zeppelinPageHeader: Locator;
1826

1927
constructor(page: Page) {
2028
this.page = page;
21-
this.loadingScreen = page.locator('.spin-text');
29+
this.zeppelinNodeList = page.locator('zeppelin-node-list');
30+
this.zeppelinWorkspace = page.locator('zeppelin-workspace');
31+
this.zeppelinHeader = page.locator('zeppelin-header');
32+
this.zeppelinPageHeader = page.locator('zeppelin-page-header');
2233
}
2334

2435
async waitForPageLoad(): Promise<void> {
25-
await this.page.waitForLoadState('domcontentloaded');
26-
try {
27-
await this.loadingScreen.waitFor({ state: 'hidden', timeout: 5000 });
28-
} catch {
29-
console.log('Loading screen not found');
30-
}
36+
await this.page.waitForLoadState('domcontentloaded', { timeout: 15000 });
37+
}
38+
39+
// Common navigation patterns
40+
async navigateToRoute(
41+
route: string,
42+
options?: { timeout?: number; waitUntil?: 'load' | 'domcontentloaded' | 'networkidle' }
43+
): Promise<void> {
44+
await this.page.goto(`/#${route}`, {
45+
waitUntil: 'domcontentloaded',
46+
timeout: 60000,
47+
...options
48+
});
49+
await this.waitForPageLoad();
50+
}
51+
52+
async navigateToHome(): Promise<void> {
53+
await this.navigateToRoute('/');
54+
}
55+
56+
getCurrentPath(): string {
57+
const url = new URL(this.page.url());
58+
return url.hash || url.pathname;
59+
}
60+
61+
async waitForUrlNotContaining(fragment: string): Promise<void> {
62+
await this.page.waitForURL(url => !url.toString().includes(fragment));
63+
}
64+
65+
// Common form interaction patterns
66+
async fillInput(locator: Locator, value: string, options?: { timeout?: number; force?: boolean }): Promise<void> {
67+
await locator.fill(value, { timeout: 15000, ...options });
68+
}
69+
70+
async clickElement(locator: Locator, options?: { timeout?: number; force?: boolean }): Promise<void> {
71+
await locator.click({ timeout: 15000, ...options });
72+
}
73+
74+
async getInputValue(locator: Locator): Promise<string> {
75+
return await locator.inputValue();
76+
}
77+
78+
async isElementVisible(locator: Locator): Promise<boolean> {
79+
return await locator.isVisible();
80+
}
81+
82+
async isElementEnabled(locator: Locator): Promise<boolean> {
83+
return await locator.isEnabled();
84+
}
85+
86+
async getElementText(locator: Locator): Promise<string> {
87+
return (await locator.textContent()) || '';
3188
}
3289
}

0 commit comments

Comments
 (0)