diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/FrameImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/FrameImpl.java index de7be595..a6544182 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/FrameImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/FrameImpl.java @@ -457,7 +457,16 @@ public class FrameImpl extends ChannelOwner implements Frame { @Override 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 diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java index df773b91..2f984f0d 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java @@ -39,6 +39,7 @@ public class PageImpl extends ChannelOwner implements Page { private final KeyboardImpl keyboard; private final MouseImpl mouse; private final AccessibilityImpl accessibility; + private final TouchscreenImpl touchscreen; private Viewport viewport; private final Router routes = new Router(); private final Set frames = new LinkedHashSet<>(); @@ -56,6 +57,7 @@ public class PageImpl extends ChannelOwner implements Page { mainFrame.page = this; keyboard = new KeyboardImpl(this); mouse = new MouseImpl(this); + touchscreen = new TouchscreenImpl(this); accessibility = new AccessibilityImpl(this); frames.add(mainFrame); timeoutSettings = new TimeoutSettings(browserContext.timeoutSettings); @@ -653,7 +655,7 @@ public class PageImpl extends ChannelOwner implements Page { @Override public Touchscreen touchscreen() { - return null; + return touchscreen; } @Override diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/TouchscreenImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/TouchscreenImpl.java new file mode 100644 index 00000000..c78ea3a2 --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/impl/TouchscreenImpl.java @@ -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); + } +} diff --git a/playwright/src/test/java/com/microsoft/playwright/TestBase.java b/playwright/src/test/java/com/microsoft/playwright/TestBase.java index c4500b9b..80386df0 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestBase.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestBase.java @@ -111,11 +111,15 @@ public class TestBase { playwright = null; } + BrowserContext createContext() { + return browser.newContext(); + } + @BeforeEach void createContextAndPage() { server.reset(); httpsServer.reset(); - context = browser.newContext(); + context = createContext(); page = context.newPage(); } diff --git a/playwright/src/test/java/com/microsoft/playwright/TestTap.java b/playwright/src/test/java/com/microsoft/playwright/TestTap.java new file mode 100644 index 00000000..ef2c1041 --- /dev/null +++ b/playwright/src/test/java/com/microsoft/playwright/TestTap.java @@ -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("
a
\n" + + "
b
"); + 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("
"); + 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("
"); + 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("link;"); + Semaphore responseWritten = new Semaphore(0); + List 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. + } +}