feat: implement page.tap() (#106)

This commit is contained in:
Yury Semikhatsky 2020-12-09 20:36:20 -08:00 committed by GitHub
parent b895f90fd6
commit 4a18f5b1b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 251 additions and 3 deletions

View File

@ -457,7 +457,16 @@ public class FrameImpl extends ChannelOwner implements Frame {
@Override @Override
public void tap(String selector, TapOptions options) { public void tap(String selector, TapOptions options) {
if (options == null) {
options = new TapOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
params.remove("modifiers");
if (options.modifiers != null) {
params.add("modifiers", Serialization.toProtocol(options.modifiers));
}
params.addProperty("selector", selector);
sendMessage("tap", params);
} }
@Override @Override

View File

@ -39,6 +39,7 @@ public class PageImpl extends ChannelOwner implements Page {
private final KeyboardImpl keyboard; private final KeyboardImpl keyboard;
private final MouseImpl mouse; private final MouseImpl mouse;
private final AccessibilityImpl accessibility; private final AccessibilityImpl accessibility;
private final TouchscreenImpl touchscreen;
private Viewport viewport; private Viewport viewport;
private final Router routes = new Router(); private final Router routes = new Router();
private final Set<FrameImpl> frames = new LinkedHashSet<>(); private final Set<FrameImpl> frames = new LinkedHashSet<>();
@ -56,6 +57,7 @@ public class PageImpl extends ChannelOwner implements Page {
mainFrame.page = this; mainFrame.page = this;
keyboard = new KeyboardImpl(this); keyboard = new KeyboardImpl(this);
mouse = new MouseImpl(this); mouse = new MouseImpl(this);
touchscreen = new TouchscreenImpl(this);
accessibility = new AccessibilityImpl(this); accessibility = new AccessibilityImpl(this);
frames.add(mainFrame); frames.add(mainFrame);
timeoutSettings = new TimeoutSettings(browserContext.timeoutSettings); timeoutSettings = new TimeoutSettings(browserContext.timeoutSettings);
@ -653,7 +655,7 @@ public class PageImpl extends ChannelOwner implements Page {
@Override @Override
public Touchscreen touchscreen() { public Touchscreen touchscreen() {
return null; return touchscreen;
} }
@Override @Override

View File

@ -0,0 +1,36 @@
/*
* 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;
import com.google.gson.JsonObject;
import com.microsoft.playwright.Touchscreen;
class TouchscreenImpl implements Touchscreen {
private final PageImpl page;
TouchscreenImpl(PageImpl page) {
this.page = page;
}
@Override
public void tap(int x, int y) {
JsonObject params = new JsonObject();
params.addProperty("x", x);
params.addProperty("y", y);
page.sendMessage("touchscreenTap", params);
}
}

View File

@ -111,11 +111,15 @@ public class TestBase {
playwright = null; playwright = null;
} }
BrowserContext createContext() {
return browser.newContext();
}
@BeforeEach @BeforeEach
void createContextAndPage() { void createContextAndPage() {
server.reset(); server.reset();
httpsServer.reset(); httpsServer.reset();
context = browser.newContext(); context = createContext();
page = context.newPage(); page = context.newPage();
} }

View File

@ -0,0 +1,197 @@
/*
* 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;
import org.junit.jupiter.api.Test;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicBoolean;
import static com.microsoft.playwright.Utils.mapOf;
import static java.util.Arrays.asList;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class TestTap extends TestBase {
@Override
BrowserContext createContext() {
return browser.newContext(new Browser.NewContextOptions().withHasTouch(true));
}
private JSHandle trackEvents(ElementHandle target) {
return target.evaluateHandle("target => {\n" +
" const events = [];\n" +
" for (const event of [\n" +
" 'mousedown', 'mouseenter', 'mouseleave', 'mousemove', 'mouseout', 'mouseover', 'mouseup', 'click',\n" +
" 'pointercancel', 'pointerdown', 'pointerenter', 'pointerleave', 'pointermove', 'pointerout', 'pointerover', 'pointerup',\n" +
" 'touchstart', 'touchend', 'touchmove', 'touchcancel'])\n" +
" target.addEventListener(event, () => events.push(event), false);\n" +
" return events;\n" +
"}");
}
@Test
void shouldSendAllOfTheCorrectEvents() {
page.setContent("<div id='a' style='background: lightblue; width: 50px; height: 50px'>a</div>\n" +
"<div id='b' style='background: pink; width: 50px; height: 50px'>b</div>");
page.tap("#a");
JSHandle eventsHandle = trackEvents(page.querySelector("#b"));
page.tap("#b");
// webkit doesnt send pointerenter or pointerleave or mouseout
assertEquals(asList("pointerover", "pointerenter",
"pointerdown", "touchstart",
"pointerup", "pointerout",
"pointerleave", "touchend",
"mouseover", "mouseenter",
"mousemove", "mousedown",
"mouseup", "click"),
eventsHandle.jsonValue());
}
@Test
void shouldNotSendMouseEventsTouchstartIsCanceled() {
page.setContent("<div style='width: 50px; height: 50px; background: red'>");
page.evaluate("() => {\n" +
" // touchstart is not cancelable unless passive is false\n" +
" document.addEventListener('touchstart', t => t.preventDefault(), {passive: false});\n" +
" }");
JSHandle eventsHandle = trackEvents(page.querySelector("div"));
page.tap("div");
assertEquals(asList("pointerover", "pointerenter",
"pointerdown", "touchstart",
"pointerup", "pointerout",
"pointerleave", "touchend"),
eventsHandle.jsonValue());
}
@Test
void shouldNotSendMouseEventsWhenTouchendIsCanceled() {
page.setContent("<div style='width: 50px; height: 50px; background: red'>");
page.evaluate("() => document.addEventListener('touchend', t => t.preventDefault())");
JSHandle eventsHandle = trackEvents(page.querySelector("div"));
page.tap("div");
assertEquals(asList("pointerover", "pointerenter",
"pointerdown", "touchstart",
"pointerup", "pointerout",
"pointerleave", "touchend"),
eventsHandle.jsonValue());
}
@Test
void shouldWaitForANavigationCausedByATap() throws InterruptedException {
page.navigate(server.EMPTY_PAGE);
page.setContent("<a href='/intercept-this.html'>link</a>;");
Semaphore responseWritten = new Semaphore(0);
List<String> events = Collections.synchronizedList(new ArrayList<>());
server.setRoute("/intercept-this.html", exchange -> {
// make sure the tap doesnt resolve too early
try {
Thread.sleep(100);
} catch (InterruptedException e) {
events.add("interrupted");
}
exchange.getResponseHeaders().add("Content-Type", "application/octet-stream");
exchange.sendResponseHeaders(200, 0);
try (OutputStreamWriter writer = new OutputStreamWriter(exchange.getResponseBody())) {
writer.write("foo");
}
events.add("sent response");
responseWritten.release();
});
page.tap("a");
events.add("tap finished");
responseWritten.acquire();
assertEquals(asList("sent response", "tap finished"), events);
}
@Test
void shouldWorkWithModifiers() {
page.setContent("hello world");
page.evaluate("() => {\n" +
" window.touchPromise = new Promise(resolve => {\n" +
" document.addEventListener('touchstart', event => {\n" +
" resolve(event.altKey);\n" +
" }, {passive: false});\n" +
" });\n" +
"}");
page.tap("body", new Page.TapOptions().withModifiers(Keyboard.Modifier.ALT));
Object altKey = page.evaluate("() => window.touchPromise");
assertEquals(true, altKey);
}
@Test
void shouldSendWellFormedTouchPoints() {
page.evaluate("() => {\n" +
" window.touchStartPromise = new Promise(resolve => {\n" +
" document.addEventListener('touchstart', event => {\n" +
" resolve([...event.touches].map(t => ({\n" +
" identifier: t.identifier,\n" +
" clientX: t.clientX,\n" +
" clientY: t.clientY,\n" +
" pageX: t.pageX,\n" +
" pageY: t.pageY,\n" +
" radiusX: 'radiusX' in t ? t.radiusX : t['webkitRadiusX'],\n" +
" radiusY: 'radiusY' in t ? t.radiusY : t['webkitRadiusY'],\n" +
" rotationAngle: 'rotationAngle' in t ? t.rotationAngle : t['webkitRotationAngle'],\n" +
" force: 'force' in t ? t.force : t['webkitForce'],\n" +
" })));\n" +
" }, false);\n" +
" })\n" +
" }");
page.evaluate("() => {\n" +
" window.touchEndPromise = new Promise(resolve => {\n" +
" document.addEventListener('touchend', event => {\n" +
" resolve([...event.touches].map(t => ({\n" +
" identifier: t.identifier,\n" +
" clientX: t.clientX,\n" +
" clientY: t.clientY,\n" +
" pageX: t.pageX,\n" +
" pageY: t.pageY,\n" +
" radiusX: 'radiusX' in t ? t.radiusX : t['webkitRadiusX'],\n" +
" radiusY: 'radiusY' in t ? t.radiusY : t['webkitRadiusY'],\n" +
" rotationAngle: 'rotationAngle' in t ? t.rotationAngle : t['webkitRotationAngle'],\n" +
" force: 'force' in t ? t.force : t['webkitForce'],\n" +
" })));\n" +
" }, false);\n" +
" })\n" +
" }");
page.touchscreen().tap(40, 60);
Object touchStart = page.evaluate("() => window.touchStartPromise");
assertEquals(asList(mapOf(
"clientX", 40,
"clientY", 60,
"force", 1,
"identifier", 0,
"pageX", 40,
"pageY", 60,
"radiusX", 1,
"radiusY", 1,
"rotationAngle", 0
)), touchStart);
Object touchEnd = page.evaluate("() => window.touchEndPromise");
assertEquals(Collections.emptyList(), touchEnd);
}
void shouldWaitUntilAnElementIsVisibleToTapIt() {
// Ignored in sync api.
}
}