| // Copyright (c) 2026, the R8 project authors. Please see the AUTHORS file | |
| // for details. All rights reserved. Use of this source code is governed by a | |
| // BSD-style license that can be found in the LICENSE file. | |
| package com.android.tools.r8; | |
| import static org.junit.Assert.assertTrue; | |
| import static org.junit.Assert.fail; | |
| import com.microsoft.playwright.Browser; | |
| import com.microsoft.playwright.BrowserType; | |
| import com.microsoft.playwright.ConsoleMessage; | |
| import com.microsoft.playwright.Page; | |
| import com.microsoft.playwright.Playwright; | |
| import com.microsoft.playwright.Request; | |
| import java.io.IOException; | |
| import java.nio.file.Files; | |
| import java.nio.file.Path; | |
| import java.util.ArrayList; | |
| import java.util.HashMap; | |
| import java.util.List; | |
| import java.util.Map; | |
| import org.junit.After; | |
| import org.junit.AfterClass; | |
| public class PlaywrightTestBase extends TestBase { | |
| private static Playwright playwright; | |
| private static Browser browser; | |
| protected Page page; | |
| private final List<ConsoleMessage> consoleMessages = new ArrayList<>(); | |
| private final List<String> pageErrors = new ArrayList<>(); | |
| private final List<Request> failedRequests = new ArrayList<>(); | |
| public Page getPage(boolean headful) { | |
| if (page == null) { | |
| initPage(headful); | |
| } | |
| return page; | |
| } | |
| private static synchronized void initBrowser(boolean headful) { | |
| if (browser != null) { | |
| return; | |
| } | |
| boolean headless = !headful; | |
| Path chromeExecutable = headless ? resolveChromeExecutable() : null; | |
| Map<String, String> env = new HashMap<>(); | |
| if (headless) { | |
| env.put("PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD", "1"); | |
| } else { | |
| // Allow downloading when headful. We set it to 0 to override any inherited env var. | |
| env.put("PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD", "0"); | |
| } | |
| Playwright.CreateOptions createOptions = new Playwright.CreateOptions().setEnv(env); | |
| playwright = Playwright.create(createOptions); | |
| BrowserType.LaunchOptions launchOptions = new BrowserType.LaunchOptions(); | |
| if (headless) { | |
| if (chromeExecutable == null) { | |
| throw new RuntimeException("chrome-headless-shell binary is missing"); | |
| } | |
| launchOptions.setExecutablePath(chromeExecutable); | |
| launchOptions.setHeadless(true); | |
| } else { | |
| launchOptions.setHeadless(false); | |
| launchOptions.setSlowMo(50); | |
| } | |
| browser = playwright.chromium().launch(launchOptions); | |
| } | |
| private void initPage(boolean headful) { | |
| initBrowser(headful); | |
| page = browser.newPage(); | |
| consoleMessages.clear(); | |
| pageErrors.clear(); | |
| failedRequests.clear(); | |
| page.onConsoleMessage(consoleMessages::add); | |
| page.onPageError(pageErrors::add); | |
| page.onRequestFailed(failedRequests::add); | |
| } | |
| @After | |
| public void tearDownPage() { | |
| if (page != null) { | |
| try { | |
| verifyNoConsoleOrNetworkErrors(); | |
| } finally { | |
| page.close(); | |
| page = null; | |
| } | |
| } | |
| } | |
| private void verifyNoConsoleOrNetworkErrors() { | |
| // 1. Assert no console messages | |
| for (ConsoleMessage message : consoleMessages) { | |
| System.err.println("Unexpected console message: [" + message.type() + "] " + message.text()); | |
| fail("Unexpected console message: [" + message.type() + "] " + message.text()); | |
| } | |
| // 2. Assert no page errors (unhandled exceptions) | |
| for (String error : pageErrors) { | |
| System.err.println("Page error: " + error); | |
| } | |
| assertTrue(pageErrors.isEmpty()); | |
| // 3. Assert no failed requests (missing assets etc) | |
| for (Request request : failedRequests) { | |
| System.err.println( | |
| "Failed request: " | |
| + request.url() | |
| + " - " | |
| + (request.failure() != null ? request.failure() : "unknown")); | |
| } | |
| assertTrue(failedRequests.isEmpty()); | |
| } | |
| @AfterClass | |
| public static void tearDownBrowser() { | |
| if (browser != null) { | |
| browser.close(); | |
| browser = null; | |
| } | |
| if (playwright != null) { | |
| playwright.close(); | |
| playwright = null; | |
| } | |
| } | |
| private static Path resolveChromeExecutable() { | |
| Path chromeDir = TestDeps.getChromeHeadlessDir(); | |
| boolean isMac = ToolHelper.isMac(); | |
| String[] executables = { | |
| "chrome-headless-shell", "chrome", "chromium", "chrome-headless-shell.exe", "chrome.exe" | |
| }; | |
| String macArch = ""; | |
| if (isMac) { | |
| String arch = System.getProperty("os.arch").toLowerCase(); | |
| if (arch.contains("aarch64") || arch.contains("arm64")) { | |
| macArch = "mac-arm64"; | |
| } else { | |
| macArch = "mac-x64"; | |
| } | |
| } | |
| final String targetMacArch = macArch; | |
| try { | |
| return Files.walk(chromeDir, 3) | |
| .filter( | |
| p -> { | |
| String name = p.getFileName().toString(); | |
| if (isMac && !p.toString().contains(targetMacArch)) { | |
| return false; | |
| } | |
| for (String exec : executables) { | |
| if (name.equalsIgnoreCase(exec) && Files.isExecutable(p)) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| }) | |
| .findFirst() | |
| .orElse(null); | |
| } catch (IOException e) { | |
| return null; | |
| } | |
| } | |
| } |