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>

Reply via email to