diff --git a/README.md b/README.md index d696729b..71dbfdb8 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,9 @@ Playwright is a Java library to automate [Chromium](https://www.chromium.org/Hom | | Linux | macOS | Windows | | :--- | :---: | :---: | :---: | -| Chromium 119.0.6045.9 | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Chromium 120.0.6099.18 | :white_check_mark: | :white_check_mark: | :white_check_mark: | | WebKit 17.4 | ✅ | ✅ | ✅ | -| Firefox 118.0.1 | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Firefox 119.0 | :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. diff --git a/playwright/src/main/java/com/microsoft/playwright/APIRequestContext.java b/playwright/src/main/java/com/microsoft/playwright/APIRequestContext.java index 8e173b05..1b1a4004 100644 --- a/playwright/src/main/java/com/microsoft/playwright/APIRequestContext.java +++ b/playwright/src/main/java/com/microsoft/playwright/APIRequestContext.java @@ -80,8 +80,8 @@ public interface APIRequestContext { APIResponse delete(String url, RequestOptions params); /** * All responses returned by {@link APIRequestContext#get APIRequestContext.get()} and similar methods are stored in the - * memory, so that you can later call {@link APIResponse#body APIResponse.body()}. This method discards all stored - * responses, and makes {@link APIResponse#body APIResponse.body()} throw "Response disposed" error. + * memory, so that you can later call {@link APIResponse#body APIResponse.body()}.This method discards all its resources, + * calling any method on disposed {@code APIRequestContext} will throw an exception. * * @since v1.16 */ diff --git a/playwright/src/main/java/com/microsoft/playwright/Browser.java b/playwright/src/main/java/com/microsoft/playwright/Browser.java index a93f98f5..13f5c88f 100644 --- a/playwright/src/main/java/com/microsoft/playwright/Browser.java +++ b/playwright/src/main/java/com/microsoft/playwright/Browser.java @@ -56,6 +56,20 @@ public interface Browser extends AutoCloseable { */ void offDisconnected(Consumer handler); + class CloseOptions { + /** + * The reason to be reported to the operations interrupted by the browser closure. + */ + public String reason; + + /** + * The reason to be reported to the operations interrupted by the browser closure. + */ + public CloseOptions setReason(String reason) { + this.reason = reason; + return this; + } + } class NewContextOptions { /** * Whether to automatically download all the attachments. Defaults to {@code true} where all the downloads are accepted. @@ -1165,7 +1179,25 @@ public interface Browser extends AutoCloseable { * * @since v1.8 */ - void close(); + default void close() { + close(null); + } + /** + * In case this browser is obtained using {@link BrowserType#launch BrowserType.launch()}, closes the browser and all of + * its pages (if any were opened). + * + *

In case this browser is connected to, clears all created contexts belonging to this browser and disconnects from the + * browser server. + * + *

NOTE: This is similar to force quitting the browser. Therefore, you should call {@link BrowserContext#close + * BrowserContext.close()} on any {@code BrowserContext}'s you explicitly created earlier with {@link Browser#newContext + * Browser.newContext()} **before** calling {@link Browser#close Browser.close()}. + * + *

The {@code Browser} object itself is considered to be disposed and cannot be used anymore. + * + * @since v1.8 + */ + void close(CloseOptions options); /** * Returns an array of all open browser contexts. In a newly created browser, this will return zero browser contexts. * diff --git a/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java b/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java index a6360042..86e71d89 100644 --- a/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java +++ b/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java @@ -186,6 +186,20 @@ public interface BrowserContext extends AutoCloseable { */ void offResponse(Consumer handler); + class CloseOptions { + /** + * The reason to be reported to the operations interrupted by the context closure. + */ + public String reason; + + /** + * The reason to be reported to the operations interrupted by the context closure. + */ + public CloseOptions setReason(String reason) { + this.reason = reason; + return this; + } + } class ExposeBindingOptions { /** * Whether to pass the argument as a handle, instead of passing by value. When passing a handle, only one argument is @@ -504,7 +518,17 @@ public interface BrowserContext extends AutoCloseable { * * @since v1.8 */ - void close(); + default void close() { + close(null); + } + /** + * Closes the browser context. All the pages that belong to the browser context will be closed. + * + *

NOTE: The default browser context cannot be closed. + * + * @since v1.8 + */ + void close(CloseOptions options); /** * If no URLs are specified, this method returns all cookies. If URLs are specified, only cookies that affect those URLs * are returned. diff --git a/playwright/src/main/java/com/microsoft/playwright/BrowserType.java b/playwright/src/main/java/com/microsoft/playwright/BrowserType.java index b94888bd..ac0bba3e 100644 --- a/playwright/src/main/java/com/microsoft/playwright/BrowserType.java +++ b/playwright/src/main/java/com/microsoft/playwright/BrowserType.java @@ -486,6 +486,11 @@ public interface BrowserType { * An object containing additional HTTP headers to be sent with every request. Defaults to none. */ public Map extraHTTPHeaders; + /** + * Firefox user preferences. Learn more about the Firefox user preferences at {@code about:config}. + */ + public Map firefoxUserPrefs; /** * Emulates {@code "forced-colors"} media feature, supported values are {@code "active"}, {@code "none"}. See {@link * Page#emulateMedia Page.emulateMedia()} for more details. Passing {@code null} resets emulation to system defaults. @@ -786,6 +791,14 @@ public interface BrowserType { this.extraHTTPHeaders = extraHTTPHeaders; return this; } + /** + * Firefox user preferences. Learn more about the Firefox user preferences at {@code about:config}. + */ + public LaunchPersistentContextOptions setFirefoxUserPrefs(Map firefoxUserPrefs) { + this.firefoxUserPrefs = firefoxUserPrefs; + return this; + } /** * Emulates {@code "forced-colors"} media feature, supported values are {@code "active"}, {@code "none"}. See {@link * Page#emulateMedia Page.emulateMedia()} for more details. Passing {@code null} resets emulation to system defaults. diff --git a/playwright/src/main/java/com/microsoft/playwright/Page.java b/playwright/src/main/java/com/microsoft/playwright/Page.java index 50611e91..faa89b07 100644 --- a/playwright/src/main/java/com/microsoft/playwright/Page.java +++ b/playwright/src/main/java/com/microsoft/playwright/Page.java @@ -650,12 +650,23 @@ public interface Page extends AutoCloseable { } } class CloseOptions { + /** + * The reason to be reported to the operations interrupted by the page closure. + */ + public String reason; /** * Defaults to {@code false}. Whether to run the before unload page handlers. */ public Boolean runBeforeUnload; + /** + * The reason to be reported to the operations interrupted by the page closure. + */ + public CloseOptions setReason(String reason) { + this.reason = reason; + return this; + } /** * Defaults to {@code false}. Whether to run the before unload page handlers. diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/APIRequestContextImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/APIRequestContextImpl.java index 61953e35..013ef41e 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/APIRequestContextImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/APIRequestContextImpl.java @@ -1,8 +1,7 @@ package com.microsoft.playwright.impl; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; +import com.google.gson.*; +import com.google.gson.stream.JsonReader; import com.microsoft.playwright.APIRequestContext; import com.microsoft.playwright.APIResponse; import com.microsoft.playwright.PlaywrightException; @@ -11,6 +10,7 @@ import com.microsoft.playwright.options.FilePayload; import com.microsoft.playwright.options.RequestOptions; import java.io.File; +import java.io.StringReader; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.Base64; @@ -85,11 +85,14 @@ class APIRequestContextImpl extends ChannelOwner implements APIRequestContext { byte[] bytes = null; if (options.data instanceof byte[]) { bytes = (byte[]) options.data; - } else if (options.data instanceof String && !isJsonContentType(options.headers)) { - bytes = ((String) options.data).getBytes(StandardCharsets.UTF_8); + } else if (options.data instanceof String) { + String stringData = (String) options.data; + if (!isJsonContentType(options.headers) || isJsonParsable(stringData)) { + bytes = (stringData).getBytes(StandardCharsets.UTF_8); + } } if (bytes == null) { - params.add("jsonData", gson().toJsonTree(options.data)); + params.addProperty("jsonData", gson().toJson(options.data)); } else { String base64 = Base64.getEncoder().encodeToString(bytes); params.addProperty("postData", base64); @@ -202,4 +205,21 @@ class APIRequestContextImpl extends ChannelOwner implements APIRequestContext { } return impl; } + + private static boolean isJsonParsable(String value) { + try { + JsonElement result = JsonParser.parseString(value); + if (result != null && result.isJsonPrimitive()) { + JsonPrimitive primitive = result.getAsJsonPrimitive(); + if (primitive.isString() && value.equals(primitive.getAsString())) { + // Gson parses unquoted strings too, but we don't want to treat them + // as valid JSON. + return false; + } + } + return true; + } catch (JsonSyntaxException error) { + return false; + } + } } diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/ArtifactImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/ArtifactImpl.java index aab984c2..b937c761 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/ArtifactImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/ArtifactImpl.java @@ -34,9 +34,6 @@ class ArtifactImpl extends ChannelOwner { public InputStream createReadStream() { JsonObject result = sendMessage("stream").getAsJsonObject(); - if (!result.has("stream")) { - return null; - } Stream stream = connection.getExistingObject(result.getAsJsonObject("stream").get("guid").getAsString()); return stream.stream(); } diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java index e2edadeb..f0e85770 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java @@ -51,6 +51,8 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext { private final WaitableEvent closePromise; final Map bindings = new HashMap<>(); PageImpl ownerPage; + private String closeReason; + private static final Map eventSubscriptions() { Map result = new HashMap<>(); result.put(EventType.CONSOLE, "console"); @@ -115,6 +117,16 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext { } } + String effectiveCloseReason() { + if (closeReason != null) { + return closeReason; + } + if (browser != null) { + return browser.closeReason; + } + return null; + } + @Override public void onClose(Consumer handler) { listeners.add(EventType.CLOSE, handler); @@ -242,8 +254,8 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext { } @Override - public void close() { - withLogging("BrowserContext.close", () -> closeImpl()); + public void close(CloseOptions options) { + withLogging("BrowserContext.close", () -> closeImpl(options)); } @Override @@ -251,9 +263,13 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext { return cookies(url == null ? new ArrayList<>() : Collections.singletonList(url)); } - private void closeImpl() { + private void closeImpl(CloseOptions options) { if (!closeWasCalled) { closeWasCalled = true; + if (options == null) { + options = new CloseOptions(); + } + closeReason = options.reason; for (Map.Entry entry : harRecorders.entrySet()) { JsonObject params = new JsonObject(); params.addProperty("harId", entry.getKey()); @@ -275,7 +291,8 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext { } artifact.delete(); } - sendMessage("close"); + JsonObject params = gson().toJsonTree(options).getAsJsonObject(); + sendMessage("close", params); } runUntil(() -> {}, closePromise); } @@ -594,7 +611,7 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext { @Override public R get() { - throw new PlaywrightException("Context closed"); + throw new TargetClosedError(effectiveCloseReason()); } } @@ -752,9 +769,10 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext { listeners.notify(EventType.CLOSE, this); } - WritableStream createTempFile(String name) { + WritableStream createTempFile(String name, long lastModifiedMs) { JsonObject params = new JsonObject(); params.addProperty("name", name); + params.addProperty("lastModifiedMs", lastModifiedMs); JsonObject json = sendMessage("createTempFile", params).getAsJsonObject(); return connection.getExistingObject(json.getAsJsonObject("writableStream").get("guid").getAsString()); } diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/BrowserImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/BrowserImpl.java index 538e7f03..ff76555c 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/BrowserImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/BrowserImpl.java @@ -42,6 +42,7 @@ class BrowserImpl extends ChannelOwner implements Browser { BrowserTypeImpl browserType; BrowserType.LaunchOptions launchOptions; private Path tracePath; + String closeReason; enum EventType { DISCONNECTED, @@ -67,11 +68,15 @@ class BrowserImpl extends ChannelOwner implements Browser { } @Override - public void close() { - withLogging("Browser.close", () -> closeImpl()); + public void close(CloseOptions options) { + withLogging("Browser.close", () -> closeImpl(options)); } - private void closeImpl() { + private void closeImpl(CloseOptions options) { + if (options == null) { + options = new CloseOptions(); + } + closeReason = options.reason; if (isConnectedOverWebSocket) { try { connection.close(); diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java b/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java index e06a0d3a..d2776818 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java @@ -25,7 +25,9 @@ import com.microsoft.playwright.TimeoutError; import java.io.IOException; import java.time.Duration; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import static com.microsoft.playwright.impl.Serialization.gson; @@ -38,6 +40,7 @@ class Message { JsonObject params; JsonElement result; SerializedError error; + JsonArray log; @Override public String toString() { @@ -206,6 +209,30 @@ public class Connection { dispatch(messageObj); } + private static String formatCallLog(JsonArray log) { + if (log == null) { + return ""; + } + boolean allEmpty = true; + for (JsonElement e: log) { + if (!e.getAsString().isEmpty()) { + allEmpty = false; + break; + } + } + if (allEmpty) { + return ""; + } + List lines = new ArrayList<>(); + lines.add(""); + lines.add("Call log:"); + for (JsonElement e: log) { + lines.add("- " + e.getAsString()); + } + lines.add(""); + return String.join("\n", lines); + } + private void dispatch(Message message) { // System.out.println("Message: " + message.method + " " + message.id); if (message.id != 0) { @@ -218,12 +245,18 @@ public class Connection { if (message.error == null) { callback.complete(message.result); } else { + String callLog = formatCallLog(message.log); if (message.error.error == null) { - callback.completeExceptionally(new PlaywrightException(message.error.toString())); + callback.completeExceptionally(new PlaywrightException(message.error + callLog)); + } else if ("Expect".equals(message.error.error.name)) { + callback.complete(message.result); } else if ("TimeoutError".equals(message.error.error.name)) { - callback.completeExceptionally(new TimeoutError(message.error.error.toString())); + callback.completeExceptionally(new TimeoutError(message.error.error + callLog)); + } else if ("TargetClosedError".equals(message.error.error.name)) { + callback.completeExceptionally(new TargetClosedError(message.error.error + callLog)); + } else { - callback.completeExceptionally(new DriverException(message.error.error)); + callback.completeExceptionally(new DriverException(message.error.error + callLog)); } } return; diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/DriverException.java b/playwright/src/main/java/com/microsoft/playwright/impl/DriverException.java index 171b8410..45fcbdb5 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/DriverException.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/DriverException.java @@ -22,7 +22,7 @@ import java.io.PrintStream; import java.io.PrintWriter; class DriverException extends PlaywrightException { - DriverException(SerializedError.Error error) { - super(error.toString()); + DriverException(String error) { + super(error); } } diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/ElementHandleImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/ElementHandleImpl.java index d59ad689..1218749b 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/ElementHandleImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/ElementHandleImpl.java @@ -28,13 +28,12 @@ import com.microsoft.playwright.options.SelectOption; import java.nio.file.Path; import java.util.ArrayList; -import java.util.Arrays; import java.util.Base64; import java.util.List; import static com.microsoft.playwright.impl.Serialization.*; import static com.microsoft.playwright.impl.Utils.*; -import static com.microsoft.playwright.impl.Utils.addLargeFileUploadParams; +import static com.microsoft.playwright.impl.Utils.addFilePathUploadParams; import static com.microsoft.playwright.options.ScreenshotType.JPEG; import static com.microsoft.playwright.options.ScreenshotType.PNG; @@ -467,16 +466,12 @@ public class ElementHandleImpl extends JSHandleImpl implements ElementHandle { if (frame == null) { throw new Error("Cannot set input files to detached element"); } - if (hasLargeFile(files)) { - if (options == null) { - options = new SetInputFilesOptions(); - } - JsonObject params = gson().toJsonTree(options).getAsJsonObject(); - addLargeFileUploadParams(files, params, frame.page().context()); - sendMessage("setInputFilePaths", params); - } else { - setInputFilesImpl(Utils.toFilePayloads(files), options); + if (options == null) { + options = new SetInputFilesOptions(); } + JsonObject params = gson().toJsonTree(options).getAsJsonObject(); + addFilePathUploadParams(files, params, frame.page().context()); + sendMessage("setInputFiles", params); } @Override 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 1e363e64..1d14d676 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/FrameImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/FrameImpl.java @@ -761,17 +761,13 @@ public class FrameImpl extends ChannelOwner implements Frame { } void setInputFilesImpl(String selector, Path[] files, SetInputFilesOptions options) { - if (hasLargeFile(files)) { - if (options == null) { - options = new SetInputFilesOptions(); - } - JsonObject params = gson().toJsonTree(options).getAsJsonObject(); - addLargeFileUploadParams(files, params, page.context()); - params.addProperty("selector", selector); - sendMessage("setInputFilePaths", params); - } else { - setInputFilesImpl(selector, Utils.toFilePayloads(files), options); + if (options == null) { + options = new SetInputFilesOptions(); } + JsonObject params = gson().toJsonTree(options).getAsJsonObject(); + addFilePathUploadParams(files, params, page.context()); + params.addProperty("selector", selector); + sendMessage("setInputFiles", params); } @Override @@ -791,7 +787,7 @@ public class FrameImpl extends ChannelOwner implements Frame { } JsonObject params = gson().toJsonTree(options).getAsJsonObject(); params.addProperty("selector", selector); - params.add("files", toJsonArray(files)); + params.add("payloads", toJsonArray(files)); sendMessage("setInputFiles", params); } 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 8bb1c22e..aa27b619 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java @@ -68,6 +68,7 @@ public class PageImpl extends ChannelOwner implements Page { private final TimeoutSettings timeoutSettings; private VideoImpl video; private final PageImpl opener; + private String closeReason; enum EventType { CLOSE, @@ -200,6 +201,13 @@ public class PageImpl extends ChannelOwner implements Page { listeners.notify(EventType.CLOSE, this); } + private String effectiveCloseReason() { + if (closeReason != null) { + return closeReason; + } + return browserContext.effectiveCloseReason(); + } + @Override public void onClose(Consumer handler) { listeners.add(EventType.CLOSE, handler); @@ -488,6 +496,7 @@ public class PageImpl extends ChannelOwner implements Page { if (options == null) { options = new CloseOptions(); } + closeReason = options.reason; try { if (ownedContext != null) { ownedContext.close(); @@ -1341,7 +1350,7 @@ public class PageImpl extends ChannelOwner implements Page { @Override public T get() { - throw new PlaywrightException("Page closed"); + throw new TargetClosedError(effectiveCloseReason()); } } @@ -1352,7 +1361,7 @@ public class PageImpl extends ChannelOwner implements Page { @Override public T get() { - throw new PlaywrightException("Page crashed"); + throw new TargetClosedError("Page crashed"); } } diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/TargetClosedError.java b/playwright/src/main/java/com/microsoft/playwright/impl/TargetClosedError.java new file mode 100644 index 00000000..eb62cb4b --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/impl/TargetClosedError.java @@ -0,0 +1,29 @@ +/* + * 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.microsoft.playwright.PlaywrightException; + +public class TargetClosedError extends PlaywrightException { + public TargetClosedError() { + super(null); + } + + public TargetClosedError(String message) { + super(message != null ? message : "Target page, context or browser has been closed"); + } +} 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 de3b2bca..e1ed8b73 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/Utils.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/Utils.java @@ -18,11 +18,9 @@ package com.microsoft.playwright.impl; import com.google.gson.JsonArray; import com.google.gson.JsonObject; -import com.microsoft.playwright.ElementHandle; import com.microsoft.playwright.PlaywrightException; import com.microsoft.playwright.options.FilePayload; import com.microsoft.playwright.options.HttpHeader; -import com.microsoft.playwright.options.SelectOption; import java.io.FileOutputStream; import java.io.IOException; @@ -32,6 +30,7 @@ import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.attribute.FileTime; import java.util.*; import java.util.regex.Pattern; @@ -174,26 +173,21 @@ class Utils { return mimeType; } - static final long maxUploadBufferSize = 50 * 1024 * 1024; - - static boolean hasLargeFile(Path[] files) { - long totalSize = 0; - for (Path file: files) { - try { - totalSize += Files.size(file); - } catch (IOException e) { - throw new PlaywrightException("Cannot get file size.", e); - } - } - return totalSize > maxUploadBufferSize; - } - - static void addLargeFileUploadParams(Path[] files, JsonObject params, BrowserContextImpl context) { - if (context.connection.isRemote) { + static void addFilePathUploadParams(Path[] files, JsonObject params, BrowserContextImpl context) { + if (files.length == 0) { + // FIXME: shouldBeAbleToResetSelectedFilesWithEmptyFileList tesst hangs in Chromium if we pass empty paths list. + params.add("payloads", new JsonArray()); + } else if (context.connection.isRemote) { List streams = new ArrayList<>(); JsonArray jsonStreams = new JsonArray(); for (Path path : files) { - WritableStream temp = context.createTempFile(path.getFileName().toString()); + long lastModifiedMs; + try { + lastModifiedMs = Files.getLastModifiedTime(path).toMillis(); + } catch (IOException e) { + throw new PlaywrightException("Cannot read file timestamp: " + path, e); + } + WritableStream temp = context.createTempFile(path.getFileName().toString(), lastModifiedMs); streams.add(temp); try (OutputStream out = temp.stream()) { Files.copy(path, out); @@ -220,7 +214,7 @@ class Utils { for (FilePayload file: files) { totalSize += file.buffer.length; } - if (totalSize > maxUploadBufferSize) { + if (totalSize > 50 * 1024 * 1024) { throw new PlaywrightException("Cannot set buffer larger than 50Mb, please write it to a file and pass its path instead."); } } diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/WritableStream.java b/playwright/src/main/java/com/microsoft/playwright/impl/WritableStream.java index b1216ebd..1dcc5b20 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/WritableStream.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/WritableStream.java @@ -28,6 +28,12 @@ class WritableStream extends ChannelOwner { params.addProperty("binary", new String(encoded.array(), StandardCharsets.UTF_8)); sendMessage("write", params); } + + @Override + public void close() throws IOException { + super.close(); + sendMessage("close"); + } }; } } diff --git a/playwright/src/test/java/com/microsoft/playwright/TestBrowser.java b/playwright/src/test/java/com/microsoft/playwright/TestBrowser.java index 9abfed89..3fbd936b 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestBrowser.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestBrowser.java @@ -127,4 +127,15 @@ public class TestBrowser extends TestBase { session.detach(); } + + @Test + void shouldPropagateCloseReasonToPendingActions() { + Browser browser = browserType.launch(); + BrowserContext context = browser.newContext(); + PlaywrightException e = assertThrows(PlaywrightException.class, () -> context.waitForPage(() -> { + browser.close(new Browser.CloseOptions().setReason("The reason.")); + })); + assertTrue(e.getMessage().contains("The reason."), e.getMessage()); + } + } diff --git a/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextBasic.java b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextBasic.java index ac8f51c6..5f477a3a 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextBasic.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextBasic.java @@ -144,7 +144,7 @@ public class TestBrowserContextBasic extends TestBase { PlaywrightException e = assertThrows(PlaywrightException.class, () -> { context.waitForPage(() -> context.close()); }); - assertTrue(e.getMessage().contains("Context closed")); + assertTrue(e.getMessage().contains("Target page, context or browser has been closed"), e.getMessage()); } @Test @@ -281,7 +281,16 @@ public class TestBrowserContextBasic extends TestBase { context.close(); return false; })); - assertTrue(e.getMessage().contains("Context closed"), e.getMessage()); + assertTrue(e.getMessage().contains("Target page, context or browser has been closed"), e.getMessage()); } + @Test + void shouldPropagateCloseReasonToPendingActions() { + BrowserContext context = browser.newContext(); + Page page = context.newPage(); + PlaywrightException e = assertThrows(PlaywrightException.class, () -> page.waitForPopup(() -> { + context.close(new BrowserContext.CloseOptions().setReason("The reason.")); + })); + assertTrue(e.getMessage().contains("The reason."), e.getMessage()); + } } diff --git a/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextFetch.java b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextFetch.java index 2c574944..2587d40e 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextFetch.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextFetch.java @@ -672,7 +672,7 @@ public class TestBrowserContextFetch extends TestBase { }); page.evaluate("() => setTimeout(closeContext, 1000);"); PlaywrightException e = assertThrows(PlaywrightException.class, () -> context.request().get(server.EMPTY_PAGE)); - assertTrue(e.getMessage().contains("Request context disposed"), e.getMessage()); + assertTrue(e.getMessage().contains("Target page, context or browser has been closed"), e.getMessage()); e = assertThrows(PlaywrightException.class, () -> context.request().post(server.EMPTY_PAGE)); assertTrue(e.getMessage().contains("Target page, context or browser has been closed"), e.getMessage()); diff --git a/playwright/src/test/java/com/microsoft/playwright/TestBrowserTypeConnect.java b/playwright/src/test/java/com/microsoft/playwright/TestBrowserTypeConnect.java index 1d275ccb..6307e244 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestBrowserTypeConnect.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestBrowserTypeConnect.java @@ -28,6 +28,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.attribute.FileTime; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; @@ -39,6 +40,8 @@ import static java.util.Arrays.asList; import static org.junit.jupiter.api.Assertions.*; public class TestBrowserTypeConnect extends TestBase { + static Path FILE_TO_UPLOAD = Paths.get("src/test/resources/file-to-upload.txt"); + private Process browserServer; private String wsEndpoint; @@ -349,7 +352,7 @@ public class TestBrowserTypeConnect extends TestBase { } browser.close(); } - + @Test void shouldSaveAsVideosFromRemoteBrowser(@TempDir Path tempDir) { Path videosPath = tempDir.resolve("videosPath"); @@ -501,4 +504,17 @@ public class TestBrowserTypeConnect extends TestBase { assertEquals("200MB.zip", fields.get(0).filename); assertEquals(200 * 1024 * 1024, fields.get(0).content.length()); } + + @Test + void setInputFilesDhouldPreserveLastModifiedTimestamp() throws IOException { + page.setContent(""); + Locator input = page.locator("input"); + input.setInputFiles(FILE_TO_UPLOAD); + assertEquals(asList("file-to-upload.txt"), input.evaluate("e => [...e.files].map(f => f.name)")); + List timestamps = (List) input.evaluate("e => [...e.files].map(f => f.lastModified)"); + FileTime expected = Files.getLastModifiedTime(FILE_TO_UPLOAD); + // On Linux browser sometimes reduces the timestamp by 1ms: 1696272058110.0715 -> 1696272058109 or even + // rounds it to seconds in WebKit: 1696272058110 -> 1696272058000. + assertTrue(Math.abs(timestamps.get(0) - expected.toMillis()) < 1000, "expected: " + expected.toMillis() + "; actual: " + timestamps.get(0)); + } } diff --git a/playwright/src/test/java/com/microsoft/playwright/TestEvalOnSelector.java b/playwright/src/test/java/com/microsoft/playwright/TestEvalOnSelector.java index 5b99c702..380ecb46 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestEvalOnSelector.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestEvalOnSelector.java @@ -122,7 +122,7 @@ public class TestEvalOnSelector extends TestBase { PlaywrightException e = assertThrows(PlaywrightException.class, () -> { page.evalOnSelector("section", "e => e.id"); }); - assertTrue(e.getMessage().contains("failed to find element matching selector \"section\"")); + assertTrue(e.getMessage().contains("Failed to find element matching selector \"section\""), e.getMessage()); } @Test diff --git a/playwright/src/test/java/com/microsoft/playwright/TestPageBasic.java b/playwright/src/test/java/com/microsoft/playwright/TestPageBasic.java index bc1f239b..95463179 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestPageBasic.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestPageBasic.java @@ -112,12 +112,12 @@ public class TestPageBasic extends TestBase { PlaywrightException e2 = assertThrows(PlaywrightException.class, () -> { newPage.waitForRequest(server.EMPTY_PAGE, () -> newPage.close()); }); - assertTrue(e2.getMessage().contains("Page closed")); - assertFalse(e2.getMessage().contains("Timeout")); + assertTrue(e2.getMessage().contains("Target page, context or browser has been closed"), e2.getMessage()); + assertFalse(e2.getMessage().contains("Timeout"), e2.getMessage()); }); }); - assertTrue(e1.getMessage().contains("Page closed")); - assertFalse(e1.getMessage().contains("Timeout")); + assertTrue(e1.getMessage().contains("Target page, context or browser has been closed"), e1.getMessage()); + assertFalse(e1.getMessage().contains("Timeout"), e1.getMessage()); } @Test @@ -336,6 +336,15 @@ public class TestPageBasic extends TestBase { page.close(); return false; })); - assertTrue(e.getMessage().contains("Page closed"), e.getMessage()); + assertTrue(e.getMessage().contains("Target page, context or browser has been closed"), e.getMessage()); + } + + @Test + void shouldPropagateCloseReasonToPendingActions() { + Page page = context.newPage(); + PlaywrightException e = assertThrows(PlaywrightException.class, () -> page.waitForPopup(() -> { + page.close(new Page.CloseOptions().setReason("The reason.")); + })); + assertTrue(e.getMessage().contains("The reason."), e.getMessage()); } } diff --git a/playwright/src/test/java/com/microsoft/playwright/TestPageEvaluate.java b/playwright/src/test/java/com/microsoft/playwright/TestPageEvaluate.java index f426a025..173af86f 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestPageEvaluate.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestPageEvaluate.java @@ -391,7 +391,7 @@ public class TestPageEvaluate extends TestBase { assertNotNull(element); element.dispose(); PlaywrightException e = assertThrows(PlaywrightException.class, () -> page.evaluate("e => e.textContent", element)); - assertTrue(e.getMessage().contains("JSHandle is disposed")); + assertTrue(e.getMessage().contains("no object with guid"), e.getMessage()); } @Test diff --git a/playwright/src/test/java/com/microsoft/playwright/TestPageSetInputFiles.java b/playwright/src/test/java/com/microsoft/playwright/TestPageSetInputFiles.java index 489716e8..6e44703b 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestPageSetInputFiles.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestPageSetInputFiles.java @@ -27,6 +27,7 @@ import java.io.*; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.attribute.FileTime; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; @@ -35,6 +36,7 @@ import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import static com.microsoft.playwright.Utils.relativePathOrSkipTest; @@ -403,5 +405,18 @@ public class TestPageSetInputFiles extends TestBase { FileChooser fileChooser = page.waitForFileChooser(() -> page.click("input")); assertTrue(fileChooser.isMultiple()); } + + @Test + void shouldPreserveLastModifiedTimestamp() throws IOException { + page.setContent(""); + Locator input = page.locator("input"); + input.setInputFiles(FILE_TO_UPLOAD); + assertEquals(asList("file-to-upload.txt"), input.evaluate("e => [...e.files].map(f => f.name)")); + List timestamps = (List) input.evaluate("e => [...e.files].map(f => f.lastModified)"); + FileTime expected = Files.getLastModifiedTime(FILE_TO_UPLOAD); + // On Linux browser sometimes reduces the timestamp by 1ms: 1696272058110.0715 -> 1696272058109 or even + // rounds it to seconds in WebKit: 1696272058110 -> 1696272058000. + assertTrue(Math.abs(timestamps.get(0) - expected.toMillis()) < 1000, "expected: " + expected.toMillis() + "; actual: " + timestamps.get(0)); + } } diff --git a/playwright/src/test/java/com/microsoft/playwright/TestWebSocket.java b/playwright/src/test/java/com/microsoft/playwright/TestWebSocket.java index 5a96afff..08dcd9c0 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestWebSocket.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestWebSocket.java @@ -199,7 +199,7 @@ public class TestWebSocket extends TestBase { PlaywrightException e = assertThrows(PlaywrightException.class, () -> { ws.waitForFrameSent(() -> page.close()); }); - assertTrue(e.getMessage().contains("Page closed")); + assertTrue(e.getMessage().contains("Target page, context or browser has been closed"), e.getMessage()); } @Test diff --git a/playwright/src/test/java/com/microsoft/playwright/TestWorkers.java b/playwright/src/test/java/com/microsoft/playwright/TestWorkers.java index aa99d015..b1bf63cd 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestWorkers.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestWorkers.java @@ -57,7 +57,7 @@ public class TestWorkers extends TestBase { PlaywrightException e = assertThrows(PlaywrightException.class, () -> { workerThisObj.getProperty("self"); }); - assertTrue(e.getMessage().contains("Target closed") || e.getMessage().contains("Worker was closed"), e.getMessage()); + assertTrue(e.getMessage().contains("Target page, context or browser has been closed") || e.getMessage().contains("Worker was closed"), e.getMessage()); } diff --git a/scripts/CLI_VERSION b/scripts/CLI_VERSION index 5edffce6..a42ff661 100644 --- a/scripts/CLI_VERSION +++ b/scripts/CLI_VERSION @@ -1 +1 @@ -1.39.0 +1.40.0-alpha-nov-13-2023