From 5ac01167aadb6691ec3762b7c5947928d83ab938 Mon Sep 17 00:00:00 2001 From: melloware Date: Sat, 26 Oct 2024 11:33:44 -0400 Subject: [PATCH] Native Build work --- .github/workflows/build.yml | 4 +- README.md | 51 ++++++- .../deployment/PlaywrightProcessor.java | 127 +++++++++++++++++- docs/modules/ROOT/pages/index.adoc | 58 ++++++-- integration-tests/pom.xml | 58 +++++++- .../src/main/docker/Dockerfile.native | 28 ++++ .../playwright/it/PlaywrightResource.java | 11 +- .../runtime/PlaywrightRecorder.java | 62 +++++++++ .../resources/META-INF/quarkus-extension.yaml | 6 +- .../playwright/InjectPlaywright.java | 32 ++++- .../playwright/QuarkusPlaywrightManager.java | 86 +++++++++++- .../playwright/WithPlaywright.java | 98 +++++++++++--- 12 files changed, 571 insertions(+), 50 deletions(-) create mode 100644 integration-tests/src/main/docker/Dockerfile.native create mode 100644 runtime/src/main/java/io/quarkiverse/playwright/runtime/PlaywrightRecorder.java diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 67d527b..2a3a214 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -55,5 +55,5 @@ jobs: - name: Build with Maven run: mvn -B clean install -Dno-format --no-transfer-progress - #- name: Build with Maven (Native) - # run: mvn -B install -Dnative -Dquarkus.native.container-build -Dnative.surefire.skip --no-transfer-progress \ No newline at end of file + - name: Build with Maven (Native) + run: mvn -B install -Dnative -Dquarkus.native.container-build -Dnative.surefire.skip --no-transfer-progress \ No newline at end of file diff --git a/README.md b/README.md index e962a3e..87f58b1 100644 --- a/README.md +++ b/README.md @@ -9,17 +9,23 @@ [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg?style=flat-square)](https://opensource.org/licenses/Apache-2.0) [![Build](https://github.com/quarkiverse/quarkus-playwright/actions/workflows/build.yml/badge.svg)](https://github.com/quarkiverse/quarkus-playwright/actions/workflows/build.yml) +[Playwright](https://playwright.dev/) is an open-source automation library designed for browser testing and web scraping. This extension supports two primary use cases: -Easily create effective cross-browsers e2e tests for your Quarkus web-app using Playwright (Qute, Quinoa, Renarde, Web-Bundler, ...): +1. **Testing:** Perform end-to-end tests for your Quarkus web application. +2. **Runtime:** Leverage Playwright for screen scraping or other browser tasks in your runtime application, including support for GraalVM native compilation. All the information you need to use Quarkus Playwright is in the [user documentation](https://docs.quarkiverse.io/quarkus-playwright/dev/). -## Usage -Add to pom.xml: +## Test Usage + +The primary use case for Playwright is integration with `@QuarkusTest` for end-to-end testing of your application. You can easily create effective cross-browser end-to-end tests for your Quarkus web application using Playwright with frameworks such as Qute, Quinoa, Renarde, Web-Bundler, and MyFaces. Playwright Test was specifically designed to meet the requirements of end-to-end testing. It supports all modern rendering engines, including Chromium, WebKit, and Firefox. You can run tests on Windows, Linux, and macOS—either locally or in CI—both in headless and headed modes, with native mobile emulation for Google Chrome on Android and Mobile Safari. + + +Just add the `test` dependency to pom.xml: ```xml io.quarkiverse.playwright - quarkus-playwright + quarkus-playwright-test ${playwright.version} test @@ -55,9 +61,44 @@ public class WithDefaultPlaywrightTest { } ```` +Use the annotation `@WithPlaywright()` to change the browser (Chromium, Firefox, Webkit), headless, enable debug, logs and other options. + Debug your tests with the Playwright inspector `@WithPlaywright(debug=true)`: ![Debug](https://github.com/quarkiverse/quarkus-playwright/blob/main/docs/modules/ROOT/assets/images/playwright-debug.gif) + +## Runtime Usage + +Leverage Playwright for screen scraping or other browser tasks in your runtime application, including support for GraalVM native compilation. + +Just add the `runtime` dependency to pom.xml: +```xml + + io.quarkiverse.playwright + quarkus-playwright + ${playwright.version} + +``` + +## Native + +If you plan on running in a Docker image we highly recommend you use a pre-built image from Microsoft `mcr.microsoft.com/playwright:v1.48.1` which is based on Ubuntu and already has all libraries and tools necessary for PlayWright. + +```yaml +FROM mcr.microsoft.com/playwright:v1.48.1-noble +WORKDIR /work/ +RUN chown 1001:root /work \ + && chmod g+rwX /work \ + && chown 1001:root /work +COPY --chown=1001:root target/*.properties target/*.so /work/ +COPY --chown=1001:root target/*-runner /work/application +# Make application executable for all users +RUN chmod ugo+x /work/application +EXPOSE 8080 +USER 1001 +CMD ["./application", "-Dquarkus.http.host=0.0.0.0"] +``` + ## Contributors ✨ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): @@ -87,4 +128,4 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d -This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! +This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! \ No newline at end of file diff --git a/deployment/src/main/java/io/quarkiverse/playwright/deployment/PlaywrightProcessor.java b/deployment/src/main/java/io/quarkiverse/playwright/deployment/PlaywrightProcessor.java index 30cff28..c88ba00 100644 --- a/deployment/src/main/java/io/quarkiverse/playwright/deployment/PlaywrightProcessor.java +++ b/deployment/src/main/java/io/quarkiverse/playwright/deployment/PlaywrightProcessor.java @@ -1,7 +1,36 @@ package io.quarkiverse.playwright.deployment; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Collectors; + +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; + +import com.microsoft.playwright.Browser; +import com.microsoft.playwright.ElementHandle; +import com.microsoft.playwright.Playwright; +import com.microsoft.playwright.impl.driver.jar.DriverJar; +import com.microsoft.playwright.options.HttpHeader; +import com.microsoft.playwright.options.Timing; +import com.microsoft.playwright.options.ViewportSize; + +import io.quarkiverse.playwright.runtime.PlaywrightRecorder; +import io.quarkus.deployment.IsNormal; +import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.IndexDependencyBuildItem; +import io.quarkus.deployment.builditem.NativeImageEnableAllCharsetsBuildItem; +import io.quarkus.deployment.builditem.nativeimage.NativeImageResourcePatternsBuildItem; +import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; +import io.quarkus.logging.Log; class PlaywrightProcessor { @@ -11,4 +40,100 @@ class PlaywrightProcessor { FeatureBuildItem feature() { return new FeatureBuildItem(FEATURE); } -} + + @BuildStep + void indexTransitiveDependencies(BuildProducer index) { + index.produce(new IndexDependencyBuildItem("com.microsoft.playwright", "driver")); + index.produce(new IndexDependencyBuildItem("com.microsoft.playwright", "driver-bundle")); + index.produce(new IndexDependencyBuildItem("com.microsoft.playwright", "playwright")); + } + + @BuildStep + NativeImageEnableAllCharsetsBuildItem enableAllCharsetsBuildItem() { + return new NativeImageEnableAllCharsetsBuildItem(); + } + + @BuildStep + void registerForReflection(CombinedIndexBuildItem combinedIndex, BuildProducer reflectiveClass) { + //@formatter:off + final List classNames = new ArrayList<>(); + + classNames.add("com.microsoft.playwright.impl.Message"); + classNames.add("com.microsoft.playwright.impl.SerializedArgument"); + classNames.add("com.microsoft.playwright.impl.SerializedValue"); + classNames.add("com.microsoft.playwright.impl.SerializedValue$O"); + classNames.add(Browser.CloseOptions.class.getName()); + classNames.add(Browser.NewContextOptions.class.getName()); + classNames.add(Browser.NewPageOptions.class.getName()); + classNames.add(Browser.StartTracingOptions.class.getName()); + classNames.add(DriverJar.class.getName()); + classNames.add(ElementHandle.CheckOptions.class.getName()); + classNames.add(ElementHandle.ClickOptions.class.getName()); + classNames.add(ElementHandle.DblclickOptions.class.getName()); + classNames.add(ElementHandle.FillOptions.class.getName()); + classNames.add(ElementHandle.HoverOptions.class.getName()); + classNames.add(ElementHandle.InputValueOptions.class.getName()); + classNames.add(ElementHandle.PressOptions.class.getName()); + classNames.add(ElementHandle.ScreenshotOptions.class.getName()); + classNames.add(ElementHandle.ScrollIntoViewIfNeededOptions.class.getName()); + classNames.add(ElementHandle.SelectTextOptions.class.getName()); + classNames.add(ElementHandle.SetInputFilesOptions.class.getName()); + classNames.add(ElementHandle.TapOptions.class.getName()); + classNames.add(ElementHandle.TypeOptions.class.getName()); + classNames.add(ElementHandle.UncheckOptions.class.getName()); + classNames.add(ElementHandle.WaitForElementStateOptions.class.getName()); + classNames.add(ElementHandle.WaitForSelectorOptions.class.getName()); + classNames.add(HttpHeader.class.getName()); + classNames.add(Timing.class.getName()); + classNames.add(ViewportSize.class.getName()); + classNames.addAll(collectImplementors(combinedIndex, Playwright.class.getName())); + + //@formatter:on + final TreeSet uniqueClasses = new TreeSet<>(classNames); + Log.debugf("Playwright Reflection: %s", uniqueClasses); + + reflectiveClass.produce( + ReflectiveClassBuildItem.builder(uniqueClasses.toArray(new String[0])).constructors().methods().fields() + .serialization().unsafeAllocated().build()); + } + + @BuildStep(onlyIf = IsNormal.class) + @Record(ExecutionTime.RUNTIME_INIT) + void registerRuntimeDrivers(PlaywrightRecorder recorder) { + recorder.initialize(); + } + + @BuildStep(onlyIf = IsNormal.class) + void registerNativeDrivers(BuildProducer nativeImageResourcePatterns) { + final NativeImageResourcePatternsBuildItem.Builder builder = NativeImageResourcePatternsBuildItem.builder(); + builder.includeGlob("driver/**"); + nativeImageResourcePatterns.produce(builder.build()); + } + + private List collectSubclasses(CombinedIndexBuildItem combinedIndex, String className) { + List classes = combinedIndex.getIndex() + .getAllKnownSubclasses(DotName.createSimple(className)) + .stream() + .map(ClassInfo::toString) + .collect(Collectors.toList()); + classes.add(className); + Log.debugf("Subclasses: %s", classes); + return classes; + } + + private List collectImplementors(CombinedIndexBuildItem combinedIndex, String className) { + Set classes = combinedIndex.getIndex() + .getAllKnownImplementors(DotName.createSimple(className)) + .stream() + .map(ClassInfo::toString) + .collect(Collectors.toCollection(HashSet::new)); + classes.add(className); + Set subclasses = new HashSet<>(); + for (String implementationClass : classes) { + subclasses.addAll(collectSubclasses(combinedIndex, implementationClass)); + } + classes.addAll(subclasses); + Log.debugf("Implementors: %s", classes); + return new ArrayList<>(classes); + } +} \ No newline at end of file diff --git a/docs/modules/ROOT/pages/index.adoc b/docs/modules/ROOT/pages/index.adoc index a750a99..d7ced97 100644 --- a/docs/modules/ROOT/pages/index.adoc +++ b/docs/modules/ROOT/pages/index.adoc @@ -2,27 +2,30 @@ include::./includes/attributes.adoc[] -Easily create effective cross-browsers e2e tests for your Quarkus web-app using Playwright (Qute, Quinoa, Renarde, Web-Bundler, ...): +https://playwright.dev/[Playwright] is an open-source automation library designed for browser testing and web scraping. This extension supports two primary use cases: -== Installation +1. **Testing:** Perform end-to-end tests for your Quarkus web application. +2. **Runtime:** Leverage Playwright for screen scraping or other browser tasks in your runtime application, including support for GraalVM native compilation. -If you want to use this extension, you need to add the `io.quarkiverse.playwright:quarkus-playwright` extension first to your build file. +== Test Usage -For instance, with Maven, add the following dependency to your POM file: +The primary use case for Playwright is integration with `@QuarkusTest` for end-to-end testing of your application. You can easily create effective cross-browser end-to-end tests for your Quarkus web application using Playwright with frameworks such as Qute, Quinoa, Renarde, Web-Bundler, and MyFaces. Playwright Test was specifically designed to meet the requirements of end-to-end testing. It supports all modern rendering engines, including Chromium, WebKit, and Firefox. You can run tests on Windows, Linux, and macOS—either locally or in CI—both in headless and headed modes, with native mobile emulation for Google Chrome on Android and Mobile Safari. + +Just add the `test` dependency to `pom.xml`: [source,xml,subs=attributes+] ---- io.quarkiverse.playwright - quarkus-playwright + quarkus-playwright-test {project-version} test ---- -== Usage +Write your tests: -[source,java] +[source, java] ---- @QuarkusTest @WithPlaywright @@ -32,6 +35,7 @@ public class WithDefaultPlaywrightTest { BrowserContext context; @TestHTTPResource("/") + URL index; @Test @@ -53,7 +57,43 @@ public class WithDefaultPlaywrightTest { } ---- -Use the annotation `@WithPlaywright()` to change the browser (Chromium, Firefox, Webkit), headless, enable debug, logs and other options. +Use the annotation `@WithPlaywright()` to change the browser (Chromium, Firefox, Webkit), headless, enable debug, logs, and other options. + +Debug your tests with the Playwright inspector `@WithPlaywright(debug=true)`. + +== Runtime Usage + +Leverage Playwright for screen scraping or other browser tasks in your runtime application, including support for GraalVM native compilation. + +Just add the `runtime` dependency to `pom.xml`: + +[source,xml,subs=attributes+] +---- + + io.quarkiverse.playwright + quarkus-playwright + {project-version} + +---- + +== Native + +If you plan on running in a Docker image we highly recommend you use a pre-built image from Microsoft `mcr.microsoft.com/playwright:v1.48.1` which is based on Ubuntu and already has all libraries and tools necessary for PlayWright. +[source] +---- +FROM mcr.microsoft.com/playwright:v1.48.1-noble +WORKDIR /work/ +RUN chown 1001:root /work \ + && chmod g+rwX /work \ + && chown 1001:root /work +COPY --chown=1001:root target/*.properties target/*.so /work/ +COPY --chown=1001:root target/*-runner /work/application +# Make application executable for all users +RUN chmod ugo+x /work/application +EXPOSE 8080 +USER 1001 +CMD ["./application", "-Dquarkus.http.host=0.0.0.0"] +---- -include::includes/quarkus-playwright.adoc[leveloffset=+1, opts=optional] +include::includes/quarkus-playwright.adoc[leveloffset=+1, opts=optional] \ No newline at end of file diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 9a60037..a06f3fd 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -51,9 +51,21 @@ rest-assured test - + + + + org.codehaus.mojo + exec-maven-plugin + 3.5.0 + + docker + ${project.basedir} + + + + io.quarkus @@ -122,5 +134,49 @@ native + + native-docker + + + native-docker + + + + true + true + + --trace-object-instantiation=java.awt.BasicStroke + + + + clean package + + + org.codehaus.mojo + exec-maven-plugin + + + + docker-build + package + + exec + + + + build + -f + src/main/docker/Dockerfile.native + -t + playwright/integration-test:${project.version} + . + + + + + + + + \ No newline at end of file diff --git a/integration-tests/src/main/docker/Dockerfile.native b/integration-tests/src/main/docker/Dockerfile.native new file mode 100644 index 0000000..0476fd7 --- /dev/null +++ b/integration-tests/src/main/docker/Dockerfile.native @@ -0,0 +1,28 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode. +# +# Before building the container image run: +# +# ./mvnw package -Pnative +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.native -t playwright/integration-test . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 playwright/integration-test +# +### +FROM mcr.microsoft.com/playwright:v1.48.1-noble +WORKDIR /work/ +RUN chown 1001:root /work \ + && chmod g+rwX /work \ + && chown 1001:root /work +COPY --chown=1001:root target/*.properties target/*.so /work/ +COPY --chown=1001:root target/*-runner /work/application +# Make application executable for all users +RUN chmod ugo+x /work/application +EXPOSE 8080 +USER 1001 +CMD ["./application", "-Dquarkus.http.host=0.0.0.0"] \ No newline at end of file diff --git a/integration-tests/src/main/java/io/quarkiverse/playwright/it/PlaywrightResource.java b/integration-tests/src/main/java/io/quarkiverse/playwright/it/PlaywrightResource.java index b6264eb..f7ee45a 100644 --- a/integration-tests/src/main/java/io/quarkiverse/playwright/it/PlaywrightResource.java +++ b/integration-tests/src/main/java/io/quarkiverse/playwright/it/PlaywrightResource.java @@ -24,6 +24,8 @@ import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; +import org.jboss.logging.Logger; + import com.microsoft.playwright.Browser; import com.microsoft.playwright.BrowserType; import com.microsoft.playwright.Page; @@ -32,24 +34,27 @@ @Path("/playwright") @ApplicationScoped public class PlaywrightResource { - // add some rest methods here + + private static final Logger log = Logger.getLogger(PlaywrightResource.class); @GET public String google() { - String pageTitle = "Hello playwright"; + String pageTitle; final BrowserType.LaunchOptions launchOptions = new BrowserType.LaunchOptions() .setHeadless(true) .setChromiumSandbox(false) .setChannel("") .setArgs(List.of("--disable-gpu")); final Map env = new HashMap<>(System.getenv()); + env.put("DEBUG", "pw:api"); try (Playwright playwright = Playwright.create(new Playwright.CreateOptions().setEnv(env))) { try (Browser browser = playwright.chromium().launch(launchOptions)) { Page page = browser.newPage(); page.navigate("https://www.google.com/"); pageTitle = page.title(); + log.infof("Page title: %s", pageTitle); } } return pageTitle; } -} \ No newline at end of file +} diff --git a/runtime/src/main/java/io/quarkiverse/playwright/runtime/PlaywrightRecorder.java b/runtime/src/main/java/io/quarkiverse/playwright/runtime/PlaywrightRecorder.java new file mode 100644 index 0000000..c0570f5 --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/playwright/runtime/PlaywrightRecorder.java @@ -0,0 +1,62 @@ +package io.quarkiverse.playwright.runtime; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.util.Collections; + +import org.jboss.logging.Logger; + +import com.microsoft.playwright.impl.driver.jar.DriverJar; + +import io.quarkus.runtime.annotations.Recorder; + +/** + * A recorder for managing Playwright driver initialization. + * This class handles the setup of the Playwright driver by + * loading the driver resources and creating a file system + * for those resources. + */ +@Recorder +public class PlaywrightRecorder { + + private static final Logger log = Logger.getLogger(PlaywrightRecorder.class); + + /** + * Initializes the Playwright driver by obtaining its resource URI, + * setting up a file system, and logging relevant information. + * + *

