From 475c61104d6516a4c0c7a037b2449596bfd35517 Mon Sep 17 00:00:00 2001 From: tanin47 Date: Mon, 10 Nov 2025 14:10:41 -0800 Subject: [PATCH] Improve ergonomics + Github Actions --- .github/workflows/ci.yml | 59 ++++ .../workflows/create-release-and-docker.yml | 87 ++++++ .github/workflows/publish-jar.yml | 64 +++++ README.md | 50 +++- build.gradle.kts | 22 +- ...l_dev_marker.ejwf => local_dev_marker.ejwf | 0 src/main/java/tanin/ejwf/MinumBuilder.java | 262 +++++++++--------- src/test/java/tanin/ejwf/Base.java | 3 + 8 files changed, 400 insertions(+), 147 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/create-release-and-docker.yml create mode 100644 .github/workflows/publish-jar.yml rename src/main/resources/local_dev_marker.ejwf => local_dev_marker.ejwf (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..90d41c2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,59 @@ +name: CI + +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] + +permissions: + contents: read + actions: read + checks: write + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '21' + cache: 'gradle' + - uses: actions/setup-node@v4 + with: + node-version: '22' + - uses: browser-actions/setup-chrome@v2 + with: + chrome-version: 'stable' + install-chromedriver: true + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + with: + cache-read-only: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/master' }} + - name: Install Node modules + run: npm install + - name: Run tests + run: | + npm run hmr & + ./gradlew --no-daemon jacocoTestReport + env: + HEADLESS: true + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_ORG_TOKEN }} + - name: Publish Test Results + uses: dorny/test-reporter@v1 + if: ${{ !cancelled() }} + with: + name: CI test result + path: build/test-results/test/*.xml + reporter: java-junit diff --git a/.github/workflows/create-release-and-docker.yml b/.github/workflows/create-release-and-docker.yml new file mode 100644 index 0000000..fb7e761 --- /dev/null +++ b/.github/workflows/create-release-and-docker.yml @@ -0,0 +1,87 @@ +name: create-release-and-docker + +on: + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + tag: + description: 'Optional tag. Default to the tag of the selected branch.' + required: false + type: string + + +permissions: + contents: write + +jobs: + create-release-and-docker: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || github.ref_name }} + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '21' + cache: 'gradle' + - uses: actions/setup-node@v4 + with: + node-version: '22' + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + with: + cache-read-only: ${{ github.ref != 'refs/heads/main' }} + - name: Get version + id: version + run: echo "VERSION=$(./gradlew -q printVersion)" >> "$GITHUB_OUTPUT" + - name: Validate the tag name + run: | + if [ "${{ github.event_name }}" == "workflow_dispatch" ] && [ ! -z "${{ github.event.inputs.tag }}" ]; then + TAG=${{ github.event.inputs.tag }} + else + TAG=${GITHUB_REF#refs/tags/} + fi + if [[ ! "$TAG" =~ ^v ]]; then + echo "Error: Tag must start with 'v'" + exit 1 + fi + TAG_VERSION=${TAG#v} + if [ "$TAG_VERSION" != "${{ steps.version.outputs.VERSION }}" ]; then + echo "Error: Git tag version ($TAG_VERSION) doesn't match project version (v${{ steps.version.outputs.VERSION }})" + exit 1 + fi + - name: Install Node modules + run: npm install + - name: Build a publishable JAR + run: ./gradlew clean publish + - name: Upload Release Asset + uses: softprops/action-gh-release@v2 + with: + draft: true + prerelease: true + files: ./build/staging-deploy/io/github/tanin47/embeddable-java-web-framework/**/* + overwrite_files: true + fail_on_unmatched_files: true + generate_release_notes: true + tag_name: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || github.ref_name }} + - name: Log in to Docker Hub + uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Build and push Docker image + id: push + uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671 + with: + platforms: linux/amd64,linux/arm64 + context: . + file: ./Dockerfile + push: true + tags: tanin47/embeddable-java-web-framework:${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || github.ref_name }} diff --git a/.github/workflows/publish-jar.yml b/.github/workflows/publish-jar.yml new file mode 100644 index 0000000..db4313b --- /dev/null +++ b/.github/workflows/publish-jar.yml @@ -0,0 +1,64 @@ +name: publish-jar + +on: + workflow_dispatch: + inputs: + tag: + description: 'Optional tag. Default to the tag of the selected branch.' + required: false + type: string + +permissions: + contents: read + +jobs: + publish-jar: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || github.ref_name }} + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '21' + cache: 'gradle' + - uses: actions/setup-node@v4 + with: + node-version: '22' + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + with: + cache-read-only: ${{ github.ref != 'refs/heads/main' }} + - name: Get version + id: version + run: echo "VERSION=$(./gradlew -q printVersion)" >> "$GITHUB_OUTPUT" + - name: Validate tag format and version + run: | + if [ "${{ github.event_name }}" == "workflow_dispatch" ] && [ ! -z "${{ github.event.inputs.tag }}" ]; then + TAG=${{ github.event.inputs.tag }} + else + TAG=${GITHUB_REF#refs/tags/} + fi + if [[ ! $TAG =~ ^v ]]; then + echo "Error: Tag must start with 'v'" + exit 1 + fi + if [[ ! $TAG == v${{ steps.version.outputs.VERSION }} ]]; then + echo "Error: Tag version ($TAG) does not match project version (v${{ steps.version.outputs.VERSION }})" + exit 1 + fi + - name: Install Node modules + run: npm install + - name: Build a publishable JAR + run: ./gradlew clean publish + - name: Publish to Sonatype + run: ./gradlew jreleaserDeploy + env: + JRELEASER_MAVENCENTRAL_USERNAME: ${{ secrets.JRELEASER_MAVENCENTRAL_USERNAME }} + JRELEASER_MAVENCENTRAL_PASSWORD: ${{ secrets.JRELEASER_MAVENCENTRAL_PASSWORD }} + CI: true + JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.JRELEASER_GPG_PUBLIC_KEY }} + JRELEASER_GPG_SECRET_KEY: ${{ secrets.JRELEASER_GPG_SECRET_KEY }} diff --git a/README.md b/README.md index 628f8ea..075854c 100644 --- a/README.md +++ b/README.md @@ -9,16 +9,16 @@ It is suitable for a sidecar-style website embeddable on a larger JVM syste The main selling point of EJWF is that it comes with productive and useful conventions and libraries such as: 1. Support Typescripts + Svelte + Tailwind + DaisyUI with Hot-Reload Module (HMR). -2. Support hot-reloading Java through the plugin sbt-revolver. -3. Support packaging a fat JAR with [shading](https://stackoverflow.com/questions/13620281/what-is-the-maven-shade-plugin-used-for-and-why-would-you-want-to-relocate-java). +2. Support packaging a fat JAR with [shading](https://stackoverflow.com/questions/13620281/what-is-the-maven-shade-plugin-used-for-and-why-would-you-want-to-relocate-java). The JAR is 350KB in size, has *zero* external dependencies, and eliminates any potential dependency conflict when embedding into another JVM system. -4. Avoid Java reflection and magic. This is largely a feature of [Minum](https://github.com/byronka/minum). Any potential runtime errors and conflicts are minimized, which is important when embedding into a larger system. -5. Browser tests are setup and ready to go. +3. Avoid Java reflection and magic. This is largely a feature of [Minum](https://github.com/byronka/minum). Any potential runtime errors and conflicts are minimized, which is important when embedding into a larger system. +4. Browser tests are setup and ready to go. +5. Github actions for testing, code coverage reporting, and publishing have been implemented. In contrast, most of the lightweight web frameworks focus on being a bare metal web server serving HTML and JSON. They don't provide support for any frontend framework like React or Svelte; you would have to do it yourself. This is exactly what EJWF provides. -Initially, EJWF was built as a foundation for [Backdoor](https://github.com/tanin47/backdoor), an embeddable sidecar-style JVM-based database administration tool, where +Initially, EJWF was built as a foundation for [embeddable-java-web-framework](https://github.com/tanin47/embeddable-java-web-framework), a self-hosted database querying and editing tool, where you can embed it into your larger application like SpringBoot or PlayFramework. How to develop @@ -29,18 +29,17 @@ How to develop 3. On a separate terminal, run `npm run hmr` in order to hot-reload the frontend code changes. -Publish --------- +Publish JAR +------------ + +This flow has been set up as the Github Actions workflow: `publish-jar`. EJWF is a template repository with collections of libraries and conventions. It's important that you understand each build process and are able to customize to your needs. Here's how you can build your fat JAR: -1. Run `./gradlew clean`. This step is IMPORTANT to clean out the previous versions. -2. Build the tailwindbase.css with: `./node_modules/.bin/postcss ./frontend/stylesheets/tailwindbase.css --config . --output ./src/main/resources/assets/stylesheets/tailwindbase.css` -3. Build the production Svelte code with: `ENABLE_SVELTE_CHECK=true ./node_modules/webpack/bin/webpack.js --config ./webpack.config.js --output-path ./src/main/resources/assets --mode production` -4. Build the fat JAR with: `./gradlew shadowJar` +1. Run `./gradlew clean publish`. This step is IMPORTANT to clean out the previous versions. The far JAR is built at `./build/libs/embeddablee-java-web-framework-VERSION.jar` @@ -48,10 +47,31 @@ You can run your server with: `java -jar ./build/libs/embeddable-java-web-framew To publish to a Maven repository, please follow the below steps: -1. Remove `./build/staging-deploy` by running `rm -rf ./build/staging-deploy` -2. Run `./gradlew publish` -3. Set up `~/.jreleaser/config.toml` with `JRELEASER_MAVENCENTRAL_USERNAME` and `JRELEASER_MAVENCENTRAL_PASSWORD` -4. Run `./gradlew jreleaserDeploy` +1. Set up `~/.jreleaser/config.toml` with `JRELEASER_MAVENCENTRAL_USERNAME` and `JRELEASER_MAVENCENTRAL_PASSWORD` +2. Run `./gradlew jreleaserDeploy` + + +Publish Docker +--------------- + +This flow has been set up as a part of the Github Actions workflow: `create-release-and-docker`. + +1. Run `docker buildx build --platform linux/amd64,linux/arm64 -t embeddable-java-web-framework:v0.4.0 .` +2. Test locally with: + `docker run -p 9090:9090 --entrypoint "" embeddable-java-web-framework:v0.4.0 java -jar embeddable-java-web-framework-0.4.0.jar -port 9090` +3. Run: `docker tag embeddable-java-web-framework:v0.4.0 tanin47/embeddable-java-web-framework:v0.4.0` +4. Run: `docker push tanin47/embeddable-java-web-framework:v0.4.0` +5. Go to Render.com, sync the blueprint, and test that it works + +Release a new version +---------------------- + +1. Create an empty release with a new tag. The tag must follow the format: `vX.Y.Z`. +2. Go to Actions and wait for the `create-release-and-docker` (which is triggered automatically) workflow to finish. +3. Test the docker with + `docker run -p 9090:9090 --entrypoint "" tanin47/embeddable-java-web-framework:v0.4.0 java -jar embeddable-java-web-framework-0.4.0.jar -port 9090`. +4. Go to Actions and trigger the workflow `publish-jar` on the tag `vX.Y.Z` in order to publish the JAR to Central + Sonatype. Embed your website into a larger system ---------------------------------------- diff --git a/build.gradle.kts b/build.gradle.kts index 8cb5542..7579c51 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,3 +1,4 @@ +import org.gradle.api.tasks.testing.logging.TestExceptionFormat import org.jreleaser.model.Active import org.jreleaser.model.Signing.Mode @@ -5,12 +6,13 @@ plugins { `java-library` application `maven-publish` + jacoco id("org.jreleaser") version "1.21.0" id("com.gradleup.shadow") version "9.2.2" } group = "tanin.ejwf" -version = "0.4.0" +version = "1.0.0-rc1" description = "Embeddable Java Web Framework (EJWF)" @@ -29,6 +31,16 @@ java { } } +tasks.jacocoTestReport { + dependsOn(tasks.test) // tests are required to run before generating the report + + reports { + xml.required = true + csv.required = false + html.outputLocation = layout.buildDirectory.dir("jacocoHtml") + } +} + repositories { mavenCentral() } @@ -47,8 +59,14 @@ tasks.named("test") { maxHeapSize = "1G" testLogging { - events("passed") + events("started", "passed", "skipped", "failed") + showStandardStreams = true + showStackTraces = true + showExceptions = true + showCauses = true + exceptionFormat = TestExceptionFormat.FULL } + } application { diff --git a/src/main/resources/local_dev_marker.ejwf b/local_dev_marker.ejwf similarity index 100% rename from src/main/resources/local_dev_marker.ejwf rename to local_dev_marker.ejwf diff --git a/src/main/java/tanin/ejwf/MinumBuilder.java b/src/main/java/tanin/ejwf/MinumBuilder.java index be7a871..115805b 100644 --- a/src/main/java/tanin/ejwf/MinumBuilder.java +++ b/src/main/java/tanin/ejwf/MinumBuilder.java @@ -8,9 +8,10 @@ import com.renomad.minum.web.StatusLine; import java.net.URI; -import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Map; import java.util.Properties; import java.util.concurrent.Executors; @@ -19,141 +20,142 @@ import static com.renomad.minum.web.RequestLine.Method.GET; public class MinumBuilder { - private static String inferContentType(String assetPath) { - var extension = assetPath.substring(assetPath.lastIndexOf(".") + 1).toLowerCase(); - return switch (extension) { - case "js" -> "application/javascript"; - case "css" -> "text/css"; - case "png" -> "image/png"; - case "jpg", "jpeg" -> "image/jpeg"; - case "gif" -> "image/gif"; - case "svg" -> "image/svg+xml"; - case "ico" -> "image/x-icon"; - case "woff" -> "font/woff"; - case "woff2" -> "font/woff2"; - case "ttf" -> "font/ttf"; - case "eot" -> "application/vnd.ms-fontobject"; - default -> "application/octet-stream"; - }; + private static final java.util.logging.Logger logger = java.util.logging.Logger.getLogger(MinumBuilder.class.getName()); + private static String inferContentType(String assetPath) { + var extension = assetPath.substring(assetPath.lastIndexOf(".") + 1).toLowerCase(); + return switch (extension) { + case "js" -> "application/javascript"; + case "css" -> "text/css"; + case "png" -> "image/png"; + case "jpg", "jpeg" -> "image/jpeg"; + case "gif" -> "image/gif"; + case "svg" -> "image/svg+xml"; + case "ico" -> "image/x-icon"; + case "woff" -> "font/woff"; + case "woff2" -> "font/woff2"; + case "ttf" -> "font/ttf"; + case "eot" -> "application/vnd.ms-fontobject"; + default -> "application/octet-stream"; + }; + } + + + public static final boolean IS_LOCAL_DEV = Files.exists(Path.of("local_dev_marker.ejwf")); + + public static FullSystem build(int port) { + if (IS_LOCAL_DEV) { + logger.info("Running in the local development mode. Hot-Reload Module is enabled. `npm run hmr` must be running in a separate terminal"); + } else { + logger.info("Running in the production mode."); } - private static final HttpClient httpClient = HttpClient.newHttpClient(); - - public static FullSystem build(int port) { - var isLocalDev = Main.class.getResourceAsStream("/local_dev_marker.ejwf") != null; - - if (isLocalDev) { - System.out.println("Running in the local development mode. Hot-Reload Module is enabled. `npm run hmr` must be running in a separate terminal"); - } else { - System.out.println("Running in the production mode."); + var props = new Properties(); + props.setProperty("SERVER_PORT", "" + port); + props.setProperty("LOG_LEVELS", "ASYNC_ERROR,AUDIT"); + props.setProperty("IS_THE_BRIG_ENABLED", "false"); + + var context = new Context(Executors.newVirtualThreadPerTaskExecutor(), new Constants(props)); + context.setLogger(new Logger(context.getConstants(), context.getExecutorService(), "primary logger")); + var minum = new FullSystem(context).start(); + var wf = minum.getWebFramework(); + + if (IS_LOCAL_DEV) { + var httpClient = java.net.http.HttpClient.newHttpClient(); + wf.registerPartialPath( + GET, + "__webpack_hmr", + request -> { + var httpRequest = HttpRequest + .newBuilder() + .uri(URI.create("http://localhost:8090/__webpack_hmr")) + .GET() + .build(); + var response = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofByteArray()); + + return Response.buildResponse( + StatusLine.StatusCode.CODE_200_OK, + response.headers().map().entrySet().stream().collect(java.util.stream.Collectors.toMap(Map.Entry::getKey, e -> String.join(",", e.getValue()))), + response.body() + ); } - - var props = new Properties(); - props.setProperty("SERVER_PORT", "" + port); - props.setProperty("LOG_LEVELS", "ASYNC_ERROR,AUDIT"); - - var context = new Context(Executors.newVirtualThreadPerTaskExecutor(), new Constants(props)); - var logger = new Logger(context.getConstants(), context.getExecutorService(), "primary logger"); - context.setLogger(logger); - var minum = new FullSystem(context).start(); - var wf = minum.getWebFramework(); - - if (isLocalDev) { - wf.registerPartialPath( - GET, - "__webpack_hmr", - request -> { - var httpRequest = HttpRequest - .newBuilder() - .uri(URI.create("http://localhost:8090/__webpack_hmr")) - .GET() - .build(); - var response = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofByteArray()); - - return Response.buildResponse( - StatusLine.StatusCode.CODE_200_OK, - response.headers().map().entrySet().stream().collect(java.util.stream.Collectors.toMap(Map.Entry::getKey, e -> String.join(",", e.getValue()))), - response.body() - ); - } - ); - wf.registerPartialPath( - GET, - "assets/", - request -> { - var pattern = Pattern.compile("assets/(?.*$)"); - var path = request.getRequestLine().getPathDetails().getIsolatedPath(); - var matcher = pattern.matcher(path); - - if (!matcher.find()) { - return Response.buildLeanResponse(StatusLine.StatusCode.CODE_404_NOT_FOUND); - } - - var assetPath = matcher.group("assetPath"); - - if (assetPath.startsWith("images/")) { - return Response.buildResponse( - StatusLine.StatusCode.CODE_200_OK, - Map.of( - "Content-Type", "image/png" - ), - Main.class.getResourceAsStream("/assets/" + assetPath).readAllBytes() - ); - } - - var httpRequest = HttpRequest - .newBuilder() - .uri(URI.create("http://localhost:8090/assets/" + assetPath)) - .GET() - .build(); - var response = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofByteArray()); - - return Response.buildResponse( - StatusLine.StatusCode.CODE_200_OK, - response.headers().map().entrySet().stream().collect(java.util.stream.Collectors.toMap(Map.Entry::getKey, e -> String.join(",", e.getValue()))), - response.body() - ); - } - ); - } else { - wf.registerPartialPath( - GET, - "assets/", - request -> { - var pattern = Pattern.compile("assets/(?.*$)"); - var path = request.getRequestLine().getPathDetails().getIsolatedPath(); - var matcher = pattern.matcher(path); - - if (!matcher.find()) { - return Response.buildLeanResponse(StatusLine.StatusCode.CODE_404_NOT_FOUND); - } - - var assetPath = matcher.group("assetPath"); - var resource = Main.class.getResourceAsStream("/assets/" + assetPath); - - if (resource == null) { - return Response.buildLeanResponse(StatusLine.StatusCode.CODE_404_NOT_FOUND); - } - - return Response.buildResponse( - StatusLine.StatusCode.CODE_200_OK, - Map.of( - "Content-Type", inferContentType(assetPath) - ), - resource.readAllBytes() - ); - } + ); + wf.registerPartialPath( + GET, + "assets/", + request -> { + var pattern = Pattern.compile("assets/(?.*$)"); + var path = request.getRequestLine().getPathDetails().getIsolatedPath(); + var matcher = pattern.matcher(path); + + if (!matcher.find()) { + return Response.buildLeanResponse(StatusLine.StatusCode.CODE_404_NOT_FOUND); + } + + var assetPath = matcher.group("assetPath"); + + if (assetPath.startsWith("images/")) { + return Response.buildResponse( + StatusLine.StatusCode.CODE_200_OK, + Map.of( + "Content-Type", "image/png" + ), + Main.class.getResourceAsStream("/assets/" + assetPath).readAllBytes() ); + } + + var httpRequest = HttpRequest + .newBuilder() + .uri(URI.create("http://localhost:8090/assets/" + assetPath)) + .GET() + .build(); + var response = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofByteArray()); + + return Response.buildResponse( + StatusLine.StatusCode.CODE_200_OK, + response.headers().map().entrySet().stream().collect(java.util.stream.Collectors.toMap(Map.Entry::getKey, e -> String.join(",", e.getValue()))), + response.body() + ); + } + ); + } else { + wf.registerPartialPath( + GET, + "assets/", + request -> { + var pattern = Pattern.compile("assets/(?.*$)"); + var path = request.getRequestLine().getPathDetails().getIsolatedPath(); + var matcher = pattern.matcher(path); + + if (!matcher.find()) { + return Response.buildLeanResponse(StatusLine.StatusCode.CODE_404_NOT_FOUND); + } + + var assetPath = matcher.group("assetPath"); + var resource = Main.class.getResourceAsStream("/assets/" + assetPath); + + if (resource == null) { + return Response.buildLeanResponse(StatusLine.StatusCode.CODE_404_NOT_FOUND); + } + + return Response.buildResponse( + StatusLine.StatusCode.CODE_200_OK, + Map.of( + "Content-Type", inferContentType(assetPath) + ), + resource.readAllBytes() + ); } + ); + } - Runtime.getRuntime().addShutdownHook(new Thread(minum::shutdown)); + Runtime.getRuntime().addShutdownHook(new Thread(minum::shutdown)); - // In SBT console, pressing Ctrl+C only sends SIGINT. Therefore, we have to trigger a shutdown when SIGINT occurs. - sun.misc.Signal.handle(new sun.misc.Signal("INT"), sig -> { - System.out.println("Received SIGINT signal. Shutting down..."); - minum.shutdown(); - }); + // In SBT console, pressing Ctrl+C only sends SIGINT. Therefore, we have to trigger a shutdown when SIGINT occurs. + sun.misc.Signal.handle(new sun.misc.Signal("INT"), sig -> { + logger.info("Received SIGINT signal. Shutting down..."); + minum.shutdown(); + }); - return minum; - } + return minum; + } } diff --git a/src/test/java/tanin/ejwf/Base.java b/src/test/java/tanin/ejwf/Base.java index b5fe56c..4dff317 100644 --- a/src/test/java/tanin/ejwf/Base.java +++ b/src/test/java/tanin/ejwf/Base.java @@ -38,6 +38,9 @@ public void initializeWebDriver() { } var options = new ChromeOptions(); + if (System.getenv("HEADLESS") != null) { + options.addArguments("--headless"); + } options.addArguments("--guest"); options.addArguments("--disable-extensions"); options.addArguments("--disable-web-security");