feat: roll 1.42.0-alpha driver, implement page.addLocatorHandler (#1494)
This commit is contained in:
@@ -11,9 +11,9 @@ Playwright is a Java library to automate [Chromium](https://www.chromium.org/Hom
|
||||
|
||||
| | Linux | macOS | Windows |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| Chromium <!-- GEN:chromium-version -->121.0.6167.57<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| Chromium <!-- GEN:chromium-version -->122.0.6261.29<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| WebKit <!-- GEN:webkit-version -->17.4<!-- GEN:stop --> | ✅ | ✅ | ✅ |
|
||||
| Firefox <!-- GEN:firefox-version -->121.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| Firefox <!-- GEN:firefox-version -->122.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
|
||||
Headless execution is supported for all the browsers on all platforms. Check out [system requirements](https://playwright.dev/java/docs/intro#system-requirements) for details.
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ public interface BrowserContext extends AutoCloseable {
|
||||
|
||||
/**
|
||||
* Emitted when JavaScript within the page calls one of console API methods, e.g. {@code console.log} or {@code
|
||||
* console.dir}. Also emitted if the page throws an error or a warning.
|
||||
* console.dir}.
|
||||
*
|
||||
* <p> The arguments passed into {@code console.log} and the page are available on the {@code ConsoleMessage} event handler
|
||||
* argument.
|
||||
|
||||
@@ -1779,8 +1779,8 @@ public interface ElementHandle extends JSHandle {
|
||||
* <p> If {@code key} is a single character, it is case-sensitive, so the values {@code a} and {@code A} will generate
|
||||
* different respective texts.
|
||||
*
|
||||
* <p> Shortcuts such as {@code key: "Control+o"} or {@code key: "Control+Shift+T"} are supported as well. When specified with
|
||||
* the modifier, modifier is pressed and being held while the subsequent key is being pressed.
|
||||
* <p> Shortcuts such as {@code key: "Control+o"}, {@code key: "Control++} or {@code key: "Control+Shift+T"} are supported as
|
||||
* well. When specified with the modifier, modifier is pressed and being held while the subsequent key is being pressed.
|
||||
*
|
||||
* @param key Name of the key to press or a character to generate, such as {@code ArrowLeft} or {@code a}.
|
||||
* @since v1.8
|
||||
@@ -1809,8 +1809,8 @@ public interface ElementHandle extends JSHandle {
|
||||
* <p> If {@code key} is a single character, it is case-sensitive, so the values {@code a} and {@code A} will generate
|
||||
* different respective texts.
|
||||
*
|
||||
* <p> Shortcuts such as {@code key: "Control+o"} or {@code key: "Control+Shift+T"} are supported as well. When specified with
|
||||
* the modifier, modifier is pressed and being held while the subsequent key is being pressed.
|
||||
* <p> Shortcuts such as {@code key: "Control+o"}, {@code key: "Control++} or {@code key: "Control+Shift+T"} are supported as
|
||||
* well. When specified with the modifier, modifier is pressed and being held while the subsequent key is being pressed.
|
||||
*
|
||||
* @param key Name of the key to press or a character to generate, such as {@code ArrowLeft} or {@code a}.
|
||||
* @since v1.8
|
||||
|
||||
@@ -3991,8 +3991,8 @@ public interface Frame {
|
||||
* <p> If {@code key} is a single character, it is case-sensitive, so the values {@code a} and {@code A} will generate
|
||||
* different respective texts.
|
||||
*
|
||||
* <p> Shortcuts such as {@code key: "Control+o"} or {@code key: "Control+Shift+T"} are supported as well. When specified with
|
||||
* the modifier, modifier is pressed and being held while the subsequent key is being pressed.
|
||||
* <p> Shortcuts such as {@code key: "Control+o"}, {@code key: "Control++} or {@code key: "Control+Shift+T"} are supported as
|
||||
* well. When specified with the modifier, modifier is pressed and being held while the subsequent key is being pressed.
|
||||
*
|
||||
* @param selector A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used.
|
||||
* @param key Name of the key to press or a character to generate, such as {@code ArrowLeft} or {@code a}.
|
||||
@@ -4020,8 +4020,8 @@ public interface Frame {
|
||||
* <p> If {@code key} is a single character, it is case-sensitive, so the values {@code a} and {@code A} will generate
|
||||
* different respective texts.
|
||||
*
|
||||
* <p> Shortcuts such as {@code key: "Control+o"} or {@code key: "Control+Shift+T"} are supported as well. When specified with
|
||||
* the modifier, modifier is pressed and being held while the subsequent key is being pressed.
|
||||
* <p> Shortcuts such as {@code key: "Control+o"}, {@code key: "Control++} or {@code key: "Control+Shift+T"} are supported as
|
||||
* well. When specified with the modifier, modifier is pressed and being held while the subsequent key is being pressed.
|
||||
*
|
||||
* @param selector A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used.
|
||||
* @param key Name of the key to press or a character to generate, such as {@code ArrowLeft} or {@code a}.
|
||||
|
||||
@@ -152,8 +152,8 @@ public interface Keyboard {
|
||||
* <p> If {@code key} is a single character, it is case-sensitive, so the values {@code a} and {@code A} will generate
|
||||
* different respective texts.
|
||||
*
|
||||
* <p> Shortcuts such as {@code key: "Control+o"} or {@code key: "Control+Shift+T"} are supported as well. When specified with
|
||||
* the modifier, modifier is pressed and being held while the subsequent key is being pressed.
|
||||
* <p> Shortcuts such as {@code key: "Control+o"}, {@code key: "Control++} or {@code key: "Control+Shift+T"} are supported as
|
||||
* well. When specified with the modifier, modifier is pressed and being held while the subsequent key is being pressed.
|
||||
*
|
||||
* <p> **Usage**
|
||||
* <pre>{@code
|
||||
@@ -197,8 +197,8 @@ public interface Keyboard {
|
||||
* <p> If {@code key} is a single character, it is case-sensitive, so the values {@code a} and {@code A} will generate
|
||||
* different respective texts.
|
||||
*
|
||||
* <p> Shortcuts such as {@code key: "Control+o"} or {@code key: "Control+Shift+T"} are supported as well. When specified with
|
||||
* the modifier, modifier is pressed and being held while the subsequent key is being pressed.
|
||||
* <p> Shortcuts such as {@code key: "Control+o"}, {@code key: "Control++} or {@code key: "Control+Shift+T"} are supported as
|
||||
* well. When specified with the modifier, modifier is pressed and being held while the subsequent key is being pressed.
|
||||
*
|
||||
* <p> **Usage**
|
||||
* <pre>{@code
|
||||
|
||||
@@ -4084,8 +4084,8 @@ public interface Locator {
|
||||
* <p> If {@code key} is a single character, it is case-sensitive, so the values {@code a} and {@code A} will generate
|
||||
* different respective texts.
|
||||
*
|
||||
* <p> Shortcuts such as {@code key: "Control+o"} or {@code key: "Control+Shift+T"} are supported as well. When specified with
|
||||
* the modifier, modifier is pressed and being held while the subsequent key is being pressed.
|
||||
* <p> Shortcuts such as {@code key: "Control+o"}, {@code key: "Control++} or {@code key: "Control+Shift+T"} are supported as
|
||||
* well. When specified with the modifier, modifier is pressed and being held while the subsequent key is being pressed.
|
||||
*
|
||||
* @param key Name of the key to press or a character to generate, such as {@code ArrowLeft} or {@code a}.
|
||||
* @since v1.14
|
||||
@@ -4123,8 +4123,8 @@ public interface Locator {
|
||||
* <p> If {@code key} is a single character, it is case-sensitive, so the values {@code a} and {@code A} will generate
|
||||
* different respective texts.
|
||||
*
|
||||
* <p> Shortcuts such as {@code key: "Control+o"} or {@code key: "Control+Shift+T"} are supported as well. When specified with
|
||||
* the modifier, modifier is pressed and being held while the subsequent key is being pressed.
|
||||
* <p> Shortcuts such as {@code key: "Control+o"}, {@code key: "Control++} or {@code key: "Control+Shift+T"} are supported as
|
||||
* well. When specified with the modifier, modifier is pressed and being held while the subsequent key is being pressed.
|
||||
*
|
||||
* @param key Name of the key to press or a character to generate, such as {@code ArrowLeft} or {@code a}.
|
||||
* @since v1.14
|
||||
|
||||
@@ -80,7 +80,7 @@ public interface Page extends AutoCloseable {
|
||||
|
||||
/**
|
||||
* Emitted when JavaScript within the page calls one of console API methods, e.g. {@code console.log} or {@code
|
||||
* console.dir}. Also emitted if the page throws an error or a warning.
|
||||
* console.dir}.
|
||||
*
|
||||
* <p> The arguments passed into {@code console.log} are available on the {@code ConsoleMessage} event handler argument.
|
||||
*
|
||||
@@ -5804,8 +5804,8 @@ public interface Page extends AutoCloseable {
|
||||
* <p> If {@code key} is a single character, it is case-sensitive, so the values {@code a} and {@code A} will generate
|
||||
* different respective texts.
|
||||
*
|
||||
* <p> Shortcuts such as {@code key: "Control+o"} or {@code key: "Control+Shift+T"} are supported as well. When specified with
|
||||
* the modifier, modifier is pressed and being held while the subsequent key is being pressed.
|
||||
* <p> Shortcuts such as {@code key: "Control+o"}, {@code key: "Control++} or {@code key: "Control+Shift+T"} are supported as
|
||||
* well. When specified with the modifier, modifier is pressed and being held while the subsequent key is being pressed.
|
||||
*
|
||||
* <p> **Usage**
|
||||
* <pre>{@code
|
||||
@@ -5847,8 +5847,8 @@ public interface Page extends AutoCloseable {
|
||||
* <p> If {@code key} is a single character, it is case-sensitive, so the values {@code a} and {@code A} will generate
|
||||
* different respective texts.
|
||||
*
|
||||
* <p> Shortcuts such as {@code key: "Control+o"} or {@code key: "Control+Shift+T"} are supported as well. When specified with
|
||||
* the modifier, modifier is pressed and being held while the subsequent key is being pressed.
|
||||
* <p> Shortcuts such as {@code key: "Control+o"}, {@code key: "Control++} or {@code key: "Control+Shift+T"} are supported as
|
||||
* well. When specified with the modifier, modifier is pressed and being held while the subsequent key is being pressed.
|
||||
*
|
||||
* <p> **Usage**
|
||||
* <pre>{@code
|
||||
@@ -5895,6 +5895,77 @@ public interface Page extends AutoCloseable {
|
||||
* @since v1.9
|
||||
*/
|
||||
List<ElementHandle> querySelectorAll(String selector);
|
||||
/**
|
||||
* Sometimes, the web page can show an overlay that obstructs elements behind it and prevents certain actions, like click,
|
||||
* from completing. When such an overlay is shown predictably, we recommend dismissing it as a part of your test flow.
|
||||
* However, sometimes such an overlay may appear non-deterministically, for example certain cookies consent dialogs behave
|
||||
* this way. In this case, {@link Page#addLocatorHandler Page.addLocatorHandler()} allows handling an overlay during an
|
||||
* action that it would block.
|
||||
*
|
||||
* <p> This method registers a handler for an overlay that is executed once the locator is visible on the page. The handler
|
||||
* should get rid of the overlay so that actions blocked by it can proceed. This is useful for nondeterministic
|
||||
* interstitial pages or dialogs, like a cookie consent dialog.
|
||||
*
|
||||
* <p> Note that execution time of the handler counts towards the timeout of the action/assertion that executed the handler.
|
||||
*
|
||||
* <p> You can register multiple handlers. However, only a single handler will be running at a time. Any actions inside a
|
||||
* handler must not require another handler to run.
|
||||
*
|
||||
* <p> <strong>NOTE:</strong> Running the interceptor will alter your page state mid-test. For example it will change the currently focused element
|
||||
* and move the mouse. Make sure that the actions that run after the interceptor are self-contained and do not rely on the
|
||||
* focus and mouse state. <br /> <br /> For example, consider a test that calls {@link Locator#focus Locator.focus()}
|
||||
* followed by {@link Keyboard#press Keyboard.press()}. If your handler clicks a button between these two actions, the
|
||||
* focused element most likely will be wrong, and key press will happen on the unexpected element. Use {@link Locator#press
|
||||
* Locator.press()} instead to avoid this problem. <br /> <br /> Another example is a series of mouse actions, where {@link
|
||||
* Mouse#move Mouse.move()} is followed by {@link Mouse#down Mouse.down()}. Again, when the handler runs between these two
|
||||
* actions, the mouse position will be wrong during the mouse down. Prefer methods like {@link Locator#click
|
||||
* Locator.click()} that are self-contained.
|
||||
*
|
||||
* <p> **Usage**
|
||||
*
|
||||
* <p> An example that closes a cookie dialog when it appears:
|
||||
* <pre>{@code
|
||||
* // Setup the handler.
|
||||
* page.addLocatorHandler(page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Accept all cookies")), () => {
|
||||
* page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Reject all cookies")).click();
|
||||
* });
|
||||
*
|
||||
* // Write the test as usual.
|
||||
* page.goto("https://example.com");
|
||||
* page.getByRole("button", Page.GetByRoleOptions().setName("Start here")).click();
|
||||
* }</pre>
|
||||
*
|
||||
* <p> An example that skips the "Confirm your security details" page when it is shown:
|
||||
* <pre>{@code
|
||||
* // Setup the handler.
|
||||
* page.addLocatorHandler(page.getByText("Confirm your security details")), () => {
|
||||
* page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Remind me later")).click();
|
||||
* });
|
||||
*
|
||||
* // Write the test as usual.
|
||||
* page.goto("https://example.com");
|
||||
* page.getByRole("button", Page.GetByRoleOptions().setName("Start here")).click();
|
||||
* }</pre>
|
||||
*
|
||||
* <p> An example with a custom callback on every actionability check. It uses a {@code <body>} locator that is always visible,
|
||||
* so the handler is called before every actionability check:
|
||||
* <pre>{@code
|
||||
* // Setup the handler.
|
||||
* page.addLocatorHandler(page.locator("body")), () => {
|
||||
* page.evaluate("window.removeObstructionsForTestIfNeeded()");
|
||||
* });
|
||||
*
|
||||
* // Write the test as usual.
|
||||
* page.goto("https://example.com");
|
||||
* page.getByRole("button", Page.GetByRoleOptions().setName("Start here")).click();
|
||||
* }</pre>
|
||||
*
|
||||
* @param locator Locator that triggers the handler.
|
||||
* @param handler Function that should be run once {@code locator} appears. This function should get rid of the element that blocks
|
||||
* actions like click.
|
||||
* @since v1.42
|
||||
*/
|
||||
void addLocatorHandler(Locator locator, Runnable handler);
|
||||
/**
|
||||
* This method reloads the current page, in the same way as if the user had triggered a browser refresh. Returns the main
|
||||
* resource response. In case of multiple redirects, the navigation will resolve with the response of the last redirect.
|
||||
|
||||
@@ -186,7 +186,7 @@ public class FrameImpl extends ChannelOwner implements Frame {
|
||||
throw new PlaywrightException("Failed to read from file", e);
|
||||
}
|
||||
String content = new String(encoded, StandardCharsets.UTF_8);
|
||||
content += "//# sourceURL=" + options.path.toString().replace("\n", "");
|
||||
content = addSourceUrlToScript(content, options.path);
|
||||
jsonOptions.addProperty("content", content);
|
||||
}
|
||||
JsonElement json = sendMessage("addScriptTag", jsonOptions);
|
||||
|
||||
@@ -73,6 +73,14 @@ public class HARRouter {
|
||||
|
||||
if ("fulfill".equals(action)) {
|
||||
int status = response.get("status").getAsInt();
|
||||
// If the response status is -1, the request was canceled or stalled, so we just stall it here.
|
||||
// See https://github.com/microsoft/playwright/issues/29311.
|
||||
// TODO: it'd be better to abort such requests, but then we likely need to respect the timing,
|
||||
// because the request might have been stalled for a long time until the very end of the
|
||||
// test when HAR was recorded but we'd abort it immediately.
|
||||
if (status == -1) {
|
||||
return;
|
||||
}
|
||||
Map<String, String> headers = fromNameValues(response.getAsJsonArray("headers"));
|
||||
byte[] buffer = Base64.getDecoder().decode(response.get("body").getAsString());
|
||||
route.fulfill(new Route.FulfillOptions()
|
||||
|
||||
@@ -49,6 +49,8 @@ public class PageImpl extends ChannelOwner implements Page {
|
||||
private ViewportSize viewport;
|
||||
private final Router routes = new Router();
|
||||
private final Set<FrameImpl> frames = new LinkedHashSet<>();
|
||||
private final Map<Integer, Runnable> locatorHandlers = new HashMap<>();
|
||||
|
||||
private static final Map<EventType, String> eventSubscriptions() {
|
||||
Map<EventType, String> result = new HashMap<>();
|
||||
result.put(EventType.CONSOLE, "console");
|
||||
@@ -170,6 +172,9 @@ public class PageImpl extends ChannelOwner implements Page {
|
||||
frame.parentFrame.childFrames.remove(frame);
|
||||
}
|
||||
listeners.notify(EventType.FRAMEDETACHED, frame);
|
||||
} else if ("locatorHandlerTriggered".equals(event)) {
|
||||
int uid = params.get("uid").getAsInt();
|
||||
onLocatorHandlerTriggered(uid);
|
||||
} else if ("route".equals(event)) {
|
||||
RouteImpl route = connection.getExistingObject(params.getAsJsonObject("route").get("guid").getAsString());
|
||||
route.browserContext = browserContext;
|
||||
@@ -523,6 +528,34 @@ public class PageImpl extends ChannelOwner implements Page {
|
||||
return withLogging("Page.querySelectorAll", () -> mainFrame.querySelectorAllImpl(selector));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addLocatorHandler(Locator locator, Runnable handler) {
|
||||
LocatorImpl locatorImpl = (LocatorImpl) locator;
|
||||
if (locatorImpl.frame != mainFrame) {
|
||||
throw new PlaywrightException("Locator must belong to the main frame of this page");
|
||||
}
|
||||
withLogging("Page.addLocatorHandler", () -> {
|
||||
JsonObject params = new JsonObject();
|
||||
params.addProperty("selector", locatorImpl.selector);
|
||||
JsonObject json = (JsonObject) sendMessage("registerLocatorHandler", params);
|
||||
int uid = json.get("uid").getAsInt();
|
||||
locatorHandlers.put(uid, handler);
|
||||
});
|
||||
}
|
||||
|
||||
private void onLocatorHandlerTriggered(int uid) {
|
||||
try {
|
||||
Runnable handler = locatorHandlers.get(uid);
|
||||
if (handler != null) {
|
||||
handler.run();
|
||||
}
|
||||
} finally {
|
||||
JsonObject params = new JsonObject();
|
||||
params.addProperty("uid", uid);
|
||||
sendMessageAsync("resolveLocatorHandlerNoReply", params);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object evalOnSelector(String selector, String pageFunction, Object arg, EvalOnSelectorOptions options) {
|
||||
return withLogging("Page.evalOnSelector", () -> mainFrame.evalOnSelectorImpl(
|
||||
@@ -544,7 +577,8 @@ public class PageImpl extends ChannelOwner implements Page {
|
||||
withLogging("Page.addInitScript", () -> {
|
||||
try {
|
||||
byte[] bytes = readAllBytes(path);
|
||||
addInitScriptImpl(new String(bytes, UTF_8));
|
||||
String script = addSourceUrlToScript(new String(bytes, UTF_8), path);
|
||||
addInitScriptImpl(script);
|
||||
} catch (IOException e) {
|
||||
throw new PlaywrightException("Failed to read script from file", e);
|
||||
}
|
||||
|
||||
@@ -341,4 +341,8 @@ public class Utils {
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
|
||||
static String addSourceUrlToScript(String source, Path path) {
|
||||
return source + "\n//# sourceURL=" + path.toString().replace("\n", "");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static com.microsoft.playwright.Utils.copy;
|
||||
@@ -469,4 +470,37 @@ public class TestBrowserContextHar extends TestBase {
|
||||
assertThat(page2.locator("body")).hasCSS("background-color", "rgb(255, 192, 203)");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldIgnoreAbortedRequests(@TempDir Path tmpDir) {
|
||||
Path path = tmpDir.resolve("test.har");
|
||||
try (BrowserContext context1 = browser.newContext()) {
|
||||
server.setRoute("/x", exchange -> exchange.close());
|
||||
context1.routeFromHAR(path, new BrowserContext.RouteFromHAROptions().setUpdate(true));
|
||||
Page page1 = context1.newPage();
|
||||
page1.navigate(server.EMPTY_PAGE);
|
||||
Future<Server.Request> reqPromise = server.futureRequest("/x");
|
||||
Object req = page1.evaluate("url => fetch(url).catch(e => 'cancelled')", server.PREFIX + "/x");
|
||||
assertEquals("cancelled", req);
|
||||
}
|
||||
server.reset();
|
||||
|
||||
try (BrowserContext context2 = browser.newContext()) {
|
||||
server.setRoute("/x", exchange -> {
|
||||
exchange.getResponseHeaders().add("content-type", "text/html");
|
||||
exchange.sendResponseHeaders(200, 4);
|
||||
try (OutputStreamWriter writer = new OutputStreamWriter(exchange.getResponseBody())) {
|
||||
writer.write("test");
|
||||
}
|
||||
});
|
||||
context2.routeFromHAR(path);
|
||||
Page page2 = context2.newPage();
|
||||
page2.navigate(server.EMPTY_PAGE);
|
||||
page2.evaluate("url => {\n" +
|
||||
" fetch(url).catch(e => 'cancelled').then(r => { window.result = r; })\n" +
|
||||
"}", server.PREFIX + "/x");
|
||||
page2.waitForTimeout(1000);
|
||||
assertNull(page.evaluate("window.result"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -151,7 +151,7 @@ public class TestBrowserContextStorageState extends TestBase {
|
||||
" origin: 'http://localhost:" + server.PORT + "',\n" +
|
||||
" localStorage: [{\n" +
|
||||
" name: 'foo',\n" +
|
||||
" value: '" + (char)55934 + "'\n" +
|
||||
" value: '" + (char)65533 + "'\n" +
|
||||
" }]\n" +
|
||||
"}]}", new Gson().fromJson(storageState, JsonObject.class));
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ public class TestBrowserTypeConnect extends TestBase {
|
||||
Driver driver = Driver.ensureDriverInstalled(Collections.emptyMap(), false);
|
||||
Path dir = driver.driverPath().getParent();
|
||||
String node = dir.resolve(isWindows ? "node.exe" : "node").toString();
|
||||
String cliJs = dir.resolve("package/lib/cli/cli.js").toString();
|
||||
String cliJs = dir.resolve("package/cli.js").toString();
|
||||
// We launch node process directly instead of using playwright.sh script as killing the script
|
||||
// process will leave node process running and killing it would be more hassle.
|
||||
ProcessBuilder pb = new ProcessBuilder(node, cliJs, "launch-server", "--browser", browserType.name());
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
package com.microsoft.playwright;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class TestPageAddLocatorHandler extends TestBase {
|
||||
@Test
|
||||
void shouldWork() {
|
||||
page.navigate(server.PREFIX + "/input/handle-locator.html");
|
||||
|
||||
int[] beforeCount = {0};
|
||||
int[] afterCount = {0};
|
||||
|
||||
page.addLocatorHandler(page.getByText("This interstitial covers the button"), () -> {
|
||||
++beforeCount[0];
|
||||
page.locator("#close").click();
|
||||
++afterCount[0];
|
||||
});
|
||||
|
||||
String[][] argsList = {
|
||||
{"mouseover", "1"},
|
||||
{"mouseover", "1", "capture"},
|
||||
{"mouseover", "2"},
|
||||
{"mouseover", "2", "capture"},
|
||||
{"pointerover", "1"},
|
||||
{"pointerover", "1", "capture"},
|
||||
{"none", "1"},
|
||||
{"remove", "1"},
|
||||
{"hide", "1"},
|
||||
};
|
||||
|
||||
for (String[] args : argsList) {
|
||||
page.locator("#aside").hover();
|
||||
beforeCount[0] = 0;
|
||||
afterCount[0] = 0;
|
||||
|
||||
page.evaluate("(args) => {\n" +
|
||||
" window.clicked = 0;\n" +
|
||||
" window.setupAnnoyingInterstitial(...args);\n" +
|
||||
"}", args);
|
||||
|
||||
assertEquals(0, beforeCount[0]);
|
||||
assertEquals(0, afterCount[0]);
|
||||
|
||||
page.locator("#target").click();
|
||||
|
||||
assertEquals(Integer.parseInt(args[1]), beforeCount[0]);
|
||||
assertEquals(Integer.parseInt(args[1]), afterCount[0]);
|
||||
assertEquals(1, page.evaluate("window.clicked"));
|
||||
assertThat(page.locator("#interstitial")).not().isVisible();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldWorkWithCustomCheck() {
|
||||
page.navigate(server.PREFIX + "/input/handle-locator.html");
|
||||
|
||||
page.addLocatorHandler(page.locator("body"), () -> {
|
||||
if (page.getByText("This interstitial covers the button").isVisible())
|
||||
page.locator("#close").click();
|
||||
});
|
||||
|
||||
String[][] argsList = {
|
||||
{"mouseover", "2"},
|
||||
{"none", "1"},
|
||||
{"remove", "1"},
|
||||
{"hide", "1"},
|
||||
};
|
||||
|
||||
for (String[] args : argsList) {
|
||||
page.hover("#aside");
|
||||
page.evaluate("(args) => {\n" +
|
||||
" window.clicked = 0;\n" +
|
||||
" window.setupAnnoyingInterstitial(...args);\n" +
|
||||
"}", args);
|
||||
|
||||
page.locator("#target").click();
|
||||
|
||||
assertEquals(1, page.evaluate("window.clicked"));
|
||||
assertThat(page.locator("#interstitial")).not().isVisible();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldWorkWithLocatorHover() {
|
||||
page.navigate(server.PREFIX + "/input/handle-locator.html");
|
||||
|
||||
page.addLocatorHandler(page.getByText("This interstitial covers the button"), () -> {
|
||||
page.locator("#close").click();
|
||||
});
|
||||
|
||||
page.locator("#aside").hover();
|
||||
page.evaluate("() => {\n" +
|
||||
" window.setupAnnoyingInterstitial('pointerover', 1, 'capture');\n" +
|
||||
" }");
|
||||
page.locator("#target").hover();
|
||||
assertThat(page.locator("#interstitial")).not().isVisible();
|
||||
assertEquals("rgb(255, 255, 0)", page.evalOnSelector("#target", "element => getComputedStyle(element).backgroundColor"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotWorkWithForceTrue() {
|
||||
page.navigate(server.PREFIX + "/input/handle-locator.html");
|
||||
|
||||
page.addLocatorHandler(page.getByText("This interstitial covers the button"), () -> {
|
||||
page.locator("#close").click();
|
||||
});
|
||||
|
||||
page.locator("#aside").hover();
|
||||
page.evaluate("() => {\n" +
|
||||
" window.setupAnnoyingInterstitial('none', 1);\n" +
|
||||
" }");
|
||||
|
||||
page.locator("#target").click(new Locator.ClickOptions().setForce(true).setTimeout(2000));
|
||||
assertTrue(page.locator("#interstitial").isVisible());
|
||||
assertNull(page.evaluate("window.clicked"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowWhenPageCloses() {
|
||||
page.navigate(server.PREFIX + "/input/handle-locator.html");
|
||||
|
||||
page.addLocatorHandler(page.getByText("This interstitial covers the button"), () -> {
|
||||
page.close();
|
||||
});
|
||||
|
||||
page.locator("#aside").hover();
|
||||
page.evaluate("() => {\n" +
|
||||
" window.clicked = 0;\n" +
|
||||
" window.setupAnnoyingInterstitial('mouseover', 1);\n" +
|
||||
" }");
|
||||
PlaywrightException e = assertThrows(PlaywrightException.class, () -> page.locator("#target").click());
|
||||
assertTrue(e.getMessage().contains("Target page, context or browser has been closed"), e.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldWorkWithToBeVisible() {
|
||||
page.navigate(server.PREFIX + "/input/handle-locator.html");
|
||||
|
||||
int[] called = {0};
|
||||
page.addLocatorHandler(page.getByText("This interstitial covers the button"), () -> {
|
||||
++called[0];
|
||||
page.locator("#close").click();
|
||||
});
|
||||
|
||||
page.evaluate("() => {\n" +
|
||||
" window.clicked = 0;\n" +
|
||||
" window.setupAnnoyingInterstitial('remove', 1);\n" +
|
||||
"}");
|
||||
assertThat(page.locator("#target")).isVisible();
|
||||
assertThat(page.locator("#interstitial")).not().isVisible();
|
||||
assertEquals(1, called[0]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.microsoft.playwright;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.DisabledIf;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
public class TestPageAddScriptTag extends TestBase {
|
||||
@Test
|
||||
@DisabledIf(value="com.microsoft.playwright.TestBase#isWebKit", disabledReason="Upstream behavior")
|
||||
void shouldIncludeSourceURLWhenPathIsProvided() {
|
||||
page.navigate(server.EMPTY_PAGE);
|
||||
Path path = Paths.get("src/test/resources/injectedfile.js");
|
||||
page.addScriptTag(new Page.AddScriptTagOptions().setPath(path));
|
||||
String result = (String) page.evaluate("() => window['__injectedError'].stack");
|
||||
assertTrue(result.contains("resources/injectedfile.js"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Interstitial test</title>
|
||||
</head>
|
||||
<body>
|
||||
<style>
|
||||
body {
|
||||
position: relative;
|
||||
}
|
||||
#target.removed {
|
||||
display: none;
|
||||
}
|
||||
#target.hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
#target:hover {
|
||||
background: yellow;
|
||||
}
|
||||
#interstitial {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
border: 1px solid black;
|
||||
background: rgba(255, 180, 180);
|
||||
display: none;
|
||||
}
|
||||
#interstitial.visible {
|
||||
display: block;
|
||||
}
|
||||
#close {
|
||||
margin: 50px;
|
||||
}
|
||||
</style>
|
||||
<div><button id="target">Click me</button></div>
|
||||
<div id="aside">A place on the side to hover</div>
|
||||
<div id="interstitial">
|
||||
<div>This interstitial covers the button</div>
|
||||
<button id="close">Close the interstitial</button>
|
||||
</div>
|
||||
<script>
|
||||
const target = document.querySelector('#target');
|
||||
const interstitial = document.querySelector('#interstitial');
|
||||
const close = document.querySelector('#close');
|
||||
|
||||
target.addEventListener('click', () => {
|
||||
window.clicked = (window.clicked ?? 0) + 1;
|
||||
}, false);
|
||||
|
||||
close.addEventListener('click', () => {
|
||||
interstitial.classList.remove('visible');
|
||||
target.classList.remove('hidden');
|
||||
target.classList.remove('removed');
|
||||
});
|
||||
|
||||
let timesToShow = 0;
|
||||
function setupAnnoyingInterstitial(event, times, capture) {
|
||||
timesToShow = times;
|
||||
const listener = () => {
|
||||
timesToShow--;
|
||||
interstitial.classList.add('visible');
|
||||
interstitial.getBoundingClientRect();
|
||||
if (!timesToShow && event !== 'none')
|
||||
target.removeEventListener(event, listener, capture === 'capture');
|
||||
};
|
||||
if (event === 'hide') {
|
||||
target.classList.add('hidden');
|
||||
listener();
|
||||
} else if (event === 'remove') {
|
||||
target.classList.add('removed');
|
||||
listener();
|
||||
} else if (event === 'none') {
|
||||
listener();
|
||||
} else {
|
||||
target.addEventListener(event, listener, capture === 'capture');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+1
-1
@@ -1 +1 @@
|
||||
1.41.0
|
||||
1.42.0-alpha-1707967288000
|
||||
|
||||
Reference in New Issue
Block a user