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..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,16 +16,83 @@ 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.UsePlaywright; +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; +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.*; + +import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.joining; public class OptionsExtension implements AfterAllCallback { - private static final ThreadLocal threadLocalOptions = new ThreadLocal<>(); + // 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; + final Map, Source> sources; + + CachedOptions(Options options, Map, Source> sources) { + this.options = options; + this.sources = sources; + } + } @Override public void afterAll(ExtensionContext extensionContext) { @@ -33,18 +100,114 @@ public void afterAll(ExtensionContext extensionContext) { } static Options getOptions(ExtensionContext extensionContext) { - Options options = threadLocalOptions.get(); - if (options != null) { - return options; + Map, Source> sources = findSources(extensionContext.getRequiredTestClass(), extensionContext.getTestInstances()); + CachedOptions cached = threadLocalOptions.get(); + if (cached != null && cached.sources.equals(sources)) { + return cached.options; } + Options options = createOptions(sources); + threadLocalOptions.set(new CachedOptions(options, sources)); + return options; + } + + // Visible for testing. + static Options createOptions(Class testClass, Optional testInstances) { + return createOptions(findSources(testClass, testInstances)); + } - 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); + 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); + } + } + 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 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)); + } + + // 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(); + } else { + Method method = (Method) member; + if (method.getParameterCount() != 0) { + throw new PlaywrightException(describe(member) + " must not have parameters."); + } + type = method.getReturnType(); + } + 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 Source toSource(Member member, Optional testInstances) { + if (ModifierSupport.isStatic(member)) { + return new Source(member, null); + } + 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) { + 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..bb89c5333 --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/junit/PlaywrightOptions.java @@ -0,0 +1,115 @@ +/* + * 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 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).setDeviceName("iPhone 14");
+ *
+ *   @PlaywrightOptions
+ *   static final Browser.NewContextOptions contextOptions = new Browser.NewContextOptions()
+ *       .setBaseURL("https://github.com")
+ *       .setLocale("de-DE");
+ *
+ *   @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
+ *   Browser.NewContextOptions contextOptions() {
+ *     return new Browser.NewContextOptions().setBaseURL(server.getURL().toString());
+ *   }
+ *
+ *   @Test
+ *   void testWithEmbeddedServer(Page page) {
+ *     page.navigate("/");
+ *   }
+ * }
+ * }
+ * 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 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 {
+ *   @PlaywrightOptions
+ *   static final Options options = new Options().setBaseUrl("https://example.com");
+ *
+ *   @Nested
+ *   class InGerman {
+ *     @PlaywrightOptions
+ *     Browser.NewContextOptions contextOptions = new Browser.NewContextOptions().setLocale("de-DE");
+ *
+ *     @Test
+ *     void testInGerman(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. + */ +@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..d7f35a14f --- /dev/null +++ b/playwright/src/test/java/com/microsoft/playwright/impl/junit/TestOptionsExtension.java @@ -0,0 +1,212 @@ +/* + * 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.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.util.Arrays; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +public class TestOptionsExtension { + static class AllTypes { + @PlaywrightOptions + 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 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 { + @PlaywrightOptions + Options options() { + return new Options(); + } + } + + static class TwoOfSameType { + @PlaywrightOptions + static final Options options = new Options(); + + @PlaywrightOptions + static Options moreOptions() { + return new Options(); + } + } + + static class WrongType { + @PlaywrightOptions + static final String options = ""; + } + + static class MethodWithParameters { + @PlaywrightOptions + static Options options(String name) { + return new Options(); + } + } + + 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 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() { + Options options = OptionsExtension.createOptions(Subclass.class, Optional.empty()); + assertEquals("http://localhost:1234/base/", options.baseUrl); + } + + @Test + 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 shouldReturnDefaultOptionsWhenThereAreNoMembers() { + Options options = OptionsExtension.createOptions(NoOptions.class, Optional.empty()); + assertNull(options.baseUrl); + assertEquals(Options.Trace.OFF, options.trace); + } + + @Test + 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.createOptions(WrongType.class, Optional.empty())); + assertEquals("@PlaywrightOptions field " + WrongType.class.getName() + ".options must be of type " + + "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.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/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..ea4e6111c 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,26 @@ 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 + 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/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..0816411ec 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,22 @@ 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); + + @PlaywrightOptions + static final Browser.NewContextOptions contextOptions = 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 +55,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 +72,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 +85,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..67aa9b6d2 --- /dev/null +++ b/playwright/src/test/java/com/microsoft/playwright/junit/TestFixturePlaywrightOptionsStatic.java @@ -0,0 +1,87 @@ +/* + * 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.AfterAll; +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/"; + private static final String NESTED_BASE_URL = "http://localhost:1234/nested/"; + + @PlaywrightOptions + static final Options options = new Options().setBaseUrl(BASE_URL).setIgnoreHTTPSErrors(true); + + // 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); + } + + // 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 + 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); + } + } + + @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); + } + } +} 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