From f28ca44fa018130331c17906e2ef31a991a78d0a Mon Sep 17 00:00:00 2001 From: uchagani Date: Mon, 4 Dec 2023 20:32:59 -0500 Subject: [PATCH] feat(junit-playwright) (#1412) --- .../com/microsoft/playwright/impl/Utils.java | 4 +- .../microsoft/playwright/junit/Options.java | 112 ++++++++++++++++++ .../playwright/junit/UsePlaywright.java | 17 +++ .../impl/APIRequestContextExtension.java | 45 +++++++ .../junit/impl/BrowserContextExtension.java | 68 +++++++++++ .../junit/impl/BrowserExtension.java | 79 ++++++++++++ .../playwright/junit/impl/ExtensionUtils.java | 30 +++++ .../junit/impl/OptionsExtension.java | 34 ++++++ .../playwright/junit/impl/PageExtension.java | 43 +++++++ .../junit/impl/PlaywrightExtension.java | 43 +++++++ .../playwright/TestPlaywrightFixtures.java | 55 +++++++++ pom.xml | 6 + 12 files changed, 534 insertions(+), 2 deletions(-) create mode 100644 playwright/src/main/java/com/microsoft/playwright/junit/Options.java create mode 100644 playwright/src/main/java/com/microsoft/playwright/junit/UsePlaywright.java create mode 100644 playwright/src/main/java/com/microsoft/playwright/junit/impl/APIRequestContextExtension.java create mode 100644 playwright/src/main/java/com/microsoft/playwright/junit/impl/BrowserContextExtension.java create mode 100644 playwright/src/main/java/com/microsoft/playwright/junit/impl/BrowserExtension.java create mode 100644 playwright/src/main/java/com/microsoft/playwright/junit/impl/ExtensionUtils.java create mode 100644 playwright/src/main/java/com/microsoft/playwright/junit/impl/OptionsExtension.java create mode 100644 playwright/src/main/java/com/microsoft/playwright/junit/impl/PageExtension.java create mode 100644 playwright/src/main/java/com/microsoft/playwright/junit/impl/PlaywrightExtension.java create mode 100644 playwright/src/test/java/com/microsoft/playwright/TestPlaywrightFixtures.java diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/Utils.java b/playwright/src/main/java/com/microsoft/playwright/impl/Utils.java index e1ed8b737..981a4249f 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/Utils.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/Utils.java @@ -36,7 +36,7 @@ import static com.microsoft.playwright.impl.Serialization.toJsonArray; -class Utils { +public class Utils { static T convertType(F f, Class t) { if (f == null) { return null; @@ -80,7 +80,7 @@ static T convertType(F f, Class t) { } } - static T clone(T f) { + public static T clone(T f) { if (f == null) { return f; } diff --git a/playwright/src/main/java/com/microsoft/playwright/junit/Options.java b/playwright/src/main/java/com/microsoft/playwright/junit/Options.java new file mode 100644 index 000000000..4ccdd19ac --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/junit/Options.java @@ -0,0 +1,112 @@ +package com.microsoft.playwright.junit; + +import com.microsoft.playwright.APIRequest; +import com.microsoft.playwright.Browser; +import com.microsoft.playwright.BrowserType; +import com.microsoft.playwright.Playwright; +import com.microsoft.playwright.options.ViewportSize; + +import java.nio.file.Path; + +public class Options { + public String baseUrl; + public Path storageStatePath; + public ViewportSize viewportSize; + public String channel; + public Boolean headless; + public String browserName = "chromium"; + public BrowserType.LaunchOptions launchOptions; + public Browser.NewContextOptions contextOption; + public APIRequest.NewContextOptions apiRequestOptions; + public Playwright.CreateOptions playwrightCreateOptions; + + public Playwright.CreateOptions getPlaywrightCreateOptions() { + return playwrightCreateOptions; + } + + public Options setPlaywrightCreateOptions(Playwright.CreateOptions playwrightCreateOptions) { + this.playwrightCreateOptions = playwrightCreateOptions; + return this; + } + + public BrowserType.LaunchOptions getLaunchOptions() { + return launchOptions; + } + + public Options setLaunchOptions(BrowserType.LaunchOptions launchOptions) { + this.launchOptions = launchOptions; + return this; + } + + public Browser.NewContextOptions getContextOption() { + return contextOption; + } + + public Options setContextOption(Browser.NewContextOptions contextOption) { + this.contextOption = contextOption; + return this; + } + + public APIRequest.NewContextOptions getApiRequestOptions() { + return apiRequestOptions; + } + + public Options setApiRequestOptions(APIRequest.NewContextOptions apiRequestOptions) { + this.apiRequestOptions = apiRequestOptions; + return this; + } + + public String getBaseUrl() { + return baseUrl; + } + + public Options setBaseUrl(String baseUrl) { + this.baseUrl = baseUrl; + return this; + } + + public Path getStorageStatePath() { + return storageStatePath; + } + + public Options setStorageStatePath(Path storageStatePath) { + this.storageStatePath = storageStatePath; + return this; + } + + public String getBrowserName() { + return browserName; + } + + public Options setBrowserName(String browserName) { + this.browserName = browserName; + return this; + } + + public String getChannel() { + return channel; + } + + public Options setChannel(String channel) { + this.channel = channel; + return this; + } + + public Boolean isHeadless() { + return headless; + } + + public Options setHeadless(Boolean headless) { + this.headless = headless; + return this; + } + + public ViewportSize getViewportSize() { + return viewportSize; + } + + public Options setViewportSize(ViewportSize viewportSize) { + this.viewportSize = viewportSize; + return this; + } +} diff --git a/playwright/src/main/java/com/microsoft/playwright/junit/UsePlaywright.java b/playwright/src/main/java/com/microsoft/playwright/junit/UsePlaywright.java new file mode 100644 index 000000000..323805121 --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/junit/UsePlaywright.java @@ -0,0 +1,17 @@ +package com.microsoft.playwright.junit; + +import com.microsoft.playwright.junit.impl.*; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@ExtendWith({OptionsExtension.class, PlaywrightExtension.class, BrowserExtension.class, BrowserContextExtension.class, + PageExtension.class, APIRequestContextExtension.class}) +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface UsePlaywright { + Class options() default Options.class; +} diff --git a/playwright/src/main/java/com/microsoft/playwright/junit/impl/APIRequestContextExtension.java b/playwright/src/main/java/com/microsoft/playwright/junit/impl/APIRequestContextExtension.java new file mode 100644 index 000000000..7696a7be8 --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/junit/impl/APIRequestContextExtension.java @@ -0,0 +1,45 @@ +package com.microsoft.playwright.junit.impl; + +import com.microsoft.playwright.APIRequestContext; +import com.microsoft.playwright.Playwright; +import com.microsoft.playwright.junit.Options; +import org.junit.jupiter.api.extension.*; + +import static com.microsoft.playwright.junit.impl.ExtensionUtils.isParameterSupported; + +public class APIRequestContextExtension implements ParameterResolver, BeforeEachCallback, AfterAllCallback { + private static final ThreadLocal threadLocalAPIRequestContext = new ThreadLocal<>(); + + @Override + public void beforeEach(ExtensionContext extensionContext) { + threadLocalAPIRequestContext.remove(); + } + + @Override + public void afterAll(ExtensionContext extensionContext) { + threadLocalAPIRequestContext.remove(); + } + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { + return isParameterSupported(parameterContext, extensionContext, APIRequestContext.class); + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { + return getOrCreateAPIRequestContext(extensionContext); + } + + static APIRequestContext getOrCreateAPIRequestContext(ExtensionContext extensionContext) { + APIRequestContext apiRequestContext = threadLocalAPIRequestContext.get(); + if (apiRequestContext != null) { + return apiRequestContext; + } + + Options options = OptionsExtension.getOptions(extensionContext); + Playwright playwright = PlaywrightExtension.getOrCreatePlaywright(extensionContext); + apiRequestContext = playwright.request().newContext(options.getApiRequestOptions()); + threadLocalAPIRequestContext.set(apiRequestContext); + return apiRequestContext; + } +} diff --git a/playwright/src/main/java/com/microsoft/playwright/junit/impl/BrowserContextExtension.java b/playwright/src/main/java/com/microsoft/playwright/junit/impl/BrowserContextExtension.java new file mode 100644 index 000000000..5e32f628b --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/junit/impl/BrowserContextExtension.java @@ -0,0 +1,68 @@ +package com.microsoft.playwright.junit.impl; + +import com.microsoft.playwright.Browser; +import com.microsoft.playwright.BrowserContext; +import com.microsoft.playwright.impl.Utils; +import com.microsoft.playwright.junit.Options; +import org.junit.jupiter.api.extension.*; + +import static com.microsoft.playwright.junit.impl.ExtensionUtils.isClassHook; +import static com.microsoft.playwright.junit.impl.ExtensionUtils.isParameterSupported; + +public class BrowserContextExtension implements ParameterResolver, AfterEachCallback { + private static final ThreadLocal threadLocalBrowserContext = new ThreadLocal<>(); + + @Override + public void afterEach(ExtensionContext extensionContext) { + BrowserContext browserContext = threadLocalBrowserContext.get(); + threadLocalBrowserContext.remove(); + if (browserContext != null) { + browserContext.close(); + } + } + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { + return !isClassHook(extensionContext) && isParameterSupported(parameterContext, extensionContext, BrowserContext.class); + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { + return getOrCreateBrowserContext(extensionContext); + } + + static BrowserContext getOrCreateBrowserContext(ExtensionContext extensionContext) { + BrowserContext browserContext = threadLocalBrowserContext.get(); + if (browserContext != null) { + return browserContext; + } + + Options options = OptionsExtension.getOptions(extensionContext); + Browser browser = BrowserExtension.getOrCreateBrowser(extensionContext); + Browser.NewContextOptions contextOptions = getContextOptions(options); + browserContext = browser.newContext(contextOptions); + threadLocalBrowserContext.set(browserContext); + return browserContext; + } + + private static Browser.NewContextOptions getContextOptions(Options options) { + Browser.NewContextOptions contextOptions = Utils.clone(options.getContextOption()); + if (contextOptions == null) { + contextOptions = new Browser.NewContextOptions(); + } + + if (options.getBaseUrl() != null) { + contextOptions.setBaseURL(options.getBaseUrl()); + } + + if (options.getStorageStatePath() != null) { + contextOptions.setStorageStatePath(options.getStorageStatePath()); + } + + if (options.getViewportSize() != null) { + contextOptions.setViewportSize(options.getViewportSize()); + } + + return contextOptions; + } +} diff --git a/playwright/src/main/java/com/microsoft/playwright/junit/impl/BrowserExtension.java b/playwright/src/main/java/com/microsoft/playwright/junit/impl/BrowserExtension.java new file mode 100644 index 000000000..64862c171 --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/junit/impl/BrowserExtension.java @@ -0,0 +1,79 @@ +package com.microsoft.playwright.junit.impl; + +import com.microsoft.playwright.Browser; +import com.microsoft.playwright.BrowserType; +import com.microsoft.playwright.Playwright; +import com.microsoft.playwright.PlaywrightException; +import com.microsoft.playwright.impl.Utils; +import com.microsoft.playwright.junit.Options; +import org.junit.jupiter.api.extension.*; + +import static com.microsoft.playwright.junit.impl.ExtensionUtils.isParameterSupported; + +public class BrowserExtension implements ParameterResolver, AfterAllCallback { + private static final ThreadLocal threadLocalBrowser = new ThreadLocal<>(); + + @Override + public void afterAll(ExtensionContext extensionContext) { + Browser browser = threadLocalBrowser.get(); + threadLocalBrowser.remove(); + if (browser != null) { + browser.close(); + } + } + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { + return isParameterSupported(parameterContext, extensionContext, Browser.class); + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { + return getOrCreateBrowser(extensionContext); + } + + static Browser getOrCreateBrowser(ExtensionContext extensionContext) { + Browser browser = threadLocalBrowser.get(); + if (browser != null) { + return browser; + } + + Options options = OptionsExtension.getOptions(extensionContext); + Playwright playwright = PlaywrightExtension.getOrCreatePlaywright(extensionContext); + BrowserType.LaunchOptions launchOptions = getLaunchOptions(options); + + switch (options.getBrowserName()) { + case "webkit": + browser = playwright.webkit().launch(launchOptions); + break; + case "firefox": + browser = playwright.firefox().launch(launchOptions); + break; + case "chromium": + browser = playwright.chromium().launch(launchOptions); + break; + default: + throw new PlaywrightException("Invalid browser name."); + } + + threadLocalBrowser.set(browser); + return browser; + } + + private static BrowserType.LaunchOptions getLaunchOptions(Options options) { + BrowserType.LaunchOptions launchOptions = Utils.clone(options.getLaunchOptions()); + if (launchOptions == null) { + launchOptions = new BrowserType.LaunchOptions(); + } + + if (options.isHeadless() != null) { + launchOptions.setHeadless(options.isHeadless()); + } + + if (options.getChannel() != null) { + options.setChannel(options.getChannel()); + } + + return launchOptions; + } +} diff --git a/playwright/src/main/java/com/microsoft/playwright/junit/impl/ExtensionUtils.java b/playwright/src/main/java/com/microsoft/playwright/junit/impl/ExtensionUtils.java new file mode 100644 index 000000000..355dcdd06 --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/junit/impl/ExtensionUtils.java @@ -0,0 +1,30 @@ +package com.microsoft.playwright.junit.impl; + +import com.microsoft.playwright.junit.UsePlaywright; +import org.junit.jupiter.api.extension.ExtensionContext; +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) { + return AnnotationSupport.isAnnotated(extensionContext.getTestClass(), UsePlaywright.class); + } + + static UsePlaywright getUsePlaywrightAnnotation(ExtensionContext extensionContext) { + return findAnnotation(extensionContext.getTestClass(), UsePlaywright.class).get(); + } + + static boolean isClassHook(ExtensionContext extensionContext) { + return !extensionContext.getTestMethod().isPresent(); + } + + static boolean isParameterSupported(ParameterContext parameterContext, ExtensionContext extensionContext, Class subject) { + if (!hasUsePlaywrightAnnotation(extensionContext)) { + return false; + } + Class clazz = parameterContext.getParameter().getType(); + return subject.equals(clazz); + } +} diff --git a/playwright/src/main/java/com/microsoft/playwright/junit/impl/OptionsExtension.java b/playwright/src/main/java/com/microsoft/playwright/junit/impl/OptionsExtension.java new file mode 100644 index 000000000..3cd67eb6e --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/junit/impl/OptionsExtension.java @@ -0,0 +1,34 @@ +package com.microsoft.playwright.junit.impl; + +import com.microsoft.playwright.PlaywrightException; +import com.microsoft.playwright.junit.Options; +import com.microsoft.playwright.junit.UsePlaywright; +import org.junit.jupiter.api.extension.AfterAllCallback; +import org.junit.jupiter.api.extension.ExtensionContext; + +import static com.microsoft.playwright.junit.impl.ExtensionUtils.getUsePlaywrightAnnotation; + +public class OptionsExtension implements AfterAllCallback { + private static final ThreadLocal threadLocalOptions = new ThreadLocal<>(); + + @Override + public void afterAll(ExtensionContext extensionContext) { + threadLocalOptions.remove(); + } + + static Options getOptions(ExtensionContext extensionContext) { + Options options = threadLocalOptions.get(); + if (options != null) { + return options; + } + + UsePlaywright usePlaywrightAnnotation = getUsePlaywrightAnnotation(extensionContext); + try { + options = usePlaywrightAnnotation.options().newInstance(); + threadLocalOptions.set(options); + } catch (InstantiationException | IllegalAccessException e) { + throw new PlaywrightException("Failed to create options", e); + } + return options; + } +} diff --git a/playwright/src/main/java/com/microsoft/playwright/junit/impl/PageExtension.java b/playwright/src/main/java/com/microsoft/playwright/junit/impl/PageExtension.java new file mode 100644 index 000000000..d1adfbe58 --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/junit/impl/PageExtension.java @@ -0,0 +1,43 @@ +package com.microsoft.playwright.junit.impl; + +import com.microsoft.playwright.BrowserContext; +import com.microsoft.playwright.Page; +import org.junit.jupiter.api.extension.*; + +import static com.microsoft.playwright.junit.impl.ExtensionUtils.isClassHook; +import static com.microsoft.playwright.junit.impl.ExtensionUtils.isParameterSupported; + +public class PageExtension implements ParameterResolver, AfterEachCallback { + private static final ThreadLocal threadLocalPage = new ThreadLocal<>(); + + @Override + public void afterEach(ExtensionContext extensionContext) { + Page page = threadLocalPage.get(); + threadLocalPage.remove(); + if (page != null) { + page.close(); + } + } + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { + return !isClassHook(extensionContext) && isParameterSupported(parameterContext, extensionContext, Page.class); + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { + return getOrCreatePage(extensionContext); + } + + static Page getOrCreatePage(ExtensionContext extensionContext) { + Page page = threadLocalPage.get(); + if (page != null) { + return page; + } + + BrowserContext browserContext = BrowserContextExtension.getOrCreateBrowserContext(extensionContext); + page = browserContext.newPage(); + threadLocalPage.set(page); + return page; + } +} diff --git a/playwright/src/main/java/com/microsoft/playwright/junit/impl/PlaywrightExtension.java b/playwright/src/main/java/com/microsoft/playwright/junit/impl/PlaywrightExtension.java new file mode 100644 index 000000000..f324e6e10 --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/junit/impl/PlaywrightExtension.java @@ -0,0 +1,43 @@ +package com.microsoft.playwright.junit.impl; + +import com.microsoft.playwright.Playwright; +import com.microsoft.playwright.junit.Options; +import org.junit.jupiter.api.extension.*; + +import static com.microsoft.playwright.junit.impl.ExtensionUtils.isParameterSupported; + +public class PlaywrightExtension implements ParameterResolver, AfterAllCallback { + private static final ThreadLocal threadLocalPlaywright = new ThreadLocal<>(); + + @Override + public void afterAll(ExtensionContext extensionContext) { + Playwright playwright = threadLocalPlaywright.get(); + threadLocalPlaywright.remove(); + if (playwright != null) { + playwright.close(); + } + } + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { + return isParameterSupported(parameterContext, extensionContext, Playwright.class); + + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { + return getOrCreatePlaywright(extensionContext); + } + + static Playwright getOrCreatePlaywright(ExtensionContext extensionContext) { + Playwright playwright = threadLocalPlaywright.get(); + if (playwright != null) { + return playwright; + } + + Options options = OptionsExtension.getOptions(extensionContext); + playwright = Playwright.create(options.getPlaywrightCreateOptions()); + threadLocalPlaywright.set(playwright); + return playwright; + } +} diff --git a/playwright/src/test/java/com/microsoft/playwright/TestPlaywrightFixtures.java b/playwright/src/test/java/com/microsoft/playwright/TestPlaywrightFixtures.java new file mode 100644 index 000000000..44974de41 --- /dev/null +++ b/playwright/src/test/java/com/microsoft/playwright/TestPlaywrightFixtures.java @@ -0,0 +1,55 @@ +package com.microsoft.playwright; + +import com.microsoft.playwright.junit.UsePlaywright; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +@UsePlaywright +public class TestPlaywrightFixtures { + private static Playwright playwrightFromBeforeAll; + private static Browser browserFromBeforeAll; + private BrowserContext browserContextFromBeforeEach; + private Page pageFromBeforeEach; + private static APIRequestContext apiRequestContextFromBeforeAll; + private APIRequestContext apiRequestContextFromBeforeEach; + + @BeforeAll + public static void beforeAll(Playwright playwright, Browser browser, APIRequestContext apiRequestContext) { + assertNotNull(playwright); + assertNotNull(browser); + assertNotNull(apiRequestContext); + + playwrightFromBeforeAll = playwright; + browserFromBeforeAll = browser; + apiRequestContextFromBeforeAll = apiRequestContext; + } + + @BeforeEach + public void beforeEach(Playwright playwright, Browser browser, BrowserContext browserContext, Page page, APIRequestContext apiRequestContext) { + assertEquals(playwrightFromBeforeAll, playwright); + assertEquals(browserFromBeforeAll, browser); + assertNotEquals(apiRequestContextFromBeforeAll, apiRequestContext); + + assertNotNull(browserContext); + assertNotNull(page); + browserContextFromBeforeEach = browserContext; + pageFromBeforeEach = page; + apiRequestContextFromBeforeEach = apiRequestContext; + } + + @Test + public void objectShouldBeSameAsBeforeAll(Playwright playwright, Browser browser) { + assertEquals(playwrightFromBeforeAll, playwright); + assertEquals(browserFromBeforeAll, browser); + } + + @Test + public void objectShouldBeSameAsBeforeEach(BrowserContext browserContext, Page page, APIRequestContext apiRequestContext) { + assertEquals(browserContextFromBeforeEach, browserContext); + assertEquals(pageFromBeforeEach, page); + assertEquals(apiRequestContextFromBeforeEach, apiRequestContext); + } +} diff --git a/pom.xml b/pom.xml index 780da61cf..6e9ecb664 100644 --- a/pom.xml +++ b/pom.xml @@ -86,6 +86,12 @@ ${websocket.version} test + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + provided +