This is an automated email from the ASF dual-hosted git repository.
benjobs pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/incubator-streampark.git
The following commit(s) were added to refs/heads/dev by this push:
new 4729015c6 [Feature-3740] Add end-to-end test framework and User
Management and Team Management test cases (#3741)
4729015c6 is described below
commit 4729015c67fd0f0c4e43ede6705c5301fe60e078
Author: xiangzihao <[email protected]>
AuthorDate: Wed Jun 12 21:10:41 2024 +0800
[Feature-3740] Add end-to-end test framework and User Management and Team
Management test cases (#3741)
* Add end-to-end test framework and User Management and Team Management
test cases
* add readme.md
* fix flaky e2e test
---
.editorconfig | 5 +
.github/workflows/e2e.yml | 137 +++++++++++++
.../src/views/base/login/LoginForm.vue | 1 +
.../src/views/base/login/teamModal.vue | 1 +
streampark-e2e/README.md | 150 ++++++++++++++
.editorconfig => streampark-e2e/lombok.config | 35 +---
streampark-e2e/pom.xml | 138 +++++++++++++
streampark-e2e/streampark-e2e-case/pom.xml | 40 ++++
.../streampark/e2e/cases/TeamManagementTest.java | 115 +++++++++++
.../streampark/e2e/cases/UserManagementTest.java | 98 +++++++++
.../org/apache/streampark/e2e/pages/LoginPage.java | 108 ++++++++++
.../streampark/e2e/pages/common/NavBarPage.java | 69 +++++++
.../streampark/e2e/pages/system/SystemPage.java | 79 ++++++++
.../e2e/pages/system/TeamManagementPage.java | 134 +++++++++++++
.../e2e/pages/system/UserManagementPage.java | 163 +++++++++++++++
.../pages/system/entity/UserManagementStatus.java | 25 +++
.../system/entity/UserManagementUserType.java | 25 +++
.../src/test/resources/docker/basic/Dockerfile | 31 +--
.../resources/docker/basic/docker-compose.yaml | 59 +++---
streampark-e2e/streampark-e2e-core/pom.xml | 32 +++
.../org/apache/streampark/e2e/core/Constants.java | 41 ++++
.../org/apache/streampark/e2e/core/StreamPark.java | 41 ++++
.../streampark/e2e/core/StreamParkExtension.java | 219 +++++++++++++++++++++
.../streampark/e2e/core/TestDescription.java | 53 +++++
.../src/main/resources/log4j2.xml | 31 +++
25 files changed, 1738 insertions(+), 92 deletions(-)
diff --git a/.editorconfig b/.editorconfig
index 38eee8c40..0ce913252 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -43,5 +43,10 @@ indent_size = 2
[*.md]
trim_trailing_whitespace = false
+[*.yaml]
+indent_size = 2
+
+[*.yml]
+indent_size = 2
diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml
new file mode 100644
index 000000000..baa3e9642
--- /dev/null
+++ b/.github/workflows/e2e.yml
@@ -0,0 +1,137 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+#
+
+on:
+ pull_request:
+ paths-ignore:
+ - '**.md'
+ push:
+ branches:
+ - dev
+ - release-*
+ - dev-*
+
+name: E2E
+
+concurrency:
+ group: e2e-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+
+jobs:
+ license-header:
+ if: github.repository == 'apache/incubator-streampark'
+ name: License header
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ submodules: true
+ - name: Check license header
+ uses: apache/skywalking-eyes/header@main
+ build:
+ name: E2E-Build
+ runs-on: ubuntu-latest
+ needs: license-header
+ timeout-minutes: 20
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ submodules: true
+ - name: Cache local Maven repository
+ uses: actions/cache@v4
+ with:
+ path: ~/.m2/repository
+ key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}-e2e
+ restore-keys: ${{ runner.os }}-maven-
+ - name: Build Image
+ run: |
+ ./build.sh \
+ && docker build -f docker/Dockerfile . -t apache/streampark:ci
+ - name: Export Docker Images
+ run: |
+ docker save apache/streampark:ci -o /tmp/standalone-image.tar \
+ && du -sh /tmp/standalone-image.tar
+ - uses: actions/upload-artifact@v4
+ name: Upload Docker Images
+ with:
+ name: standalone-image
+ path: /tmp/standalone-image.tar
+ retention-days: 1
+ e2e:
+ name: ${{ matrix.case.name }}
+ needs: build
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ strategy:
+ matrix:
+ case:
+ - name: UserManagementTest
+ class: org.apache.streampark.e2e.cases.UserManagementTest
+ - name: TeamManagementTest
+ class: org.apache.streampark.e2e.cases.TeamManagementTest
+ env:
+ RECORDING_PATH: /tmp/recording-${{ matrix.case.name }}
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ submodules: true
+ - name: Set up JDK 8
+ uses: actions/setup-java@v4
+ with:
+ java-version: 8
+ distribution: 'adopt'
+ - name: Cache local Maven repository
+ uses: actions/cache@v4
+ with:
+ path: ~/.m2/repository
+ key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}-e2e
+ restore-keys: ${{ runner.os }}-maven-
+ - uses: actions/download-artifact@v4
+ name: Download Docker Images
+ with:
+ name: standalone-image
+ path: /tmp
+ - name: Load Docker Images
+ run: |
+ docker load -i /tmp/standalone-image.tar
+ - name: Run Test
+ run: |
+ ./mvnw -B -f streampark-e2e/pom.xml -am \
+ -DfailIfNoTests=false \
+ -Dtest=${{ matrix.case.class }} test
+ - uses: actions/upload-artifact@v4
+ if: always()
+ name: Upload Recording
+ with:
+ name: recording-${{ matrix.case.name }}
+ path: ${{ env.RECORDING_PATH }}
+ retention-days: 1
+ result:
+ name: E2E - Result
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ needs: e2e
+ if: always()
+ steps:
+ - name: Status
+ run: |
+ if [[ ${{ needs.e2e.result }} != 'success' ]]; then
+ echo "E2E Failed!"
+ exit -1
+ fi
diff --git
a/streampark-console/streampark-console-webapp/src/views/base/login/LoginForm.vue
b/streampark-console/streampark-console-webapp/src/views/base/login/LoginForm.vue
index ce7b1307e..d1cb92ad3 100644
---
a/streampark-console/streampark-console-webapp/src/views/base/login/LoginForm.vue
+++
b/streampark-console/streampark-console-webapp/src/views/base/login/LoginForm.vue
@@ -62,6 +62,7 @@
block
@click="handleLogin"
:loading="loading"
+ classNames="login-button"
>
{{ loginText.buttonText }}
</Button>
diff --git
a/streampark-console/streampark-console-webapp/src/views/base/login/teamModal.vue
b/streampark-console/streampark-console-webapp/src/views/base/login/teamModal.vue
index ec5408101..6e1e16950 100644
---
a/streampark-console/streampark-console-webapp/src/views/base/login/teamModal.vue
+++
b/streampark-console/streampark-console-webapp/src/views/base/login/teamModal.vue
@@ -50,6 +50,7 @@
options: userStore.getTeamList,
// getPopupContainer: (triggerNode) => triggerNode.parentNode,
placeholder: t('sys.login.selectTeam'),
+ popupClassName: 'team-select-popup',
},
required: true,
},
diff --git a/streampark-e2e/README.md b/streampark-e2e/README.md
new file mode 100644
index 000000000..462a9aebf
--- /dev/null
+++ b/streampark-e2e/README.md
@@ -0,0 +1,150 @@
+<!--
+ ~ Licensed to the Apache Software Foundation (ASF) under one or more
+ ~ contributor license agreements. See the NOTICE file distributed with
+ ~ this work for additional information regarding copyright ownership.
+ ~ The ASF licenses this file to You 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.
+ ~
+ -->
+# StreamPark End-to-End Test
+
+## Page Object Model
+
+StreamPark End-to-End test respects
+the [Page Object Model
(POM)](https://www.selenium.dev/documentation/guidelines/page_object_models/)
design pattern.
+Every page of StreamPark is abstracted into a class for better maintainability.
+
+### Example
+
+The login page is abstracted
+as
[`LoginPage`](streampark-e2e-case/src/test/java/org/apache/streampark/e2e/pages/LoginPage.java),
with the
+following fields,
+
+```java
+public final class LoginPage {
+ @FindBy(id = "form_item_account")
+ private WebElement inputUsername;
+
+ @FindBy(id = "form_item_password")
+ private WebElement inputPassword;
+
+ @FindBy(xpath = "//button[contains(@classnames, 'login-button')]")
+ private WebElement buttonLogin;
+}
+```
+
+where `inputUsername`, `inputPassword` and `buttonLogin` are the main elements
on UI that we are interested in. They are
+annotated with `FindBy` so that the test framework knows how to locate the
elements on UI. You can locate the elements
+by `id`, `className`, `css` selector, `tagName`, or even `xpath`, please refer
+to [the
JavaDoc](https://www.selenium.dev/selenium/docs/api/java/org/openqa/selenium/support/FindBy.html).
+
+**Note:** for better maintainability, it's essential to add some convenient
`id` or `class` on UI for the wanted
+elements if needed, avoid using too complex `xpath` selector or `css` selector
that is not maintainable when UI have
+styles changes.
+
+With those fields declared, we should also initialize them with a web driver.
Here we pass the web driver into the
+constructor and invoke `PageFactory.initElements` to initialize those fields,
+
+```java
+public final class LoginPage {
+ // ...
+ public LoginPage(RemoteWebDriver driver) {
+ this.driver = driver;
+
+ PageFactory.initElements(driver, this);
+ }
+}
+```
+
+then, all those UI elements are properly filled in.
+
+## Test Environment Setup
+
+StreamPark End-to-End test uses
[testcontainers](https://www.testcontainers.org) to set up the testing
+environment, with docker compose.
+
+Typically, every test case needs one or more `docker-compose.yaml` files to
set up all needed components, and expose the
+StreamPark UI port for testing. You can use `@StreamPark(composeFiles = "")`
and pass
+the `docker-compose.yaml` files to automatically set up the environment in the
test class.
+
+```java
+
+@StreamPark(composeFiles = "docker/basic/docker-compose.yaml")
+class UserManagementTest {
+}
+```
+
+You can get the web driver that is ready for testing in the class by adding a
field of type `RemoteWebDriver`, which
+will be automatically injected via the testing framework.
+
+```java
+
+@StreamPark(composeFiles = "docker/basic/docker-compose.yaml")
+class UserManagementTest {
+ private RemoteWebDriver browser;
+}
+```
+
+Then the field `browser` can be used in the test methods.
+
+```java
+
+@StreamPark(composeFiles = "docker/basic/docker-compose.yaml")
+class UserManagementTest {
+ private RemoteWebDriver browser;
+
+ @Test
+ @Order(10)
+ void testCreateUser() {
+ final UserManagementPage userManagementPage = new
UserManagementPage(browser);
+ userManagementPage.createUser(newUserName, "test", password,
newUserEmail, UserManagementUserType.ADMIN);
+
+ Awaitility.await().untilAsserted(() ->
assertThat(userManagementPage.userList())
+ .as("User list should contain newly-created user")
+ .extracting(WebElement::getText)
+ .anyMatch(it -> it.contains(newUserName)));
+ }
+}
+```
+
+## Notes
+
+- For UI tests, it's common that the pages might need some time to load, or
the operations might need some time to
+ complete, we can use `await().untilAsserted(() -> {})` to wait for the
assertions. And use `new webDriverWait(driver, ExpectedConditions)` to wait for
the elements to be present or clickable.
+- For better maintainability, it's recommended to abstract the pages into
classes, and use the Page Object Model design
+ pattern.
+
+
+## Local development
+
+### Mac M1
+Add VM options to the test configuration in IntelliJ IDEA:
+```
+# In this mode you need to install docker desktop for mac and run it with
locally
+-Dm1_chip=true
+```
+
+### Running locally(without Docker)
+```
+# In this mode you need to start frontend and backend services locally
+-Dlocal=true
+```
+
+### Running locally(with Docker)
+```
+# In this mode you only need to install docker locally
+```
+
+- To run the tests locally, you need to have the StreamPark running locally.
You should add `streampark-e2e/pom.xml` to the maven project
+Since it does not participate in project compilation, it is not in the main
project.
+- Running run test class `org.apache.streampark.e2e.cases.UserManagementTest`
in the IDE. After execution, the test video will be saved as mp4 in a local
temporary directory. Such as
+`/var/folders/hf/123/T/record-3123/PASSED-[engine:junit-jupiter]/[class:org.apache.streampark.e2e.cases.UserManagementTest]-20240606-152333.mp4`
diff --git a/.editorconfig b/streampark-e2e/lombok.config
similarity index 65%
copy from .editorconfig
copy to streampark-e2e/lombok.config
index 38eee8c40..cc34fa231 100644
--- a/.editorconfig
+++ b/streampark-e2e/lombok.config
@@ -6,7 +6,7 @@
# (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
+# 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,
@@ -14,34 +14,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
-root = true
-
-[*]
-charset = utf-8
-end_of_line = lf
-indent_style = space
-indent_size = 4
-trim_trailing_whitespace = true
-insert_final_newline = true
-max_line_length = 140
-
-[*.scala]
-indent_size = 2
-
-[{*.yml, *.yaml}]
-indent_size = 2
-
-[*.json]
-indent_size = 2
-
-[*.py]
-indent_size = 4
-
-[*.sh]
-indent_size = 2
-
-[*.md]
-trim_trailing_whitespace = false
-
-
+lombok.accessors.fluent=true
+lombok.log.fieldname=log
+lombok.accessors.fluent=true
diff --git a/streampark-e2e/pom.xml b/streampark-e2e/pom.xml
new file mode 100644
index 000000000..a01218310
--- /dev/null
+++ b/streampark-e2e/pom.xml
@@ -0,0 +1,138 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Licensed to the Apache Software Foundation (ASF) under one or more
+ ~ contributor license agreements. See the NOTICE file distributed with
+ ~ this work for additional information regarding copyright ownership.
+ ~ The ASF licenses this file to You 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.
+ -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <groupId>org.apache.streampark</groupId>
+ <artifactId>streampark-e2e</artifactId>
+ <packaging>pom</packaging>
+ <version>1.0-SNAPSHOT</version>
+
+ <modules>
+ <module>streampark-e2e-core</module>
+ <module>streampark-e2e-case</module>
+ </modules>
+
+ <properties>
+ <maven.compiler.source>8</maven.compiler.source>
+ <maven.compiler.target>8</maven.compiler.target>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+
+ <junit.version>5.8.1</junit.version>
+ <selenium.version>4.13.0</selenium.version>
+ <lombok.version>1.18.20</lombok.version>
+ <assertj-core.version>3.20.2</assertj-core.version>
+ <kotlin.version>1.5.30</kotlin.version>
+ <slf4j-api.version>1.7.36</slf4j-api.version>
+ <log4j-slf4j-impl.version>2.17.2</log4j-slf4j-impl.version>
+ <guava.version>31.0.1-jre</guava.version>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ <version>${slf4j-api.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.logging.log4j</groupId>
+ <artifactId>log4j-slf4j-impl</artifactId>
+ <version>${log4j-slf4j-impl.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.testcontainers</groupId>
+ <artifactId>testcontainers</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.testcontainers</groupId>
+ <artifactId>junit-jupiter</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.testcontainers</groupId>
+ <artifactId>selenium</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.seleniumhq.selenium</groupId>
+ <artifactId>selenium-chrome-driver</artifactId>
+ <version>${selenium.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.seleniumhq.selenium</groupId>
+ <artifactId>selenium-support</artifactId>
+ <version>${selenium.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.assertj</groupId>
+ <artifactId>assertj-core</artifactId>
+ <version>${assertj-core.version}</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.projectlombok</groupId>
+ <artifactId>lombok</artifactId>
+ <version>${lombok.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+
+ <dependencyManagement>
+ <dependencies>
+ <dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava</artifactId>
+ <version>${guava.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.junit</groupId>
+ <artifactId>junit-bom</artifactId>
+ <version>${junit.version}</version>
+ <scope>import</scope>
+ <type>pom</type>
+ </dependency>
+ <dependency>
+ <groupId>org.testcontainers</groupId>
+ <artifactId>testcontainers-bom</artifactId>
+ <version>1.19.8</version>
+ <scope>import</scope>
+ <type>pom</type>
+ </dependency>
+ </dependencies>
+ </dependencyManagement>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <version>2.22.2</version>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/streampark-e2e/streampark-e2e-case/pom.xml
b/streampark-e2e/streampark-e2e-case/pom.xml
new file mode 100644
index 000000000..f086355ec
--- /dev/null
+++ b/streampark-e2e/streampark-e2e-case/pom.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Licensed to Apache Software Foundation (ASF) under one or more contributor
+ ~ license agreements. See the NOTICE file distributed with
+ ~ this work for additional information regarding copyright
+ ~ ownership. Apache Software Foundation (ASF) licenses this file to you 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.
+ -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <artifactId>streampark-e2e</artifactId>
+ <groupId>org.apache.streampark</groupId>
+ <version>1.0-SNAPSHOT</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>streampark-e2e-case</artifactId>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.streampark</groupId>
+ <artifactId>streampark-e2e-core</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ </dependencies>
+</project>
diff --git
a/streampark-e2e/streampark-e2e-case/src/test/java/org/apache/streampark/e2e/cases/TeamManagementTest.java
b/streampark-e2e/streampark-e2e-case/src/test/java/org/apache/streampark/e2e/cases/TeamManagementTest.java
new file mode 100644
index 000000000..a98d5b881
--- /dev/null
+++
b/streampark-e2e/streampark-e2e-case/src/test/java/org/apache/streampark/e2e/cases/TeamManagementTest.java
@@ -0,0 +1,115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ *
+ */
+package org.apache.streampark.e2e.cases;
+
+import org.apache.streampark.e2e.core.StreamPark;
+import org.apache.streampark.e2e.pages.LoginPage;
+import org.apache.streampark.e2e.pages.system.SystemPage;
+import org.apache.streampark.e2e.pages.system.TeamManagementPage;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.remote.RemoteWebDriver;
+import org.testcontainers.shaded.org.awaitility.Awaitility;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@StreamPark(composeFiles = "docker/basic/docker-compose.yaml")
+public class TeamManagementTest {
+ private static RemoteWebDriver browser;
+
+ private static final String userName = "admin";
+
+ private static final String password = "streampark";
+
+ private static final String teamName = "default";
+
+ private static final String newTeamName = "test_new_team";
+
+ private static final String newTeamDescription =
"test_new_team_description";
+
+ @BeforeAll
+ public static void setup() {
+ new LoginPage(browser)
+ .login(userName, password, teamName)
+ .goToNav(SystemPage.class)
+ .goToTab(TeamManagementPage.class);
+ }
+
+ @Test
+ @Order(10)
+ void testCreateTeam() {
+ final TeamManagementPage teamManagementPage = new
TeamManagementPage(browser);
+ teamManagementPage.createTeam(newTeamName, newTeamDescription);
+
+ Awaitility.await().untilAsserted(() ->
assertThat(teamManagementPage.teamList())
+ .as("Team list should contain newly-created team")
+ .extracting(WebElement::getText)
+ .anyMatch(it -> it.contains(newTeamName)));
+ }
+
+ @Test
+ @Order(20)
+ void testCreateDuplicateTeam() {
+ final TeamManagementPage teamManagementPage = new
TeamManagementPage(browser);
+ teamManagementPage.createTeam(newTeamName, newTeamDescription);
+
+ Awaitility.await().untilAsserted(() ->
assertThat(teamManagementPage.errorMessageList())
+ .as("Team Duplicated Error message should be displayed")
+ .extracting(WebElement::getText)
+ .anyMatch(it -> it.contains("Create team failed.")));
+
+ teamManagementPage.errorMessageConfirmButton().click();
+ teamManagementPage.createTeamForm().buttonCancel().click();
+ }
+
+ @Test
+ @Order(30)
+ void testEditTeam() {
+ final TeamManagementPage teamManagementPage = new
TeamManagementPage(browser);
+ String editDescription = "edit_" + newTeamDescription;
+
+ teamManagementPage.editTeam(newTeamName, editDescription);
+
+ Awaitility.await().untilAsserted(() ->
assertThat(teamManagementPage.teamList())
+ .as("Team list should contain edited team")
+ .extracting(WebElement::getText)
+ .anyMatch(it -> it.contains(editDescription)));
+ }
+
+ @Test
+ @Order(40)
+ void testDeleteTeam() {
+ final TeamManagementPage teamManagementPage = new
TeamManagementPage(browser);
+
+ teamManagementPage.deleteTeam(newTeamName);
+
+ Awaitility.await().untilAsserted(() -> {
+ browser.navigate().refresh();
+
+ assertThat(
+ teamManagementPage.teamList()
+ ).noneMatch(
+ it -> it.getText().contains(newTeamName)
+ );
+ });
+ }
+}
diff --git
a/streampark-e2e/streampark-e2e-case/src/test/java/org/apache/streampark/e2e/cases/UserManagementTest.java
b/streampark-e2e/streampark-e2e-case/src/test/java/org/apache/streampark/e2e/cases/UserManagementTest.java
new file mode 100644
index 000000000..39e962aed
--- /dev/null
+++
b/streampark-e2e/streampark-e2e-case/src/test/java/org/apache/streampark/e2e/cases/UserManagementTest.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ *
+ */
+package org.apache.streampark.e2e.cases;
+
+import org.apache.streampark.e2e.core.StreamPark;
+import org.apache.streampark.e2e.pages.LoginPage;
+import org.apache.streampark.e2e.pages.system.SystemPage;
+import org.apache.streampark.e2e.pages.system.UserManagementPage;
+import org.apache.streampark.e2e.pages.system.entity.UserManagementStatus;
+import org.apache.streampark.e2e.pages.system.entity.UserManagementUserType;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.remote.RemoteWebDriver;
+import org.testcontainers.shaded.org.awaitility.Awaitility;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@StreamPark(composeFiles = "docker/basic/docker-compose.yaml")
+public class UserManagementTest {
+ private static RemoteWebDriver browser;
+
+ private static final String userName = "admin";
+
+ private static final String password = "streampark";
+
+ private static final String teamName = "default";
+
+ private static final String newUserName = "test_new";
+
+ private static final String newUserEmail = "[email protected]";
+
+ @BeforeAll
+ public static void setup() {
+ new LoginPage(browser)
+ .login(userName, password, teamName)
+ .goToNav(SystemPage.class)
+ .goToTab(UserManagementPage.class);
+ }
+
+ @Test
+ @Order(10)
+ void testCreateUser() {
+ final UserManagementPage userManagementPage = new
UserManagementPage(browser);
+ userManagementPage.createUser(newUserName, "test", password,
newUserEmail, UserManagementUserType.ADMIN);
+
+ Awaitility.await().untilAsserted(() ->
assertThat(userManagementPage.userList())
+ .as("User list should contain newly-created user")
+ .extracting(WebElement::getText)
+ .anyMatch(it -> it.contains(newUserName)));
+ }
+
+ @Test
+ @Order(20)
+ void testCreateDuplicateUser() {
+ final UserManagementPage userManagementPage = new
UserManagementPage(browser);
+ userManagementPage.createUser(newUserName, "test", password,
"[email protected]", UserManagementUserType.ADMIN);
+
+ Awaitility.await().untilAsserted(() ->
assertThat(userManagementPage.errorMessageList())
+ .as("User Name Duplicated Error message should be displayed")
+ .extracting(WebElement::getText)
+ .anyMatch(it -> it.contains("Sorry the username already exists")));
+
+ userManagementPage.createUserForm().buttonCancel().click();
+ }
+
+ @Test
+ @Order(30)
+ void testEditUser() {
+ final UserManagementPage userManagementPage = new
UserManagementPage(browser);
+ String editEmail = "edit_" + newUserEmail;
+
+ userManagementPage.editUser(newUserName, editEmail,
UserManagementUserType.ADMIN, UserManagementStatus.LOCKED);
+
+ Awaitility.await().untilAsserted(() ->
assertThat(userManagementPage.userList())
+ .as("User list should contain edited user")
+ .extracting(WebElement::getText)
+ .anyMatch(it ->
it.contains(UserManagementStatus.LOCKED.toString().toLowerCase())));
+ }
+}
diff --git
a/streampark-e2e/streampark-e2e-case/src/test/java/org/apache/streampark/e2e/pages/LoginPage.java
b/streampark-e2e/streampark-e2e-case/src/test/java/org/apache/streampark/e2e/pages/LoginPage.java
new file mode 100644
index 000000000..ce067d23d
--- /dev/null
+++
b/streampark-e2e/streampark-e2e-case/src/test/java/org/apache/streampark/e2e/pages/LoginPage.java
@@ -0,0 +1,108 @@
+/*
+ * Licensed to Apache Software Foundation (ASF) under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Apache Software Foundation (ASF) licenses this file to you 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.
+ */
+
+package org.apache.streampark.e2e.pages;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.streampark.e2e.pages.common.NavBarPage;
+
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.remote.RemoteWebDriver;
+import org.openqa.selenium.support.FindBy;
+import org.openqa.selenium.support.FindBys;
+import org.openqa.selenium.support.PageFactory;
+import org.openqa.selenium.support.ui.ExpectedConditions;
+import org.openqa.selenium.support.ui.WebDriverWait;
+
+import lombok.Getter;
+import lombok.SneakyThrows;
+
+import java.time.Duration;
+import java.util.List;
+
+@Getter
+@Slf4j
+public final class LoginPage extends NavBarPage {
+ @FindBy(id = "form_item_account")
+ private WebElement inputUsername;
+
+ @FindBy(id = "form_item_password")
+ private WebElement inputPassword;
+
+ @FindBy(xpath = "//button[contains(@classnames, 'login-button')]")
+ private WebElement buttonLogin;
+
+ private final TeamForm teamForm = new TeamForm();
+
+ public LoginPage(RemoteWebDriver driver) {
+ super(driver);
+ }
+
+ @SneakyThrows
+ public NavBarPage login(String username, String password, String teamName)
{
+ new WebDriverWait(driver, Duration.ofSeconds(10))
+ .until(ExpectedConditions.elementToBeClickable(buttonLogin));
+
+ inputUsername().sendKeys(username);
+ inputPassword().sendKeys(password);
+ buttonLogin().click();
+
+ try {
+ new WebDriverWait(driver, Duration.ofSeconds(10))
+
.until(ExpectedConditions.visibilityOfAllElements(teamForm.btnSelectTeamDropdown));
+
+ teamForm.btnSelectTeamDropdown.click();
+ teamForm.selectTeam
+ .stream()
+ .filter(it -> it.getText().contains(teamName))
+ .findFirst()
+ .orElseThrow(() -> new RuntimeException(String.format("No %s
in team dropdown list", teamName)))
+ .click();
+ teamForm.buttonSubmit.click();
+ } catch (Exception e) {
+ log.warn("No team selection required:", e);
+ }
+
+ new WebDriverWait(driver, Duration.ofSeconds(30))
+ .until(ExpectedConditions.urlContains("/flink/app"));
+ return new NavBarPage(driver);
+ }
+
+ @Getter
+ public class TeamForm {
+ TeamForm() {
+ PageFactory.initElements(driver, this);
+ }
+
+ @FindBys({
+ @FindBy(css = "[popupClassName=team-select-popup]"),
+ @FindBy(className = "ant-select-item-option-content")
+ })
+ private List<WebElement> selectTeam;
+
+ @FindBy(css = "[popupClassName=team-select-popup] >
.ant-select-selector")
+ private WebElement btnSelectTeamDropdown;
+
+ @FindBy(xpath = "//button[contains(@class,
'ant-btn')]//span[contains(text(), 'OK')]")
+ private WebElement buttonSubmit;
+
+ @FindBy(xpath = "//button[contains(@class,
'ant-btn')]//span[contains(text(), 'Cancel')]")
+ private WebElement buttonCancel;
+ }
+}
diff --git
a/streampark-e2e/streampark-e2e-case/src/test/java/org/apache/streampark/e2e/pages/common/NavBarPage.java
b/streampark-e2e/streampark-e2e-case/src/test/java/org/apache/streampark/e2e/pages/common/NavBarPage.java
new file mode 100644
index 000000000..3d44142d8
--- /dev/null
+++
b/streampark-e2e/streampark-e2e-case/src/test/java/org/apache/streampark/e2e/pages/common/NavBarPage.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ *
+ */
+
+package org.apache.streampark.e2e.pages.common;
+
+import org.apache.streampark.e2e.pages.system.SystemPage;
+
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.remote.RemoteWebDriver;
+import org.openqa.selenium.support.FindBy;
+import org.openqa.selenium.support.PageFactory;
+import org.openqa.selenium.support.ui.ExpectedConditions;
+import org.openqa.selenium.support.ui.WebDriverWait;
+
+import lombok.Getter;
+
+import java.time.Duration;
+
+@Getter
+public class NavBarPage {
+ protected final RemoteWebDriver driver;
+
+ @FindBy(xpath = "//span[contains(@class, 'ml-2') and contains(@class,
'streampark-simple-menu-sub-title') and contains(text(), 'Apache Flink')]")
+ private WebElement apacheFlinkTab;
+
+ @FindBy(xpath = "//span[contains(@class, 'ml-2') and contains(@class,
'streampark-simple-menu-sub-title') and contains(text(), 'Resources')]")
+ private WebElement resourcesTab;
+
+ @FindBy(xpath = "//span[contains(@class, 'ml-2') and contains(@class,
'streampark-simple-menu-sub-title') and contains(text(), 'Settings')]")
+ private WebElement settingsTab;
+
+ @FindBy(xpath = "//span[contains(@class, 'ml-2') and contains(@class,
'streampark-simple-menu-sub-title') and contains(text(), 'System')]")
+ private WebElement systemTab;
+
+ public NavBarPage(RemoteWebDriver driver) {
+ this.driver = driver;
+ PageFactory.initElements(driver, this);
+ }
+
+ public <T extends NavBarItem> T goToNav(Class<T> nav) {
+ if (nav == SystemPage.class) {
+ new WebDriverWait(driver,
Duration.ofSeconds(60)).until(ExpectedConditions.elementToBeClickable(systemTab));
+ systemTab.click();
+ return nav.cast(new SystemPage(driver));
+ }
+
+ throw new UnsupportedOperationException("Unknown nav bar");
+ }
+
+ public interface NavBarItem {
+ }
+}
diff --git
a/streampark-e2e/streampark-e2e-case/src/test/java/org/apache/streampark/e2e/pages/system/SystemPage.java
b/streampark-e2e/streampark-e2e-case/src/test/java/org/apache/streampark/e2e/pages/system/SystemPage.java
new file mode 100644
index 000000000..a7e877998
--- /dev/null
+++
b/streampark-e2e/streampark-e2e-case/src/test/java/org/apache/streampark/e2e/pages/system/SystemPage.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ *
+ */
+package org.apache.streampark.e2e.pages.system;
+
+import org.apache.streampark.e2e.pages.common.NavBarPage;
+import org.apache.streampark.e2e.pages.common.NavBarPage.NavBarItem;
+
+import java.time.Duration;
+import java.util.List;
+
+import org.openqa.selenium.By;
+import org.openqa.selenium.JavascriptExecutor;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.remote.RemoteWebDriver;
+import org.openqa.selenium.support.FindBy;
+import org.openqa.selenium.support.FindBys;
+import org.openqa.selenium.support.PageFactory;
+
+import lombok.Getter;
+import org.openqa.selenium.support.ui.ExpectedConditions;
+import org.openqa.selenium.support.ui.WebDriverWait;
+
+@Getter
+public final class SystemPage extends NavBarPage implements NavBarItem {
+ @FindBy(xpath = "//span[contains(@class,
'streampark-simple-menu-sub-title') and contains(text(), 'User
Management')]//..")
+ private WebElement menuUserManagement;
+
+ @FindBy(xpath = "//span[contains(@class,
'streampark-simple-menu-sub-title') and contains(text(), 'Token
Management')]//..")
+ private WebElement menuTokenManagement;
+
+ @FindBy(xpath = "//span[contains(@class,
'streampark-simple-menu-sub-title') and contains(text(), 'Role
Management')]//..")
+ private WebElement menuRoleManagement;
+
+ @FindBy(xpath = "//span[contains(@class,
'streampark-simple-menu-sub-title') and contains(text(), 'Team
Management')]//..")
+ private WebElement menuTeamManagement;
+
+ @FindBy(xpath = "//span[contains(@class,
'streampark-simple-menu-sub-title') and contains(text(), 'Member
Management')]//..")
+ private WebElement menuMemberManagement;
+
+ public SystemPage(RemoteWebDriver driver) {
+ super(driver);
+ }
+
+ public <T extends SystemPage.Tab> T goToTab(Class<T> tab) {
+ if (tab == UserManagementPage.class) {
+ new WebDriverWait(driver,
Duration.ofSeconds(10)).until(ExpectedConditions.elementToBeClickable(menuUserManagement));
+ menuUserManagement.click();
+ return tab.cast(new UserManagementPage(driver));
+ }
+
+ if (tab == TeamManagementPage.class) {
+ new WebDriverWait(driver,
Duration.ofSeconds(10)).until(ExpectedConditions.elementToBeClickable(menuTeamManagement));
+ menuTeamManagement.click();
+ return tab.cast(new TeamManagementPage(driver));
+ }
+
+ throw new UnsupportedOperationException("Unknown tab: " +
tab.getName());
+ }
+
+ public interface Tab {
+ }
+}
diff --git
a/streampark-e2e/streampark-e2e-case/src/test/java/org/apache/streampark/e2e/pages/system/TeamManagementPage.java
b/streampark-e2e/streampark-e2e-case/src/test/java/org/apache/streampark/e2e/pages/system/TeamManagementPage.java
new file mode 100644
index 000000000..a8e8d1669
--- /dev/null
+++
b/streampark-e2e/streampark-e2e-case/src/test/java/org/apache/streampark/e2e/pages/system/TeamManagementPage.java
@@ -0,0 +1,134 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ *
+ */
+package org.apache.streampark.e2e.pages.system;
+
+import lombok.Getter;
+import org.apache.streampark.e2e.pages.common.NavBarPage;
+import org.openqa.selenium.By;
+import org.openqa.selenium.Keys;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.remote.RemoteWebDriver;
+import org.openqa.selenium.support.FindBy;
+import org.openqa.selenium.support.PageFactory;
+import org.openqa.selenium.support.ui.ExpectedConditions;
+import org.openqa.selenium.support.ui.WebDriverWait;
+
+import java.time.Duration;
+import java.util.List;
+
+@Getter
+public class TeamManagementPage extends NavBarPage implements SystemPage.Tab {
+ @FindBy(xpath = "//span[contains(., 'Team
List')]/..//button[contains(@class, 'ant-btn-primary')]/span[contains(text(),
'Add New')]")
+ private WebElement buttonCreateTeam;
+
+ @FindBy(xpath = "//tbody[contains(@class, 'ant-table-tbody')]")
+ private List<WebElement> teamList;
+
+ @FindBy(className = "swal2-html-container")
+ private List<WebElement> errorMessageList;
+
+ @FindBy(xpath = "//button[contains(text(), 'OK')]")
+ private WebElement errorMessageConfirmButton;
+
+ @FindBy(xpath = "//button[contains(@class, 'ant-btn')]/span[contains(.,
'OK')]")
+ private WebElement deleteConfirmButton;
+
+ private final CreateTeamForm createTeamForm = new CreateTeamForm();
+
+ public TeamManagementPage(RemoteWebDriver driver) {
+ super(driver);
+ }
+
+ public TeamManagementPage createTeam(String teamName, String description) {
+ waitForPageLoading();
+
+ new WebDriverWait(driver,
Duration.ofSeconds(10)).until(ExpectedConditions.elementToBeClickable(buttonCreateTeam));
+ buttonCreateTeam.click();
+ createTeamForm.inputTeamName().sendKeys(teamName);
+ createTeamForm.inputDescription().sendKeys(description);
+
+ createTeamForm.buttonSubmit().click();
+ return this;
+ }
+
+ public TeamManagementPage editTeam(String teamName, String description) {
+ waitForPageLoading();
+
+ teamList()
+ .stream()
+ .filter(it -> it.getText().contains(teamName))
+ .flatMap(it ->
it.findElements(By.xpath("//button[contains(@tooltip,'Modify
Team')]")).stream())
+ .filter(WebElement::isDisplayed)
+ .findFirst()
+ .orElseThrow(() -> new RuntimeException("No edit button in team
list"))
+ .click();
+
+ new WebDriverWait(driver,
Duration.ofSeconds(10)).until(ExpectedConditions.elementToBeClickable(createTeamForm.buttonSubmit));
+ createTeamForm.inputDescription().sendKeys(Keys.CONTROL+"a");
+ createTeamForm.inputDescription().sendKeys(Keys.BACK_SPACE);
+ createTeamForm.inputDescription().sendKeys(description);
+
+ createTeamForm.buttonSubmit().click();
+
+ return this;
+ }
+
+ public TeamManagementPage deleteTeam(String teamName) {
+ waitForPageLoading();
+
+ teamList()
+ .stream()
+ .filter(it -> it.getText().contains(teamName))
+ .flatMap(it ->
it.findElements(By.xpath("//button[contains(@tooltip,'Delete
Team')]")).stream())
+ .filter(WebElement::isDisplayed)
+ .findFirst()
+ .orElseThrow(() -> new RuntimeException("No delete button in team
list"))
+ .click();
+
+ new WebDriverWait(driver,
Duration.ofSeconds(10)).until(ExpectedConditions.elementToBeClickable(deleteConfirmButton));
+
+ deleteConfirmButton.click();
+
+ return this;
+ }
+
+ private void waitForPageLoading() {
+ new WebDriverWait(driver,
Duration.ofSeconds(10)).until(ExpectedConditions.urlContains("/system/team"));
+ }
+
+ @Getter
+ public class CreateTeamForm {
+ CreateTeamForm() {
+ PageFactory.initElements(driver, this);
+ }
+
+ @FindBy(id = "TeamEditForm_teamName")
+ private WebElement inputTeamName;
+
+ @FindBy(id = "TeamEditForm_description")
+ private WebElement inputDescription;
+
+ @FindBy(xpath = "//button[contains(@class,
'ant-btn')]//span[contains(., 'Submit')]")
+ private WebElement buttonSubmit;
+
+ @FindBy(xpath = "//button[contains(@class,
'ant-btn')]//span[contains(., 'Cancel')]")
+ private WebElement buttonCancel;
+ }
+}
diff --git
a/streampark-e2e/streampark-e2e-case/src/test/java/org/apache/streampark/e2e/pages/system/UserManagementPage.java
b/streampark-e2e/streampark-e2e-case/src/test/java/org/apache/streampark/e2e/pages/system/UserManagementPage.java
new file mode 100644
index 000000000..dcfc2f58a
--- /dev/null
+++
b/streampark-e2e/streampark-e2e-case/src/test/java/org/apache/streampark/e2e/pages/system/UserManagementPage.java
@@ -0,0 +1,163 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ *
+ */
+package org.apache.streampark.e2e.pages.system;
+
+import lombok.Getter;
+import org.apache.streampark.e2e.pages.common.NavBarPage;
+import org.apache.streampark.e2e.pages.system.entity.UserManagementStatus;
+import org.apache.streampark.e2e.pages.system.entity.UserManagementUserType;
+import org.openqa.selenium.By;
+import org.openqa.selenium.Keys;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.remote.RemoteWebDriver;
+import org.openqa.selenium.support.FindBy;
+import org.openqa.selenium.support.FindBys;
+import org.openqa.selenium.support.PageFactory;
+import org.openqa.selenium.support.ui.ExpectedConditions;
+import org.openqa.selenium.support.ui.WebDriverWait;
+
+import java.time.Duration;
+import java.util.List;
+
+@Getter
+public class UserManagementPage extends NavBarPage implements SystemPage.Tab {
+ @FindBy(xpath = "//span[contains(., 'User
List')]/..//button[contains(@class, 'ant-btn-primary')]/span[contains(text(),
'Add New')]")
+ private WebElement buttonCreateUser;
+
+ @FindBy(xpath = "//tbody[contains(@class, 'ant-table-tbody')]")
+ private List<WebElement> userList;
+
+ @FindBy(className = "ant-form-item-explain-error")
+ private List<WebElement> errorMessageList;
+
+ private final CreateUserForm createUserForm = new CreateUserForm();
+
+ public UserManagementPage(RemoteWebDriver driver) {
+ super(driver);
+ }
+
+ public UserManagementPage createUser(String userName, String nickName,
String password, String email, UserManagementUserType userManagementUserType) {
+ waitForPageLoading();
+
+ new WebDriverWait(driver,
Duration.ofSeconds(10)).until(ExpectedConditions.elementToBeClickable(buttonCreateUser));
+ buttonCreateUser.click();
+ createUserForm.inputUserName().sendKeys(userName);
+ createUserForm.inputNickName().sendKeys(nickName);
+ createUserForm.inputPassword().sendKeys(password);
+ createUserForm.inputEmail().sendKeys(email);
+
+ createUserForm.btnSelectUserTypeDropdown().click();
+ new WebDriverWait(driver,
Duration.ofSeconds(10)).until(ExpectedConditions.visibilityOfAllElements(createUserForm.selectUserType));
+ createUserForm.selectUserType
+ .stream()
+ .filter(e ->
e.getText().equalsIgnoreCase(String.valueOf(userManagementUserType)))
+ .findFirst()
+ .orElseThrow(() -> new RuntimeException(String.format("No %s in
userType dropdown list", userManagementUserType)))
+ .click();
+
+ createUserForm.buttonSubmit().click();
+ return this;
+ }
+
+ public UserManagementPage editUser(String userName, String email,
UserManagementUserType userManagementUserType, UserManagementStatus
userManagementStatus) {
+ waitForPageLoading();
+
+ userList()
+ .stream()
+ .filter(it -> it.getText().contains(userName))
+ .flatMap(it ->
it.findElements(By.xpath("//button[contains(@tooltip,'modify
user')]")).stream())
+ .filter(WebElement::isDisplayed)
+ .findFirst()
+ .orElseThrow(() -> new RuntimeException("No edit button in user
list"))
+ .click();
+
+ createUserForm.inputEmail().sendKeys(Keys.CONTROL+"a");
+ createUserForm.inputEmail().sendKeys(Keys.BACK_SPACE);
+ createUserForm.inputEmail().sendKeys(email);
+
+ createUserForm.btnSelectUserTypeDropdown().click();
+ new WebDriverWait(driver,
Duration.ofSeconds(10)).until(ExpectedConditions.visibilityOfAllElements(createUserForm.selectUserType));
+ createUserForm.selectUserType
+ .stream()
+ .filter(e ->
e.getText().equalsIgnoreCase(String.valueOf(userManagementUserType)))
+ .findFirst()
+ .orElseThrow(() -> new RuntimeException(String.format("No %s in
userType dropdown list", userManagementUserType)))
+ .click();
+
+ switch (userManagementStatus) {
+ case LOCKED:
+ createUserForm.radioLocked.click();
+ break;
+ case EFFECTIVE:
+ createUserForm.radioEffective.click();
+ break;
+ default:
+ throw new RuntimeException("Unknown user management status");
+ }
+
+ createUserForm.buttonSubmit().click();
+
+ return this;
+ }
+
+ private void waitForPageLoading() {
+ new WebDriverWait(driver,
Duration.ofSeconds(10)).until(ExpectedConditions.urlContains("/system/user"));
+ }
+
+ @Getter
+ public class CreateUserForm {
+ CreateUserForm() {
+ PageFactory.initElements(driver, this);
+ }
+
+ @FindBy(id = "formUserName")
+ private WebElement inputUserName;
+
+ @FindBy(id = "form_item_nickName")
+ private WebElement inputNickName;
+
+ @FindBy(id = "form_item_password")
+ private WebElement inputPassword;
+
+ @FindBy(id = "form_item_email")
+ private WebElement inputEmail;
+
+ @FindBys({
+ @FindBy(css = "[codefield=userType]"),
+ @FindBy(className = "ant-select-item-option-content")
+ })
+ private List<WebElement> selectUserType;
+
+ @FindBy(css = "[codefield=userType] > .ant-select-selector")
+ private WebElement btnSelectUserTypeDropdown;
+
+ @FindBy(xpath = "//label[contains(@class,
'ant-radio-wrapper')]/span[contains(., 'lock')]")
+ private WebElement radioLocked;
+
+ @FindBy(xpath = "//label[contains(@class,
'ant-radio-wrapper')]/span[contains(., 'effective')]")
+ private WebElement radioEffective;
+
+ @FindBy(xpath = "//button[contains(@class,
'ant-btn')]//span[contains(text(), 'Submit')]")
+ private WebElement buttonSubmit;
+
+ @FindBy(xpath = "//button[contains(@class,
'ant-btn')]//span[contains(text(), 'Cancel')]")
+ private WebElement buttonCancel;
+ }
+}
diff --git
a/streampark-e2e/streampark-e2e-case/src/test/java/org/apache/streampark/e2e/pages/system/entity/UserManagementStatus.java
b/streampark-e2e/streampark-e2e-case/src/test/java/org/apache/streampark/e2e/pages/system/entity/UserManagementStatus.java
new file mode 100644
index 000000000..3fcdab621
--- /dev/null
+++
b/streampark-e2e/streampark-e2e-case/src/test/java/org/apache/streampark/e2e/pages/system/entity/UserManagementStatus.java
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ *
+ */
+package org.apache.streampark.e2e.pages.system.entity;
+
+public enum UserManagementStatus {
+ LOCKED,
+ EFFECTIVE
+}
diff --git
a/streampark-e2e/streampark-e2e-case/src/test/java/org/apache/streampark/e2e/pages/system/entity/UserManagementUserType.java
b/streampark-e2e/streampark-e2e-case/src/test/java/org/apache/streampark/e2e/pages/system/entity/UserManagementUserType.java
new file mode 100644
index 000000000..315dd6119
--- /dev/null
+++
b/streampark-e2e/streampark-e2e-case/src/test/java/org/apache/streampark/e2e/pages/system/entity/UserManagementUserType.java
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ *
+ */
+package org.apache.streampark.e2e.pages.system.entity;
+
+public enum UserManagementUserType {
+ ADMIN,
+ USER
+}
diff --git a/.editorconfig
b/streampark-e2e/streampark-e2e-case/src/test/resources/docker/basic/Dockerfile
similarity index 69%
copy from .editorconfig
copy to
streampark-e2e/streampark-e2e-case/src/test/resources/docker/basic/Dockerfile
index 38eee8c40..9443edc26 100644
--- a/.editorconfig
+++
b/streampark-e2e/streampark-e2e-case/src/test/resources/docker/basic/Dockerfile
@@ -14,34 +14,5 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
-root = true
-
-[*]
-charset = utf-8
-end_of_line = lf
-indent_style = space
-indent_size = 4
-trim_trailing_whitespace = true
-insert_final_newline = true
-max_line_length = 140
-
-[*.scala]
-indent_size = 2
-
-[{*.yml, *.yaml}]
-indent_size = 2
-
-[*.json]
-indent_size = 2
-
-[*.py]
-indent_size = 4
-
-[*.sh]
-indent_size = 2
-
-[*.md]
-trim_trailing_whitespace = false
-
-
+FROM apache/streampark:ci
diff --git a/.editorconfig
b/streampark-e2e/streampark-e2e-case/src/test/resources/docker/basic/docker-compose.yaml
similarity index 55%
copy from .editorconfig
copy to
streampark-e2e/streampark-e2e-case/src/test/resources/docker/basic/docker-compose.yaml
index 38eee8c40..536cdc469 100644
--- a/.editorconfig
+++
b/streampark-e2e/streampark-e2e-case/src/test/resources/docker/basic/docker-compose.yaml
@@ -6,7 +6,7 @@
# (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
+# 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,
@@ -14,34 +14,31 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
-root = true
-
-[*]
-charset = utf-8
-end_of_line = lf
-indent_style = space
-indent_size = 4
-trim_trailing_whitespace = true
-insert_final_newline = true
-max_line_length = 140
-
-[*.scala]
-indent_size = 2
-
-[{*.yml, *.yaml}]
-indent_size = 2
-
-[*.json]
-indent_size = 2
-
-[*.py]
-indent_size = 4
-
-[*.sh]
-indent_size = 2
-
-[*.md]
-trim_trailing_whitespace = false
-
-
+version: "3.8"
+
+services:
+ streampark:
+ image: apache/streampark:ci-basic
+ command: bash bin/streampark.sh start_docker
+ build:
+ context: ./
+ dockerfile: ./Dockerfile
+ ports:
+ - 10000:10000
+ - 10030:10030
+ environment:
+ - SPRING_PROFILES_ACTIVE=h2
+ - TZ=Asia/Shanghai
+ privileged: true
+ restart: unless-stopped
+ networks:
+ - e2e
+ healthcheck:
+ test: [ "CMD", "curl", "http://localhost:10000" ]
+ interval: 5s
+ timeout: 5s
+ retries: 120
+
+networks:
+ e2e:
diff --git a/streampark-e2e/streampark-e2e-core/pom.xml
b/streampark-e2e/streampark-e2e-core/pom.xml
new file mode 100644
index 000000000..60c7167e8
--- /dev/null
+++ b/streampark-e2e/streampark-e2e-core/pom.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Licensed to Apache Software Foundation (ASF) under one or more contributor
+ ~ license agreements. See the NOTICE file distributed with
+ ~ this work for additional information regarding copyright
+ ~ ownership. Apache Software Foundation (ASF) licenses this file to you 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.
+ -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <artifactId>streampark-e2e</artifactId>
+ <groupId>org.apache.streampark</groupId>
+ <version>1.0-SNAPSHOT</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>streampark-e2e-core</artifactId>
+</project>
diff --git
a/streampark-e2e/streampark-e2e-core/src/main/java/org/apache/streampark/e2e/core/Constants.java
b/streampark-e2e/streampark-e2e-core/src/main/java/org/apache/streampark/e2e/core/Constants.java
new file mode 100644
index 000000000..777c2d5c6
--- /dev/null
+++
b/streampark-e2e/streampark-e2e-core/src/main/java/org/apache/streampark/e2e/core/Constants.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package org.apache.streampark.e2e.core;
+
+import lombok.experimental.UtilityClass;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+@UtilityClass
+public final class Constants {
+ /**
+ * tmp directory path
+ */
+ public static final Path HOST_TMP_PATH =
Paths.get(System.getProperty("java.io.tmpdir"));
+
+ /**
+ * chrome download path in host
+ */
+ public static final Path HOST_CHROME_DOWNLOAD_PATH =
HOST_TMP_PATH.resolve("download");
+
+ /**
+ * chrome download path in selenium/standalone-chrome-debug container
+ */
+ public static final String SELENIUM_CONTAINER_CHROME_DOWNLOAD_PATH =
"/home/seluser/Downloads";
+}
diff --git
a/streampark-e2e/streampark-e2e-core/src/main/java/org/apache/streampark/e2e/core/StreamPark.java
b/streampark-e2e/streampark-e2e-core/src/main/java/org/apache/streampark/e2e/core/StreamPark.java
new file mode 100644
index 000000000..337948735
--- /dev/null
+++
b/streampark-e2e/streampark-e2e-core/src/main/java/org/apache/streampark/e2e/core/StreamPark.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to Apache Software Foundation (ASF) under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Apache Software Foundation (ASF) licenses this file to you 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.
+ */
+
+package org.apache.streampark.e2e.core;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
+import org.junit.jupiter.api.TestMethodOrder;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.testcontainers.junit.jupiter.Testcontainers;
+
+@Inherited
+@Testcontainers
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@TestMethodOrder(OrderAnnotation.class)
+@ExtendWith(StreamParkExtension.class)
+public @interface StreamPark {
+ String[] composeFiles();
+}
diff --git
a/streampark-e2e/streampark-e2e-core/src/main/java/org/apache/streampark/e2e/core/StreamParkExtension.java
b/streampark-e2e/streampark-e2e-core/src/main/java/org/apache/streampark/e2e/core/StreamParkExtension.java
new file mode 100644
index 000000000..9592381a4
--- /dev/null
+++
b/streampark-e2e/streampark-e2e-core/src/main/java/org/apache/streampark/e2e/core/StreamParkExtension.java
@@ -0,0 +1,219 @@
+/*
+ * Licensed to Apache Software Foundation (ASF) under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Apache Software Foundation (ASF) licenses this file to you 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.
+ */
+
+package org.apache.streampark.e2e.core;
+
+import com.google.common.base.Strings;
+import com.google.common.net.HostAndPort;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.jupiter.api.extension.AfterAllCallback;
+import org.junit.jupiter.api.extension.BeforeAllCallback;
+import org.junit.jupiter.api.extension.BeforeEachCallback;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.chrome.ChromeOptions;
+import org.openqa.selenium.remote.RemoteWebDriver;
+import org.testcontainers.Testcontainers;
+import org.testcontainers.containers.BrowserWebDriverContainer;
+import org.testcontainers.containers.ComposeContainer;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.shaded.org.awaitility.Awaitility;
+import org.testcontainers.utility.DockerImageName;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.Duration;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static
org.testcontainers.containers.BrowserWebDriverContainer.VncRecordingMode.RECORD_ALL;
+import static
org.testcontainers.containers.VncRecordingContainer.VncRecordingFormat.MP4;
+
+@Slf4j
+final class StreamParkExtension implements BeforeAllCallback,
AfterAllCallback, BeforeEachCallback {
+ private final boolean LOCAL_MODE =
Objects.equals(System.getProperty("local"), "true");
+
+ private final boolean M1_CHIP_FLAG =
Objects.equals(System.getProperty("m1_chip"), "true");
+
+ private final int LOCAL_PORT = 10001;
+
+ private final int DOCKER_PORT = 10000;
+
+ private RemoteWebDriver driver;
+ private ComposeContainer compose;
+ private BrowserWebDriverContainer<?> browser;
+ private HostAndPort address;
+ private String rootPath;
+
+ private Path record;
+
+ private final String serviceName = "streampark";
+
+ @Override
+ @SuppressWarnings("UnstableApiUsage")
+ public void beforeAll(ExtensionContext context) throws IOException {
+ Awaitility.setDefaultTimeout(Duration.ofSeconds(60));
+ Awaitility.setDefaultPollInterval(Duration.ofSeconds(2));
+
+ setRecordPath();
+
+ if (LOCAL_MODE) {
+ runInLocal();
+ } else {
+ runInDockerContainer(context);
+ }
+
+ setBrowserContainerByOsName();
+
+ if (compose != null) {
+ Testcontainers.exposeHostPorts(compose.getServicePort(serviceName,
DOCKER_PORT));
+ browser.withAccessToHost(true);
+ }
+ browser.start();
+
+ driver = new RemoteWebDriver(browser.getSeleniumAddress(), new
ChromeOptions());
+
+ driver.manage().timeouts()
+ .implicitlyWait(Duration.ofSeconds(10))
+ .pageLoadTimeout(Duration.ofSeconds(10));
+ driver.manage().window()
+ .maximize();
+
+ driver.get(new URL("http", address.getHost(), address.getPort(),
rootPath).toString());
+
+ browser.beforeTest(new TestDescription(context));
+
+ final Class<?> clazz = context.getRequiredTestClass();
+ Stream.of(clazz.getDeclaredFields())
+ .filter(it -> Modifier.isStatic(it.getModifiers()))
+ .filter(f -> WebDriver.class.isAssignableFrom(f.getType()))
+ .forEach(it -> setDriver(clazz, it));
+ }
+
+ private void runInLocal() {
+ Testcontainers.exposeHostPorts(LOCAL_PORT);
+ address = HostAndPort.fromParts("host.testcontainers.internal",
LOCAL_PORT);
+ rootPath = "/";
+ }
+
+ private void runInDockerContainer(ExtensionContext context) {
+ compose = createDockerCompose(context);
+ compose.start();
+
+ address = HostAndPort.fromParts("host.testcontainers.internal",
compose.getServicePort(serviceName, DOCKER_PORT));
+ rootPath = "/";
+ }
+
+ private void setBrowserContainerByOsName() {
+ DockerImageName imageName;
+
+ if (M1_CHIP_FLAG) {
+ imageName =
DockerImageName.parse("seleniarm/standalone-chromium:124.0-chromedriver-124.0")
+ .asCompatibleSubstituteFor("selenium/standalone-chrome");
+
+ browser = new BrowserWebDriverContainer<>(imageName)
+ .withCapabilities(new ChromeOptions())
+ .withCreateContainerCmdModifier(cmd -> cmd.withUser("root"))
+
.withFileSystemBind(Constants.HOST_CHROME_DOWNLOAD_PATH.toFile().getAbsolutePath(),
+ Constants.SELENIUM_CONTAINER_CHROME_DOWNLOAD_PATH)
+ .withRecordingMode(RECORD_ALL, record.toFile(), MP4)
+ .withStartupTimeout(Duration.ofSeconds(300));
+ } else {
+ browser = new BrowserWebDriverContainer<>()
+ .withCapabilities(new ChromeOptions())
+ .withCreateContainerCmdModifier(cmd -> cmd.withUser("root"))
+
.withFileSystemBind(Constants.HOST_CHROME_DOWNLOAD_PATH.toFile().getAbsolutePath(),
+ Constants.SELENIUM_CONTAINER_CHROME_DOWNLOAD_PATH)
+ .withRecordingMode(RECORD_ALL, record.toFile(), MP4)
+ .withStartupTimeout(Duration.ofSeconds(300));
+ }
+ }
+
+ private void setRecordPath() throws IOException {
+ if (!Strings.isNullOrEmpty(System.getenv("RECORDING_PATH"))) {
+ record = Paths.get(System.getenv("RECORDING_PATH"));
+ if (!record.toFile().exists()) {
+ if (!record.toFile().mkdir()) {
+ throw new IOException("Failed to create recording
directory: " + record.toAbsolutePath());
+ }
+ }
+ } else {
+ record = Files.createTempDirectory("record-");
+ }
+ }
+
+ @Override
+ public void afterAll(ExtensionContext context) {
+ browser.afterTest(new TestDescription(context), Optional.empty());
+ browser.stop();
+ if (compose != null) {
+ compose.stop();
+ }
+ }
+
+ @Override
+ public void beforeEach(ExtensionContext context) {
+ final Object instance = context.getRequiredTestInstance();
+ Stream.of(instance.getClass().getDeclaredFields())
+ .filter(f -> WebDriver.class.isAssignableFrom(f.getType()))
+ .forEach(it -> setDriver(instance, it));
+ }
+
+ private void setDriver(Object object, Field field) {
+ try {
+ field.setAccessible(true);
+ field.set(object, driver);
+ } catch (IllegalAccessException e) {
+ log.error("Failed to inject web driver to field: {}",
field.getName(), e);
+ }
+ }
+
+ private ComposeContainer createDockerCompose(ExtensionContext context) {
+ final Class<?> clazz = context.getRequiredTestClass();
+ final StreamPark annotation = clazz.getAnnotation(StreamPark.class);
+ final List<File> files = Stream.of(annotation.composeFiles())
+ .map(it -> StreamPark.class.getClassLoader().getResource(it))
+ .filter(Objects::nonNull)
+ .map(URL::getPath)
+ .map(File::new)
+ .collect(Collectors.toList());
+
+ ComposeContainer compose = new ComposeContainer(files)
+ .withPull(true)
+ .withTailChildContainers(true)
+ .withLocalCompose(true)
+ .withExposedService(
+ serviceName,
+ DOCKER_PORT,
Wait.forListeningPort().withStartupTimeout(Duration.ofSeconds(300)))
+ .withLogConsumer(serviceName, outputFrame ->
log.info(outputFrame.getUtf8String()))
+ .waitingFor(serviceName,
Wait.forHealthcheck().withStartupTimeout(Duration.ofSeconds(300)));
+
+
+ return compose;
+ }
+}
diff --git
a/streampark-e2e/streampark-e2e-core/src/main/java/org/apache/streampark/e2e/core/TestDescription.java
b/streampark-e2e/streampark-e2e-core/src/main/java/org/apache/streampark/e2e/core/TestDescription.java
new file mode 100644
index 000000000..674118504
--- /dev/null
+++
b/streampark-e2e/streampark-e2e-core/src/main/java/org/apache/streampark/e2e/core/TestDescription.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to Apache Software Foundation (ASF) under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Apache Software Foundation (ASF) licenses this file to you 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.
+ */
+
+package org.apache.streampark.e2e.core;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+
+import org.junit.jupiter.api.extension.ExtensionContext;
+
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+final class TestDescription implements
org.testcontainers.lifecycle.TestDescription {
+ private static final String UNKNOWN_NAME = "unknown";
+
+ private final ExtensionContext context;
+
+ @Override
+ public String getTestId() {
+ return context.getUniqueId();
+ }
+
+ @Override
+ public String getFilesystemFriendlyName() {
+ final String contextId = context.getUniqueId();
+ try {
+ return (contextId == null || contextId.trim().isEmpty())
+ ? UNKNOWN_NAME
+ : URLEncoder.encode(contextId, UTF_8.toString());
+ } catch (UnsupportedEncodingException e) {
+ return UNKNOWN_NAME;
+ }
+ }
+}
diff --git a/streampark-e2e/streampark-e2e-core/src/main/resources/log4j2.xml
b/streampark-e2e/streampark-e2e-core/src/main/resources/log4j2.xml
new file mode 100644
index 000000000..167e6e6d9
--- /dev/null
+++ b/streampark-e2e/streampark-e2e-core/src/main/resources/log4j2.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Licensed to the Apache Software Foundation (ASF) under one or more
+ ~ contributor license agreements. See the NOTICE file distributed with
+ ~ this work for additional information regarding copyright ownership.
+ ~ The ASF licenses this file to You 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.
+ ~
+ -->
+
+<Configuration status="DEBUG">
+ <Appenders>
+ <Console name="Console" target="SYSTEM_OUT">
+ <PatternLayout charset="UTF-8" pattern="%d %c %L [%t] %-5p %x -
%m%n"/>
+ </Console>
+ </Appenders>
+ <Loggers>
+ <Root level="INFO">
+ <AppenderRef ref="Console"/>
+ </Root>
+ </Loggers>
+</Configuration>