+ * This method attempts to retrieve the URI of the driver resources + * and creates a new file system for accessing those resources. If the + * file system cannot be created, an error is logged. + *

+ * + * In Native mode this is `resource:/` and must be registered before anything else. + * + * @throws RuntimeException if there is an error in URI syntax or + * during file system creation. + */ + public void initialize() { + try { + // Retrieve the URI of the Playwright driver resources + URI uri = DriverJar.getDriverResourceURI(); + log.infof("Playwright Driver: %s", uri); + + // Create a new file system for the driver resources typically resource:/ + FileSystem fs = FileSystems.newFileSystem(uri, Collections.emptyMap()); + if (fs == null) { + log.errorf("FileSystem Error NULL: %s", uri); + } + + // Log the directory where the driver is located + DriverJar jar = new DriverJar(); + log.debugf("Playwright Driver Directory: %s", jar.driverDir()); + } catch (URISyntaxException | IOException e) { + // Wrap and throw any exceptions that occur during initialization + throw new RuntimeException(e); + } + } +} \ No newline at end of file diff --git a/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/runtime/src/main/resources/META-INF/quarkus-extension.yaml index 95f5c46..3378317 100644 --- a/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -1,5 +1,5 @@ name: Playwright -description: Playwright enables reliable end-to-end testing for modern web apps. +description: Playwright is an open-source automation library for browser testing and web scraping. metadata: type: test keywords: @@ -7,8 +7,12 @@ metadata: - e2e - end-to-end - playwright + - browser + - screen-scraper guide: https://quarkiverse.github.io/quarkiverse-docs/playwright/dev/ icon-url: "https://playwright.dev/java/img/playwright-logo.svg" categories: - "testing" + config: + - "quarkus.playwright." status: "stable" \ No newline at end of file diff --git a/testing/src/main/java/io/quarkiverse/playwright/InjectPlaywright.java b/testing/src/main/java/io/quarkiverse/playwright/InjectPlaywright.java index c9eb49e..7865932 100644 --- a/testing/src/main/java/io/quarkiverse/playwright/InjectPlaywright.java +++ b/testing/src/main/java/io/quarkiverse/playwright/InjectPlaywright.java @@ -5,7 +5,33 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.FIELD) +/** + * Annotation to mark a field for injection of a Playwright instance. + *

+ * This annotation is used in Quarkus applications to automatically inject + * Playwright resources into fields where it is applied. It is intended + * for use with fields of classes that depend on Playwright for browser automation + * and testing. + *

+ *

+ * Usage example: + * + *

+ * {@code
+ * @InjectPlaywright
+ * private Playwright playwright;
+ * }
+ * 
+ *

+ * + *

+ * The annotation should be retained at runtime, as it is processed by the + * Quarkus framework to inject the necessary Playwright instances. + *

+ * + * @since 1.0 + */ +@Retention(RetentionPolicy.RUNTIME) // The annotation is available at runtime for Quarkus to process. +@Target(ElementType.FIELD) // This annotation can only be applied to fields. public @interface InjectPlaywright { -} +} \ No newline at end of file diff --git a/testing/src/main/java/io/quarkiverse/playwright/QuarkusPlaywrightManager.java b/testing/src/main/java/io/quarkiverse/playwright/QuarkusPlaywrightManager.java index d60ddd8..f86d957 100644 --- a/testing/src/main/java/io/quarkiverse/playwright/QuarkusPlaywrightManager.java +++ b/testing/src/main/java/io/quarkiverse/playwright/QuarkusPlaywrightManager.java @@ -16,35 +16,85 @@ import io.quarkus.test.common.QuarkusTestResourceConfigurableLifecycleManager; +/** + * Manages the lifecycle of a Playwright instance in Quarkus tests. + *

+ * This class implements {@link QuarkusTestResourceConfigurableLifecycleManager} and is responsible + * for initializing and configuring Playwright resources, such as the {@link Browser}, + * {@link BrowserContext}, and {@link Playwright} instance, according to the settings specified + * in the {@link WithPlaywright} annotation. + *

+ * + *

+ * This manager supports injecting Playwright resources into test classes marked with the + * {@code @InjectPlaywright} annotation, allowing easy access to {@link BrowserContext}, + * {@link Browser}, or {@link Playwright} instances. + *

+ * + * @see WithPlaywright + * @see InjectPlaywright + * @see QuarkusTestResourceConfigurableLifecycleManager + * @since 1.0 + */ public class QuarkusPlaywrightManager implements QuarkusTestResourceConfigurableLifecycleManager { + + /** Holds the configuration options from the {@link WithPlaywright} annotation. */ private WithPlaywright options; + /** The global Playwright instance for managing browser creation and operations. */ private Playwright playwright; + /** The context in which the browser operates, encapsulating tabs, storage, etc. */ private BrowserContext playwrightContext; + /** The specific browser instance (Chromium, Firefox, WebKit) launched by Playwright. */ private Browser playwrightBrowser; + /** + * Initializes the Playwright manager with configuration from {@link WithPlaywright}. + * + * @param withPlaywright the Playwright configuration options + */ @Override public void init(WithPlaywright withPlaywright) { this.options = withPlaywright; } + /** + * Not used in this implementation. Throws an exception to ensure only {@code @WithPlaywright} + * annotation is used for initialization. + * + * @param initArgs ignored initialization arguments + * @throws IllegalStateException always, as this method is unsupported + */ @Override public void init(Map initArgs) { throw new IllegalStateException("Use @WithPlaywright() annotation instead"); } + /** + * Starts the Playwright environment and configures the browser and context based on + * {@link WithPlaywright} options. + * + * @return an empty map as no additional environment variables are required + */ @Override public Map start() { final Map env = new HashMap<>(System.getenv()); + + // Enable Playwright verbose logging if requested if (!env.containsKey("DEBUG") && this.options.verbose()) { env.put("DEBUG", "pw:api"); } + // Enable Playwright debug mode if specified if (!env.containsKey("PWDEBUG") && this.options.debug()) { env.put("PWDEBUG", "1"); } + + // Create Playwright instance with the specified environment variables this.playwright = Playwright.create(new Playwright.CreateOptions().setEnv(env)); + + // Configure launch options based on @WithPlaywright attributes final LaunchOptions launchOptions = new LaunchOptions() .setChannel(this.options.channel()) .setChromiumSandbox(this.options.chromiumSandbox()) @@ -52,17 +102,27 @@ public Map start() { .setSlowMo(this.options.slowMo()) .setEnv(env) .setArgs(Arrays.asList(this.options.args())); - this.playwrightBrowser = browser(playwright, this.options.browser()).launch( - launchOptions); + // Launch the browser based on the specified type (CHROMIUM, FIREFOX, or WEBKIT) + this.playwrightBrowser = browser(playwright, this.options.browser()).launch(launchOptions); + + // Configure the context, setting the video directory if specified final Browser.NewContextOptions contextOptions = new Browser.NewContextOptions(); if (StringUtils.isNotBlank(this.options.recordVideoDir())) { contextOptions.setRecordVideoDir(Paths.get(this.options.recordVideoDir())); } this.playwrightContext = playwrightBrowser.newContext(contextOptions); + return Collections.emptyMap(); } + /** + * Helper method to retrieve the correct {@link BrowserType} based on the specified browser. + * + * @param playwright the Playwright instance + * @param browser the browser type from the {@link WithPlaywright.Browser} enum + * @return the corresponding {@link BrowserType} instance + */ private static BrowserType browser(Playwright playwright, WithPlaywright.Browser browser) { return switch (browser) { case FIREFOX -> playwright.firefox(); @@ -71,26 +131,42 @@ private static BrowserType browser(Playwright playwright, WithPlaywright.Browser }; } + /** + * Closes and cleans up the Playwright resources, ensuring proper shutdown. + */ @Override public void stop() { if (this.playwrightContext != null) { - this.playwrightContext.close(); + this.playwrightContext.close(); // Closes the browser context this.playwrightContext = null; } if (playwright != null) { - playwright.close(); + playwright.close(); // Closes the Playwright instance playwright = null; } } + /** + * Injects Playwright resources into fields annotated with {@code @InjectPlaywright} in test classes. + *

+ * This method supports injection of {@link BrowserContext}, {@link Browser}, and {@link Playwright} + * into appropriately annotated fields. + *

+ * + * @param testInjector the test injector responsible for dependency injection + */ @Override public void inject(TestInjector testInjector) { + // Injects BrowserContext if @InjectPlaywright is present on a matching field testInjector.injectIntoFields(playwrightContext, new TestInjector.AnnotatedAndMatchesType(InjectPlaywright.class, BrowserContext.class)); + + // Injects Playwright if @InjectPlaywright is present on a matching field testInjector.injectIntoFields(playwright, new TestInjector.AnnotatedAndMatchesType(InjectPlaywright.class, Playwright.class)); + + // Injects Browser if @InjectPlaywright is present on a matching field testInjector.injectIntoFields(playwrightBrowser, new TestInjector.AnnotatedAndMatchesType(InjectPlaywright.class, Browser.class)); } - } \ No newline at end of file diff --git a/testing/src/main/java/io/quarkiverse/playwright/WithPlaywright.java b/testing/src/main/java/io/quarkiverse/playwright/WithPlaywright.java index fc66eba..c9fbd24 100644 --- a/testing/src/main/java/io/quarkiverse/playwright/WithPlaywright.java +++ b/testing/src/main/java/io/quarkiverse/playwright/WithPlaywright.java @@ -7,65 +7,123 @@ import io.quarkus.test.common.QuarkusTestResource; +/** + * Annotation to configure and enable Playwright for Quarkus tests. + *

+ * This annotation sets up a Playwright testing environment with options for + * browser selection, debugging, and configuration of other Playwright features. + * It is applied at the class level and managed by {@link QuarkusPlaywrightManager}. + *

+ * + *

+ * Usage example: + * + *

+ * {@code
+ * @WithPlaywright(browser = Browser.FIREFOX, headless = false, verbose = true)
+ * public class PlaywrightTest {
+ *     // Test code here
+ * }
+ * }
+ * 
+ *

+ * + * @see io.quarkus.test.common.QuarkusTestResource + * @see QuarkusPlaywrightManager + * @since 1.0 + */ @QuarkusTestResource(QuarkusPlaywrightManager.class) -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) // Annotation is retained at runtime for test setup. +@Target(ElementType.TYPE) // Applied only at the class level. public @interface WithPlaywright { /** - * Browser to use + * Specifies the browser to use for Playwright tests. + *

+ * Defaults to {@link Browser#CHROMIUM}. + *

*/ Browser browser() default Browser.CHROMIUM; /** - * Enable playwright logs + * Enables Playwright verbose logging. + *

+ * Set to {@code true} to enable detailed logging, useful for debugging. + *

*/ boolean verbose() default false; /** - * Enable Playwright Debug Inspector + * Enables the Playwright Debug Inspector for debugging tests. + *

+ * Use {@code true} to launch the inspector, which pauses tests for interactive debugging. + *

*/ boolean debug() default false; /** - * Browser distribution channel. Supported values are "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", - * "msedge-beta", "msedge-dev", "msedge-canary". Read more about using Google Chrome and Microsoft Edge. + * Specifies the distribution channel of the browser to use, such as "chrome" or "msedge". + *

+ * Supported values include "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", + * "msedge-beta", "msedge-dev", and "msedge-canary". + *

+ *

+ * Refer to the + * Playwright documentation for additional details on using these channels. + *

*/ String channel() default ""; /** - * Enable Chromium sandboxing. Defaults to {@code false}. + * Enables sandboxing for Chromium-based browsers. + *

+ * Defaults to {@code false} for compatibility. Set to {@code true} to enable sandboxing if supported. + *

*/ boolean chromiumSandbox() default false; /** - * Whether to run browser in headless mode. More details for Chromium and Firefox. Defaults to {@code true} - * unless the {@code devtools} option is {@code true}. + * Runs the browser in headless mode, which is suitable for CI environments. + *

+ * Defaults to {@code true} unless the {@code devtools} option is enabled. + *

+ *

+ * See more about headless mode in + * Chromium and Firefox. + *

*/ boolean headless() default true; /** - * Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on. + * Slows down Playwright operations by the specified number of milliseconds. + *

+ * This is useful for observing browser interactions more clearly during tests. + *

*/ double slowMo() default 0; /** - * Enables video recording for all pages into the specified directory. If not specified videos are not recorded. + * Specifies the directory to store video recordings of all pages. + *

+ * If not set, video recording is disabled. + *

*/ String recordVideoDir() default ""; /** - * Args for use to launch the browser + * Specifies command-line arguments to use when launching the browser. + *

+ * Defaults to disabling GPU with {@code "--disable-gpu"}. + *

*/ String[] args() default { "--disable-gpu" }; + /** + * Enum representing the supported browsers for Playwright testing. + */ enum Browser { - CHROMIUM, - FIREFOX, - WEBKIT + CHROMIUM, // Google Chrome and other Chromium-based browsers. + FIREFOX, // Mozilla Firefox browser. + WEBKIT // WebKit browser, primarily for Safari compatibility. } - } \ No newline at end of file