From 5b10a85547ccf7c9fb2912c94048e062b9b7746b Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Mon, 19 Oct 2026 15:01:43 -0700 Subject: [PATCH 1/3] feat(junit): replace OptionsFactory with @PlaywrightOptions annotation Fixes: https://github.com/microsoft/playwright-java/issues/1881 --- .../playwright/impl/junit/DefaultOptions.java | 27 ---- .../playwright/impl/junit/ExtensionUtils.java | 6 - .../impl/junit/OptionsExtension.java | 109 ++++++++++++++-- .../microsoft/playwright/junit/Options.java | 4 +- .../playwright/junit/OptionsFactory.java | 62 --------- .../playwright/junit/PlaywrightOptions.java | 83 ++++++++++++ .../playwright/junit/UsePlaywright.java | 7 +- .../playwright/TestAPIResponseAssertions.java | 8 +- .../TestAssertThatIsInViewport.java | 8 +- .../playwright/TestBeforeunload.java | 9 +- .../microsoft/playwright/TestBrowser1.java | 9 +- .../microsoft/playwright/TestBrowser2.java | 72 +++++------ .../TestBrowserContextAddCookies.java | 10 +- .../playwright/TestBrowserContextBaseUrl.java | 8 +- .../playwright/TestBrowserContextBasic.java | 12 +- .../TestBrowserContextClearCookies.java | 8 +- ...TestOptionsFactories.java => TestEnv.java} | 13 +- .../microsoft/playwright/TestRequestGC.java | 7 +- .../playwright/TestRouteWebSocket.java | 8 +- .../microsoft/playwright/TestScreencast.java | 10 +- .../impl/junit/TestOptionsExtension.java | 118 ++++++++++++++++++ .../playwright/junit/ServerLifecycle.java | 10 +- .../junit/TestFixtureChannelOption.java | 12 +- .../junit/TestFixtureContextOptions.java | 22 ++-- .../junit/TestFixtureDeviceOption.java | 12 +- .../junit/TestFixtureNewContextOptions.java | 30 ++--- .../playwright/junit/TestFixtureOptions.java | 25 ++-- .../junit/TestFixturePlaywrightOptions.java | 81 ++++++++++++ .../TestFixturePlaywrightOptionsStatic.java | 53 ++++++++ .../junit/TestFixtureTraceOutputDir.java | 11 +- 30 files changed, 602 insertions(+), 252 deletions(-) delete mode 100644 playwright/src/main/java/com/microsoft/playwright/impl/junit/DefaultOptions.java delete mode 100644 playwright/src/main/java/com/microsoft/playwright/junit/OptionsFactory.java create mode 100644 playwright/src/main/java/com/microsoft/playwright/junit/PlaywrightOptions.java rename playwright/src/test/java/com/microsoft/playwright/{TestOptionsFactories.java => TestEnv.java} (83%) create mode 100644 playwright/src/test/java/com/microsoft/playwright/impl/junit/TestOptionsExtension.java create mode 100644 playwright/src/test/java/com/microsoft/playwright/junit/TestFixturePlaywrightOptions.java create mode 100644 playwright/src/test/java/com/microsoft/playwright/junit/TestFixturePlaywrightOptionsStatic.java diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/junit/DefaultOptions.java b/playwright/src/main/java/com/microsoft/playwright/impl/junit/DefaultOptions.java deleted file mode 100644 index 4334f1f17..000000000 --- a/playwright/src/main/java/com/microsoft/playwright/impl/junit/DefaultOptions.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (c) Microsoft Corporation. - * - * Licensed 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 com.microsoft.playwright.impl.junit; - -import com.microsoft.playwright.junit.Options; -import com.microsoft.playwright.junit.OptionsFactory; - -public class DefaultOptions implements OptionsFactory { - @Override - public Options getOptions() { - return new Options(); - } -} diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/junit/ExtensionUtils.java b/playwright/src/main/java/com/microsoft/playwright/impl/junit/ExtensionUtils.java index ea9d6239f..755e076ac 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/junit/ExtensionUtils.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/junit/ExtensionUtils.java @@ -23,7 +23,6 @@ import org.junit.jupiter.api.extension.ParameterContext; import org.junit.platform.commons.support.AnnotationSupport; -import static org.junit.platform.commons.support.AnnotationSupport.findAnnotation; class ExtensionUtils { static boolean hasUsePlaywrightAnnotation(ExtensionContext extensionContext) { @@ -31,11 +30,6 @@ static boolean hasUsePlaywrightAnnotation(ExtensionContext extensionContext) { return AnnotationSupport.isAnnotated(outerClass, UsePlaywright.class); } - static UsePlaywright getUsePlaywrightAnnotation(ExtensionContext extensionContext) { - Class topLevelClass = getOuterClass(extensionContext.getRequiredTestClass()); - return findAnnotation(topLevelClass, UsePlaywright.class).get(); - } - static boolean isClassHook(ExtensionContext extensionContext) { return !extensionContext.getTestMethod().isPresent(); } diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/junit/OptionsExtension.java b/playwright/src/main/java/com/microsoft/playwright/impl/junit/OptionsExtension.java index d36eb700e..39c36b40e 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/junit/OptionsExtension.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/junit/OptionsExtension.java @@ -18,14 +18,39 @@ import com.microsoft.playwright.PlaywrightException; import com.microsoft.playwright.junit.Options; -import com.microsoft.playwright.junit.UsePlaywright; +import com.microsoft.playwright.junit.PlaywrightOptions; import org.junit.jupiter.api.extension.AfterAllCallback; import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.platform.commons.support.AnnotationSupport; +import org.junit.platform.commons.support.HierarchyTraversalMode; +import org.junit.platform.commons.support.ModifierSupport; +import org.junit.platform.commons.support.ReflectionSupport; -import static com.microsoft.playwright.impl.junit.ExtensionUtils.*; +import java.lang.reflect.Field; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import static java.util.stream.Collectors.joining; public class OptionsExtension implements AfterAllCallback { - private static final ThreadLocal threadLocalOptions = new ThreadLocal<>(); + private static final ThreadLocal threadLocalOptions = new ThreadLocal<>(); + + private static class CachedOptions { + final Options options; + // @PlaywrightOptions member the options were read from, null when the test class has none. + final Member source; + // Test instance the options were read from, null for static members. + final Object testInstance; + + CachedOptions(Options options, Member source, Object testInstance) { + this.options = options; + this.source = source; + this.testInstance = testInstance; + } + } @Override public void afterAll(ExtensionContext extensionContext) { @@ -33,18 +58,76 @@ public void afterAll(ExtensionContext extensionContext) { } static Options getOptions(ExtensionContext extensionContext) { - Options options = threadLocalOptions.get(); - if (options != null) { - return options; - } + Member member = findOptionsMember(extensionContext.getRequiredTestClass()); + Object testInstance = member == null || ModifierSupport.isStatic(member) ? null : getTestInstance(extensionContext, member); - UsePlaywright usePlaywrightAnnotation = getUsePlaywrightAnnotation(extensionContext); - try { - options = usePlaywrightAnnotation.value().newInstance().getOptions(); - threadLocalOptions.set(options); - } catch (InstantiationException | IllegalAccessException e) { - throw new PlaywrightException("Failed to create options", e); + CachedOptions cached = threadLocalOptions.get(); + if (cached != null && Objects.equals(cached.source, member) && cached.testInstance == testInstance) { + return cached.options; } + Options options = member == null ? new Options() : readOptions(member, testInstance); + threadLocalOptions.set(new CachedOptions(options, member, testInstance)); return options; } + + private static Object getTestInstance(ExtensionContext extensionContext, Member member) { + Class declaringClass = member.getDeclaringClass(); + return extensionContext.getTestInstances() + .flatMap(instances -> instances.findInstance(declaringClass)) + .orElseThrow(() -> new PlaywrightException(describe(member) + " requires a test instance. Make it static, or use " + + "@TestInstance(TestInstance.Lifecycle.PER_CLASS) to request Playwright fixtures in @BeforeAll methods or constructors.")); + } + + // Looks for a @PlaywrightOptions field or method in the test class, its superclasses and its enclosing classes. + static Member findOptionsMember(Class testClass) { + for (Class clazz = testClass; clazz != null; clazz = clazz.getDeclaringClass()) { + List members = new ArrayList<>(); + members.addAll(AnnotationSupport.findAnnotatedFields(clazz, PlaywrightOptions.class)); + members.addAll(AnnotationSupport.findAnnotatedMethods(clazz, PlaywrightOptions.class, HierarchyTraversalMode.BOTTOM_UP)); + if (members.size() > 1) { + throw new PlaywrightException("Only one @PlaywrightOptions field or method is allowed, found: " + + members.stream().map(OptionsExtension::describe).collect(joining(", "))); + } + if (!members.isEmpty()) { + return validateOptionsMember(members.get(0)); + } + } + return null; + } + + private static Member validateOptionsMember(Member member) { + Class type; + if (member instanceof Field) { + type = ((Field) member).getType(); + } else { + Method method = (Method) member; + if (method.getParameterCount() != 0) { + throw new PlaywrightException(describe(member) + " must not have parameters."); + } + type = method.getReturnType(); + } + if (!Options.class.isAssignableFrom(type)) { + throw new PlaywrightException(describe(member) + " must be of type " + Options.class.getName() + "."); + } + return member; + } + + private static Options readOptions(Member member, Object testInstance) { + Object options; + if (member instanceof Field) { + options = ReflectionSupport.tryToReadFieldValue((Field) member, testInstance) + .getOrThrow(e -> new PlaywrightException("Failed to read " + describe(member), e)); + } else { + options = ReflectionSupport.invokeMethod((Method) member, testInstance); + } + if (options == null) { + throw new PlaywrightException(describe(member) + " must not be null."); + } + return (Options) options; + } + + private static String describe(Member member) { + String name = member.getDeclaringClass().getName() + "." + member.getName(); + return member instanceof Field ? "@PlaywrightOptions field " + name : "@PlaywrightOptions method " + name + "()"; + } } diff --git a/playwright/src/main/java/com/microsoft/playwright/junit/Options.java b/playwright/src/main/java/com/microsoft/playwright/junit/Options.java index 0a9e8b8f9..add03047d 100644 --- a/playwright/src/main/java/com/microsoft/playwright/junit/Options.java +++ b/playwright/src/main/java/com/microsoft/playwright/junit/Options.java @@ -23,8 +23,8 @@ /** * NOTE: this API is experimental and is subject to changes. * - *

Instances of this class are expected to be created by custom {@link OptionsFactory} - * implementations. Implement custom factories to provide custom Playwright configurations. + *

Provide an instance of this class from a field or a method of the test class + * annotated with {@link PlaywrightOptions} to customize the Playwright fixtures. * *

