Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,33 +18,70 @@

import com.microsoft.playwright.PlaywrightException;
import com.microsoft.playwright.junit.Options;
import com.microsoft.playwright.junit.UsePlaywright;
import com.microsoft.playwright.junit.OptionsFactory;
import org.junit.jupiter.api.extension.AfterAllCallback;
import org.junit.jupiter.api.extension.ExtensionContext;

import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;

import static com.microsoft.playwright.impl.junit.ExtensionUtils.*;

public class OptionsExtension implements AfterAllCallback {
private static final ThreadLocal<Options> threadLocalOptions = new ThreadLocal<>();
// Test instance that the options were created for, when the factory is an inner class of the test class.
private static final ThreadLocal<Object> threadLocalEnclosingInstance = new ThreadLocal<>();

@Override
public void afterAll(ExtensionContext extensionContext) {
threadLocalOptions.remove();
threadLocalEnclosingInstance.remove();
}

static Options getOptions(ExtensionContext extensionContext) {
Class<? extends OptionsFactory> factoryClass = getUsePlaywrightAnnotation(extensionContext).value();
// Inner class factories can access fields of the test instance, so the options are
// re-created for each test instance.
Object enclosingInstance = findEnclosingInstance(factoryClass, extensionContext);
Options options = threadLocalOptions.get();
if (options != null) {
if (options != null && (enclosingInstance == null || enclosingInstance == threadLocalEnclosingInstance.get())) {
return options;
}
options = createFactory(factoryClass, enclosingInstance).getOptions();
threadLocalOptions.set(options);
threadLocalEnclosingInstance.set(enclosingInstance);
return options;
}

private static boolean isInnerClass(Class<?> clazz) {
return clazz.isMemberClass() && !Modifier.isStatic(clazz.getModifiers());
}

private static Object findEnclosingInstance(Class<?> factoryClass, ExtensionContext extensionContext) {
if (!isInnerClass(factoryClass)) {
return null;
}
return extensionContext.getTestInstances()
.flatMap(instances -> instances.findInstance(factoryClass.getDeclaringClass()))
.orElse(null);
}

UsePlaywright usePlaywrightAnnotation = getUsePlaywrightAnnotation(extensionContext);
private static OptionsFactory createFactory(Class<? extends OptionsFactory> factoryClass, Object enclosingInstance) {
if (enclosingInstance == null && isInnerClass(factoryClass)) {
throw new PlaywrightException("Cannot create " + factoryClass.getName() + " without an instance of " +
factoryClass.getDeclaringClass().getName() + ". Make the options factory a static class or use " +
"@TestInstance(TestInstance.Lifecycle.PER_CLASS) to request Playwright fixtures in @BeforeAll methods.");
}
try {
options = usePlaywrightAnnotation.value().newInstance().getOptions();
threadLocalOptions.set(options);
} catch (InstantiationException | IllegalAccessException e) {
if (enclosingInstance == null) {
return factoryClass.newInstance();
}
// Use the declaring class rather than enclosingInstance.getClass(), which may be a subclass.
Constructor<? extends OptionsFactory> constructor = factoryClass.getDeclaredConstructor(factoryClass.getDeclaringClass());
constructor.setAccessible(true);
Comment on lines +80 to +81

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think we need to use extensionContext.getExecutableInvoker().invoke(constructor, enclosingInstance) instead so JUnit creates the factory with its existing access as otherwise this would fail in named modules that open the test package to JUnit but not Playwright

return constructor.newInstance(enclosingInstance);
} catch (ReflectiveOperationException e) {
throw new PlaywrightException("Failed to create options", e);
}
return options;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,29 @@
* }
* }</pre>
*
* <p> If the options depend on the state of the test instance (e.g. a server injected by
* a dependency injection framework), declare the factory as a non-static inner class of
* the test class. It will be created for each test instance and can access its fields:
* <pre>{@code
* @UsePlaywright(MyTest.CustomOptions.class)
* public class MyTest {
* @Inject
* EmbeddedServer server;
*
* class CustomOptions implements OptionsFactory {
* @Override
* public Options getOptions() {
* return new Options().setBaseUrl(server.getURL().toString());
* }
* }
* }
* }</pre>
* Only context level options (e.g. base URL, context options, trace) are re-evaluated for each
* test instance; {@code Playwright} and {@code Browser} are created with the options of the first
* test instance and reused. Unless the test class uses {@code @TestInstance(Lifecycle.PER_CLASS)},
* Playwright fixtures cannot be requested in {@code @BeforeAll} methods because there is no
* test instance yet.
*
* <p>For more details and usage examples see our
* <a href="https://playwright.dev/java/docs/junit">JUnit guide</a>.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* 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(TestFixtureInnerOptionsFactory.CustomOptions.class)
public class TestFixtureInnerOptionsFactory {
// Set per instance, similar to fields injected by Spring or Micronaut.
private final Server server;
private boolean optionsCreated;

TestFixtureInnerOptionsFactory(Server server) {
this.server = server;
}

class CustomOptions implements OptionsFactory {
@Override
public Options getOptions() {
optionsCreated = true;
return new Options().setBaseUrl(server.PREFIX);
}
}

// Each repetition gets a new test instance.
@RepeatedTest(2)
void shouldCreateOptionsForEachTestInstance(Page page) {
assertTrue(optionsCreated);
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 shouldUseOuterInstance(Page page) {
page.navigate("/empty.html");
assertThat(page).hasURL(server.EMPTY_PAGE);
}
}
}
Loading