You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@dolphinscheduler.apache.org by le...@apache.org on 2021/10/20 13:14:34 UTC

[dolphinscheduler] branch dev updated: Add end-to-end test framework and some basic cases (#6419)

This is an automated email from the ASF dual-hosted git repository.

leonbao pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/dolphinscheduler.git


The following commit(s) were added to refs/heads/dev by this push:
     new 2512550  Add end-to-end test framework and some basic cases (#6419)
2512550 is described below

commit 251255009a857656abf0fe7776b5ae4d68eb4cf7
Author: kezhenxu94 <ke...@apache.org>
AuthorDate: Wed Oct 20 21:14:26 2021 +0800

    Add end-to-end test framework and some basic cases (#6419)
---
 .github/workflows/e2e.yml                          |  68 ++++----
 docker/build/hooks/build                           |  42 +----
 dolphinscheduler-e2e/README.md                     |  98 +++++++++++
 .../dolphinscheduler-e2e-case/pom.xml              |  40 +++++
 .../e2e/cases/security/TenantE2ETest.java          |  98 +++++++++++
 .../dolphinscheduler/e2e/pages/LoginPage.java      |  47 ++++++
 .../dolphinscheduler/e2e/pages/TenantPage.java     |  78 +++++++++
 .../resources/docker/tenant/docker-compose.yaml    |  35 ++++
 .../dolphinscheduler-e2e-core/pom.xml              |  32 ++++
 .../e2e/core/DolphinScheduler.java                 |  41 +++++
 .../e2e/core/DolphinSchedulerExtension.java        | 187 +++++++++++++++++++++
 .../dolphinscheduler/e2e/core/TestDescription.java |  55 ++++++
 .../src/main/resources/log4j2.xml                  |  31 ++++
 dolphinscheduler-e2e/lombok.config                 |  20 +++
 dolphinscheduler-e2e/pom.xml                       | 138 +++++++++++++++
 .../dolphinscheduler/server/StandaloneServer.java  |   2 +
 .../pages/tenement/_source/createTenement.vue      |   5 +
 .../pages/security/pages/tenement/_source/list.vue |   4 +-
 .../home/pages/security/pages/tenement/index.vue   |   2 +-
 dolphinscheduler-ui/src/js/conf/login/App.vue      |   4 +-
 .../src/js/module/components/popup/popover.vue     |  12 +-
 21 files changed, 963 insertions(+), 76 deletions(-)

diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml
index 2fbbffa..e707259 100644
--- a/.github/workflows/e2e.yml
+++ b/.github/workflows/e2e.yml
@@ -15,60 +15,54 @@
 # limitations under the License.
 #
 
-on: ["pull_request"]
+on:
+  pull_request:
+  push:
+    branches:
+      - e2e
+
 env:
-  DOCKER_DIR: ./docker
-  LOG_DIR: /tmp/dolphinscheduler
+  TAG: ci
+  RECORDING_PATH: /tmp/recording
 
-name: Test
+name: E2E
 
 concurrency:
   group: e2e-${{ github.event.pull_request.number || github.ref }}
   cancel-in-progress: true
 
 jobs:
-  test:
-    name: E2E
+  e2e:
+    name: ${{ matrix.case.name }}
     runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        case:
+          - name: Tenant
+            class: org.apache.dolphinscheduler.e2e.cases.security.TenantE2ETest
     steps:
       - uses: actions/checkout@v2
         with:
           submodules: true
       - name: Sanity Check
         uses: ./.github/actions/sanity-check
-      - uses: actions/cache@v1
+      - name: Cache local Maven repository
+        uses: actions/cache@v2
         with:
           path: ~/.m2/repository
-          key: ${{ runner.os }}-maven
+          key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
+          restore-keys: ${{ runner.os }}-maven-
       - name: Build Image
+        run: TAG=ci sh ./docker/build/hooks/build
+      - name: Run Test
         run: |
-          sh ./docker/build/hooks/build
-      - name: Docker Run
-        run: |
-          export VERSION=$(cat $(pwd)/pom.xml | grep '<revision>' -m 1 | awk '{print $1}' | sed 's/<revision>//' | sed 's/<\/revision>//')
-          sed -i "s/apache\/dolphinscheduler:latest/apache\/dolphinscheduler:${VERSION}/g" $(pwd)/docker/docker-swarm/docker-compose.yml
-          docker-compose -f $(pwd)/docker/docker-swarm/docker-compose.yml up -d
-      - name: Check Server Status
-        run: sh $(pwd)/docker/docker-swarm/check
-      - name: Prepare e2e env
-        run: |
-          sudo apt-get install -y libxss1 libappindicator1 libindicator7 xvfb unzip libgbm1
-          wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
-          sudo dpkg -i google-chrome*.deb
-          sudo apt-get install -f -y
-          google-chrome -version
-          googleVersion=$(curl -s https://chromedriver.storage.googleapis.com/LATEST_RELEASE)
-          wget -N https://chromedriver.storage.googleapis.com/${googleVersion}/chromedriver_linux64.zip
-          unzip chromedriver_linux64.zip
-          sudo mv -f chromedriver /usr/local/share/chromedriver
-          sudo ln -s /usr/local/share/chromedriver /usr/local/bin/chromedriver
-#     - name: Run e2e Test
-#       run: cd ./e2e && mvn -B clean test
-      - name: Collect logs
-        if: failure()
-        uses: actions/upload-artifact@v2
+          ./mvnw -f dolphinscheduler-e2e/pom.xml -am \
+            -DfailIfNoTests=false \
+            -Dtest=${{ matrix.case.class }} test
+      - uses: actions/upload-artifact@v2
+        if: always()
+        name: Upload Recording
         with:
-          name: dslogs
-          path: ${{ github.workspace }}/docker/docker-swarm/dolphinscheduler-logs
-
-
+          name: recording
+          path: ${{ env.RECORDING_PATH }}
+          retention-days: 1
diff --git a/docker/build/hooks/build b/docker/build/hooks/build
index 70ea260..1e6a965 100755
--- a/docker/build/hooks/build
+++ b/docker/build/hooks/build
@@ -18,41 +18,17 @@
 
 set -e
 
-echo "------ dolphinscheduler start - build -------"
-printenv
+ROOT_DIR=$(dirname "$0")/../../..
+MVN="$ROOT_DIR"/mvnw
+VERSION=$("$MVN" -q -DforceStdout -N org.apache.maven.plugins:maven-help-plugin:3.2.0:evaluate -Dexpression=project.version)
 
-if [ -z "${VERSION}" ]
-then
-    echo "set default environment variable [VERSION]"
-    export VERSION=$(cat $(pwd)/pom.xml | grep '<version>' -m 1 | awk '{print $1}' | sed 's/<version>//' | sed 's/<\/version>//')
-fi
+DOCKER_REPO=${DOCKER_REPO:-"apache/dolphinscheduler"}
+TAG=${TAG:-"$VERSION"}
 
-if [ "${DOCKER_REPO}x" = "x" ]
-then
-    echo "set default environment variable [DOCKER_REPO]"
-    export DOCKER_REPO='apache/dolphinscheduler'
-fi
+echo "Building Docker image as: $DOCKER_REPO:$TAG"
 
-echo "Version: $VERSION"
-echo "Repo: $DOCKER_REPO"
+"$MVN" -B clean package -Prelease -Dmaven.test.skip=true -Dhttp.keepAlive=false -Dmaven.wagon.http.pool=false -Dmaven.wagon.httpconnectionManager.ttlSeconds=120
 
-echo -e "Current Directory is $(pwd)\n"
+cp "$ROOT_DIR"/dolphinscheduler-dist/target/apache-dolphinscheduler-$VERSION-bin.tar.gz "$ROOT_DIR"/docker/build/
 
-# maven package(Project Directory)
-echo -e "./mvnw -B clean package -Prelease -Dmaven.test.skip=true -Dhttp.keepAlive=false -Dmaven.wagon.http.pool=false -Dmaven.wagon.httpconnectionManager.ttlSeconds=120"
-./mvnw -B clean package -Prelease -Dmaven.test.skip=true -Dhttp.keepAlive=false -Dmaven.wagon.http.pool=false -Dmaven.wagon.httpconnectionManager.ttlSeconds=120
-
-# mv dolphinscheduler-bin.tar.gz file to docker/build directory
-echo -e "mv $(pwd)/dolphinscheduler-dist/target/apache-dolphinscheduler-${VERSION}-bin.tar.gz $(pwd)/docker/build/\n"
-mv $(pwd)/dolphinscheduler-dist/target/apache-dolphinscheduler-${VERSION}-bin.tar.gz $(pwd)/docker/build/
-
-# docker build
-BUILD_COMMAND="docker build --build-arg VERSION=${VERSION} -t $DOCKER_REPO:${VERSION} $(pwd)/docker/build/"
-echo -e "$BUILD_COMMAND\n"
-if (docker info 2> /dev/null | grep -i "ERROR"); then
-    sudo $BUILD_COMMAND
-else
-    $BUILD_COMMAND
-fi
-
-echo "------ dolphinscheduler end   - build -------"
+docker build --build-arg VERSION=$VERSION -t $DOCKER_REPO:$TAG "$ROOT_DIR"/docker/build/
diff --git a/dolphinscheduler-e2e/README.md b/dolphinscheduler-e2e/README.md
new file mode 100644
index 0000000..35c3043
--- /dev/null
+++ b/dolphinscheduler-e2e/README.md
@@ -0,0 +1,98 @@
+# DolphinScheduler End-to-End Test
+
+## Page Object Model
+
+DolphinScheduler End-to-End test respects
+the [Page Object Model (POM)](https://www.selenium.dev/documentation/guidelines/page_object_models/) design pattern.
+Every page of DolphinScheduler is abstracted into a class for better maintainability.
+
+### Example
+
+The login page is abstracted
+as [`LoginPage`](dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/LoginPage.java), with the
+following fields,
+
+```java
+public final class LoginPage {
+    @FindBy(id = "input-username")
+    private WebElement inputUsername;
+
+    @FindBy(id = "input-password")
+    private WebElement inputPassword;
+
+    @FindBy(id = "button-login")
+    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
+
+DolphinScheduler 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
+DolphinScheduler UI port for testing. You can use `@DolphinScheduler(composeFiles = "")` and pass
+the `docker-compose.yaml` files to automatically set up the environment in the test class.
+
+```java
+
+@DolphinScheduler(composeFiles = "docker/tenant/docker-compose.yaml")
+class TenantE2ETest {
+}
+```
+
+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
+
+@DolphinScheduler(composeFiles = "docker/tenant/docker-compose.yaml")
+class TenantE2ETest {
+    private RemoteWebDriver browser;
+}
+```
+
+Then the field `browser` can be used in the test methods.
+
+```java
+
+@DolphinScheduler(composeFiles = "docker/tenant/docker-compose.yaml")
+class TenantE2ETest {
+    private RemoteWebDriver browser;
+
+    @Test
+    void testLogin() {
+        final LoginPage page = new LoginPage(browser); // <<-- use the browser injected
+    }
+}
+```
+
+## 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.
diff --git a/dolphinscheduler-e2e/dolphinscheduler-e2e-case/pom.xml b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/pom.xml
new file mode 100644
index 0000000..17e63be
--- /dev/null
+++ b/dolphinscheduler-e2e/dolphinscheduler-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>dolphinscheduler-e2e</artifactId>
+        <groupId>org.apache.dolphinscheduler</groupId>
+        <version>1.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>dolphinscheduler-e2e-case</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.dolphinscheduler</groupId>
+            <artifactId>dolphinscheduler-e2e-core</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/cases/security/TenantE2ETest.java b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/cases/security/TenantE2ETest.java
new file mode 100644
index 0000000..ab3159a
--- /dev/null
+++ b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/cases/security/TenantE2ETest.java
@@ -0,0 +1,98 @@
+/*
+ * 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.dolphinscheduler.e2e.cases.security;
+
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+
+import org.apache.dolphinscheduler.e2e.core.DolphinScheduler;
+import org.apache.dolphinscheduler.e2e.pages.LoginPage;
+import org.apache.dolphinscheduler.e2e.pages.TenantPage;
+
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.remote.RemoteWebDriver;
+
+@DolphinScheduler(composeFiles = "docker/tenant/docker-compose.yaml")
+class TenantE2ETest {
+    private RemoteWebDriver browser;
+
+    @Test
+    @Order(1)
+    void testLogin() {
+        final LoginPage page = new LoginPage(browser);
+        page.inputUsername().sendKeys("admin");
+        page.inputPassword().sendKeys("dolphinscheduler123");
+        page.buttonLogin().click();
+    }
+
+    @Test
+    @Order(10)
+    void testCreateTenant() {
+        final TenantPage page = new TenantPage(browser);
+        final String tenant = System.getProperty("user.name");
+
+        page.buttonCreateTenant().click();
+        page.createTenantForm().inputTenantCode().sendKeys(tenant);
+        page.createTenantForm().inputDescription().sendKeys("Test");
+        page.createTenantForm().buttonSubmit().click();
+
+        await().untilAsserted(() -> assertThat(page.tenantList())
+                .as("Tenant list should contain newly-created tenant")
+                .extracting(WebElement::getText)
+                .anyMatch(it -> it.contains(tenant)));
+    }
+
+    @Test
+    @Order(20)
+    void testCreateDuplicateTenant() {
+        final String tenant = System.getProperty("user.name");
+        final TenantPage page = new TenantPage(browser);
+        page.buttonCreateTenant().click();
+        page.createTenantForm().inputTenantCode().sendKeys(tenant);
+        page.createTenantForm().inputDescription().sendKeys("Test");
+        page.createTenantForm().buttonSubmit().click();
+
+        await().untilAsserted(() -> assertThat(browser.findElementByTagName("body")
+                                                      .getText().contains("already exists"))
+                .as("Should fail when creating a duplicate tenant")
+                .isTrue());
+
+        page.createTenantForm().buttonCancel().click();
+    }
+
+    @Test
+    @Order(30)
+    void testDeleteTenant() {
+        final String tenant = System.getProperty("user.name");
+        final TenantPage page = new TenantPage(browser);
+
+        page.tenantList()
+            .stream()
+            .filter(it -> it.getText().contains(tenant))
+            .findFirst()
+            .ifPresent(it -> it.findElement(By.className("delete")).click());
+
+        page.buttonConfirm().click();
+    }
+}
diff --git a/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/LoginPage.java b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/LoginPage.java
new file mode 100644
index 0000000..a772517
--- /dev/null
+++ b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/LoginPage.java
@@ -0,0 +1,47 @@
+/*
+ * 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.dolphinscheduler.e2e.pages;
+
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.remote.RemoteWebDriver;
+import org.openqa.selenium.support.FindBy;
+import org.openqa.selenium.support.PageFactory;
+
+import lombok.Getter;
+
+@Getter
+public final class LoginPage {
+    private final RemoteWebDriver driver;
+
+    @FindBy(id = "input-username")
+    private WebElement inputUsername;
+
+    @FindBy(id = "input-password")
+    private WebElement inputPassword;
+
+    @FindBy(id = "button-login")
+    private WebElement buttonLogin;
+
+    public LoginPage(RemoteWebDriver driver) {
+        this.driver = driver;
+
+        PageFactory.initElements(driver, this);
+    }
+}
diff --git a/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/TenantPage.java b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/TenantPage.java
new file mode 100644
index 0000000..da97349
--- /dev/null
+++ b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/TenantPage.java
@@ -0,0 +1,78 @@
+/*
+ * 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.dolphinscheduler.e2e.pages;
+
+import java.util.List;
+
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.FindBy;
+import org.openqa.selenium.support.FindBys;
+import org.openqa.selenium.support.PageFactory;
+
+import lombok.Getter;
+
+@Getter
+public final class TenantPage {
+    private final WebDriver driver;
+
+    @FindBy(id = "button-create-tenant")
+    private WebElement buttonCreateTenant;
+
+    @FindBy(className = "rows-tenant")
+    private List<WebElement> tenantList;
+
+    @FindBys({
+            @FindBy(className = "el-popconfirm"),
+            @FindBy(className = "el-button--primary"),
+    })
+    private WebElement buttonConfirm;
+
+    private final CreateTenantForm createTenantForm;
+
+    public TenantPage(WebDriver driver) {
+        this.driver = driver;
+        this.createTenantForm = new CreateTenantForm();
+
+        PageFactory.initElements(driver, this);
+    }
+
+    @Getter
+    public class CreateTenantForm {
+        CreateTenantForm() {
+            PageFactory.initElements(driver, this);
+        }
+
+        @FindBy(id = "input-tenant-code")
+        private WebElement inputTenantCode;
+
+        @FindBy(id = "select-queue")
+        private WebElement selectQueue;
+
+        @FindBy(id = "input-description")
+        private WebElement inputDescription;
+
+        @FindBy(id = "button-submit")
+        private WebElement buttonSubmit;
+
+        @FindBy(id = "button-cancel")
+        private WebElement buttonCancel;
+    }
+}
diff --git a/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/resources/docker/tenant/docker-compose.yaml b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/resources/docker/tenant/docker-compose.yaml
new file mode 100644
index 0000000..13075d3
--- /dev/null
+++ b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/resources/docker/tenant/docker-compose.yaml
@@ -0,0 +1,35 @@
+#
+# 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.
+#
+
+version: "2.1"
+
+services:
+  dolphinscheduler:
+    image: apache/dolphinscheduler:ci
+    command: [ standalone-server ]
+    expose:
+      - 12345
+    networks:
+      - e2e
+    healthcheck:
+      test: [ "CMD", "bash", "-c", "cat < /dev/null > /dev/tcp/127.0.0.1/12345" ]
+      interval: 5s
+      timeout: 60s
+      retries: 120
+
+networks:
+  e2e:
diff --git a/dolphinscheduler-e2e/dolphinscheduler-e2e-core/pom.xml b/dolphinscheduler-e2e/dolphinscheduler-e2e-core/pom.xml
new file mode 100644
index 0000000..060810c
--- /dev/null
+++ b/dolphinscheduler-e2e/dolphinscheduler-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>dolphinscheduler-e2e</artifactId>
+        <groupId>org.apache.dolphinscheduler</groupId>
+        <version>1.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>dolphinscheduler-e2e-core</artifactId>
+</project>
diff --git a/dolphinscheduler-e2e/dolphinscheduler-e2e-core/src/main/java/org/apache/dolphinscheduler/e2e/core/DolphinScheduler.java b/dolphinscheduler-e2e/dolphinscheduler-e2e-core/src/main/java/org/apache/dolphinscheduler/e2e/core/DolphinScheduler.java
new file mode 100644
index 0000000..8d49ca3
--- /dev/null
+++ b/dolphinscheduler-e2e/dolphinscheduler-e2e-core/src/main/java/org/apache/dolphinscheduler/e2e/core/DolphinScheduler.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.dolphinscheduler.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(DolphinSchedulerExtension.class)
+public @interface DolphinScheduler {
+    String[] composeFiles();
+}
diff --git a/dolphinscheduler-e2e/dolphinscheduler-e2e-core/src/main/java/org/apache/dolphinscheduler/e2e/core/DolphinSchedulerExtension.java b/dolphinscheduler-e2e/dolphinscheduler-e2e-core/src/main/java/org/apache/dolphinscheduler/e2e/core/DolphinSchedulerExtension.java
new file mode 100644
index 0000000..d7f3232
--- /dev/null
+++ b/dolphinscheduler-e2e/dolphinscheduler-e2e-core/src/main/java/org/apache/dolphinscheduler/e2e/core/DolphinSchedulerExtension.java
@@ -0,0 +1,187 @@
+/*
+ * 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.dolphinscheduler.e2e.core;
+
+import static org.testcontainers.containers.BrowserWebDriverContainer.VncRecordingMode.RECORD_ALL;
+import static org.testcontainers.containers.VncRecordingContainer.VncRecordingFormat.MP4;
+
+import java.io.File;
+import java.io.IOException;
+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.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+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.junit.runner.Description;
+import org.junit.runners.model.Statement;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.chrome.ChromeOptions;
+import org.openqa.selenium.remote.RemoteWebDriver;
+import org.testcontainers.containers.BrowserWebDriverContainer;
+import org.testcontainers.containers.ContainerState;
+import org.testcontainers.containers.DockerComposeContainer;
+import org.testcontainers.containers.Network;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.shaded.org.apache.commons.lang.SystemUtils;
+import org.testcontainers.shaded.org.awaitility.Awaitility;
+
+import com.google.common.base.Strings;
+import com.google.common.net.HostAndPort;
+
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+final class DolphinSchedulerExtension
+        implements BeforeAllCallback, AfterAllCallback,
+        BeforeEachCallback {
+    private final boolean LOCAL_MODE = Objects.equals(System.getProperty("local"), "true");
+
+    private RemoteWebDriver driver;
+    private DockerComposeContainer<?> compose;
+    private BrowserWebDriverContainer<?> browser;
+
+    @Override
+    @SuppressWarnings("UnstableApiUsage")
+    public void beforeAll(ExtensionContext context) throws IOException {
+        Awaitility.setDefaultTimeout(Duration.ofSeconds(5));
+        Awaitility.setDefaultPollInterval(Duration.ofSeconds(1));
+
+        Network network = null;
+        HostAndPort address = null;
+        String rootPath = "/";
+        if (!LOCAL_MODE) {
+            compose = createDockerCompose(context);
+            compose.start();
+
+            final ContainerState dsContainer = compose.getContainerByServiceName("dolphinscheduler_1")
+                                                      .orElseThrow(() -> new RuntimeException("Failed to find a container named 'dolphinscheduler'"));
+            final String networkId = dsContainer.getContainerInfo().getNetworkSettings().getNetworks().keySet().iterator().next();
+            network = new Network() {
+                @Override
+                public String getId() {
+                    return networkId;
+                }
+
+                @Override
+                public void close() {
+                }
+
+                @Override
+                public Statement apply(Statement base, Description description) {
+                    return null;
+                }
+            };
+            address = HostAndPort.fromParts("dolphinscheduler", 12345);
+            rootPath = "/dolphinscheduler";
+        }
+
+        final Path record;
+        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-");
+        }
+        browser = new BrowserWebDriverContainer<>()
+                .withCapabilities(new ChromeOptions())
+                .withRecordingMode(RECORD_ALL, record.toFile(), MP4);
+        if (network != null) {
+            browser.withNetwork(network);
+        }
+        browser.start();
+
+        driver = browser.getWebDriver();
+
+        driver.manage().timeouts()
+              .implicitlyWait(5, TimeUnit.SECONDS)
+              .pageLoadTimeout(5, TimeUnit.SECONDS);
+        if (address == null) {
+            try {
+                address = HostAndPort.fromParts(browser.getTestHostIpAddress(), 8888);
+            } catch (UnsupportedOperationException ignored) {
+                if (SystemUtils.IS_OS_MAC || SystemUtils.IS_OS_MAC_OSX) {
+                    address = HostAndPort.fromParts("host.docker.internal", 8888);
+                }
+            }
+        }
+        if (address == null) {
+            throw new UnsupportedOperationException("Unsupported operation system");
+        }
+        driver.get(new URL("http", address.getHost(), address.getPort(), rootPath).toString());
+
+        browser.beforeTest(new TestDescription(context));
+    }
+
+    @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 -> {
+                  try {
+                      it.setAccessible(true);
+                      it.set(instance, driver);
+                  } catch (IllegalAccessException e) {
+                      LOGGER.error("Failed to inject web driver to field: {}", it.getName(), e);
+                  }
+              });
+    }
+
+    private DockerComposeContainer<?> createDockerCompose(ExtensionContext context) {
+        final Class<?> clazz = context.getRequiredTestClass();
+        final DolphinScheduler annotation = clazz.getAnnotation(DolphinScheduler.class);
+        final List<File> files = Stream.of(annotation.composeFiles())
+                                       .map(it -> DolphinScheduler.class.getClassLoader().getResource(it))
+                                       .filter(Objects::nonNull)
+                                       .map(URL::getPath)
+                                       .map(File::new)
+                                       .collect(Collectors.toList());
+        compose = new DockerComposeContainer<>(files)
+                .withPull(true)
+                .withTailChildContainers(true)
+                .waitingFor("dolphinscheduler_1", Wait.forHealthcheck());
+
+        return compose;
+    }
+}
diff --git a/dolphinscheduler-e2e/dolphinscheduler-e2e-core/src/main/java/org/apache/dolphinscheduler/e2e/core/TestDescription.java b/dolphinscheduler-e2e/dolphinscheduler-e2e-core/src/main/java/org/apache/dolphinscheduler/e2e/core/TestDescription.java
new file mode 100644
index 0000000..16f366c
--- /dev/null
+++ b/dolphinscheduler-e2e/dolphinscheduler-e2e-core/src/main/java/org/apache/dolphinscheduler/e2e/core/TestDescription.java
@@ -0,0 +1,55 @@
+/*
+ * 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.dolphinscheduler.e2e.core;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import static org.junit.platform.commons.util.StringUtils.isBlank;
+
+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 (isBlank(contextId))
+                    ? UNKNOWN_NAME
+                    : URLEncoder.encode(contextId, UTF_8.toString());
+        } catch (UnsupportedEncodingException e) {
+            return UNKNOWN_NAME;
+        }
+    }
+}
diff --git a/dolphinscheduler-e2e/dolphinscheduler-e2e-core/src/main/resources/log4j2.xml b/dolphinscheduler-e2e/dolphinscheduler-e2e-core/src/main/resources/log4j2.xml
new file mode 100644
index 0000000..167e6e6
--- /dev/null
+++ b/dolphinscheduler-e2e/dolphinscheduler-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>
diff --git a/dolphinscheduler-e2e/lombok.config b/dolphinscheduler-e2e/lombok.config
new file mode 100644
index 0000000..0056b8f
--- /dev/null
+++ b/dolphinscheduler-e2e/lombok.config
@@ -0,0 +1,20 @@
+#
+# 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.
+#
+
+lombok.accessors.fluent=true
+lombok.log.fieldname=LOGGER
+lombok.accessors.fluent=true
diff --git a/dolphinscheduler-e2e/pom.xml b/dolphinscheduler-e2e/pom.xml
new file mode 100644
index 0000000..cb841af
--- /dev/null
+++ b/dolphinscheduler-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.dolphinscheduler</groupId>
+    <artifactId>dolphinscheduler-e2e</artifactId>
+    <packaging>pom</packaging>
+    <version>1.0-SNAPSHOT</version>
+
+    <modules>
+        <module>dolphinscheduler-e2e-core</module>
+        <module>dolphinscheduler-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.7.2</junit.version>
+        <selenium.version>3.141.59</selenium.version>
+        <lombok.version>1.18.20</lombok.version>
+        <assertj-core.version>3.20.2</assertj-core.version>
+        <awaitility.version>4.1.0</awaitility.version>
+        <kotlin.version>1.5.30</kotlin.version>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <version>1.7.30</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.logging.log4j</groupId>
+            <artifactId>log4j-slf4j-impl</artifactId>
+            <version>2.14.1</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.awaitility</groupId>
+            <artifactId>awaitility</artifactId>
+            <version>${awaitility.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>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.16.0</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/dolphinscheduler-standalone-server/src/main/java/org/apache/dolphinscheduler/server/StandaloneServer.java b/dolphinscheduler-standalone-server/src/main/java/org/apache/dolphinscheduler/server/StandaloneServer.java
index bc42871..e5a9b00 100644
--- a/dolphinscheduler-standalone-server/src/main/java/org/apache/dolphinscheduler/server/StandaloneServer.java
+++ b/dolphinscheduler-standalone-server/src/main/java/org/apache/dolphinscheduler/server/StandaloneServer.java
@@ -125,6 +125,8 @@ public class StandaloneServer {
         if (Files.exists(taskPluginPath)) {
             System.setProperty("task.plugin.binding", taskPluginPath.toString());
             System.setProperty("task.plugin.dir", "");
+        } else {
+            System.setProperty("task.plugin.binding", "lib/plugin/task/shell");
         }
     }
 }
diff --git a/dolphinscheduler-ui/src/js/conf/home/pages/security/pages/tenement/_source/createTenement.vue b/dolphinscheduler-ui/src/js/conf/home/pages/security/pages/tenement/_source/createTenement.vue
index 3bc83a0..1e9b125 100644
--- a/dolphinscheduler-ui/src/js/conf/home/pages/security/pages/tenement/_source/createTenement.vue
+++ b/dolphinscheduler-ui/src/js/conf/home/pages/security/pages/tenement/_source/createTenement.vue
@@ -16,6 +16,8 @@
  */
 <template>
   <m-popover
+          okId="button-submit"
+          cancelId="button-cancel"
           ref="popover"
           :ok-text="item ? $t('Edit') : $t('Submit')"
           @ok="_ok"
@@ -26,6 +28,7 @@
           <template slot="name"><strong>*</strong>{{$t('OS Tenant Code')}}</template>
           <template slot="content">
             <el-input
+                id="input-tenant-code"
                 type="input"
                 :disabled="item ? true : false"
                 v-model="tenantCode"
@@ -40,6 +43,7 @@
           <template slot="content">
             <el-select v-model="queueId" size="small">
               <el-option
+                      id="select-queue"
                       v-for="city in queueList"
                       :key="city.id"
                       :value="city.id"
@@ -52,6 +56,7 @@
           <template slot="name">{{$t('Description')}}</template>
           <template slot="content">
             <el-input
+                    id="input-description"
                     type="textarea"
                     v-model="description"
                     size="small"
diff --git a/dolphinscheduler-ui/src/js/conf/home/pages/security/pages/tenement/_source/list.vue b/dolphinscheduler-ui/src/js/conf/home/pages/security/pages/tenement/_source/list.vue
index 4579137..df980f8 100644
--- a/dolphinscheduler-ui/src/js/conf/home/pages/security/pages/tenement/_source/list.vue
+++ b/dolphinscheduler-ui/src/js/conf/home/pages/security/pages/tenement/_source/list.vue
@@ -17,7 +17,7 @@
 <template>
   <div class="list-model">
     <div class="table-box">
-      <el-table :data="list" size="mini" style="width: 100%">
+      <el-table :data="list" size="mini" style="width: 100%" row-class-name="rows-tenant">
         <el-table-column type="index" :label="$t('#')" width="50"></el-table-column>
         <el-table-column prop="tenantCode" :label="$t('OS Tenant Code')" min-width="100"></el-table-column>
         <el-table-column :label="$t('Description')" min-width="100">
@@ -51,7 +51,7 @@
                 :title="$t('Delete?')"
                 @onConfirm="_delete(scope.row,scope.row.id)"
               >
-                <el-button type="danger" size="mini" icon="el-icon-delete" circle slot="reference"></el-button>
+                <el-button type="danger" size="mini" icon="el-icon-delete" circle slot="reference" class="delete"></el-button>
               </el-popconfirm>
             </el-tooltip>
           </template>
diff --git a/dolphinscheduler-ui/src/js/conf/home/pages/security/pages/tenement/index.vue b/dolphinscheduler-ui/src/js/conf/home/pages/security/pages/tenement/index.vue
index 5c82073..d6c5226 100644
--- a/dolphinscheduler-ui/src/js/conf/home/pages/security/pages/tenement/index.vue
+++ b/dolphinscheduler-ui/src/js/conf/home/pages/security/pages/tenement/index.vue
@@ -19,7 +19,7 @@
     <template slot="conditions">
       <m-conditions @on-conditions="_onConditions">
         <template slot="button-group" v-if="isADMIN">
-          <el-button size="mini" @click="_create('')">{{$t('Create Tenant')}}</el-button>
+          <el-button id="button-create-tenant" size="mini" @click="_create('')">{{$t('Create Tenant')}}</el-button>
           <el-dialog
             :title="item ? $t('Edit Tenant') : $t('Create Tenant')"
             v-if="createTenementDialog"
diff --git a/dolphinscheduler-ui/src/js/conf/login/App.vue b/dolphinscheduler-ui/src/js/conf/login/App.vue
index 29a4cdb..c4beda9 100644
--- a/dolphinscheduler-ui/src/js/conf/login/App.vue
+++ b/dolphinscheduler-ui/src/js/conf/login/App.vue
@@ -24,6 +24,7 @@
         <label>{{$t('User Name')}}</label>
         <div>
           <el-input
+                  id="input-username"
                   type="text"
                   v-model.trim="userName"
                   :placeholder="$t('Please enter user name')"
@@ -39,6 +40,7 @@
         <label>{{$t('Password')}}</label>
         <div>
           <el-input
+                  id="input-password"
                   type="password"
                   v-model="userPassword"
                   :placeholder="$t('Please enter your password')"
@@ -51,7 +53,7 @@
         </p>
       </div>
       <div class="list" style="margin-top: 10px;">
-        <el-button style="width: 365px" type="primary" round :loading="spinnerLoading" long @click="_ok">{{spinnerLoading ? $t('Loading...') : ` ${$t('Login')} `}} </el-button>
+        <el-button id="button-login" style="width: 365px" type="primary" round :loading="spinnerLoading" long @click="_ok">{{spinnerLoading ? $t('Loading...') : ` ${$t('Login')} `}} </el-button>
       </div>
     </div>
   </div>
diff --git a/dolphinscheduler-ui/src/js/module/components/popup/popover.vue b/dolphinscheduler-ui/src/js/module/components/popup/popover.vue
index ee7ecc8..9d3bb6a 100644
--- a/dolphinscheduler-ui/src/js/module/components/popup/popover.vue
+++ b/dolphinscheduler-ui/src/js/module/components/popup/popover.vue
@@ -20,8 +20,8 @@
       <slot name="content"></slot>
     </div>
     <div class="bottom-p">
-      <el-button type="text" size="mini" round @click="close()" :disabled="disabled"> {{$t('Cancel')}} </el-button>
-      <el-button type="primary" size="mini" round :loading="spinnerLoading" @click="ok()" :disabled="disabled || apDisabled">{{spinnerLoading ? $t('Loading...') : okText}} </el-button>
+      <el-button :id="cancelId" type="text" size="mini" round @click="close()" :disabled="disabled"> {{$t('Cancel')}} </el-button>
+      <el-button :id="okId" type="primary" size="mini" round :loading="spinnerLoading" @click="ok()" :disabled="disabled || apDisabled">{{spinnerLoading ? $t('Loading...') : okText}} </el-button>
     </div>
   </div>
 </template>
@@ -47,6 +47,14 @@
       asynLoading: {
         type: Boolean,
         default: false
+      },
+      cancelId: {
+        type: String,
+        default: ''
+      },
+      okId: {
+        type: String,
+        default: ''
       }
     },
     methods: {