For more details and usage examples see our * JUnit guide. diff --git a/playwright/src/main/java/com/microsoft/playwright/junit/OptionsFactory.java b/playwright/src/main/java/com/microsoft/playwright/junit/OptionsFactory.java deleted file mode 100644 index 3a6256a1a..000000000 --- a/playwright/src/main/java/com/microsoft/playwright/junit/OptionsFactory.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (c) Microsoft Corporation. - * - * Licensed 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 com.microsoft.playwright.junit; - -/** - * NOTE: this API is experimental and is subject to changes. - * - *

Implement this interface to pass custom options to {@link UsePlaywright} - * annotation. - * - *

An example of implementing {@code @OptionsFactory}: - *

{@code
- * import com.microsoft.playwright.junit.Options;
- * import com.microsoft.playwright.junit.OptionsFactory;
- * import com.microsoft.playwright.junit.UsePlaywright;
- *
- * @UsePlaywright(MyTest.CustomOptions.class)
- * public class MyTest {
- *
- *   public static class CustomOptions implements OptionsFactory {
- *     @Override
- *     public Options getOptions() {
- *       return new Options()
- *           .setHeadless(false)
- *           .setContextOptions(new Browser.NewContextOptions()
- *               .setBaseURL("https://github.com"))
- *           .setApiRequestOptions(new APIRequest.NewContextOptions()
- *               .setBaseURL("https://playwright.dev"));
- *     }
- *   }
- *
- *   @Test
- *   public void testWithCustomOptions(Page page, APIRequestContext request) {
- *     page.navigate("/");
- *     assertThat(page).hasURL(Pattern.compile("github"));
- *
- *     APIResponse response = request.get("/");
- *     assertTrue(response.text().contains("Playwright"));
- *   }
- * }
- * }
- * - *

For more details and usage examples see our - * JUnit guide. - */ -public interface OptionsFactory { - Options getOptions(); -} diff --git a/playwright/src/main/java/com/microsoft/playwright/junit/PlaywrightOptions.java b/playwright/src/main/java/com/microsoft/playwright/junit/PlaywrightOptions.java new file mode 100644 index 000000000..bce8c2c30 --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/junit/PlaywrightOptions.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) Microsoft Corporation. + * + * Licensed 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 com.microsoft.playwright.junit; + +import java.lang.annotation.*; + +/** + * NOTE: this API is experimental and is subject to changes. + * + *

Marks the field or method of a test class annotated with {@link UsePlaywright} that + * provides the {@link Options} for the Playwright fixtures. A field must be of type + * {@code Options}, a method must return {@code Options} and take no parameters. + * + *

A static field or method is read once per test class: + *

{@code
+ * @UsePlaywright
+ * public class MyTest {
+ *   @PlaywrightOptions
+ *   static final Options options = new Options()
+ *       .setHeadless(false)
+ *       .setContextOptions(new Browser.NewContextOptions()
+ *           .setBaseURL("https://github.com"));
+ *
+ *   @Test
+ *   void testWithCustomOptions(Page page) {
+ *     page.navigate("/");
+ *     assertThat(page).hasURL(Pattern.compile("github"));
+ *   }
+ * }
+ * }
+ * + *

A non-static field or method is read for each test instance, so the options can depend + * on the state of the instance, e.g. on a server injected by a dependency injection framework: + *

{@code
+ * @UsePlaywright
+ * @MicronautTest
+ * public class MyTest {
+ *   @Inject
+ *   EmbeddedServer server;
+ *
+ *   @PlaywrightOptions
+ *   Options options() {
+ *     return new Options().setBaseUrl(server.getURL().toString());
+ *   }
+ *
+ *   @Test
+ *   void testWithEmbeddedServer(Page page) {
+ *     page.navigate("/");
+ *   }
+ * }
+ * }
+ * {@code Playwright} and {@code Browser} are shared between tests, so they are created with the + * options of the first test instance; only the options of {@code BrowserContext} and + * {@code APIRequestContext} take effect for every test instance. Unless the test class uses + * {@code @TestInstance(TestInstance.Lifecycle.PER_CLASS)}, a non-static member cannot be used + * when Playwright fixtures are requested in {@code @BeforeAll} methods or in the constructor, + * because there is no test instance yet. + * + *

The annotated member may be declared in a superclass or in an enclosing class of the test + * class. + * + *

For more details and usage examples see our + * JUnit guide. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.METHOD}) +@Documented +public @interface PlaywrightOptions { +} diff --git a/playwright/src/main/java/com/microsoft/playwright/junit/UsePlaywright.java b/playwright/src/main/java/com/microsoft/playwright/junit/UsePlaywright.java index 6f81c3fa4..a7bbed4d6 100644 --- a/playwright/src/main/java/com/microsoft/playwright/junit/UsePlaywright.java +++ b/playwright/src/main/java/com/microsoft/playwright/junit/UsePlaywright.java @@ -25,8 +25,8 @@ * NOTE: this API is experimental and is subject to changes. * * Use {@code @UsePlaywright} annotation to automatically manage Playwright objects - * used in your test. Custom configuration can be provided by implementing - * {@link OptionsFactory} and passing the class as a parameter. + * used in your test. Custom configuration can be provided by annotating a field or + * a method of the test class with {@link PlaywrightOptions}. * *

When a test class is annotated with {@code @UsePlaywright} each test method can * use any of the following arguments that will be automatically created at run time: @@ -41,7 +41,7 @@ * {@code Page} and {@code BrowserContext} are created before each test and closed * after the test has finished. {@code Browser} and {@code Playwright} are reused * between tests for better efficiency. {@code Browser.NewContextOptions} is a new instance - * every time it is requested, configured from the {@link OptionsFactory} the same way as for + * every time it is requested, configured from the {@link Options} the same way as for * the {@code BrowserContext} fixture. Unlike {@code Page} and {@code BrowserContext}, it can also * be used in {@code @BeforeAll} and {@code @AfterAll} methods to create a context manually: *

{@code
@@ -90,5 +90,4 @@
 @Target(ElementType.TYPE)
 @Inherited
 public @interface UsePlaywright {
-  Class value() default DefaultOptions.class;
 }
diff --git a/playwright/src/test/java/com/microsoft/playwright/TestAPIResponseAssertions.java b/playwright/src/test/java/com/microsoft/playwright/TestAPIResponseAssertions.java
index 3f124b9e2..0961c3792 100644
--- a/playwright/src/test/java/com/microsoft/playwright/TestAPIResponseAssertions.java
+++ b/playwright/src/test/java/com/microsoft/playwright/TestAPIResponseAssertions.java
@@ -17,6 +17,8 @@
 package com.microsoft.playwright;
 
 import com.microsoft.playwright.junit.FixtureTest;
+import com.microsoft.playwright.junit.Options;
+import com.microsoft.playwright.junit.PlaywrightOptions;
 import com.microsoft.playwright.junit.UsePlaywright;
 import org.junit.jupiter.api.Test;
 import org.opentest4j.AssertionFailedError;
@@ -24,12 +26,16 @@
 import java.io.OutputStreamWriter;
 import java.io.Writer;
 
+import static com.microsoft.playwright.TestEnv.getBrowserName;
 import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
 import static org.junit.jupiter.api.Assertions.*;
 
 @FixtureTest
-@UsePlaywright(TestOptionsFactories.BasicOptionsFactory.class)
+@UsePlaywright
 public class TestAPIResponseAssertions {
+  @PlaywrightOptions
+  static final Options options = new Options().setBrowserName(getBrowserName());
+
   @Test
   void passWithResponse(Page page, Server server) {
     APIResponse res = page.request().get(server.EMPTY_PAGE);
diff --git a/playwright/src/test/java/com/microsoft/playwright/TestAssertThatIsInViewport.java b/playwright/src/test/java/com/microsoft/playwright/TestAssertThatIsInViewport.java
index b2b0e56d0..38c7f494c 100644
--- a/playwright/src/test/java/com/microsoft/playwright/TestAssertThatIsInViewport.java
+++ b/playwright/src/test/java/com/microsoft/playwright/TestAssertThatIsInViewport.java
@@ -18,17 +18,23 @@
 
 import com.microsoft.playwright.assertions.LocatorAssertions;
 import com.microsoft.playwright.junit.FixtureTest;
+import com.microsoft.playwright.junit.Options;
+import com.microsoft.playwright.junit.PlaywrightOptions;
 import com.microsoft.playwright.junit.UsePlaywright;
 import org.junit.jupiter.api.Test;
 import org.opentest4j.AssertionFailedError;
 
+import static com.microsoft.playwright.TestEnv.getBrowserName;
 import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
 import static org.junit.jupiter.api.Assertions.*;
 
 // Copied from expect-misc.spec.ts > toBeInViewport
 @FixtureTest
-@UsePlaywright(TestOptionsFactories.BasicOptionsFactory.class)
+@UsePlaywright
 public class TestAssertThatIsInViewport {
+  @PlaywrightOptions
+  static final Options options = new Options().setBrowserName(getBrowserName());
+
   @Test
   void shouldWork(Page page) {
     page.setContent("
\n" + diff --git a/playwright/src/test/java/com/microsoft/playwright/TestBeforeunload.java b/playwright/src/test/java/com/microsoft/playwright/TestBeforeunload.java index 112b49ac0..97ad983df 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestBeforeunload.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestBeforeunload.java @@ -17,12 +17,19 @@ package com.microsoft.playwright; import com.microsoft.playwright.junit.FixtureTest; +import com.microsoft.playwright.junit.Options; +import com.microsoft.playwright.junit.PlaywrightOptions; import com.microsoft.playwright.junit.UsePlaywright; import org.junit.jupiter.api.Test; +import static com.microsoft.playwright.TestEnv.getBrowserName; + @FixtureTest -@UsePlaywright(TestOptionsFactories.BasicOptionsFactory.class) +@UsePlaywright public class TestBeforeunload { + @PlaywrightOptions + static final Options options = new Options().setBrowserName(getBrowserName()); + @Test void shouldBeAbleToNavigateAwayFromPageWithBeforeunload(Page page, Server server) { page.navigate(server.PREFIX + "/beforeunload.html"); diff --git a/playwright/src/test/java/com/microsoft/playwright/TestBrowser1.java b/playwright/src/test/java/com/microsoft/playwright/TestBrowser1.java index 4d3ef7203..3db968f1a 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestBrowser1.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestBrowser1.java @@ -19,6 +19,8 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.microsoft.playwright.junit.FixtureTest; +import com.microsoft.playwright.junit.Options; +import com.microsoft.playwright.junit.PlaywrightOptions; import com.microsoft.playwright.junit.UsePlaywright; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -27,12 +29,15 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Pattern; +import static com.microsoft.playwright.TestEnv.getBrowserName; import static org.junit.jupiter.api.Assertions.*; @FixtureTest -@UsePlaywright(TestOptionsFactories.BasicOptionsFactory.class) +@UsePlaywright @Tag("smoke") public class TestBrowser1 { + @PlaywrightOptions + static final Options options = new Options().setBrowserName(getBrowserName()); @Test void shouldCreateNewPage(Browser browser) { @@ -81,7 +86,7 @@ void shouldReturnBrowserType(BrowserType browserType, Browser browser) { } @Test - @EnabledIf(value = "com.microsoft.playwright.TestOptionsFactories#isChromium", + @EnabledIf(value = "com.microsoft.playwright.TestEnv#isChromium", disabledReason = "Chrome Devtools Protocol supported by chromium only") void shouldWorkWithNewBrowserCDPSession(Browser browser) { CDPSession session = browser.newBrowserCDPSession(); diff --git a/playwright/src/test/java/com/microsoft/playwright/TestBrowser2.java b/playwright/src/test/java/com/microsoft/playwright/TestBrowser2.java index f82fadd4e..1d926fb05 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestBrowser2.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestBrowser2.java @@ -18,56 +18,50 @@ import com.microsoft.playwright.junit.FixtureTest; import com.microsoft.playwright.junit.Options; -import com.microsoft.playwright.junit.OptionsFactory; +import com.microsoft.playwright.junit.PlaywrightOptions; import com.microsoft.playwright.junit.UsePlaywright; import com.microsoft.playwright.options.BrowserChannel; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Test; -import static com.microsoft.playwright.TestBrowser2.ChannelOptionsFactory.getBrowserChannelEnumFromEnv; -import static com.microsoft.playwright.TestOptionsFactories.createLaunchOptions; -import static com.microsoft.playwright.TestOptionsFactories.getBrowserChannelFromEnv; +import static com.microsoft.playwright.TestEnv.createLaunchOptions; +import static com.microsoft.playwright.TestEnv.getBrowserChannelFromEnv; import static org.junit.jupiter.api.Assertions.assertNotNull; @FixtureTest -@UsePlaywright(TestBrowser2.ChannelOptionsFactory.class) +@UsePlaywright public class TestBrowser2 { + @PlaywrightOptions + static Options options() { + BrowserType.LaunchOptions launchOptions = createLaunchOptions(); + launchOptions.channel = getBrowserChannelEnumFromEnv(); + return new Options().setLaunchOptions(launchOptions); + } - public static class ChannelOptionsFactory implements OptionsFactory { - @Override - public Options getOptions() { - BrowserChannel channel = getBrowserChannelEnumFromEnv(); - - BrowserType.LaunchOptions launchOptions = createLaunchOptions(); - launchOptions.channel = channel; - return new Options().setLaunchOptions(launchOptions); + static BrowserChannel getBrowserChannelEnumFromEnv() { + String channel = getBrowserChannelFromEnv(); + if (channel == null) { + return null; } - - public static BrowserChannel getBrowserChannelEnumFromEnv() { - String channel = getBrowserChannelFromEnv(); - if (channel == null) { - return null; - } - switch (channel) { - case "chrome": - return BrowserChannel.CHROME; - case "chrome-beta": - return BrowserChannel.CHROME_BETA; - case "chrome-dev": - return BrowserChannel.CHROME_DEV; - case "chrome-canary": - return BrowserChannel.CHROME_CANARY; - case "msedge": - return BrowserChannel.MSEDGE; - case "msedge-beta": - return BrowserChannel.MSEDGE_BETA; - case "msedge-dev": - return BrowserChannel.MSEDGE_DEV; - case "msedge-canary": - return BrowserChannel.MSEDGE_CANARY; - default: - throw new IllegalArgumentException("Unknown BROWSER_CHANNEL " + channel); - } + switch (channel) { + case "chrome": + return BrowserChannel.CHROME; + case "chrome-beta": + return BrowserChannel.CHROME_BETA; + case "chrome-dev": + return BrowserChannel.CHROME_DEV; + case "chrome-canary": + return BrowserChannel.CHROME_CANARY; + case "msedge": + return BrowserChannel.MSEDGE; + case "msedge-beta": + return BrowserChannel.MSEDGE_BETA; + case "msedge-dev": + return BrowserChannel.MSEDGE_DEV; + case "msedge-canary": + return BrowserChannel.MSEDGE_CANARY; + default: + throw new IllegalArgumentException("Unknown BROWSER_CHANNEL " + channel); } } diff --git a/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextAddCookies.java b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextAddCookies.java index 1bc1fe15b..43e226f88 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextAddCookies.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextAddCookies.java @@ -17,6 +17,8 @@ package com.microsoft.playwright; import com.microsoft.playwright.junit.FixtureTest; +import com.microsoft.playwright.junit.Options; +import com.microsoft.playwright.junit.PlaywrightOptions; import com.microsoft.playwright.junit.UsePlaywright; import com.microsoft.playwright.options.Cookie; import org.junit.jupiter.api.Test; @@ -27,15 +29,19 @@ import java.util.concurrent.Future; import static com.microsoft.playwright.TestBase.defaultSameSiteCookieValue; -import static com.microsoft.playwright.TestOptionsFactories.isFirefox; +import static com.microsoft.playwright.TestEnv.getBrowserName; +import static com.microsoft.playwright.TestEnv.isFirefox; import static com.microsoft.playwright.Utils.assertJsonEquals; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static org.junit.jupiter.api.Assertions.*; @FixtureTest -@UsePlaywright(TestOptionsFactories.BasicOptionsFactory.class) +@UsePlaywright public class TestBrowserContextAddCookies { + @PlaywrightOptions + static final Options options = new Options().setBrowserName(getBrowserName()); + @Test void shouldWork(Page page, Server server, BrowserContext context) { page.navigate(server.EMPTY_PAGE); diff --git a/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextBaseUrl.java b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextBaseUrl.java index 91e127df1..e48e6325a 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextBaseUrl.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextBaseUrl.java @@ -17,17 +17,23 @@ package com.microsoft.playwright; import com.microsoft.playwright.junit.FixtureTest; +import com.microsoft.playwright.junit.Options; +import com.microsoft.playwright.junit.PlaywrightOptions; import com.microsoft.playwright.junit.UsePlaywright; import org.junit.jupiter.api.Test; import java.net.MalformedURLException; +import static com.microsoft.playwright.TestEnv.getBrowserName; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @FixtureTest -@UsePlaywright(TestOptionsFactories.BasicOptionsFactory.class) +@UsePlaywright public class TestBrowserContextBaseUrl { + @PlaywrightOptions + static final Options options = new Options().setBrowserName(getBrowserName()); + @Test void shouldConstructANewURLWhenABaseURLInBrowserNewContextIsPassedToPageGoto(Browser browser, Server server) { try (BrowserContext context = browser.newContext(new Browser.NewContextOptions().setBaseURL(server.PREFIX))) { diff --git a/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextBasic.java b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextBasic.java index f78dd8703..d4db5e23d 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextBasic.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextBasic.java @@ -17,6 +17,8 @@ package com.microsoft.playwright; import com.microsoft.playwright.junit.FixtureTest; +import com.microsoft.playwright.junit.Options; +import com.microsoft.playwright.junit.PlaywrightOptions; import com.microsoft.playwright.junit.UsePlaywright; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -25,16 +27,20 @@ import java.util.ArrayList; import java.util.List; -import static com.microsoft.playwright.TestOptionsFactories.isFirefox; -import static com.microsoft.playwright.TestOptionsFactories.isWebKit; +import static com.microsoft.playwright.TestEnv.getBrowserName; +import static com.microsoft.playwright.TestEnv.isFirefox; +import static com.microsoft.playwright.TestEnv.isWebKit; import static com.microsoft.playwright.Utils.verifyViewport; import static java.util.Arrays.asList; import static org.junit.jupiter.api.Assertions.*; @FixtureTest -@UsePlaywright(TestOptionsFactories.BasicOptionsFactory.class) +@UsePlaywright @Tag("smoke") public class TestBrowserContextBasic { + @PlaywrightOptions + static final Options options = new Options().setBrowserName(getBrowserName()); + @Test void shouldCreateNewContext(Browser browser) { assertEquals(0, browser.contexts().size()); diff --git a/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextClearCookies.java b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextClearCookies.java index 7d8d24536..bea3f18fc 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextClearCookies.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextClearCookies.java @@ -17,6 +17,8 @@ package com.microsoft.playwright; import com.microsoft.playwright.junit.FixtureTest; +import com.microsoft.playwright.junit.Options; +import com.microsoft.playwright.junit.PlaywrightOptions; import com.microsoft.playwright.junit.UsePlaywright; import com.microsoft.playwright.options.Cookie; import org.junit.jupiter.api.Test; @@ -26,13 +28,17 @@ import java.util.Arrays; import java.util.regex.Pattern; +import static com.microsoft.playwright.TestEnv.getBrowserName; import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static org.junit.jupiter.api.Assertions.assertEquals; @FixtureTest -@UsePlaywright(TestOptionsFactories.BasicOptionsFactory.class) +@UsePlaywright public class TestBrowserContextClearCookies { + @PlaywrightOptions + static final Options options = new Options().setBrowserName(getBrowserName()); + @Test void shouldClearCookies(Page page, BrowserContext context, Server server) { page.navigate(server.EMPTY_PAGE); diff --git a/playwright/src/test/java/com/microsoft/playwright/TestOptionsFactories.java b/playwright/src/test/java/com/microsoft/playwright/TestEnv.java similarity index 83% rename from playwright/src/test/java/com/microsoft/playwright/TestOptionsFactories.java rename to playwright/src/test/java/com/microsoft/playwright/TestEnv.java index ce28fa3fa..6a0d226f1 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestOptionsFactories.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestEnv.java @@ -16,18 +16,7 @@ package com.microsoft.playwright; -import com.microsoft.playwright.junit.Options; -import com.microsoft.playwright.junit.OptionsFactory; - -public class TestOptionsFactories { - - public static class BasicOptionsFactory implements OptionsFactory { - @Override - public Options getOptions() { - return new Options().setBrowserName(getBrowserName()); - } - } - +public class TestEnv { public static String getBrowserChannelFromEnv() { return System.getenv("BROWSER_CHANNEL"); } diff --git a/playwright/src/test/java/com/microsoft/playwright/TestRequestGC.java b/playwright/src/test/java/com/microsoft/playwright/TestRequestGC.java index 9e31d14de..15eb96107 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestRequestGC.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestRequestGC.java @@ -1,17 +1,22 @@ package com.microsoft.playwright; import com.microsoft.playwright.junit.FixtureTest; +import com.microsoft.playwright.junit.Options; +import com.microsoft.playwright.junit.PlaywrightOptions; import com.microsoft.playwright.junit.UsePlaywright; import org.junit.jupiter.api.Test; +import static com.microsoft.playwright.TestEnv.getBrowserName; import static com.microsoft.playwright.Utils.mapOf; import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; @FixtureTest -@UsePlaywright(TestOptionsFactories.BasicOptionsFactory.class) +@UsePlaywright public class TestRequestGC { + @PlaywrightOptions + static final Options options = new Options().setBrowserName(getBrowserName()); @Test void shouldWork(Page page) { diff --git a/playwright/src/test/java/com/microsoft/playwright/TestRouteWebSocket.java b/playwright/src/test/java/com/microsoft/playwright/TestRouteWebSocket.java index 9f3ff45b9..293c25750 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestRouteWebSocket.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestRouteWebSocket.java @@ -1,6 +1,8 @@ package com.microsoft.playwright; import com.microsoft.playwright.junit.FixtureTest; +import com.microsoft.playwright.junit.Options; +import com.microsoft.playwright.junit.PlaywrightOptions; import com.microsoft.playwright.junit.UsePlaywright; import org.java_websocket.WebSocket; import org.junit.jupiter.api.*; @@ -15,14 +17,18 @@ import java.util.concurrent.Future; import java.util.regex.Pattern; +import static com.microsoft.playwright.TestEnv.getBrowserName; import static com.microsoft.playwright.Utils.mapOf; import static java.util.Arrays.asList; import static org.junit.jupiter.api.Assertions.assertEquals; @FixtureTest -@UsePlaywright(TestOptionsFactories.BasicOptionsFactory.class) +@UsePlaywright @TestInstance(TestInstance.Lifecycle.PER_CLASS) public class TestRouteWebSocket { + @PlaywrightOptions + static final Options options = new Options().setBrowserName(getBrowserName()); + private WebSocketServerImpl webSocketServer; @BeforeAll diff --git a/playwright/src/test/java/com/microsoft/playwright/TestScreencast.java b/playwright/src/test/java/com/microsoft/playwright/TestScreencast.java index 4b4831813..72a16ddcc 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestScreencast.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestScreencast.java @@ -17,6 +17,8 @@ package com.microsoft.playwright; import com.microsoft.playwright.junit.FixtureTest; +import com.microsoft.playwright.junit.Options; +import com.microsoft.playwright.junit.PlaywrightOptions; import com.microsoft.playwright.junit.UsePlaywright; import com.microsoft.playwright.options.AnnotatePosition; import com.microsoft.playwright.options.ScreencastCursor; @@ -34,12 +36,16 @@ import java.util.List; import java.util.stream.Collectors; -import static com.microsoft.playwright.TestOptionsFactories.createLaunchOptions; +import static com.microsoft.playwright.TestEnv.createLaunchOptions; +import static com.microsoft.playwright.TestEnv.getBrowserName; import static org.junit.jupiter.api.Assertions.*; @FixtureTest -@UsePlaywright(TestOptionsFactories.BasicOptionsFactory.class) +@UsePlaywright public class TestScreencast { + @PlaywrightOptions + static final Options options = new Options().setBrowserName(getBrowserName()); + private static Browser.NewContextOptions recordVideoOptions(Path videosDir) { return new Browser.NewContextOptions() .setRecordVideoDir(videosDir) diff --git a/playwright/src/test/java/com/microsoft/playwright/impl/junit/TestOptionsExtension.java b/playwright/src/test/java/com/microsoft/playwright/impl/junit/TestOptionsExtension.java new file mode 100644 index 000000000..8aa6ff924 --- /dev/null +++ b/playwright/src/test/java/com/microsoft/playwright/impl/junit/TestOptionsExtension.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) Microsoft Corporation. + * + * Licensed 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 com.microsoft.playwright.impl.junit; + +import com.microsoft.playwright.PlaywrightException; +import com.microsoft.playwright.junit.Options; +import com.microsoft.playwright.junit.PlaywrightOptions; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Member; + +import static org.junit.jupiter.api.Assertions.*; + +public class TestOptionsExtension { + static class StaticField { + @PlaywrightOptions + static Options options = new Options(); + } + + static class Subclass extends StaticField { + } + + static class InstanceMethod { + @PlaywrightOptions + Options options() { + return new Options(); + } + + class Nested { + } + } + + static class FieldAndMethod { + @PlaywrightOptions + static Options options = new Options(); + + @PlaywrightOptions + static Options moreOptions() { + return new Options(); + } + } + + static class WrongType { + @PlaywrightOptions + static String options = ""; + } + + static class MethodWithParameters { + @PlaywrightOptions + static Options options(String name) { + return new Options(); + } + } + + static class NoOptions { + } + + @Test + void shouldFindStaticField() { + Member member = OptionsExtension.findOptionsMember(StaticField.class); + assertEquals("options", member.getName()); + assertEquals(StaticField.class, member.getDeclaringClass()); + } + + @Test + void shouldFindMemberInSuperclass() { + Member member = OptionsExtension.findOptionsMember(Subclass.class); + assertEquals(StaticField.class, member.getDeclaringClass()); + } + + @Test + void shouldFindMemberInEnclosingClass() { + Member member = OptionsExtension.findOptionsMember(InstanceMethod.Nested.class); + assertEquals("options", member.getName()); + assertEquals(InstanceMethod.class, member.getDeclaringClass()); + } + + @Test + void shouldReturnNullWhenThereIsNoMember() { + assertNull(OptionsExtension.findOptionsMember(NoOptions.class)); + } + + @Test + void shouldThrowWhenThereAreSeveralMembers() { + PlaywrightException e = assertThrows(PlaywrightException.class, () -> OptionsExtension.findOptionsMember(FieldAndMethod.class)); + assertEquals("Only one @PlaywrightOptions field or method is allowed, found: " + + "@PlaywrightOptions field " + FieldAndMethod.class.getName() + ".options, " + + "@PlaywrightOptions method " + FieldAndMethod.class.getName() + ".moreOptions()", e.getMessage()); + } + + @Test + void shouldThrowOnWrongType() { + PlaywrightException e = assertThrows(PlaywrightException.class, () -> OptionsExtension.findOptionsMember(WrongType.class)); + assertEquals("@PlaywrightOptions field " + WrongType.class.getName() + ".options must be of type " + + Options.class.getName() + ".", e.getMessage()); + } + + @Test + void shouldThrowOnMethodWithParameters() { + PlaywrightException e = assertThrows(PlaywrightException.class, () -> OptionsExtension.findOptionsMember(MethodWithParameters.class)); + assertEquals("@PlaywrightOptions method " + MethodWithParameters.class.getName() + ".options() must not have parameters.", + e.getMessage()); + } +} diff --git a/playwright/src/test/java/com/microsoft/playwright/junit/ServerLifecycle.java b/playwright/src/test/java/com/microsoft/playwright/junit/ServerLifecycle.java index 38b27c5f1..39b74d28c 100644 --- a/playwright/src/test/java/com/microsoft/playwright/junit/ServerLifecycle.java +++ b/playwright/src/test/java/com/microsoft/playwright/junit/ServerLifecycle.java @@ -26,14 +26,8 @@ import static com.microsoft.playwright.Utils.nextFreePort; public class ServerLifecycle implements BeforeAllCallback, AfterAllCallback, ParameterResolver { - // This is a public map so that objects outside test scope can access the server. - // For example, nested classes inside test classes that define custom options and need the server. - public static Map, Server> serverMap; - - static { - // Test classes run concurrently. - serverMap = new ConcurrentHashMap<>(); - } + // Test classes run concurrently. + private static final Map, Server> serverMap = new ConcurrentHashMap<>(); @Override public void beforeAll(ExtensionContext extensionContext) throws Exception { diff --git a/playwright/src/test/java/com/microsoft/playwright/junit/TestFixtureChannelOption.java b/playwright/src/test/java/com/microsoft/playwright/junit/TestFixtureChannelOption.java index c35e44d8c..a5c204335 100644 --- a/playwright/src/test/java/com/microsoft/playwright/junit/TestFixtureChannelOption.java +++ b/playwright/src/test/java/com/microsoft/playwright/junit/TestFixtureChannelOption.java @@ -22,20 +22,14 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledIf; -import static com.microsoft.playwright.junit.ServerLifecycle.serverMap; import static org.junit.jupiter.api.Assertions.*; @FixtureTest -@UsePlaywright(TestFixtureChannelOption.CustomOptions.class) +@UsePlaywright @EnabledIf("isChannelSpecified") public class TestFixtureChannelOption { - - public static class CustomOptions implements OptionsFactory { - @Override - public Options getOptions() { - return new Options().setChannel(channel()); - } - } + @PlaywrightOptions + static final Options options = new Options().setChannel(channel()); public static boolean isChannelSpecified() { return channel() != null; diff --git a/playwright/src/test/java/com/microsoft/playwright/junit/TestFixtureContextOptions.java b/playwright/src/test/java/com/microsoft/playwright/junit/TestFixtureContextOptions.java index 81c857884..ca4145ebe 100644 --- a/playwright/src/test/java/com/microsoft/playwright/junit/TestFixtureContextOptions.java +++ b/playwright/src/test/java/com/microsoft/playwright/junit/TestFixtureContextOptions.java @@ -21,23 +21,23 @@ import java.util.regex.Pattern; -import static com.microsoft.playwright.junit.ServerLifecycle.serverMap; import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat; import static org.junit.jupiter.api.Assertions.assertTrue; @FixtureTest -@UsePlaywright(TestFixtureContextOptions.CustomOptions.class) +@UsePlaywright public class TestFixtureContextOptions { + private final Server server; - public static class CustomOptions implements OptionsFactory { - @Override - public Options getOptions() { - return new Options() - .setApiRequestOptions(new APIRequest.NewContextOptions() - .setBaseURL(serverMap.get(TestFixtureContextOptions.class).EMPTY_PAGE)) - .setContextOptions(new Browser.NewContextOptions() - .setBaseURL(serverMap.get(TestFixtureContextOptions.class).EMPTY_PAGE)); - } + TestFixtureContextOptions(Server server) { + this.server = server; + } + + @PlaywrightOptions + Options options() { + return new Options() + .setApiRequestOptions(new APIRequest.NewContextOptions().setBaseURL(server.EMPTY_PAGE)) + .setContextOptions(new Browser.NewContextOptions().setBaseURL(server.EMPTY_PAGE)); } @Test diff --git a/playwright/src/test/java/com/microsoft/playwright/junit/TestFixtureDeviceOption.java b/playwright/src/test/java/com/microsoft/playwright/junit/TestFixtureDeviceOption.java index 9c8ea3368..7981e1145 100644 --- a/playwright/src/test/java/com/microsoft/playwright/junit/TestFixtureDeviceOption.java +++ b/playwright/src/test/java/com/microsoft/playwright/junit/TestFixtureDeviceOption.java @@ -20,20 +20,14 @@ import com.microsoft.playwright.Server; import org.junit.jupiter.api.Test; -import static com.microsoft.playwright.junit.ServerLifecycle.serverMap; import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; @FixtureTest -@UsePlaywright(TestFixtureDeviceOption.CustomOptions.class) +@UsePlaywright public class TestFixtureDeviceOption { - - public static class CustomOptions implements OptionsFactory { - @Override - public Options getOptions() { - return new Options().setDeviceName("iPhone 14"); - } - } + @PlaywrightOptions + static final Options options = new Options().setDeviceName("iPhone 14"); @Test public void testPredefinedDeviceParameters(Server server, Page page) { diff --git a/playwright/src/test/java/com/microsoft/playwright/junit/TestFixtureNewContextOptions.java b/playwright/src/test/java/com/microsoft/playwright/junit/TestFixtureNewContextOptions.java index 0c448c211..908c31073 100644 --- a/playwright/src/test/java/com/microsoft/playwright/junit/TestFixtureNewContextOptions.java +++ b/playwright/src/test/java/com/microsoft/playwright/junit/TestFixtureNewContextOptions.java @@ -26,24 +26,20 @@ import static org.junit.jupiter.api.Assertions.*; @FixtureTest -@UsePlaywright(TestFixtureNewContextOptions.CustomOptions.class) +@UsePlaywright public class TestFixtureNewContextOptions { // Only checked as an option value, never navigated to. private static final String BASE_URL = "http://localhost:1234/base/"; private static Browser.NewContextOptions contextOptionsFromBeforeAll; - public static class CustomOptions implements OptionsFactory { - @Override - public Options getOptions() { - return new Options() - .setBaseUrl(BASE_URL) - .setDeviceName("iPhone 14") - .setIgnoreHTTPSErrors(true) - .setContextOptions(new Browser.NewContextOptions().setLocale("de-DE")); - } - } + @PlaywrightOptions + static final Options options = new Options() + .setBaseUrl(BASE_URL) + .setDeviceName("iPhone 14") + .setIgnoreHTTPSErrors(true) + .setContextOptions(new Browser.NewContextOptions().setLocale("de-DE")); - private static void assertConfiguredFromOptionsFactory(Browser.NewContextOptions contextOptions) { + private static void assertConfiguredFromOptions(Browser.NewContextOptions contextOptions) { assertNotNull(contextOptions); assertEquals(BASE_URL, contextOptions.baseURL); assertEquals(true, contextOptions.ignoreHTTPSErrors); @@ -57,7 +53,7 @@ private static void assertConfiguredFromOptionsFactory(Browser.NewContextOptions @BeforeAll static void beforeAll(Browser browser, Browser.NewContextOptions contextOptions) { - assertConfiguredFromOptionsFactory(contextOptions); + assertConfiguredFromOptions(contextOptions); contextOptionsFromBeforeAll = contextOptions; BrowserContext context = browser.newContext(contextOptions); @@ -74,12 +70,12 @@ static void beforeAll(Browser browser, Browser.NewContextOptions contextOptions) @AfterAll static void afterAll(Browser.NewContextOptions contextOptions) { - assertConfiguredFromOptionsFactory(contextOptions); + assertConfiguredFromOptions(contextOptions); } @Test - void shouldBeConfiguredFromOptionsFactory(Browser.NewContextOptions contextOptions) { - assertConfiguredFromOptionsFactory(contextOptions); + void shouldBeConfiguredFromOptions(Browser.NewContextOptions contextOptions) { + assertConfiguredFromOptions(contextOptions); } @Test @@ -87,7 +83,7 @@ void shouldReturnNewInstanceEveryTime(Browser.NewContextOptions contextOptions1, assertNotSame(contextOptionsFromBeforeAll, contextOptions1); assertNotSame(contextOptions1, contextOptions2); contextOptions1.setBaseURL("http://localhost:1234/modified/").setLocale("fr-FR"); - assertConfiguredFromOptionsFactory(contextOptions2); + assertConfiguredFromOptions(contextOptions2); } @Test diff --git a/playwright/src/test/java/com/microsoft/playwright/junit/TestFixtureOptions.java b/playwright/src/test/java/com/microsoft/playwright/junit/TestFixtureOptions.java index 675494e52..6f132a120 100644 --- a/playwright/src/test/java/com/microsoft/playwright/junit/TestFixtureOptions.java +++ b/playwright/src/test/java/com/microsoft/playwright/junit/TestFixtureOptions.java @@ -18,27 +18,30 @@ import com.microsoft.playwright.Browser; import com.microsoft.playwright.Page; +import com.microsoft.playwright.Server; import org.junit.jupiter.api.Test; import java.util.regex.Pattern; -import static com.microsoft.playwright.junit.ServerLifecycle.serverMap; import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; @FixtureTest -@UsePlaywright(TestFixtureOptions.CustomOptions.class) +@UsePlaywright public class TestFixtureOptions { + private final Server server; - public static class CustomOptions implements OptionsFactory { - @Override - public Options getOptions() { - return new Options() - .setBaseUrl(serverMap.get(TestFixtureOptions.class).EMPTY_PAGE) - .setBrowserName("webkit") - .setDefaultTimeout(10_000.0) - .setTestIdAttribute("data-my-custom-testid"); - } + TestFixtureOptions(Server server) { + this.server = server; + } + + @PlaywrightOptions + Options options() { + return new Options() + .setBaseUrl(server.EMPTY_PAGE) + .setBrowserName("webkit") + .setDefaultTimeout(10_000.0) + .setTestIdAttribute("data-my-custom-testid"); } @Test diff --git a/playwright/src/test/java/com/microsoft/playwright/junit/TestFixturePlaywrightOptions.java b/playwright/src/test/java/com/microsoft/playwright/junit/TestFixturePlaywrightOptions.java new file mode 100644 index 000000000..89492aa48 --- /dev/null +++ b/playwright/src/test/java/com/microsoft/playwright/junit/TestFixturePlaywrightOptions.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) Microsoft Corporation. + * + * Licensed 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 com.microsoft.playwright.junit; + +import com.microsoft.playwright.Browser; +import com.microsoft.playwright.Page; +import com.microsoft.playwright.Server; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@FixtureTest +@UsePlaywright +public class TestFixturePlaywrightOptions { + // Set per instance, similar to fields injected by Spring or Micronaut. + private final Server server; + private boolean optionsRead; + + TestFixturePlaywrightOptions(Server server) { + this.server = server; + } + + @PlaywrightOptions + Options options() { + optionsRead = true; + return new Options().setBaseUrl(server.PREFIX); + } + + // Each repetition gets a new test instance. + @RepeatedTest(2) + void shouldReadOptionsForEachTestInstance(Page page) { + assertTrue(optionsRead); + page.navigate("/empty.html"); + assertThat(page).hasURL(server.EMPTY_PAGE); + } + + @Nested + class NestedClass { + @Test + void shouldUseOptionsOfEnclosingClass(Page page) { + assertTrue(optionsRead); + page.navigate("/empty.html"); + assertThat(page).hasURL(server.EMPTY_PAGE); + } + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + class PerClassLifecycle { + @BeforeAll + void beforeAll(Browser.NewContextOptions contextOptions) { + assertEquals(server.PREFIX, contextOptions.baseURL); + } + + @Test + void shouldUseOptionsOfEnclosingClass(Page page) { + page.navigate("/empty.html"); + assertThat(page).hasURL(server.EMPTY_PAGE); + } + } +} diff --git a/playwright/src/test/java/com/microsoft/playwright/junit/TestFixturePlaywrightOptionsStatic.java b/playwright/src/test/java/com/microsoft/playwright/junit/TestFixturePlaywrightOptionsStatic.java new file mode 100644 index 000000000..b3921d1a4 --- /dev/null +++ b/playwright/src/test/java/com/microsoft/playwright/junit/TestFixturePlaywrightOptionsStatic.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) Microsoft Corporation. + * + * Licensed 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 com.microsoft.playwright.junit; + +import com.microsoft.playwright.Browser; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@FixtureTest +@UsePlaywright +public class TestFixturePlaywrightOptionsStatic { + // Only checked as an option value, never navigated to. + private static final String BASE_URL = "http://localhost:1234/base/"; + + @PlaywrightOptions + static final Options options = new Options().setBaseUrl(BASE_URL); + + // A static member does not need a test instance. + @BeforeAll + static void beforeAll(Browser.NewContextOptions contextOptions) { + assertEquals(BASE_URL, contextOptions.baseURL); + } + + @Test + void shouldUseStaticOptions(Browser.NewContextOptions contextOptions) { + assertEquals(BASE_URL, contextOptions.baseURL); + } + + @Nested + class NestedClass { + @Test + void shouldUseOptionsOfEnclosingClass(Browser.NewContextOptions contextOptions) { + assertEquals(BASE_URL, contextOptions.baseURL); + } + } +} diff --git a/playwright/src/test/java/com/microsoft/playwright/junit/TestFixtureTraceOutputDir.java b/playwright/src/test/java/com/microsoft/playwright/junit/TestFixtureTraceOutputDir.java index 041f0c0c2..f31b14659 100644 --- a/playwright/src/test/java/com/microsoft/playwright/junit/TestFixtureTraceOutputDir.java +++ b/playwright/src/test/java/com/microsoft/playwright/junit/TestFixtureTraceOutputDir.java @@ -36,17 +36,16 @@ import static org.junit.jupiter.api.Assertions.assertTrue; @FixtureTest -@UsePlaywright(TestFixtureTraceOutputDir.TraceOptions.class) +@UsePlaywright public class TestFixtureTraceOutputDir { @TempDir static Path outputDir; private static String browserName; - public static class TraceOptions implements OptionsFactory { - @Override - public Options getOptions() { - return new Options().setTrace(Options.Trace.ON).setOutputDir(outputDir); - } + // A method rather than a field because outputDir is injected after the class is initialized. + @PlaywrightOptions + static Options options() { + return new Options().setTrace(Options.Trace.ON).setOutputDir(outputDir); } @Test From aa913fee5d4206b5ce5e8d4205060ce67012e621 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Mon, 19 Oct 2026 15:15:26 -0700 Subject: [PATCH 2/3] test(junit): cover @PlaywrightOptions in nested classes --- .../playwright/junit/PlaywrightOptions.java | 34 +++++++++++++++---- .../TestFixturePlaywrightOptionsStatic.java | 20 +++++++++++ 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/playwright/src/main/java/com/microsoft/playwright/junit/PlaywrightOptions.java b/playwright/src/main/java/com/microsoft/playwright/junit/PlaywrightOptions.java index bce8c2c30..ae0be0941 100644 --- a/playwright/src/main/java/com/microsoft/playwright/junit/PlaywrightOptions.java +++ b/playwright/src/main/java/com/microsoft/playwright/junit/PlaywrightOptions.java @@ -63,15 +63,35 @@ * } * } * }
- * {@code Playwright} and {@code Browser} are shared between tests, so they are created with the - * options of the first test instance; only the options of {@code BrowserContext} and - * {@code APIRequestContext} take effect for every test instance. Unless the test class uses - * {@code @TestInstance(TestInstance.Lifecycle.PER_CLASS)}, a non-static member cannot be used - * when Playwright fixtures are requested in {@code @BeforeAll} methods or in the constructor, - * because there is no test instance yet. + * Unless the test class uses {@code @TestInstance(TestInstance.Lifecycle.PER_CLASS)}, a non-static + * member cannot be used when Playwright fixtures are requested in {@code @BeforeAll} methods or in + * the constructor, because there is no test instance yet. * *

The annotated member may be declared in a superclass or in an enclosing class of the test - * class. + * class. A {@code @Nested} class can declare its own member to run its tests with different options: + *

{@code
+ * @UsePlaywright
+ * public class MyTest {
+ *   @PlaywrightOptions
+ *   static final Options options = new Options().setBaseUrl("https://example.com");
+ *
+ *   @Nested
+ *   class Mobile {
+ *     @PlaywrightOptions
+ *     Options options = new Options().setBaseUrl("https://m.example.com").setDeviceName("iPhone 14");
+ *
+ *     @Test
+ *     void testOnMobile(Page page) {
+ *       page.navigate("/");
+ *     }
+ *   }
+ * }
+ * }
+ * + *

{@code Playwright} and {@code Browser} are shared between the tests of a class, so they are + * created with the options in effect for the first test; only the options of {@code BrowserContext} + * and {@code APIRequestContext} (base URL, device, context and API request options, timeout, tracing) + * take effect for every test instance and nested class. * *

For more details and usage examples see our * JUnit guide. diff --git a/playwright/src/test/java/com/microsoft/playwright/junit/TestFixturePlaywrightOptionsStatic.java b/playwright/src/test/java/com/microsoft/playwright/junit/TestFixturePlaywrightOptionsStatic.java index b3921d1a4..d9bf96d10 100644 --- a/playwright/src/test/java/com/microsoft/playwright/junit/TestFixturePlaywrightOptionsStatic.java +++ b/playwright/src/test/java/com/microsoft/playwright/junit/TestFixturePlaywrightOptionsStatic.java @@ -17,6 +17,7 @@ package com.microsoft.playwright.junit; import com.microsoft.playwright.Browser; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -28,6 +29,7 @@ public class TestFixturePlaywrightOptionsStatic { // Only checked as an option value, never navigated to. private static final String BASE_URL = "http://localhost:1234/base/"; + private static final String NESTED_BASE_URL = "http://localhost:1234/nested/"; @PlaywrightOptions static final Options options = new Options().setBaseUrl(BASE_URL); @@ -43,6 +45,12 @@ void shouldUseStaticOptions(Browser.NewContextOptions contextOptions) { assertEquals(BASE_URL, contextOptions.baseURL); } + // Runs after the nested classes, so the options of the enclosing class must be in effect again. + @AfterAll + static void afterAll(Browser.NewContextOptions contextOptions) { + assertEquals(BASE_URL, contextOptions.baseURL); + } + @Nested class NestedClass { @Test @@ -50,4 +58,16 @@ void shouldUseOptionsOfEnclosingClass(Browser.NewContextOptions contextOptions) assertEquals(BASE_URL, contextOptions.baseURL); } } + + @Nested + class NestedClassWithOwnOptions { + // Inner classes cannot declare static fields, so the options are read for each nested test instance. + @PlaywrightOptions + Options options = new Options().setBaseUrl(NESTED_BASE_URL); + + @Test + void shouldOverrideOptionsOfEnclosingClass(Browser.NewContextOptions contextOptions) { + assertEquals(NESTED_BASE_URL, contextOptions.baseURL); + } + } } From 7e8aadb93eb38118e1acc10e660d5ad79701bd8c Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Mon, 19 Oct 2026 15:23:40 -0700 Subject: [PATCH 3/3] feat(junit): accept typed options in @PlaywrightOptions members --- .../impl/junit/OptionsExtension.java | 180 +++++++++++++----- .../playwright/junit/PlaywrightOptions.java | 40 ++-- .../impl/junit/TestOptionsExtension.java | 158 +++++++++++---- .../junit/TestFixtureContextOptions.java | 11 +- .../junit/TestFixtureNewContextOptions.java | 6 +- .../TestFixturePlaywrightOptionsStatic.java | 16 +- 6 files changed, 308 insertions(+), 103 deletions(-) diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/junit/OptionsExtension.java b/playwright/src/main/java/com/microsoft/playwright/impl/junit/OptionsExtension.java index 39c36b40e..737345a52 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/junit/OptionsExtension.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/junit/OptionsExtension.java @@ -16,11 +16,15 @@ package com.microsoft.playwright.impl.junit; +import com.microsoft.playwright.APIRequest; +import com.microsoft.playwright.Browser; +import com.microsoft.playwright.BrowserType; import com.microsoft.playwright.PlaywrightException; import com.microsoft.playwright.junit.Options; import com.microsoft.playwright.junit.PlaywrightOptions; import org.junit.jupiter.api.extension.AfterAllCallback; import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.TestInstances; import org.junit.platform.commons.support.AnnotationSupport; import org.junit.platform.commons.support.HierarchyTraversalMode; import org.junit.platform.commons.support.ModifierSupport; @@ -29,26 +33,64 @@ import java.lang.reflect.Field; import java.lang.reflect.Member; import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; +import java.util.*; +import static java.util.stream.Collectors.groupingBy; import static java.util.stream.Collectors.joining; public class OptionsExtension implements AfterAllCallback { + // Types a @PlaywrightOptions member may have. The typed options override the corresponding field of Options. + private static final List> OPTIONS_TYPES = Arrays.asList( + Options.class, BrowserType.LaunchOptions.class, Browser.NewContextOptions.class, APIRequest.NewContextOptions.class); + private static final ThreadLocal threadLocalOptions = new ThreadLocal<>(); + // A @PlaywrightOptions member and the test instance to read it from, null for static members. + private static class Source { + final Member member; + final Object testInstance; + + Source(Member member, Object testInstance) { + this.member = member; + this.testInstance = testInstance; + } + + Object read() { + Object value; + if (member instanceof Field) { + value = ReflectionSupport.tryToReadFieldValue((Field) member, testInstance) + .getOrThrow(e -> new PlaywrightException("Failed to read " + describe(member), e)); + } else { + value = ReflectionSupport.invokeMethod((Method) member, testInstance); + } + if (value == null) { + throw new PlaywrightException(describe(member) + " must not be null."); + } + return value; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Source)) { + return false; + } + Source other = (Source) o; + return member.equals(other.member) && testInstance == other.testInstance; + } + + @Override + public int hashCode() { + return member.hashCode(); + } + } + private static class CachedOptions { final Options options; - // @PlaywrightOptions member the options were read from, null when the test class has none. - final Member source; - // Test instance the options were read from, null for static members. - final Object testInstance; + final Map, Source> sources; - CachedOptions(Options options, Member source, Object testInstance) { + CachedOptions(Options options, Map, Source> sources) { this.options = options; - this.source = source; - this.testInstance = testInstance; + this.sources = sources; } } @@ -58,44 +100,86 @@ public void afterAll(ExtensionContext extensionContext) { } static Options getOptions(ExtensionContext extensionContext) { - Member member = findOptionsMember(extensionContext.getRequiredTestClass()); - Object testInstance = member == null || ModifierSupport.isStatic(member) ? null : getTestInstance(extensionContext, member); - + Map, Source> sources = findSources(extensionContext.getRequiredTestClass(), extensionContext.getTestInstances()); CachedOptions cached = threadLocalOptions.get(); - if (cached != null && Objects.equals(cached.source, member) && cached.testInstance == testInstance) { + if (cached != null && cached.sources.equals(sources)) { return cached.options; } - Options options = member == null ? new Options() : readOptions(member, testInstance); - threadLocalOptions.set(new CachedOptions(options, member, testInstance)); + Options options = createOptions(sources); + threadLocalOptions.set(new CachedOptions(options, sources)); return options; } - private static Object getTestInstance(ExtensionContext extensionContext, Member member) { - Class declaringClass = member.getDeclaringClass(); - return extensionContext.getTestInstances() - .flatMap(instances -> instances.findInstance(declaringClass)) - .orElseThrow(() -> new PlaywrightException(describe(member) + " requires a test instance. Make it static, or use " + - "@TestInstance(TestInstance.Lifecycle.PER_CLASS) to request Playwright fixtures in @BeforeAll methods or constructors.")); + // Visible for testing. + static Options createOptions(Class testClass, Optional testInstances) { + return createOptions(findSources(testClass, testInstances)); } - // Looks for a @PlaywrightOptions field or method in the test class, its superclasses and its enclosing classes. - static Member findOptionsMember(Class testClass) { - for (Class clazz = testClass; clazz != null; clazz = clazz.getDeclaringClass()) { - List members = new ArrayList<>(); - members.addAll(AnnotationSupport.findAnnotatedFields(clazz, PlaywrightOptions.class)); - members.addAll(AnnotationSupport.findAnnotatedMethods(clazz, PlaywrightOptions.class, HierarchyTraversalMode.BOTTOM_UP)); - if (members.size() > 1) { - throw new PlaywrightException("Only one @PlaywrightOptions field or method is allowed, found: " + - members.stream().map(OptionsExtension::describe).collect(joining(", "))); + private static Options createOptions(Map, Source> sources) { + Options base = read(sources, Options.class); + // Copy, so that the typed options below do not modify the user's object, which may be shared between tests. + Options options = base == null ? new Options() : copyOf(base); + BrowserType.LaunchOptions launchOptions = read(sources, BrowserType.LaunchOptions.class); + if (launchOptions != null) { + options.launchOptions = launchOptions; + } + Browser.NewContextOptions contextOptions = read(sources, Browser.NewContextOptions.class); + if (contextOptions != null) { + options.contextOptions = contextOptions; + } + APIRequest.NewContextOptions apiRequestOptions = read(sources, APIRequest.NewContextOptions.class); + if (apiRequestOptions != null) { + options.apiRequestOptions = apiRequestOptions; + } + return options; + } + + private static T read(Map, Source> sources, Class type) { + Source source = sources.get(type); + return source == null ? null : type.cast(source.read()); + } + + // Shallow copy, the extensions clone the nested options before modifying them. + private static Options copyOf(Options options) { + Options copy = new Options(); + for (Field field : Options.class.getFields()) { + try { + field.set(copy, field.get(options)); + } catch (IllegalAccessException e) { + throw new PlaywrightException("Failed to copy options", e); } - if (!members.isEmpty()) { - return validateOptionsMember(members.get(0)); + } + return copy; + } + + // Finds the @PlaywrightOptions members of the test class, its superclasses and its enclosing classes, one per type. + // A member of a nested class takes precedence over a member of the same type in an enclosing class. + private static Map, Source> findSources(Class testClass, Optional testInstances) { + Map, Source> sources = new HashMap<>(); + for (Class clazz = testClass; clazz != null; clazz = clazz.getDeclaringClass()) { + for (Map.Entry, List> entry : findMembersByType(clazz).entrySet()) { + List members = entry.getValue(); + if (members.size() > 1) { + throw new PlaywrightException("Only one @PlaywrightOptions field or method of type " + entry.getKey().getCanonicalName() + + " is allowed, found: " + members.stream().map(OptionsExtension::describe).collect(joining(", "))); + } + if (!sources.containsKey(entry.getKey())) { + sources.put(entry.getKey(), toSource(members.get(0), testInstances)); + } } } - return null; + return sources; + } + + private static Map, List> findMembersByType(Class clazz) { + List members = new ArrayList<>(); + members.addAll(AnnotationSupport.findAnnotatedFields(clazz, PlaywrightOptions.class)); + members.addAll(AnnotationSupport.findAnnotatedMethods(clazz, PlaywrightOptions.class, HierarchyTraversalMode.BOTTOM_UP)); + return members.stream().collect(groupingBy(OptionsExtension::optionsType)); } - private static Member validateOptionsMember(Member member) { + // Returns which of OPTIONS_TYPES the member provides, validating the member on the way. + private static Class optionsType(Member member) { Class type; if (member instanceof Field) { type = ((Field) member).getType(); @@ -106,24 +190,20 @@ private static Member validateOptionsMember(Member member) { } type = method.getReturnType(); } - if (!Options.class.isAssignableFrom(type)) { - throw new PlaywrightException(describe(member) + " must be of type " + Options.class.getName() + "."); - } - return member; + return OPTIONS_TYPES.stream().filter(optionsType -> optionsType.isAssignableFrom(type)).findFirst() + .orElseThrow(() -> new PlaywrightException(describe(member) + " must be of type " + + OPTIONS_TYPES.stream().map(Class::getCanonicalName).collect(joining(", ")) + ".")); } - private static Options readOptions(Member member, Object testInstance) { - Object options; - if (member instanceof Field) { - options = ReflectionSupport.tryToReadFieldValue((Field) member, testInstance) - .getOrThrow(e -> new PlaywrightException("Failed to read " + describe(member), e)); - } else { - options = ReflectionSupport.invokeMethod((Method) member, testInstance); - } - if (options == null) { - throw new PlaywrightException(describe(member) + " must not be null."); + private static Source toSource(Member member, Optional testInstances) { + if (ModifierSupport.isStatic(member)) { + return new Source(member, null); } - return (Options) options; + Class declaringClass = member.getDeclaringClass(); + Object testInstance = testInstances.flatMap(instances -> instances.findInstance(declaringClass)) + .orElseThrow(() -> new PlaywrightException(describe(member) + " requires a test instance. Make it static, or use " + + "@TestInstance(TestInstance.Lifecycle.PER_CLASS) to request Playwright fixtures in @BeforeAll methods or constructors.")); + return new Source(member, testInstance); } private static String describe(Member member) { diff --git a/playwright/src/main/java/com/microsoft/playwright/junit/PlaywrightOptions.java b/playwright/src/main/java/com/microsoft/playwright/junit/PlaywrightOptions.java index ae0be0941..bb89c5333 100644 --- a/playwright/src/main/java/com/microsoft/playwright/junit/PlaywrightOptions.java +++ b/playwright/src/main/java/com/microsoft/playwright/junit/PlaywrightOptions.java @@ -21,19 +21,30 @@ /** * NOTE: this API is experimental and is subject to changes. * - *

Marks the field or method of a test class annotated with {@link UsePlaywright} that - * provides the {@link Options} for the Playwright fixtures. A field must be of type - * {@code Options}, a method must return {@code Options} and take no parameters. + *

Marks a field or method of a test class annotated with {@link UsePlaywright} that provides + * options for the Playwright fixtures. A method must take no parameters. The member must be of one + * of the following types, and a test class may have one member of each type: + *

    + *
  • {@link Options} - all options;
  • + *
  • {@link com.microsoft.playwright.BrowserType.LaunchOptions BrowserType.LaunchOptions} - + * options for launching the browser, overrides {@link Options#setLaunchOptions};
  • + *
  • {@link com.microsoft.playwright.Browser.NewContextOptions Browser.NewContextOptions} - + * options for creating the browser context, overrides {@link Options#setContextOptions};
  • + *
  • {@link com.microsoft.playwright.APIRequest.NewContextOptions APIRequest.NewContextOptions} - + * options for creating the API request context, overrides {@link Options#setApiRequestOptions}.
  • + *
* *

A static field or method is read once per test class: *

{@code
  * @UsePlaywright
  * public class MyTest {
  *   @PlaywrightOptions
- *   static final Options options = new Options()
- *       .setHeadless(false)
- *       .setContextOptions(new Browser.NewContextOptions()
- *           .setBaseURL("https://github.com"));
+ *   static final Options options = new Options().setHeadless(false).setDeviceName("iPhone 14");
+ *
+ *   @PlaywrightOptions
+ *   static final Browser.NewContextOptions contextOptions = new Browser.NewContextOptions()
+ *       .setBaseURL("https://github.com")
+ *       .setLocale("de-DE");
  *
  *   @Test
  *   void testWithCustomOptions(Page page) {
@@ -53,8 +64,8 @@
  *   EmbeddedServer server;
  *
  *   @PlaywrightOptions
- *   Options options() {
- *     return new Options().setBaseUrl(server.getURL().toString());
+ *   Browser.NewContextOptions contextOptions() {
+ *     return new Browser.NewContextOptions().setBaseURL(server.getURL().toString());
  *   }
  *
  *   @Test
@@ -67,8 +78,9 @@
  * member cannot be used when Playwright fixtures are requested in {@code @BeforeAll} methods or in
  * the constructor, because there is no test instance yet.
  *
- * 

The annotated member may be declared in a superclass or in an enclosing class of the test - * class. A {@code @Nested} class can declare its own member to run its tests with different options: + *

The annotated members may be declared in a superclass or in an enclosing class of the test + * class. A {@code @Nested} class can declare its own members to run its tests with different options, + * the members of other types declared in the enclosing class still apply: *

{@code
  * @UsePlaywright
  * public class MyTest {
@@ -76,12 +88,12 @@
  *   static final Options options = new Options().setBaseUrl("https://example.com");
  *
  *   @Nested
- *   class Mobile {
+ *   class InGerman {
  *     @PlaywrightOptions
- *     Options options = new Options().setBaseUrl("https://m.example.com").setDeviceName("iPhone 14");
+ *     Browser.NewContextOptions contextOptions = new Browser.NewContextOptions().setLocale("de-DE");
  *
  *     @Test
- *     void testOnMobile(Page page) {
+ *     void testInGerman(Page page) {
  *       page.navigate("/");
  *     }
  *   }
diff --git a/playwright/src/test/java/com/microsoft/playwright/impl/junit/TestOptionsExtension.java b/playwright/src/test/java/com/microsoft/playwright/impl/junit/TestOptionsExtension.java
index 8aa6ff924..d7f35a14f 100644
--- a/playwright/src/test/java/com/microsoft/playwright/impl/junit/TestOptionsExtension.java
+++ b/playwright/src/test/java/com/microsoft/playwright/impl/junit/TestOptionsExtension.java
@@ -16,22 +16,56 @@
 
 package com.microsoft.playwright.impl.junit;
 
+import com.microsoft.playwright.APIRequest;
+import com.microsoft.playwright.Browser;
+import com.microsoft.playwright.BrowserType;
 import com.microsoft.playwright.PlaywrightException;
 import com.microsoft.playwright.junit.Options;
 import com.microsoft.playwright.junit.PlaywrightOptions;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.TestInstances;
 
-import java.lang.reflect.Member;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
 
 import static org.junit.jupiter.api.Assertions.*;
 
 public class TestOptionsExtension {
-  static class StaticField {
+  static class AllTypes {
     @PlaywrightOptions
-    static Options options = new Options();
+    static final Options options = new Options()
+      .setBaseUrl("http://localhost:1234/")
+      .setContextOptions(new Browser.NewContextOptions().setLocale("fr-FR"));
+
+    @PlaywrightOptions
+    static final BrowserType.LaunchOptions launchOptions = new BrowserType.LaunchOptions().setHeadless(false);
+
+    @PlaywrightOptions
+    static Browser.NewContextOptions contextOptions() {
+      return new Browser.NewContextOptions().setLocale("de-DE");
+    }
+
+    @PlaywrightOptions
+    static final APIRequest.NewContextOptions apiRequestOptions = new APIRequest.NewContextOptions().setBaseURL("http://localhost:1234/api/");
+  }
+
+  static class Base {
+    @PlaywrightOptions
+    static final Options options = new Options().setBaseUrl("http://localhost:1234/base/");
   }
 
-  static class Subclass extends StaticField {
+  static class Subclass extends Base {
+  }
+
+  static class Outer {
+    @PlaywrightOptions
+    static final Options options = new Options().setBaseUrl("http://localhost:1234/outer/").setIgnoreHTTPSErrors(true);
+
+    class Nested {
+      @PlaywrightOptions
+      Browser.NewContextOptions contextOptions = new Browser.NewContextOptions().setLocale("de-DE");
+    }
   }
 
   static class InstanceMethod {
@@ -39,14 +73,11 @@ static class InstanceMethod {
     Options options() {
       return new Options();
     }
-
-    class Nested {
-    }
   }
 
-  static class FieldAndMethod {
+  static class TwoOfSameType {
     @PlaywrightOptions
-    static Options options = new Options();
+    static final Options options = new Options();
 
     @PlaywrightOptions
     static Options moreOptions() {
@@ -56,7 +87,7 @@ static Options moreOptions() {
 
   static class WrongType {
     @PlaywrightOptions
-    static String options = "";
+    static final String options = "";
   }
 
   static class MethodWithParameters {
@@ -66,53 +97,116 @@ static Options options(String name) {
     }
   }
 
+  static class NullOptions {
+    @PlaywrightOptions
+    static final Options options = null;
+  }
+
   static class NoOptions {
   }
 
+  private static TestInstances testInstances(Object... instances) {
+    return new TestInstances() {
+      @Override
+      public Object getInnermostInstance() {
+        return instances[instances.length - 1];
+      }
+
+      @Override
+      public List getEnclosingInstances() {
+        return Arrays.asList(instances).subList(0, instances.length - 1);
+      }
+
+      @Override
+      public List getAllInstances() {
+        return Arrays.asList(instances);
+      }
+
+      @Override
+      public  Optional findInstance(Class requiredType) {
+        return getAllInstances().stream().filter(requiredType::isInstance).map(requiredType::cast).findFirst();
+      }
+    };
+  }
+
+  @Test
+  void shouldCombineMembersOfAllTypes() {
+    Options options = OptionsExtension.createOptions(AllTypes.class, Optional.empty());
+    assertEquals("http://localhost:1234/", options.baseUrl);
+    assertEquals(false, options.launchOptions.headless);
+    assertEquals("de-DE", options.contextOptions.locale);
+    assertEquals("http://localhost:1234/api/", options.apiRequestOptions.baseURL);
+  }
+
   @Test
-  void shouldFindStaticField() {
-    Member member = OptionsExtension.findOptionsMember(StaticField.class);
-    assertEquals("options", member.getName());
-    assertEquals(StaticField.class, member.getDeclaringClass());
+  void shouldNotModifyOptionsOfTheTestClass() {
+    OptionsExtension.createOptions(AllTypes.class, Optional.empty());
+    assertEquals("fr-FR", AllTypes.options.contextOptions.locale);
+    assertNull(AllTypes.options.launchOptions);
+    assertNull(AllTypes.options.apiRequestOptions);
   }
 
   @Test
   void shouldFindMemberInSuperclass() {
-    Member member = OptionsExtension.findOptionsMember(Subclass.class);
-    assertEquals(StaticField.class, member.getDeclaringClass());
+    Options options = OptionsExtension.createOptions(Subclass.class, Optional.empty());
+    assertEquals("http://localhost:1234/base/", options.baseUrl);
   }
 
   @Test
-  void shouldFindMemberInEnclosingClass() {
-    Member member = OptionsExtension.findOptionsMember(InstanceMethod.Nested.class);
-    assertEquals("options", member.getName());
-    assertEquals(InstanceMethod.class, member.getDeclaringClass());
+  void shouldOverrideOnlyMembersOfTheSameTypeInNestedClass() {
+    Outer outer = new Outer();
+    Outer.Nested nested = outer.new Nested();
+    Options options = OptionsExtension.createOptions(Outer.Nested.class, Optional.of(testInstances(outer, nested)));
+    assertEquals("de-DE", options.contextOptions.locale);
+    assertEquals("http://localhost:1234/outer/", options.baseUrl);
+    assertEquals(true, options.ignoreHTTPSErrors);
   }
 
   @Test
-  void shouldReturnNullWhenThereIsNoMember() {
-    assertNull(OptionsExtension.findOptionsMember(NoOptions.class));
+  void shouldReturnDefaultOptionsWhenThereAreNoMembers() {
+    Options options = OptionsExtension.createOptions(NoOptions.class, Optional.empty());
+    assertNull(options.baseUrl);
+    assertEquals(Options.Trace.OFF, options.trace);
   }
 
   @Test
-  void shouldThrowWhenThereAreSeveralMembers() {
-    PlaywrightException e = assertThrows(PlaywrightException.class, () -> OptionsExtension.findOptionsMember(FieldAndMethod.class));
-    assertEquals("Only one @PlaywrightOptions field or method is allowed, found: " +
-      "@PlaywrightOptions field " + FieldAndMethod.class.getName() + ".options, " +
-      "@PlaywrightOptions method " + FieldAndMethod.class.getName() + ".moreOptions()", e.getMessage());
+  void shouldThrowWithoutTestInstance() {
+    PlaywrightException e = assertThrows(PlaywrightException.class,
+      () -> OptionsExtension.createOptions(InstanceMethod.class, Optional.empty()));
+    assertEquals("@PlaywrightOptions method " + InstanceMethod.class.getName() + ".options() requires a test instance. " +
+      "Make it static, or use @TestInstance(TestInstance.Lifecycle.PER_CLASS) to request Playwright fixtures in @BeforeAll methods or constructors.",
+      e.getMessage());
+  }
+
+  @Test
+  void shouldThrowWhenThereAreSeveralMembersOfTheSameType() {
+    PlaywrightException e = assertThrows(PlaywrightException.class,
+      () -> OptionsExtension.createOptions(TwoOfSameType.class, Optional.empty()));
+    assertEquals("Only one @PlaywrightOptions field or method of type com.microsoft.playwright.junit.Options is allowed, found: " +
+      "@PlaywrightOptions field " + TwoOfSameType.class.getName() + ".options, " +
+      "@PlaywrightOptions method " + TwoOfSameType.class.getName() + ".moreOptions()", e.getMessage());
   }
 
   @Test
   void shouldThrowOnWrongType() {
-    PlaywrightException e = assertThrows(PlaywrightException.class, () -> OptionsExtension.findOptionsMember(WrongType.class));
+    PlaywrightException e = assertThrows(PlaywrightException.class,
+      () -> OptionsExtension.createOptions(WrongType.class, Optional.empty()));
     assertEquals("@PlaywrightOptions field " + WrongType.class.getName() + ".options must be of type " +
-      Options.class.getName() + ".", e.getMessage());
+      "com.microsoft.playwright.junit.Options, com.microsoft.playwright.BrowserType.LaunchOptions, " +
+      "com.microsoft.playwright.Browser.NewContextOptions, com.microsoft.playwright.APIRequest.NewContextOptions.", e.getMessage());
   }
 
   @Test
   void shouldThrowOnMethodWithParameters() {
-    PlaywrightException e = assertThrows(PlaywrightException.class, () -> OptionsExtension.findOptionsMember(MethodWithParameters.class));
-    assertEquals("@PlaywrightOptions method " + MethodWithParameters.class.getName() + ".options() must not have parameters.",
-      e.getMessage());
+    PlaywrightException e = assertThrows(PlaywrightException.class,
+      () -> OptionsExtension.createOptions(MethodWithParameters.class, Optional.empty()));
+    assertEquals("@PlaywrightOptions method " + MethodWithParameters.class.getName() + ".options() must not have parameters.", e.getMessage());
+  }
+
+  @Test
+  void shouldThrowOnNullOptions() {
+    PlaywrightException e = assertThrows(PlaywrightException.class,
+      () -> OptionsExtension.createOptions(NullOptions.class, Optional.empty()));
+    assertEquals("@PlaywrightOptions field " + NullOptions.class.getName() + ".options must not be null.", e.getMessage());
   }
 }
diff --git a/playwright/src/test/java/com/microsoft/playwright/junit/TestFixtureContextOptions.java b/playwright/src/test/java/com/microsoft/playwright/junit/TestFixtureContextOptions.java
index ca4145ebe..ea4e6111c 100644
--- a/playwright/src/test/java/com/microsoft/playwright/junit/TestFixtureContextOptions.java
+++ b/playwright/src/test/java/com/microsoft/playwright/junit/TestFixtureContextOptions.java
@@ -34,10 +34,13 @@ public class TestFixtureContextOptions {
   }
 
   @PlaywrightOptions
-  Options options() {
-    return new Options()
-      .setApiRequestOptions(new APIRequest.NewContextOptions().setBaseURL(server.EMPTY_PAGE))
-      .setContextOptions(new Browser.NewContextOptions().setBaseURL(server.EMPTY_PAGE));
+  Browser.NewContextOptions contextOptions() {
+    return new Browser.NewContextOptions().setBaseURL(server.EMPTY_PAGE);
+  }
+
+  @PlaywrightOptions
+  APIRequest.NewContextOptions apiRequestOptions() {
+    return new APIRequest.NewContextOptions().setBaseURL(server.EMPTY_PAGE);
   }
 
   @Test
diff --git a/playwright/src/test/java/com/microsoft/playwright/junit/TestFixtureNewContextOptions.java b/playwright/src/test/java/com/microsoft/playwright/junit/TestFixtureNewContextOptions.java
index 908c31073..0816411ec 100644
--- a/playwright/src/test/java/com/microsoft/playwright/junit/TestFixtureNewContextOptions.java
+++ b/playwright/src/test/java/com/microsoft/playwright/junit/TestFixtureNewContextOptions.java
@@ -36,8 +36,10 @@ public class TestFixtureNewContextOptions {
   static final Options options = new Options()
     .setBaseUrl(BASE_URL)
     .setDeviceName("iPhone 14")
-    .setIgnoreHTTPSErrors(true)
-    .setContextOptions(new Browser.NewContextOptions().setLocale("de-DE"));
+    .setIgnoreHTTPSErrors(true);
+
+  @PlaywrightOptions
+  static final Browser.NewContextOptions contextOptions = new Browser.NewContextOptions().setLocale("de-DE");
 
   private static void assertConfiguredFromOptions(Browser.NewContextOptions contextOptions) {
     assertNotNull(contextOptions);
diff --git a/playwright/src/test/java/com/microsoft/playwright/junit/TestFixturePlaywrightOptionsStatic.java b/playwright/src/test/java/com/microsoft/playwright/junit/TestFixturePlaywrightOptionsStatic.java
index d9bf96d10..67aa9b6d2 100644
--- a/playwright/src/test/java/com/microsoft/playwright/junit/TestFixturePlaywrightOptionsStatic.java
+++ b/playwright/src/test/java/com/microsoft/playwright/junit/TestFixturePlaywrightOptionsStatic.java
@@ -32,7 +32,7 @@ public class TestFixturePlaywrightOptionsStatic {
   private static final String NESTED_BASE_URL = "http://localhost:1234/nested/";
 
   @PlaywrightOptions
-  static final Options options = new Options().setBaseUrl(BASE_URL);
+  static final Options options = new Options().setBaseUrl(BASE_URL).setIgnoreHTTPSErrors(true);
 
   // A static member does not need a test instance.
   @BeforeAll
@@ -70,4 +70,18 @@ void shouldOverrideOptionsOfEnclosingClass(Browser.NewContextOptions contextOpti
       assertEquals(NESTED_BASE_URL, contextOptions.baseURL);
     }
   }
+
+  @Nested
+  class NestedClassWithOwnContextOptions {
+    @PlaywrightOptions
+    Browser.NewContextOptions contextOptions = new Browser.NewContextOptions().setLocale("de-DE");
+
+    @Test
+    void shouldOverrideOnlyContextOptionsOfEnclosingClass(Browser.NewContextOptions contextOptions) {
+      assertEquals("de-DE", contextOptions.locale);
+      // The other options of the enclosing class still apply.
+      assertEquals(BASE_URL, contextOptions.baseURL);
+      assertEquals(true, contextOptions.ignoreHTTPSErrors);
+    }
+  }
 }