From 4cc3fa3012fef4abd770e49c061228e7aeb0f9ed Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Tue, 18 Jun 2024 17:08:45 -0700 Subject: [PATCH] chore: roll driver to 1.45.0 beta, implement new features (#1600) --- README.md | 4 +- .../playwright/APIRequestContext.java | 27 +- .../microsoft/playwright/BrowserContext.java | 48 +- .../java/com/microsoft/playwright/Clock.java | 303 ++++++++++++ .../microsoft/playwright/ConsoleMessage.java | 2 +- .../microsoft/playwright/ElementHandle.java | 28 +- .../com/microsoft/playwright/Locator.java | 52 +- .../java/com/microsoft/playwright/Mouse.java | 3 +- .../java/com/microsoft/playwright/Page.java | 42 +- .../assertions/LocatorAssertions.java | 8 +- .../impl/APIRequestContextImpl.java | 17 +- .../playwright/impl/BrowserContextImpl.java | 11 +- .../microsoft/playwright/impl/ClockImpl.java | 129 +++++ .../playwright/impl/ElementHandleImpl.java | 2 + .../microsoft/playwright/impl/PageImpl.java | 5 + .../playwright/impl/Serialization.java | 1 + .../com/microsoft/playwright/impl/Utils.java | 123 +++-- .../playwright/options/HttpCredentials.java | 17 + .../options/HttpCredentialsSend.java | 22 + .../com/microsoft/playwright/TestBase.java | 18 +- .../TestBrowserContextAddCookies.java | 8 +- .../playwright/TestBrowserContextCookies.java | 15 +- .../playwright/TestBrowserContextFetch.java | 49 ++ .../TestBrowserContextStorageState.java | 16 +- .../playwright/TestBrowserTypeConnect.java | 2 +- .../TestElementHandleConvenience.java | 4 +- .../microsoft/playwright/TestGlobalFetch.java | 33 ++ .../playwright/TestNetworkRequest.java | 64 ++- .../microsoft/playwright/TestPageClock.java | 458 ++++++++++++++++++ .../playwright/TestPageNetworkRequest.java | 6 + .../playwright/TestPageSetInputFiles.java | 81 +++- .../test/resources/input/folderupload.html | 12 + scripts/CLI_VERSION | 2 +- .../playwright/tools/ApiGenerator.java | 3 + 34 files changed, 1491 insertions(+), 124 deletions(-) create mode 100644 playwright/src/main/java/com/microsoft/playwright/Clock.java create mode 100644 playwright/src/main/java/com/microsoft/playwright/impl/ClockImpl.java create mode 100644 playwright/src/main/java/com/microsoft/playwright/options/HttpCredentialsSend.java create mode 100644 playwright/src/test/java/com/microsoft/playwright/TestPageClock.java create mode 100644 playwright/src/test/resources/input/folderupload.html diff --git a/README.md b/README.md index 60b65951..11eb90c7 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 125.0.6422.26 | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Chromium 127.0.6533.5 | :white_check_mark: | :white_check_mark: | :white_check_mark: | | WebKit 17.4 | ✅ | ✅ | ✅ | -| Firefox 125.0.1 | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Firefox 127.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 b617be8a..b8774c0a 100644 --- a/playwright/src/main/java/com/microsoft/playwright/APIRequestContext.java +++ b/playwright/src/main/java/com/microsoft/playwright/APIRequestContext.java @@ -43,6 +43,20 @@ import java.nio.file.Path; * object will have its own isolated cookie storage. */ public interface APIRequestContext { + class DisposeOptions { + /** + * The reason to be reported to the operations interrupted by the context disposal. + */ + public String reason; + + /** + * The reason to be reported to the operations interrupted by the context disposal. + */ + public DisposeOptions setReason(String reason) { + this.reason = reason; + return this; + } + } class StorageStateOptions { /** * The file path to save the storage state to. If {@code path} is a relative path, then it is resolved relative to current @@ -88,7 +102,18 @@ public interface APIRequestContext { * * @since v1.16 */ - void dispose(); + default void dispose() { + dispose(null); + } + /** + * All responses returned by {@link com.microsoft.playwright.APIRequestContext#get APIRequestContext.get()} and similar + * methods are stored in the memory, so that you can later call {@link com.microsoft.playwright.APIResponse#body + * APIResponse.body()}.This method discards all its resources, calling any method on disposed {@code APIRequestContext} + * will throw an exception. + * + * @since v1.16 + */ + void dispose(DisposeOptions options); /** * Sends HTTP(S) request and returns its response. The method will populate request cookies from the context and update * context cookies from the response. The method will automatically follow redirects. diff --git a/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java b/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java index 6d4f0693..fc2d28db 100644 --- a/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java +++ b/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java @@ -501,6 +501,12 @@ public interface BrowserContext extends AutoCloseable { return this; } } + /** + * Playwright has ability to mock clock and passage of time. + * + * @since v1.45 + */ + Clock clock(); /** * Adds cookies into this browser context. All pages within this context will have these cookies installed. Cookies can be * obtained via {@link com.microsoft.playwright.BrowserContext#cookies BrowserContext.cookies()}. @@ -863,21 +869,22 @@ public interface BrowserContext extends AutoCloseable { * * @param permissions A permission or an array of permissions to grant. Permissions can be one of the following values: * * @since v1.8 */ @@ -890,21 +897,22 @@ public interface BrowserContext extends AutoCloseable { * * @param permissions A permission or an array of permissions to grant. Permissions can be one of the following values: * * @since v1.8 */ diff --git a/playwright/src/main/java/com/microsoft/playwright/Clock.java b/playwright/src/main/java/com/microsoft/playwright/Clock.java new file mode 100644 index 00000000..9d2c2a7e --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/Clock.java @@ -0,0 +1,303 @@ +/* + * 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 java.util.Date; + +/** + * Accurately simulating time-dependent behavior is essential for verifying the correctness of applications. Learn more + * about clock emulation. + * + *

Note that clock is installed for the entire {@code BrowserContext}, so the time in all the pages and iframes is + * controlled by the same clock. + */ +public interface Clock { + class InstallOptions { + /** + * Time to initialize with, current system time by default. + */ + public Object time; + + /** + * Time to initialize with, current system time by default. + */ + public InstallOptions setTime(long time) { + this.time = time; + return this; + } + /** + * Time to initialize with, current system time by default. + */ + public InstallOptions setTime(String time) { + this.time = time; + return this; + } + /** + * Time to initialize with, current system time by default. + */ + public InstallOptions setTime(Date time) { + this.time = time; + return this; + } + } + /** + * Advance the clock by jumping forward in time. Only fires due timers at most once. This is equivalent to user closing the + * laptop lid for a while and reopening it later, after given time. + * + *

Usage + *

{@code
+   * page.clock().fastForward(1000);
+   * page.clock().fastForward("30:00");
+   * }
+ * + * @param ticks Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are "08" + * for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds. + * @since v1.45 + */ + void fastForward(long ticks); + /** + * Advance the clock by jumping forward in time. Only fires due timers at most once. This is equivalent to user closing the + * laptop lid for a while and reopening it later, after given time. + * + *

Usage + *

{@code
+   * page.clock().fastForward(1000);
+   * page.clock().fastForward("30:00");
+   * }
+ * + * @param ticks Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are "08" + * for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds. + * @since v1.45 + */ + void fastForward(String ticks); + /** + * Install fake implementations for the following time-related functions: + * + * + *

Fake timers are used to manually control the flow of time in tests. They allow you to advance time, fire timers, and + * control the behavior of time-dependent functions. See {@link com.microsoft.playwright.Clock#runFor Clock.runFor()} and + * {@link com.microsoft.playwright.Clock#fastForward Clock.fastForward()} for more information. + * + * @since v1.45 + */ + default void install() { + install(null); + } + /** + * Install fake implementations for the following time-related functions: + *

+ * + *

Fake timers are used to manually control the flow of time in tests. They allow you to advance time, fire timers, and + * control the behavior of time-dependent functions. See {@link com.microsoft.playwright.Clock#runFor Clock.runFor()} and + * {@link com.microsoft.playwright.Clock#fastForward Clock.fastForward()} for more information. + * + * @since v1.45 + */ + void install(InstallOptions options); + /** + * Advance the clock, firing all the time-related callbacks. + * + *

Usage + *

{@code
+   * page.clock().runFor(1000);
+   * page.clock().runFor("30:00");
+   * }
+ * + * @param ticks Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are "08" + * for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds. + * @since v1.45 + */ + void runFor(long ticks); + /** + * Advance the clock, firing all the time-related callbacks. + * + *

Usage + *

{@code
+   * page.clock().runFor(1000);
+   * page.clock().runFor("30:00");
+   * }
+ * + * @param ticks Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are "08" + * for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds. + * @since v1.45 + */ + void runFor(String ticks); + /** + * Advance the clock by jumping forward in time and pause the time. Once this method is called, no timers are fired unless + * {@link com.microsoft.playwright.Clock#runFor Clock.runFor()}, {@link com.microsoft.playwright.Clock#fastForward + * Clock.fastForward()}, {@link com.microsoft.playwright.Clock#pauseAt Clock.pauseAt()} or {@link + * com.microsoft.playwright.Clock#resume Clock.resume()} is called. + * + *

Only fires due timers at most once. This is equivalent to user closing the laptop lid for a while and reopening it at + * the specified time and pausing. + * + *

Usage + *

{@code
+   * page.clock().pauseAt(Instant.parse("2020-02-02"));
+   * page.clock().pauseAt("2020-02-02");
+   * }
+ * + * @since v1.45 + */ + void pauseAt(long time); + /** + * Advance the clock by jumping forward in time and pause the time. Once this method is called, no timers are fired unless + * {@link com.microsoft.playwright.Clock#runFor Clock.runFor()}, {@link com.microsoft.playwright.Clock#fastForward + * Clock.fastForward()}, {@link com.microsoft.playwright.Clock#pauseAt Clock.pauseAt()} or {@link + * com.microsoft.playwright.Clock#resume Clock.resume()} is called. + * + *

Only fires due timers at most once. This is equivalent to user closing the laptop lid for a while and reopening it at + * the specified time and pausing. + * + *

Usage + *

{@code
+   * page.clock().pauseAt(Instant.parse("2020-02-02"));
+   * page.clock().pauseAt("2020-02-02");
+   * }
+ * + * @since v1.45 + */ + void pauseAt(String time); + /** + * Advance the clock by jumping forward in time and pause the time. Once this method is called, no timers are fired unless + * {@link com.microsoft.playwright.Clock#runFor Clock.runFor()}, {@link com.microsoft.playwright.Clock#fastForward + * Clock.fastForward()}, {@link com.microsoft.playwright.Clock#pauseAt Clock.pauseAt()} or {@link + * com.microsoft.playwright.Clock#resume Clock.resume()} is called. + * + *

Only fires due timers at most once. This is equivalent to user closing the laptop lid for a while and reopening it at + * the specified time and pausing. + * + *

Usage + *

{@code
+   * page.clock().pauseAt(Instant.parse("2020-02-02"));
+   * page.clock().pauseAt("2020-02-02");
+   * }
+ * + * @since v1.45 + */ + void pauseAt(Date time); + /** + * Resumes timers. Once this method is called, time resumes flowing, timers are fired as usual. + * + * @since v1.45 + */ + void resume(); + /** + * Makes {@code Date.now} and {@code new Date()} return fixed fake time at all times, keeps all the timers running. + * + *

Usage + *

{@code
+   * page.clock().setFixedTime(Instant.now());
+   * page.clock().setFixedTime(Instant.parse("2020-02-02"));
+   * page.clock().setFixedTime("2020-02-02");
+   * }
+ * + * @param time Time to be set. + * @since v1.45 + */ + void setFixedTime(long time); + /** + * Makes {@code Date.now} and {@code new Date()} return fixed fake time at all times, keeps all the timers running. + * + *

Usage + *

{@code
+   * page.clock().setFixedTime(Instant.now());
+   * page.clock().setFixedTime(Instant.parse("2020-02-02"));
+   * page.clock().setFixedTime("2020-02-02");
+   * }
+ * + * @param time Time to be set. + * @since v1.45 + */ + void setFixedTime(String time); + /** + * Makes {@code Date.now} and {@code new Date()} return fixed fake time at all times, keeps all the timers running. + * + *

Usage + *

{@code
+   * page.clock().setFixedTime(Instant.now());
+   * page.clock().setFixedTime(Instant.parse("2020-02-02"));
+   * page.clock().setFixedTime("2020-02-02");
+   * }
+ * + * @param time Time to be set. + * @since v1.45 + */ + void setFixedTime(Date time); + /** + * Sets current system time but does not trigger any timers. + * + *

Usage + *

{@code
+   * page.clock().setSystemTime(Instant.now());
+   * page.clock().setSystemTime(Instant.parse("2020-02-02"));
+   * page.clock().setSystemTime("2020-02-02");
+   * }
+ * + * @since v1.45 + */ + void setSystemTime(long time); + /** + * Sets current system time but does not trigger any timers. + * + *

Usage + *

{@code
+   * page.clock().setSystemTime(Instant.now());
+   * page.clock().setSystemTime(Instant.parse("2020-02-02"));
+   * page.clock().setSystemTime("2020-02-02");
+   * }
+ * + * @since v1.45 + */ + void setSystemTime(String time); + /** + * Sets current system time but does not trigger any timers. + * + *

Usage + *

{@code
+   * page.clock().setSystemTime(Instant.now());
+   * page.clock().setSystemTime(Instant.parse("2020-02-02"));
+   * page.clock().setSystemTime("2020-02-02");
+   * }
+ * + * @since v1.45 + */ + void setSystemTime(Date time); +} + diff --git a/playwright/src/main/java/com/microsoft/playwright/ConsoleMessage.java b/playwright/src/main/java/com/microsoft/playwright/ConsoleMessage.java index 74296f18..4d1aea69 100644 --- a/playwright/src/main/java/com/microsoft/playwright/ConsoleMessage.java +++ b/playwright/src/main/java/com/microsoft/playwright/ConsoleMessage.java @@ -20,7 +20,7 @@ import java.util.*; /** * {@code ConsoleMessage} objects are dispatched by page via the {@link com.microsoft.playwright.Page#onConsoleMessage - * Page.onConsoleMessage()} event. For each console messages logged in the page there will be corresponding event in the + * Page.onConsoleMessage()} event. For each console message logged in the page there will be corresponding event in the * Playwright context. *
{@code
  * // Listen for all console messages and print them to the standard output.
diff --git a/playwright/src/main/java/com/microsoft/playwright/ElementHandle.java b/playwright/src/main/java/com/microsoft/playwright/ElementHandle.java
index c48518e9..026dd87d 100644
--- a/playwright/src/main/java/com/microsoft/playwright/ElementHandle.java
+++ b/playwright/src/main/java/com/microsoft/playwright/ElementHandle.java
@@ -1926,6 +1926,8 @@ public interface ElementHandle extends JSHandle {
    * 

Throws when {@code elementHandle} does not point to an element connected to a Document or a ShadowRoot. * + *

See scrolling for alternative ways to scroll. + * * @since v1.8 */ default void scrollIntoViewIfNeeded() { @@ -1940,6 +1942,8 @@ public interface ElementHandle extends JSHandle { *

Throws when {@code elementHandle} does not point to an element connected to a Document or a ShadowRoot. * + *

See scrolling for alternative ways to scroll. + * * @since v1.8 */ void scrollIntoViewIfNeeded(ScrollIntoViewIfNeededOptions options); @@ -2371,7 +2375,8 @@ public interface ElementHandle extends JSHandle { void setChecked(boolean checked, SetCheckedOptions options); /** * Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then - * they are resolved relative to the current working directory. For empty array, clears the selected files. + * they are resolved relative to the current working directory. For empty array, clears the selected files. For inputs with + * a {@code [webkitdirectory]} attribute, only a single directory path is supported. * *

This method expects {@code ElementHandle} to point to an input element. However, if the element is @@ -2386,7 +2391,8 @@ public interface ElementHandle extends JSHandle { } /** * Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then - * they are resolved relative to the current working directory. For empty array, clears the selected files. + * they are resolved relative to the current working directory. For empty array, clears the selected files. For inputs with + * a {@code [webkitdirectory]} attribute, only a single directory path is supported. * *

This method expects {@code ElementHandle} to point to an input element. However, if the element is @@ -2399,7 +2405,8 @@ public interface ElementHandle extends JSHandle { void setInputFiles(Path files, SetInputFilesOptions options); /** * Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then - * they are resolved relative to the current working directory. For empty array, clears the selected files. + * they are resolved relative to the current working directory. For empty array, clears the selected files. For inputs with + * a {@code [webkitdirectory]} attribute, only a single directory path is supported. * *

This method expects {@code ElementHandle} to point to an input element. However, if the element is @@ -2414,7 +2421,8 @@ public interface ElementHandle extends JSHandle { } /** * Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then - * they are resolved relative to the current working directory. For empty array, clears the selected files. + * they are resolved relative to the current working directory. For empty array, clears the selected files. For inputs with + * a {@code [webkitdirectory]} attribute, only a single directory path is supported. * *

This method expects {@code ElementHandle} to point to an input element. However, if the element is @@ -2427,7 +2435,8 @@ public interface ElementHandle extends JSHandle { void setInputFiles(Path[] files, SetInputFilesOptions options); /** * Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then - * they are resolved relative to the current working directory. For empty array, clears the selected files. + * they are resolved relative to the current working directory. For empty array, clears the selected files. For inputs with + * a {@code [webkitdirectory]} attribute, only a single directory path is supported. * *

This method expects {@code ElementHandle} to point to an input element. However, if the element is @@ -2442,7 +2451,8 @@ public interface ElementHandle extends JSHandle { } /** * Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then - * they are resolved relative to the current working directory. For empty array, clears the selected files. + * they are resolved relative to the current working directory. For empty array, clears the selected files. For inputs with + * a {@code [webkitdirectory]} attribute, only a single directory path is supported. * *

This method expects {@code ElementHandle} to point to an input element. However, if the element is @@ -2455,7 +2465,8 @@ public interface ElementHandle extends JSHandle { void setInputFiles(FilePayload files, SetInputFilesOptions options); /** * Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then - * they are resolved relative to the current working directory. For empty array, clears the selected files. + * they are resolved relative to the current working directory. For empty array, clears the selected files. For inputs with + * a {@code [webkitdirectory]} attribute, only a single directory path is supported. * *

This method expects {@code ElementHandle} to point to an input element. However, if the element is @@ -2470,7 +2481,8 @@ public interface ElementHandle extends JSHandle { } /** * Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then - * they are resolved relative to the current working directory. For empty array, clears the selected files. + * they are resolved relative to the current working directory. For empty array, clears the selected files. For inputs with + * a {@code [webkitdirectory]} attribute, only a single directory path is supported. * *

This method expects {@code ElementHandle} to point to an input element. However, if the element is diff --git a/playwright/src/main/java/com/microsoft/playwright/Locator.java b/playwright/src/main/java/com/microsoft/playwright/Locator.java index a09fc1d5..eef4643b 100644 --- a/playwright/src/main/java/com/microsoft/playwright/Locator.java +++ b/playwright/src/main/java/com/microsoft/playwright/Locator.java @@ -4372,6 +4372,8 @@ public interface Locator { * href="https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API">IntersectionObserver's {@code * ratio}. * + *

See scrolling for alternative ways to scroll. + * * @since v1.14 */ default void scrollIntoViewIfNeeded() { @@ -4383,6 +4385,8 @@ public interface Locator { * href="https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API">IntersectionObserver's {@code * ratio}. * + *

See scrolling for alternative ways to scroll. + * * @since v1.14 */ void scrollIntoViewIfNeeded(ScrollIntoViewIfNeededOptions options); @@ -4879,7 +4883,8 @@ public interface Locator { */ void setChecked(boolean checked, SetCheckedOptions options); /** - * Upload file or multiple files into {@code }. + * Upload file or multiple files into {@code }. For inputs with a {@code [webkitdirectory]} attribute, + * only a single directory path is supported. * *

Usage *

{@code
@@ -4889,6 +4894,9 @@ public interface Locator {
    * // Select multiple files
    * page.getByLabel("Upload files").setInputFiles(new Path[] {Paths.get("file1.txt"), Paths.get("file2.txt")});
    *
+   * // Select a directory
+   * page.getByLabel("Upload directory").setInputFiles(Paths.get("mydir"));
+   *
    * // Remove all the selected files
    * page.getByLabel("Upload file").setInputFiles(new Path[0]);
    *
@@ -4914,7 +4922,8 @@ public interface Locator {
     setInputFiles(files, null);
   }
   /**
-   * Upload file or multiple files into {@code }.
+   * Upload file or multiple files into {@code }. For inputs with a {@code [webkitdirectory]} attribute,
+   * only a single directory path is supported.
    *
    * 

Usage *

{@code
@@ -4924,6 +4933,9 @@ public interface Locator {
    * // Select multiple files
    * page.getByLabel("Upload files").setInputFiles(new Path[] {Paths.get("file1.txt"), Paths.get("file2.txt")});
    *
+   * // Select a directory
+   * page.getByLabel("Upload directory").setInputFiles(Paths.get("mydir"));
+   *
    * // Remove all the selected files
    * page.getByLabel("Upload file").setInputFiles(new Path[0]);
    *
@@ -4947,7 +4959,8 @@ public interface Locator {
    */
   void setInputFiles(Path files, SetInputFilesOptions options);
   /**
-   * Upload file or multiple files into {@code }.
+   * Upload file or multiple files into {@code }. For inputs with a {@code [webkitdirectory]} attribute,
+   * only a single directory path is supported.
    *
    * 

Usage *

{@code
@@ -4957,6 +4970,9 @@ public interface Locator {
    * // Select multiple files
    * page.getByLabel("Upload files").setInputFiles(new Path[] {Paths.get("file1.txt"), Paths.get("file2.txt")});
    *
+   * // Select a directory
+   * page.getByLabel("Upload directory").setInputFiles(Paths.get("mydir"));
+   *
    * // Remove all the selected files
    * page.getByLabel("Upload file").setInputFiles(new Path[0]);
    *
@@ -4982,7 +4998,8 @@ public interface Locator {
     setInputFiles(files, null);
   }
   /**
-   * Upload file or multiple files into {@code }.
+   * Upload file or multiple files into {@code }. For inputs with a {@code [webkitdirectory]} attribute,
+   * only a single directory path is supported.
    *
    * 

Usage *

{@code
@@ -4992,6 +5009,9 @@ public interface Locator {
    * // Select multiple files
    * page.getByLabel("Upload files").setInputFiles(new Path[] {Paths.get("file1.txt"), Paths.get("file2.txt")});
    *
+   * // Select a directory
+   * page.getByLabel("Upload directory").setInputFiles(Paths.get("mydir"));
+   *
    * // Remove all the selected files
    * page.getByLabel("Upload file").setInputFiles(new Path[0]);
    *
@@ -5015,7 +5035,8 @@ public interface Locator {
    */
   void setInputFiles(Path[] files, SetInputFilesOptions options);
   /**
-   * Upload file or multiple files into {@code }.
+   * Upload file or multiple files into {@code }. For inputs with a {@code [webkitdirectory]} attribute,
+   * only a single directory path is supported.
    *
    * 

Usage *

{@code
@@ -5025,6 +5046,9 @@ public interface Locator {
    * // Select multiple files
    * page.getByLabel("Upload files").setInputFiles(new Path[] {Paths.get("file1.txt"), Paths.get("file2.txt")});
    *
+   * // Select a directory
+   * page.getByLabel("Upload directory").setInputFiles(Paths.get("mydir"));
+   *
    * // Remove all the selected files
    * page.getByLabel("Upload file").setInputFiles(new Path[0]);
    *
@@ -5050,7 +5074,8 @@ public interface Locator {
     setInputFiles(files, null);
   }
   /**
-   * Upload file or multiple files into {@code }.
+   * Upload file or multiple files into {@code }. For inputs with a {@code [webkitdirectory]} attribute,
+   * only a single directory path is supported.
    *
    * 

Usage *

{@code
@@ -5060,6 +5085,9 @@ public interface Locator {
    * // Select multiple files
    * page.getByLabel("Upload files").setInputFiles(new Path[] {Paths.get("file1.txt"), Paths.get("file2.txt")});
    *
+   * // Select a directory
+   * page.getByLabel("Upload directory").setInputFiles(Paths.get("mydir"));
+   *
    * // Remove all the selected files
    * page.getByLabel("Upload file").setInputFiles(new Path[0]);
    *
@@ -5083,7 +5111,8 @@ public interface Locator {
    */
   void setInputFiles(FilePayload files, SetInputFilesOptions options);
   /**
-   * Upload file or multiple files into {@code }.
+   * Upload file or multiple files into {@code }. For inputs with a {@code [webkitdirectory]} attribute,
+   * only a single directory path is supported.
    *
    * 

Usage *

{@code
@@ -5093,6 +5122,9 @@ public interface Locator {
    * // Select multiple files
    * page.getByLabel("Upload files").setInputFiles(new Path[] {Paths.get("file1.txt"), Paths.get("file2.txt")});
    *
+   * // Select a directory
+   * page.getByLabel("Upload directory").setInputFiles(Paths.get("mydir"));
+   *
    * // Remove all the selected files
    * page.getByLabel("Upload file").setInputFiles(new Path[0]);
    *
@@ -5118,7 +5150,8 @@ public interface Locator {
     setInputFiles(files, null);
   }
   /**
-   * Upload file or multiple files into {@code }.
+   * Upload file or multiple files into {@code }. For inputs with a {@code [webkitdirectory]} attribute,
+   * only a single directory path is supported.
    *
    * 

Usage *

{@code
@@ -5128,6 +5161,9 @@ public interface Locator {
    * // Select multiple files
    * page.getByLabel("Upload files").setInputFiles(new Path[] {Paths.get("file1.txt"), Paths.get("file2.txt")});
    *
+   * // Select a directory
+   * page.getByLabel("Upload directory").setInputFiles(Paths.get("mydir"));
+   *
    * // Remove all the selected files
    * page.getByLabel("Upload file").setInputFiles(new Path[0]);
    *
diff --git a/playwright/src/main/java/com/microsoft/playwright/Mouse.java b/playwright/src/main/java/com/microsoft/playwright/Mouse.java
index 95272dd9..28424c15 100644
--- a/playwright/src/main/java/com/microsoft/playwright/Mouse.java
+++ b/playwright/src/main/java/com/microsoft/playwright/Mouse.java
@@ -236,7 +236,8 @@ public interface Mouse {
    */
   void up(UpOptions options);
   /**
-   * Dispatches a {@code wheel} event.
+   * Dispatches a {@code wheel} event. This method is usually used to manually scroll the page. See scrolling for alternative ways to scroll.
    *
    * 

NOTE: Wheel events may cause scrolling if they are not handled, and this method does not wait for the scrolling to finish * before returning. diff --git a/playwright/src/main/java/com/microsoft/playwright/Page.java b/playwright/src/main/java/com/microsoft/playwright/Page.java index 6d6335c0..9b08a92e 100644 --- a/playwright/src/main/java/com/microsoft/playwright/Page.java +++ b/playwright/src/main/java/com/microsoft/playwright/Page.java @@ -3792,6 +3792,12 @@ public interface Page extends AutoCloseable { return this; } } + /** + * Playwright has ability to mock clock and passage of time. + * + * @since v1.45 + */ + Clock clock(); /** * Adds a script which would be evaluated in one of the following scenarios: *

    @@ -7200,7 +7206,8 @@ public interface Page extends AutoCloseable { void setExtraHTTPHeaders(Map headers); /** * Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then - * they are resolved relative to the current working directory. For empty array, clears the selected files. + * they are resolved relative to the current working directory. For empty array, clears the selected files. For inputs with + * a {@code [webkitdirectory]} attribute, only a single directory path is supported. * *

    This method expects {@code selector} to point to an input element. However, if the element is @@ -7216,7 +7223,8 @@ public interface Page extends AutoCloseable { } /** * Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then - * they are resolved relative to the current working directory. For empty array, clears the selected files. + * they are resolved relative to the current working directory. For empty array, clears the selected files. For inputs with + * a {@code [webkitdirectory]} attribute, only a single directory path is supported. * *

    This method expects {@code selector} to point to an input element. However, if the element is @@ -7230,7 +7238,8 @@ public interface Page extends AutoCloseable { void setInputFiles(String selector, Path files, SetInputFilesOptions options); /** * Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then - * they are resolved relative to the current working directory. For empty array, clears the selected files. + * they are resolved relative to the current working directory. For empty array, clears the selected files. For inputs with + * a {@code [webkitdirectory]} attribute, only a single directory path is supported. * *

    This method expects {@code selector} to point to an input element. However, if the element is @@ -7246,7 +7255,8 @@ public interface Page extends AutoCloseable { } /** * Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then - * they are resolved relative to the current working directory. For empty array, clears the selected files. + * they are resolved relative to the current working directory. For empty array, clears the selected files. For inputs with + * a {@code [webkitdirectory]} attribute, only a single directory path is supported. * *

    This method expects {@code selector} to point to an input element. However, if the element is @@ -7260,7 +7270,8 @@ public interface Page extends AutoCloseable { void setInputFiles(String selector, Path[] files, SetInputFilesOptions options); /** * Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then - * they are resolved relative to the current working directory. For empty array, clears the selected files. + * they are resolved relative to the current working directory. For empty array, clears the selected files. For inputs with + * a {@code [webkitdirectory]} attribute, only a single directory path is supported. * *

    This method expects {@code selector} to point to an input element. However, if the element is @@ -7276,7 +7287,8 @@ public interface Page extends AutoCloseable { } /** * Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then - * they are resolved relative to the current working directory. For empty array, clears the selected files. + * they are resolved relative to the current working directory. For empty array, clears the selected files. For inputs with + * a {@code [webkitdirectory]} attribute, only a single directory path is supported. * *

    This method expects {@code selector} to point to an input element. However, if the element is @@ -7290,7 +7302,8 @@ public interface Page extends AutoCloseable { void setInputFiles(String selector, FilePayload files, SetInputFilesOptions options); /** * Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then - * they are resolved relative to the current working directory. For empty array, clears the selected files. + * they are resolved relative to the current working directory. For empty array, clears the selected files. For inputs with + * a {@code [webkitdirectory]} attribute, only a single directory path is supported. * *

    This method expects {@code selector} to point to an input element. However, if the element is @@ -7306,7 +7319,8 @@ public interface Page extends AutoCloseable { } /** * Sets the value of the file input to these file paths or files. If some of the {@code filePaths} are relative paths, then - * they are resolved relative to the current working directory. For empty array, clears the selected files. + * they are resolved relative to the current working directory. For empty array, clears the selected files. For inputs with + * a {@code [webkitdirectory]} attribute, only a single directory path is supported. * *

    This method expects {@code selector} to point to an input element. However, if the element is @@ -8084,7 +8098,7 @@ public interface Page extends AutoCloseable { * }); * * // Waits for the next response matching some conditions - * Response response = page.waitForResponse(response -> "https://example.com".equals(response.url()) && response.status() == 200, () -> { + * Response response = page.waitForResponse(response -> "https://example.com".equals(response.url()) && response.status() == 200 && "GET".equals(response.request().method()), () -> { * // Triggers the response * page.getByText("trigger response").click(); * }); @@ -8112,7 +8126,7 @@ public interface Page extends AutoCloseable { * }); * * // Waits for the next response matching some conditions - * Response response = page.waitForResponse(response -> "https://example.com".equals(response.url()) && response.status() == 200, () -> { + * Response response = page.waitForResponse(response -> "https://example.com".equals(response.url()) && response.status() == 200 && "GET".equals(response.request().method()), () -> { * // Triggers the response * page.getByText("trigger response").click(); * }); @@ -8138,7 +8152,7 @@ public interface Page extends AutoCloseable { * }); * * // Waits for the next response matching some conditions - * Response response = page.waitForResponse(response -> "https://example.com".equals(response.url()) && response.status() == 200, () -> { + * Response response = page.waitForResponse(response -> "https://example.com".equals(response.url()) && response.status() == 200 && "GET".equals(response.request().method()), () -> { * // Triggers the response * page.getByText("trigger response").click(); * }); @@ -8166,7 +8180,7 @@ public interface Page extends AutoCloseable { * }); * * // Waits for the next response matching some conditions - * Response response = page.waitForResponse(response -> "https://example.com".equals(response.url()) && response.status() == 200, () -> { + * Response response = page.waitForResponse(response -> "https://example.com".equals(response.url()) && response.status() == 200 && "GET".equals(response.request().method()), () -> { * // Triggers the response * page.getByText("trigger response").click(); * }); @@ -8192,7 +8206,7 @@ public interface Page extends AutoCloseable { * }); * * // Waits for the next response matching some conditions - * Response response = page.waitForResponse(response -> "https://example.com".equals(response.url()) && response.status() == 200, () -> { + * Response response = page.waitForResponse(response -> "https://example.com".equals(response.url()) && response.status() == 200 && "GET".equals(response.request().method()), () -> { * // Triggers the response * page.getByText("trigger response").click(); * }); @@ -8220,7 +8234,7 @@ public interface Page extends AutoCloseable { * }); * * // Waits for the next response matching some conditions - * Response response = page.waitForResponse(response -> "https://example.com".equals(response.url()) && response.status() == 200, () -> { + * Response response = page.waitForResponse(response -> "https://example.com".equals(response.url()) && response.status() == 200 && "GET".equals(response.request().method()), () -> { * // Triggers the response * page.getByText("trigger response").click(); * }); diff --git a/playwright/src/main/java/com/microsoft/playwright/assertions/LocatorAssertions.java b/playwright/src/main/java/com/microsoft/playwright/assertions/LocatorAssertions.java index 55f3476a..e2a81d47 100644 --- a/playwright/src/main/java/com/microsoft/playwright/assertions/LocatorAssertions.java +++ b/playwright/src/main/java/com/microsoft/playwright/assertions/LocatorAssertions.java @@ -752,10 +752,10 @@ public interface LocatorAssertions { * assertThat(page.getByText("Welcome")).isVisible(); * * // At least one item in the list is visible. - * asserThat(page.getByTestId("todo-item").first()).isVisible(); + * assertThat(page.getByTestId("todo-item").first()).isVisible(); * * // At least one of the two elements is visible, possibly both. - * asserThat( + * assertThat( * page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Sign in")) * .or(page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Sign up"))) * .first() @@ -780,10 +780,10 @@ public interface LocatorAssertions { * assertThat(page.getByText("Welcome")).isVisible(); * * // At least one item in the list is visible. - * asserThat(page.getByTestId("todo-item").first()).isVisible(); + * assertThat(page.getByTestId("todo-item").first()).isVisible(); * * // At least one of the two elements is visible, possibly both. - * asserThat( + * assertThat( * page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Sign in")) * .or(page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Sign up"))) * .first() 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 191223af..f7aca95e 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/APIRequestContextImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/APIRequestContextImpl.java @@ -37,6 +37,7 @@ import static com.microsoft.playwright.impl.Utils.toFilePayload; class APIRequestContextImpl extends ChannelOwner implements APIRequestContext { private final TracingImpl tracing; + private String disposeReason; APIRequestContextImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) { super(parent, type, guid, initializer); @@ -49,8 +50,17 @@ class APIRequestContextImpl extends ChannelOwner implements APIRequestContext { } @Override - public void dispose() { - withLogging("APIRequestContext.dispose", () -> sendMessage("dispose")); + public void dispose(DisposeOptions options) { + withLogging("APIRequestContext.dispose", () -> disposeImpl(options)); + } + + private void disposeImpl(DisposeOptions options) { + if (options == null) { + options = new DisposeOptions(); + } + disposeReason = options.reason; + JsonObject params = gson().toJsonTree(options).getAsJsonObject(); + sendMessage("dispose", params); } @Override @@ -77,6 +87,9 @@ class APIRequestContextImpl extends ChannelOwner implements APIRequestContext { } private APIResponse fetchImpl(String url, RequestOptionsImpl options) { + if (disposeReason != null) { + throw new PlaywrightException(disposeReason); + } if (options == null) { options = new RequestOptionsImpl(); } 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 df988e8c..735fd1ba 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java @@ -36,8 +36,7 @@ import java.util.regex.Pattern; import static com.microsoft.playwright.impl.Serialization.addHarUrlFilter; import static com.microsoft.playwright.impl.Serialization.gson; -import static com.microsoft.playwright.impl.Utils.isSafeCloseError; -import static com.microsoft.playwright.impl.Utils.toJsRegexFlags; +import static com.microsoft.playwright.impl.Utils.*; import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.file.Files.readAllBytes; import static java.util.Arrays.asList; @@ -46,6 +45,7 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext { private final BrowserImpl browser; private final TracingImpl tracing; private final APIRequestContextImpl request; + private final ClockImpl clock; final List pages = new ArrayList<>(); final List backgroundPages = new ArrayList<>(); @@ -104,6 +104,7 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext { } tracing = connection.getExistingObject(initializer.getAsJsonObject("tracing").get("guid").getAsString()); request = connection.getExistingObject(initializer.getAsJsonObject("requestContext").get("guid").getAsString()); + clock = new ClockImpl(this); closePromise = new WaitableEvent<>(listeners, EventType.CLOSE); } @@ -231,6 +232,11 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext { listeners.remove(EventType.RESPONSE, handler); } + @Override + public ClockImpl clock() { + return clock; + } + private T waitForEventWithTimeout(EventType eventType, Runnable code, Predicate predicate, Double timeout) { List> waitables = new ArrayList<>(); waitables.add(new WaitableEvent<>(listeners, eventType, predicate)); @@ -284,6 +290,7 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext { options = new CloseOptions(); } closeReason = options.reason; + request.dispose(convertType(options, APIRequestContext.DisposeOptions.class)); for (Map.Entry entry : harRecorders.entrySet()) { JsonObject params = new JsonObject(); params.addProperty("harId", entry.getKey()); diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/ClockImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/ClockImpl.java new file mode 100644 index 00000000..42e30b09 --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/impl/ClockImpl.java @@ -0,0 +1,129 @@ +package com.microsoft.playwright.impl; + +import com.google.gson.JsonObject; +import com.microsoft.playwright.Clock; + +import java.util.Date; + +class ClockImpl implements Clock { + private final ChannelOwner browserContext; + + ClockImpl(BrowserContextImpl browserContext) { + this.browserContext = browserContext; + } + + @Override + public void fastForward(long ticks) { + JsonObject params = new JsonObject(); + params.addProperty("ticksNumber", ticks); + browserContext.sendMessage("clockFastForward", params); + } + + @Override + public void fastForward(String ticks) { + JsonObject params = new JsonObject(); + params.addProperty("ticksString", ticks); + browserContext.sendMessage("clockFastForward", params); + } + + @Override + public void install(InstallOptions options) { + JsonObject params = new JsonObject(); + if (options != null) { + parseTime(options.time, params); + } + browserContext.sendMessage("clockInstall", params); + } + + @Override + public void runFor(long ticks) { + JsonObject params = new JsonObject(); + params.addProperty("ticksNumber", ticks); + browserContext.sendMessage("clockRunFor", params); + } + + @Override + public void runFor(String ticks) { + JsonObject params = new JsonObject(); + params.addProperty("ticksString", ticks); + browserContext.sendMessage("clockRunFor", params); + } + + @Override + public void pauseAt(long time) { + JsonObject params = new JsonObject(); + params.addProperty("timeNumber", time); + browserContext.sendMessage("clockPauseAt", params); + } + + @Override + public void pauseAt(String time) { + JsonObject params = new JsonObject(); + params.addProperty("timeString", time); + browserContext.sendMessage("clockPauseAt", params); + } + + @Override + public void pauseAt(Date time) { + JsonObject params = new JsonObject(); + params.addProperty("timeNumber", time.getTime()); + browserContext.sendMessage("clockPauseAt", params); + } + + @Override + public void resume() { + browserContext.sendMessage("clockResume"); + } + + @Override + public void setFixedTime(long time) { + JsonObject params = new JsonObject(); + params.addProperty("timeNumber", time); + browserContext.sendMessage("clockSetFixedTime", params); + } + + @Override + public void setFixedTime(String time) { + JsonObject params = new JsonObject(); + params.addProperty("timeString", time); + browserContext.sendMessage("clockSetFixedTime", params); + } + + @Override + public void setFixedTime(Date time) { + JsonObject params = new JsonObject(); + params.addProperty("timeNumber", time.getTime()); + browserContext.sendMessage("clockSetFixedTime", params); + } + + @Override + public void setSystemTime(long time) { + JsonObject params = new JsonObject(); + params.addProperty("timeNumber", time); + browserContext.sendMessage("clockSetSystemTime", params); + } + + @Override + public void setSystemTime(String time) { + JsonObject params = new JsonObject(); + params.addProperty("timeString", time); + browserContext.sendMessage("clockSetSystemTime", params); + } + + @Override + public void setSystemTime(Date time) { + JsonObject params = new JsonObject(); + params.addProperty("timeNumber", time.getTime()); + browserContext.sendMessage("clockSetSystemTime", params); + } + + private static void parseTime(Object time, JsonObject params) { + if (time instanceof Long) { + params.addProperty("timeNumber", (Long) time); + } else if (time instanceof Date) { + params.addProperty("timeNumber", ((Date) time).getTime()); + } else if (time instanceof String) { + params.addProperty("timeString", (String) time); + } + } +} 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 35d7374b..7ef0f942 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/ElementHandleImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/ElementHandleImpl.java @@ -21,11 +21,13 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.microsoft.playwright.ElementHandle; import com.microsoft.playwright.Frame; +import com.microsoft.playwright.PlaywrightException; import com.microsoft.playwright.options.BoundingBox; import com.microsoft.playwright.options.ElementState; import com.microsoft.playwright.options.FilePayload; import com.microsoft.playwright.options.SelectOption; +import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Base64; 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 95c3803a..456cd8f1 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java @@ -431,6 +431,11 @@ public class PageImpl extends ChannelOwner implements Page { listeners.remove(EventType.WORKER, handler); } + @Override + public ClockImpl clock() { + return browserContext.clock(); + } + @Override public Page waitForClose(WaitForCloseOptions options, Runnable code) { return withWaitLogging("Page.waitForClose", logger -> waitForCloseImpl(options, code)); diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/Serialization.java b/playwright/src/main/java/com/microsoft/playwright/impl/Serialization.java index 83906a54..f14f5f53 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/Serialization.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/Serialization.java @@ -54,6 +54,7 @@ class Serialization { .registerTypeAdapter(ColorScheme.class, new ToLowerCaseAndDashSerializer()) .registerTypeAdapter(Media.class, new ToLowerCaseSerializer()) .registerTypeAdapter(ForcedColors.class, new ToLowerCaseSerializer()) + .registerTypeAdapter(HttpCredentialsSend.class, new ToLowerCaseSerializer()) .registerTypeAdapter(ReducedMotion.class, new ToLowerCaseAndDashSerializer()) .registerTypeAdapter(ScreenshotAnimations.class, new ToLowerCaseSerializer()) .registerTypeAdapter(ScreenshotType.class, new ToLowerCaseSerializer()) 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 33ffa12a..3cd721cf 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/Utils.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/Utils.java @@ -17,6 +17,7 @@ package com.microsoft.playwright.impl; import com.google.gson.JsonArray; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.microsoft.playwright.PlaywrightException; import com.microsoft.playwright.options.FilePayload; @@ -33,6 +34,7 @@ import java.nio.file.Path; import java.nio.file.attribute.FileTime; import java.util.*; import java.util.regex.Pattern; +import java.util.stream.Collectors; import static com.microsoft.playwright.impl.Serialization.toJsonArray; @@ -173,42 +175,107 @@ public class Utils { return mimeType; } - static void addFilePathUploadParams(Path[] files, JsonObject params, BrowserContextImpl context) { - if (files.length == 0) { + static void addFilePathUploadParams(Path[] items, JsonObject params, BrowserContextImpl context) { + List localPaths = new ArrayList<>(); + Path localDirectory = resolvePathsAndDirectoryForInputFiles(items, localPaths); + if (items.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) { - 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); - } catch (IOException e) { - throw new PlaywrightException("Failed to copy file to remote server.", e); - } - jsonStreams.add(temp.toProtocolRef()); + if (localDirectory != null) { + localPaths = collectFiles(localDirectory); + } + JsonObject json = createTempFiles(context, localDirectory, localPaths); + JsonArray writableStreams = json.getAsJsonArray("writableStreams"); + JsonArray jsonStreams = copyLocalToTempFiles(context, localPaths, writableStreams); + if (json.has("rootDir")) { + params.add("directoryStream", json.get("rootDir")); + } else { + params.add("streams", jsonStreams); } - params.add("streams", jsonStreams); } else { - Path[] absolute = Arrays.stream(files).map(f -> { - try { - return f.toRealPath(); - } catch (IOException e) { - throw new PlaywrightException("Cannot get absolute file path", e); - } - }).toArray(Path[]::new); - params.add("localPaths", toJsonArray(absolute)); + if (!localPaths.isEmpty()) { + params.add("localPaths", toJsonArray(localPaths.toArray(new Path[0]))); + } + if (localDirectory != null) { + params.addProperty("localDirectory", localDirectory.toString()); + } } } + private static Path resolvePathsAndDirectoryForInputFiles(Path[] items, List outLocalPaths) { + Path localDirectory = null; + try { + for (Path item : items) { + if (Files.isDirectory(item)) { + if (localDirectory != null) { + throw new PlaywrightException("Multiple directories are not supported"); + } + localDirectory = item.toRealPath(); + } else { + outLocalPaths.add(item.toRealPath()); + } + } + } catch (IOException e) { + throw new PlaywrightException("Cannot get absolute file path", e); + } + if (!outLocalPaths.isEmpty() && localDirectory != null) { + throw new PlaywrightException("File paths must be all files or a single directory"); + } + return localDirectory; + } + + private static List collectFiles(Path localDirectory) { + try { + return Files.walk(localDirectory).filter(e -> Files.isRegularFile(e)).collect(Collectors.toList()); + } catch (IOException e) { + throw new PlaywrightException("Failed to traverse directory", e); + } + } + + private static JsonArray copyLocalToTempFiles(BrowserContextImpl context, List localPaths, JsonArray writableStreams) { + JsonArray jsonStreams = new JsonArray(); + for (int i = 0; i < localPaths.size(); i++) { + JsonObject jsonStream = writableStreams.get(i).getAsJsonObject(); + WritableStream temp = context.connection.getExistingObject(jsonStream.get("guid").getAsString()); + try (OutputStream out = temp.stream()) { + Files.copy(localPaths.get(i), out); + } catch (IOException e) { + throw new PlaywrightException("Failed to copy file to remote server.", e); + } + jsonStreams.add(temp.toProtocolRef()); + } + return jsonStreams; + } + + private static JsonObject createTempFiles(BrowserContextImpl context, Path localDirectory, List localPaths) { + JsonObject tempFilesParams = new JsonObject(); + if (localDirectory != null) { + tempFilesParams.addProperty("rootDirName", localDirectory.getFileName().toString()); + } + JsonArray items = new JsonArray(); + for (Path path : localPaths) { + long lastModifiedMs; + try { + lastModifiedMs = Files.getLastModifiedTime(path).toMillis(); + } catch (IOException e) { + throw new PlaywrightException("Cannot read file timestamp: " + path, e); + } + Path name; + if (localDirectory == null) { + name = path.getFileName(); + } else { + name = localDirectory.relativize(path); + } + JsonObject item = new JsonObject(); + item.addProperty("name", name.toString()); + item.addProperty("lastModifiedMs", lastModifiedMs); + items.add(item); + } + tempFilesParams.add("items", items); + return context.sendMessage("createTempFiles", tempFilesParams).getAsJsonObject(); + } + static void checkFilePayloadSize(FilePayload[] files) { long totalSize = 0; for (FilePayload file: files) { diff --git a/playwright/src/main/java/com/microsoft/playwright/options/HttpCredentials.java b/playwright/src/main/java/com/microsoft/playwright/options/HttpCredentials.java index 5aea74a0..d74625d1 100644 --- a/playwright/src/main/java/com/microsoft/playwright/options/HttpCredentials.java +++ b/playwright/src/main/java/com/microsoft/playwright/options/HttpCredentials.java @@ -23,6 +23,13 @@ public class HttpCredentials { * Restrain sending http credentials on specific origin (scheme://host:port). */ public String origin; + /** + * This option only applies to the requests sent from corresponding {@code APIRequestContext} and does not affect requests + * sent from the browser. {@code "always"} - {@code Authorization} header with basic authentication credentials will be + * sent with the each API request. {@code 'unauthorized} - the credentials are only sent when 401 (Unauthorized) response + * with {@code WWW-Authenticate} header is received. Defaults to {@code "unauthorized"}. + */ + public HttpCredentialsSend send; public HttpCredentials(String username, String password) { this.username = username; @@ -35,4 +42,14 @@ public class HttpCredentials { this.origin = origin; return this; } + /** + * This option only applies to the requests sent from corresponding {@code APIRequestContext} and does not affect requests + * sent from the browser. {@code "always"} - {@code Authorization} header with basic authentication credentials will be + * sent with the each API request. {@code 'unauthorized} - the credentials are only sent when 401 (Unauthorized) response + * with {@code WWW-Authenticate} header is received. Defaults to {@code "unauthorized"}. + */ + public HttpCredentials setSend(HttpCredentialsSend send) { + this.send = send; + return this; + } } \ No newline at end of file diff --git a/playwright/src/main/java/com/microsoft/playwright/options/HttpCredentialsSend.java b/playwright/src/main/java/com/microsoft/playwright/options/HttpCredentialsSend.java new file mode 100644 index 00000000..9c8202ec --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/options/HttpCredentialsSend.java @@ -0,0 +1,22 @@ +/* + * 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.options; + +public enum HttpCredentialsSend { + UNAUTHORIZED, + ALWAYS +} \ No newline at end of file diff --git a/playwright/src/test/java/com/microsoft/playwright/TestBase.java b/playwright/src/test/java/com/microsoft/playwright/TestBase.java index 94f02a31..668b5832 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestBase.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestBase.java @@ -42,9 +42,11 @@ public class TestBase { Browser browser; static final boolean isMac = Utils.getOS() == Utils.OS.MAC; + static final boolean isLinux = Utils.getOS() == Utils.OS.LINUX; static final boolean isWindows = Utils.getOS() == Utils.OS.WINDOWS; static final boolean headful; static final SameSiteAttribute defaultSameSiteCookieValue; + static { String headfulEnv = System.getenv("HEADFUL"); headful = headfulEnv != null && !"0".equals(headfulEnv) && !"false".equals(headfulEnv); @@ -160,14 +162,28 @@ public class TestBase { void waitForCondition(BooleanSupplier predicate) { waitForCondition(predicate, 5_000); } + void waitForCondition(BooleanSupplier predicate, int timeoutMs) { page.waitForCondition(predicate, new Page.WaitForConditionOptions().setTimeout(timeoutMs)); } private static SameSiteAttribute initSameSiteAttribute() { if (isChromium()) return SameSiteAttribute.LAX; - if (isWebKit()) return SameSiteAttribute.NONE; + if (isWebKit() && isLinux) return SameSiteAttribute.LAX; + if (isWebKit() && !isLinux) return SameSiteAttribute.NONE; // for firefox version >= 103 'None' is used. return SameSiteAttribute.NONE; } + + static boolean chromiumVersionLessThan(String a, String b) { + String[] aParts = a.split("\\."); + String[] bParts = b.split("\\."); + for (int i = 0; i < 4; i++) { + int aPart = Integer.parseInt(aParts[i]); + int bPart = Integer.parseInt(bParts[i]); + if (aPart > bPart) return false; + if (aPart < bPart) return true; + } + return false; + } } diff --git a/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextAddCookies.java b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextAddCookies.java index e67cf3e1..1bc1fe15 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextAddCookies.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextAddCookies.java @@ -26,7 +26,7 @@ import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; -import static com.microsoft.playwright.TestOptionsFactories.isChromium; +import static com.microsoft.playwright.TestBase.defaultSameSiteCookieValue; import static com.microsoft.playwright.TestOptionsFactories.isFirefox; import static com.microsoft.playwright.Utils.assertJsonEquals; import static java.util.Arrays.asList; @@ -227,7 +227,7 @@ public class TestBrowserContextAddCookies { " expires: -1,\n" + " httpOnly: false,\n" + " secure: false,\n" + - " sameSite: '" + (isChromium() ? "LAX" : "NONE") +"'\n" + + " sameSite: '" + defaultSameSiteCookieValue +"'\n" + "}]", cookies); } @@ -246,7 +246,7 @@ public class TestBrowserContextAddCookies { " expires: -1,\n" + " httpOnly: false,\n" + " secure: false,\n" + - " sameSite: '" + (isChromium() ? "LAX" : "NONE") +"'\n" + + " sameSite: '" + defaultSameSiteCookieValue +"'\n" + "}]", cookies); assertEquals("gridcookie=GRID", page.evaluate("document.cookie")); page.navigate(server.EMPTY_PAGE); @@ -311,7 +311,7 @@ public class TestBrowserContextAddCookies { " expires: -1,\n" + " httpOnly: false,\n" + " secure: true,\n" + - " sameSite: '" + (isChromium() ? "LAX" : "NONE") +"'\n" + + " sameSite: '" + defaultSameSiteCookieValue +"'\n" + "}]", context.cookies("https://www.example.com")); } diff --git a/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextCookies.java b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextCookies.java index d9d97653..a61ee5d8 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextCookies.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextCookies.java @@ -48,7 +48,7 @@ public class TestBrowserContextCookies extends TestBase { " expires: -1,\n" + " httpOnly: false,\n" + " secure: false,\n" + - " sameSite: '" + (isChromium() ? "LAX" : "NONE") +"'\n" + + " sameSite: '" + defaultSameSiteCookieValue +"'\n" + " }]", cookies); } @@ -72,6 +72,7 @@ public class TestBrowserContextCookies extends TestBase { assertFalse(cookies.get(0).httpOnly); assertFalse(cookies.get(0).secure); assertEquals(defaultSameSiteCookieValue, cookies.get(0).sameSite); + assertEquals(defaultSameSiteCookieValue, cookies.get(0).sameSite); // Browsers start to cap cookies with 400 days max expires value. // See https://github.com/httpwg/http-extensions/pull/1732 @@ -147,7 +148,7 @@ public class TestBrowserContextCookies extends TestBase { " expires: -1,\n" + " httpOnly: false,\n" + " secure: false,\n" + - " sameSite: '" + (isChromium() ? "LAX" : "NONE") +"'\n" + + " sameSite: '" + defaultSameSiteCookieValue +"'\n" + " },\n" + " {\n" + " name: 'username',\n" + @@ -157,7 +158,7 @@ public class TestBrowserContextCookies extends TestBase { " expires: -1,\n" + " httpOnly: false,\n" + " secure: false,\n" + - " sameSite: '" + (isChromium() ? "LAX" : "NONE") +"'\n" + + " sameSite: '" + defaultSameSiteCookieValue +"'\n" + " }\n" + "]", cookies); } @@ -178,7 +179,7 @@ public class TestBrowserContextCookies extends TestBase { " expires: -1.0,\n" + " httpOnly: false,\n" + " secure: true,\n" + - " sameSite: '" + (isChromium() ? "LAX" : "NONE") +"'\n" + + " sameSite: '" + defaultSameSiteCookieValue +"'\n" + "}, {\n" + " name: 'doggo',\n" + " value: 'woofs',\n" + @@ -187,7 +188,7 @@ public class TestBrowserContextCookies extends TestBase { " expires: -1.0,\n" + " httpOnly: false,\n" + " secure: true,\n" + - " sameSite: '" + (isChromium() ? "LAX" : "NONE") +"'\n" + + " sameSite: '" + defaultSameSiteCookieValue +"'\n" + "}]", cookies); } @@ -205,14 +206,14 @@ public class TestBrowserContextCookies extends TestBase { page.navigate(server.EMPTY_PAGE); Object documentCookie = page.evaluate("document.cookie.split('; ').sort().join('; ')"); - if (isChromium()) { + if (isChromium() || (isLinux && isWebKit())) { assertEquals("one=uno; two=dos", documentCookie); } else { assertEquals("one=uno; three=tres; two=dos", documentCookie); } List list = context.cookies().stream().map(c -> c.sameSite).sorted().collect(Collectors.toList()); - if (isChromium()) { + if (isChromium() || (isLinux && isWebKit())) { assertEquals(asList(SameSiteAttribute.STRICT, SameSiteAttribute.LAX), list); } else { assertEquals(asList(SameSiteAttribute.STRICT, SameSiteAttribute.LAX, SameSiteAttribute.NONE), list); diff --git a/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextFetch.java b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextFetch.java index 694704a6..ed38aaf9 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextFetch.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextFetch.java @@ -805,4 +805,53 @@ public class TestBrowserContextFetch extends TestBase { assertEquals(200, response.status()); assertEquals("{\"foo\":null}", new String(req.get().postBody)); } + + @Test + void shouldSupportHTTPCredentialsSendImmediatelyForNewContext() throws ExecutionException, InterruptedException { + Browser.NewContextOptions options = new Browser.NewContextOptions().setHttpCredentials( + new HttpCredentials("user", "pass") + .setOrigin(server.PREFIX.toUpperCase()) + .setSend(HttpCredentialsSend.ALWAYS)); + try (BrowserContext context = browser.newContext(options)) { + Future serverRequest = server.futureRequest("/empty.html"); + APIResponse response = context.request().get(server.EMPTY_PAGE); + assertEquals("Basic " + java.util.Base64.getEncoder().encodeToString("user:pass".getBytes()), + serverRequest.get().headers.getFirst("authorization")); + assertEquals(200, response.status()); + + serverRequest = server.futureRequest("/empty.html"); + response = context.request().get(server.CROSS_PROCESS_PREFIX + "/empty.html"); + // Not sent to another origin. + assertNull(serverRequest.get().headers.get("authorization")); + assertEquals(200, response.status()); + } + } + + @Test + void shouldSupportHTTPCredentialsSendImmediatelyForBrowserNewPage() throws ExecutionException, InterruptedException { + Browser.NewPageOptions options = new Browser.NewPageOptions().setHttpCredentials( + new HttpCredentials("user", "pass") + .setOrigin(server.PREFIX.toUpperCase()) + .setSend(HttpCredentialsSend.ALWAYS)); + try (Page newPage = browser.newPage(options)) { + Future serverRequest = server.futureRequest("/empty.html"); + APIResponse response = newPage.request().get(server.EMPTY_PAGE); + assertEquals("Basic " + java.util.Base64.getEncoder().encodeToString("user:pass".getBytes()), + serverRequest.get().headers.getFirst("authorization")); + assertEquals(200, response.status()); + + serverRequest = server.futureRequest("/empty.html"); + response = newPage.request().get(server.CROSS_PROCESS_PREFIX + "/empty.html"); + // Not sent to another origin. + assertNull(serverRequest.get().headers.get("authorization")); + assertEquals(200, response.status()); + } + } + + @Test + void shouldNotWorkAfterContextDispose() { + context.close(new BrowserContext.CloseOptions().setReason("Test ended.")); + PlaywrightException e = assertThrows(PlaywrightException.class, () -> context.request().get(server.EMPTY_PAGE)); + assertTrue(e.getMessage().contains("Test ended."), e.getMessage()); + } } diff --git a/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextStorageState.java b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextStorageState.java index 2c897c42..44fcafd9 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextStorageState.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextStorageState.java @@ -98,6 +98,20 @@ public class TestBrowserContextStorageState extends TestBase { "}"); Path path = tempDir.resolve("storage-state.json"); context.storageState(new BrowserContext.StorageStateOptions().setPath(path)); + + String sameSiteCamelCase = "Lax"; + switch (defaultSameSiteCookieValue) { + case STRICT: + sameSiteCamelCase = "Strict"; + break; + case LAX: + sameSiteCamelCase = "Lax"; + break; + case NONE: + sameSiteCamelCase = "None"; + break; + } + JsonObject expected = new Gson().fromJson( "{\n" + " 'cookies':[\n" + @@ -109,7 +123,7 @@ public class TestBrowserContextStorageState extends TestBase { " 'expires':-1,\n" + " 'httpOnly':false,\n" + " 'secure':false,\n" + - " 'sameSite':'" + (isChromium() ? "Lax" : "None") + "'\n" + + " 'sameSite':'" + sameSiteCamelCase + "'\n" + " }],\n" + " 'origins':[\n" + " {\n" + diff --git a/playwright/src/test/java/com/microsoft/playwright/TestBrowserTypeConnect.java b/playwright/src/test/java/com/microsoft/playwright/TestBrowserTypeConnect.java index d5df75c3..618a3660 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestBrowserTypeConnect.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestBrowserTypeConnect.java @@ -506,7 +506,7 @@ public class TestBrowserTypeConnect extends TestBase { } @Test - void setInputFilesDhouldPreserveLastModifiedTimestamp() throws IOException { + void setInputFilesShouldPreserveLastModifiedTimestamp() throws IOException { page.setContent(""); Locator input = page.locator("input"); input.setInputFiles(FILE_TO_UPLOAD); diff --git a/playwright/src/test/java/com/microsoft/playwright/TestElementHandleConvenience.java b/playwright/src/test/java/com/microsoft/playwright/TestElementHandleConvenience.java index 84b1cb36..90322262 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestElementHandleConvenience.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestElementHandleConvenience.java @@ -43,9 +43,9 @@ public class TestElementHandleConvenience extends TestBase { String text = String.join("", Collections.nCopies(100, "😛")); page.setContent("

    " + text + "
    "); ElementHandle handle = page.querySelector("div"); - context.waitForCondition(() -> "JSHandle@
    😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛…
    " + context.waitForCondition(() -> "JSHandle@
    😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛…
    " .equals(handle.toString()), new BrowserContext.WaitForConditionOptions().setTimeout(5_000)); - assertEquals("JSHandle@
    😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛…
    ", handle.toString()); + assertEquals("JSHandle@
    😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛😛…
    ", handle.toString()); } @Test diff --git a/playwright/src/test/java/com/microsoft/playwright/TestGlobalFetch.java b/playwright/src/test/java/com/microsoft/playwright/TestGlobalFetch.java index c16ce333..9c7a83f6 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestGlobalFetch.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestGlobalFetch.java @@ -18,6 +18,7 @@ package com.microsoft.playwright; import com.google.gson.Gson; import com.microsoft.playwright.options.HttpCredentials; +import com.microsoft.playwright.options.HttpCredentialsSend; import com.microsoft.playwright.options.HttpHeader; import com.microsoft.playwright.options.RequestOptions; import org.junit.jupiter.api.Disabled; @@ -25,6 +26,7 @@ import org.junit.jupiter.api.Test; import java.io.OutputStreamWriter; import java.io.Writer; +import java.util.Base64; import java.util.List; import java.util.Optional; import java.util.concurrent.ExecutionException; @@ -490,4 +492,35 @@ public class TestGlobalFetch extends TestBase { assertEquals(401, response.status()); } + @Test + void shouldSupportHTTPCredentialsSendImmediately() throws InterruptedException, ExecutionException { + APIRequestContext request = playwright.request().newContext(new APIRequest.NewContextOptions() + .setHttpCredentials(new HttpCredentials("user", "pass") + .setOrigin(server.PREFIX.toUpperCase()) + .setSend(HttpCredentialsSend.ALWAYS))); + + Future serverRequestFuture = server.futureRequest("/empty.html"); + APIResponse response = request.get(server.EMPTY_PAGE); + assertEquals("Basic " + Base64.getEncoder().encodeToString("user:pass".getBytes()), + serverRequestFuture.get().headers.getFirst("authorization")); + assertEquals(200, response.status()); + + // Second request and response to another origin + serverRequestFuture = server.futureRequest("/empty.html"); + response = request.get(server.CROSS_PROCESS_PREFIX + "/empty.html"); + + // Not sent to another origin + assertNull(serverRequestFuture.get().headers.get("authorization")); + assertEquals(200, response.status()); + + request.dispose(); + } + + @Test + void shouldSupportDisposeReason() { + APIRequestContext request = playwright.request().newContext(); + request.dispose(new APIRequestContext.DisposeOptions().setReason("My reason")); + PlaywrightException e = assertThrows(PlaywrightException.class, () -> request.get(server.EMPTY_PAGE)); + assertTrue(e.getMessage().contains("My reason"), e.getMessage()); + } } diff --git a/playwright/src/test/java/com/microsoft/playwright/TestNetworkRequest.java b/playwright/src/test/java/com/microsoft/playwright/TestNetworkRequest.java index f79c3db7..bedb7610 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestNetworkRequest.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestNetworkRequest.java @@ -16,14 +16,12 @@ package com.microsoft.playwright; +import com.microsoft.playwright.options.HttpHeader; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledIf; import java.io.OutputStreamWriter; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.stream.Collectors; @@ -113,15 +111,50 @@ public class TestNetworkRequest extends TestBase { assertTrue(response.request().headers().get("user-agent").contains("WebKit")); } - - static boolean isWebKitWindowsOrChromium() { - return (isWebKit() && getOS() == Utils.OS.WINDOWS) || isChromium(); - } - static boolean isWebKitWindows() { return isWebKit() && getOS() == Utils.OS.WINDOWS; } + // HTTP server in java does not preserve headers order. + static List normalizeServerHeaders(Map> headers) { + List serverHeaders = new ArrayList<>(); + for (Map.Entry> entry : headers.entrySet()) { + for (String value: entry.getValue()) { + HttpHeader header = new HttpHeader(); + header.name = entry.getKey().toLowerCase(); + header.value = value; + serverHeaders.add(header); + } + } + Comparator comparator = Comparator.comparing(h -> h.name); + serverHeaders.sort(comparator); + return serverHeaders; + } + + static List normalizeAllHeaders(Map headers) { + List serverHeaders = new ArrayList<>(); + for (Map.Entry entry : headers.entrySet()) { + HttpHeader header = new HttpHeader(); + header.name = entry.getKey().toLowerCase(); + header.value = entry.getValue(); + serverHeaders.add(header); + } + Comparator comparator = Comparator.comparing(h -> h.name); + serverHeaders.sort(comparator); + return serverHeaders; + } + + static List adjustServerHeaders(List headers) { + if (isFirefox()) { + for (Iterator it = headers.iterator(); it.hasNext(); ) { + if ("priority".equals(it.next().name)) { + it.remove(); + } + } + } + return headers; + } + @Test @DisabledIf(value="isWebKitWindows", disabledReason="Flaky, see https://github.com/microsoft/playwright/issues/6690") void shouldGetTheSameHeadersAsTheServer() throws ExecutionException, InterruptedException { @@ -133,9 +166,10 @@ public class TestNetworkRequest extends TestBase { } }); Response response = page.navigate(server.PREFIX + "/empty.html"); - Map expectedHeaders = serverRequest.get().headers.entrySet().stream().collect( - Collectors.toMap(e -> e.getKey().toLowerCase(), e -> e.getValue().get(0))); - assertEquals(expectedHeaders, response.request().allHeaders()); + + List expectedHeaders = adjustServerHeaders(normalizeServerHeaders(serverRequest.get().headers)); + List allHeaders = normalizeAllHeaders(response.request().allHeaders()); + assertJsonEquals(expectedHeaders, allHeaders); } @Test @@ -157,9 +191,9 @@ public class TestNetworkRequest extends TestBase { "}", server.CROSS_PROCESS_PREFIX + "/something"); assertEquals("done", text); }); - Map expectedHeaders = serverRequest.get().headers.entrySet().stream().collect( - Collectors.toMap(e -> e.getKey().toLowerCase(), e -> e.getValue().get(0))); - assertEquals(expectedHeaders, response.request().allHeaders()); + List expectedHeaders = adjustServerHeaders(normalizeServerHeaders(serverRequest.get().headers)); + List allHeaders = normalizeAllHeaders(response.request().allHeaders()); + assertJsonEquals(expectedHeaders, allHeaders); } @Test diff --git a/playwright/src/test/java/com/microsoft/playwright/TestPageClock.java b/playwright/src/test/java/com/microsoft/playwright/TestPageClock.java new file mode 100644 index 00000000..167fcefe --- /dev/null +++ b/playwright/src/test/java/com/microsoft/playwright/TestPageClock.java @@ -0,0 +1,458 @@ +package com.microsoft.playwright; + +import com.microsoft.playwright.junit.FixtureTest; +import com.microsoft.playwright.junit.UsePlaywright; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; + +import static com.microsoft.playwright.Utils.assertJsonEquals; +import static org.junit.jupiter.api.Assertions.*; + +@FixtureTest +@UsePlaywright +public class TestPageClock { + private ArrayList calls; + + @BeforeEach + void exposeStubFunction(Page page) { + calls = new ArrayList(); + page.exposeFunction("stub", (Object... params) -> { + calls.add(params); + return null; + }); + } + + private void setupRunForTest(Page page) { + page.clock().install(new Clock.InstallOptions().setTime(0)); + page.clock().pauseAt(1000); + } + + @Test + void runForTriggersImmediatelyWithoutSpecifiedDelay(Page page) { + setupRunForTest(page); + page.evaluate("() => setTimeout(window.stub)"); + page.clock().runFor(0); + assertEquals(1, calls.size()); + } + + @Test + void runForDoesNotTriggerWithoutSufficientDelay(Page page) { + setupRunForTest(page); + // Trigger the stub with a delay + page.evaluate("() => setTimeout(window.stub, 100)"); + page.clock().runFor(11); + assertEquals(0, calls.size()); + } + + @Test + void runForTriggersAfterSufficientDelay(Page page) { + setupRunForTest(page); + page.evaluate("() => setTimeout(window.stub, 100)"); + page.clock().runFor(100); + assertEquals(1, calls.size()); + } + + @Test + void runForTriggersSimultaneousTimers(Page page) { + setupRunForTest(page); + page.evaluate("() => { setTimeout(window.stub, 100);" + + "setTimeout(window.stub, 100); }"); + page.clock().runFor(100); + assertEquals(2, calls.size()); + } + + @Test + void runForTriggersMultipleSimultaneousTimers(Page page) { + setupRunForTest(page); + page.evaluate("() => { setTimeout(window.stub, 100);" + + "setTimeout(window.stub, 100);" + + "setTimeout(window.stub, 99);" + + "setTimeout(window.stub, 100); }"); + page.clock().runFor(100); + assertEquals(4, calls.size()); + } + + @Test + void runForWaitsAfterSetTimeoutWasCalled(Page page) { + setupRunForTest(page); + page.evaluate("() => setTimeout(window.stub, 150)"); + page.clock().runFor(50); + assertEquals(0, calls.size()); + page.clock().runFor(100); + assertEquals(1, calls.size()); + } + + @Test + void runForTriggersEventWhenSomeThrow(Page page) { + setupRunForTest(page); + page.evaluate("() => {\n" + + " setTimeout(() => {throw new Error(); }, 100);\n" + + " setTimeout(window.stub, 120);\n" + + "}"); + assertThrows(PlaywrightException.class, () -> page.clock().runFor(120)); + assertEquals(1, calls.size()); + } + + @Test + void runForCreatesUpdatedDateWhileTicking(Page page) { + setupRunForTest(page); + page.clock().setSystemTime(0); + page.evaluate("() => setInterval(() => { window.stub(new Date().getTime()); }, 10)"); + page.clock().runFor(100); + assertJsonEquals("[[10],[20],[30],[40],[50],[60],[70],[80],[90],[100]]", calls); + } + + @Test + void runForPasses8Seconds(Page page) { + setupRunForTest(page); + page.evaluate("() => setInterval(window.stub, 4000)"); + page.clock().runFor("08"); + assertEquals(2, calls.size()); + } + + @Test + void runForPasses1Minute(Page page) { + setupRunForTest(page); + page.evaluate("() => setInterval(window.stub, 6000)"); + page.clock().runFor("01:00"); + assertEquals(10, calls.size()); + } + + @Test + void runForPasses2Hours34MinutesAnd10Seconds(Page page) { + setupRunForTest(page); + page.evaluate("() => setInterval(window.stub, 10000)"); + page.clock().runFor("02:34:10"); + assertEquals(925, calls.size()); + } + + @Test + void runForThrowsForInvalidFormat(Page page) { + setupRunForTest(page); + page.evaluate("() => setInterval(window.stub, 10000)"); + assertThrows(PlaywrightException.class, () -> page.clock().runFor("12:02:34:10")); + assertEquals(0, calls.size()); + } + + @Test + void runForReturnsTheCurrentNowValue(Page page) { + setupRunForTest(page); + page.clock().setSystemTime(0); + final int value = 200; + page.clock().runFor(value); + assertEquals(value, page.evaluate("() => Date.now()")); + } + + private void setupFastForwardTest(Page page) { + page.clock().install(new Clock.InstallOptions().setTime(0)); + page.clock().pauseAt(1000); + } + + @Test + void fastForwardIgnoresTimersWhichWouldntBeRun(Page page) { + setupFastForwardTest(page); + page.evaluate("() => setTimeout(() => { window.stub('should not be logged'); }, 1000)"); + page.clock().fastForward(500); + assertEquals(0, calls.size()); + } + + @Test + void fastForwardPushesBackExecutionTimeForSkippedTimers(Page page) { + setupFastForwardTest(page); + page.evaluate("() => setTimeout(() => { window.stub(Date.now()); }, 1000)"); + page.clock().fastForward(2000); + assertEquals(1, calls.size()); + assertEquals(1000 + 2000, ((Object[])calls.get(0))[0]); + } + + @Test + void fastForwardSupportsStringTimeArguments(Page page) { + setupFastForwardTest(page); + page.evaluate("() => setTimeout(() => { window.stub(Date.now()); }, 100000)"); + page.clock().fastForward("01:50"); + assertEquals(1, calls.size()); + assertEquals(1000 + 110000, ((Object[])calls.get(0))[0]); + } + + private void setupStubTimers(Page page) { + page.clock().install(new Clock.InstallOptions().setTime(0)); + page.clock().pauseAt(1000); + } + + @Test + void setsInitialTimestamp(Page page) { + setupStubTimers(page); + page.clock().setSystemTime(1400); + assertEquals(1400, page.evaluate("() => Date.now()")); + } + + @Test + void replacesGlobalSetTimeout(Page page) { + setupStubTimers(page); + page.evaluate("() => setTimeout(window.stub, 1000)"); + page.clock().runFor(1000); + assertEquals(1, calls.size()); + } + + @Test + void globalFakeSetTimeoutShouldReturnId(Page page) { + setupStubTimers(page); + Double to = (Double) page.evaluate("() => setTimeout(window.stub, 1000)"); + assertTrue(to > 0); + } + + @Test + void replacesGlobalClearTimeout(Page page) { + setupStubTimers(page); + page.evaluate("() => { const to = setTimeout(window.stub, 1000); clearTimeout(to); }"); + page.clock().runFor(1000); + assertEquals(0, calls.size()); + } + + @Test + void replacesGlobalSetInterval(Page page) { + setupStubTimers(page); + page.evaluate("() => setInterval(window.stub, 500)"); + page.clock().runFor(1000); + assertEquals(2, calls.size()); + } + + @Test + void replacesGlobalClearInterval(Page page) { + setupStubTimers(page); + page.evaluate("() => { const to = setInterval(window.stub, 500); clearInterval(to); }"); + page.clock().runFor(1000); + assertEquals(0, calls.size()); + } + + @Test + void replacesGlobalPerformanceNow(Page page) { + } + + @Test + void fakesDateConstructor(Page page) { + setupStubTimers(page); + Integer now = (Integer) page.evaluate("() => new Date().getTime()"); + assertEquals(1000, now); + } + + @Test + void replacesGlobalPerformanceTimeOrigin(Page page) { + } + + + @Test + void shouldTickAfterPopup(Page page) throws ParseException { + page.clock().install(new Clock.InstallOptions().setTime(0)); + Date now = new SimpleDateFormat("yyyy-MM-dd").parse("2015-09-25"); + page.clock().pauseAt(now.getTime()); + Page popup = page.waitForPopup(() -> { + page.evaluate("() => window.open('about:blank')"); + }); + Double popupTime = (Double) popup.evaluate("() => Date.now()"); + assertEquals(now.getTime(), popupTime); + page.clock().runFor(1000); + Double popupTimeAfter = (Double) popup.evaluate("() => Date.now()"); + assertEquals(now.getTime() + 1000, popupTimeAfter); + } + + @Test + void shouldTickBeforePopup(Page page) throws ParseException { + page.clock().install(new Clock.InstallOptions().setTime(0)); + Date now = new SimpleDateFormat("yyyy-MM-dd").parse("2015-09-25"); + page.clock().pauseAt(now.getTime()); + page.clock().runFor(1000); + Page popup = page.waitForPopup(() -> { + page.evaluate("() => window.open('about:blank')"); + }); + Double popupTime = (Double) popup.evaluate("() => Date.now()"); + assertEquals(now.getTime() + 1000, popupTime); + } + + @Test + void shouldRunTimeBeforePopup(Page page, Server server) { + server.setRoute("/popup.html", exchange -> { + exchange.getResponseHeaders().set("Content-type", "text/html"); + exchange.sendResponseHeaders(200, 0); + try (Writer writer = new OutputStreamWriter(exchange.getResponseBody())) { + writer.write(""); + } + }); + page.navigate(server.EMPTY_PAGE); + page.waitForTimeout(2000); + Page popup = page.waitForPopup(() -> { + page.evaluate("url => window.open(url)", server.PREFIX + "/popup.html"); + }); + Double popupTime = (Double) popup.evaluate("time"); + assertTrue(popupTime >= 2000); + } + + @Test + void shouldNotRunTimeBeforePopupOnPause(Page page, Server server) { + server.setRoute("/popup.html", exchange -> { + exchange.getResponseHeaders().set("Content-type", "text/html"); + exchange.sendResponseHeaders(200, 0); + try (Writer writer = new OutputStreamWriter(exchange.getResponseBody())) { + writer.write(""); + } + }); + page.clock().install(new Clock.InstallOptions().setTime(0)); + page.clock().pauseAt(1000); + page.navigate(server.EMPTY_PAGE); + page.waitForTimeout(2000); + Page popup = page.waitForPopup(() -> { + page.evaluate("url => window.open(url)", server.PREFIX + "/popup.html"); + }); + Object popupTime = popup.evaluate("time"); + assertEquals(1000, popupTime); + } + + @Test + void setFixedTimeDoesNotFakeMethods(Page page) { + page.clock().setFixedTime(0); + // Should not stall. + page.evaluate("() => new Promise(f => setTimeout(f, 1))"); + } + + @Test + void setFixedTimeAllowsSettingTimeMultipleTimes(Page page) { + page.clock().setFixedTime(100); + assertEquals(100, page.evaluate("() => Date.now()")); + page.clock().setFixedTime(200); + assertEquals(200, page.evaluate("() => Date.now()")); + } + + @Test + void setFixedTimeFixedTimeIsNotAffectedByClockManipulation(Page page) { + page.clock().setFixedTime(100); + assertEquals(100, page.evaluate("() => Date.now()")); + page.clock().fastForward(20); + assertEquals(100, page.evaluate("() => Date.now()")); + } + + @Test + void setFixedTimeAllowsInstallingFakeTimersAfterSettingTime(Page page) { + page.clock().setFixedTime(100); + assertEquals(100, page.evaluate("() => Date.now()")); + page.clock().setFixedTime(200); + page.evaluate("async () => { setTimeout(() => window.stub(Date.now()), 0); }"); + page.clock().runFor(0); + assertEquals(1, calls.size()); + assertEquals(200, ((Object[]) calls.get(0))[0]); + } + + @Test + void whileRunningShouldProgressTime(Page page) { + page.clock().install(new Clock.InstallOptions().setTime(0)); + page.navigate("data:text/html,"); + page.waitForTimeout(1000); + int now = (int) page.evaluate("() => Date.now()"); + assertTrue(now >= 1000 && now <= 2000); + } + + @Test + void whileRunningShouldRunFor(Page page) { + page.clock().install(new Clock.InstallOptions().setTime(0)); + page.navigate("data:text/html,"); + page.clock().runFor(10000); + int now = (int) page.evaluate("() => Date.now()"); + assertTrue(now >= 10000 && now <= 11000); + } + + @Test + void whileRunningShouldFastForward(Page page) { + page.clock().install(new Clock.InstallOptions().setTime(0)); + page.navigate("data:text/html,"); + page.clock().fastForward(10000); + int now = (int) page.evaluate("() => Date.now()"); + assertTrue(now >= 10000 && now <= 11000); + } + + @Test + void whileRunningShouldFastForwardTo(Page page) { + page.clock().install(new Clock.InstallOptions().setTime(0)); + page.navigate("data:text/html,"); + page.clock().fastForward(10000); + int now = (int) page.evaluate("() => Date.now()"); + assertTrue(now >= 10000 && now <= 11000); + } + + @Test + void whileRunningShouldPause(Page page) { + page.clock().install(new Clock.InstallOptions().setTime(0)); + page.navigate("data:text/html,"); + page.clock().pauseAt(1000); + page.waitForTimeout(1000); + page.clock().resume(); + int now = (int) page.evaluate("() => Date.now()"); + assertTrue(now >= 0 && now <= 1000); + } + + @Test + void whileRunningShouldPauseAndFastForward(Page page) { + page.clock().install(new Clock.InstallOptions().setTime(0)); + page.navigate("data:text/html,"); + page.clock().pauseAt(1000); + page.clock().fastForward(1000); + int now = (int) page.evaluate("() => Date.now()"); + assertEquals(2000, now); + } + + @Test + void whileRunningShouldSetSystemTimeOnPause(Page page) { + page.clock().install(new Clock.InstallOptions().setTime(0)); + page.navigate("data:text/html,"); + page.clock().pauseAt(1000); + int now = (int) page.evaluate("() => Date.now()"); + assertEquals(1000, now); + } + + @Test + void whileOnPauseFastForwardShouldNotRunNestedImmediate(Page page) { + page.clock().install(new Clock.InstallOptions().setTime(0)); + page.navigate("data:text/html,"); + page.clock().pauseAt(1000); + page.evaluate("() => { setTimeout(() => { window.stub('outer'); setTimeout(() => window.stub('inner'), 0); }, 1000); }"); + page.clock().fastForward(1000); + assertEquals(1, calls.size()); + assertEquals("outer", ((Object[]) calls.get(0))[0]); + page.clock().fastForward(1); + assertEquals(2, calls.size()); + assertEquals("inner", ((Object[]) calls.get(1))[0]); + } + + @Test + void whileOnPauseRunForShouldNotRunNestedImmediate(Page page) { + page.clock().install(new Clock.InstallOptions().setTime(0)); + page.navigate("data:text/html,"); + page.clock().pauseAt(1000); + page.evaluate("() => { setTimeout(() => { window.stub('outer'); setTimeout(() => window.stub('inner'), 0); }, 1000); }"); + page.clock().runFor(1000); + assertEquals(1, calls.size()); + assertEquals("outer", ((Object[]) calls.get(0))[0]); + page.clock().runFor(1); + assertEquals(2, calls.size()); + assertEquals("inner", ((Object[]) calls.get(1))[0]); + } + + @Test + void whileOnPauseRunForShouldNotRunNestedImmediateFromMicrotask(Page page) { + page.clock().install(new Clock.InstallOptions().setTime(0)); + page.navigate("data:text/html,"); + page.clock().pauseAt(1000); + page.evaluate("() => { setTimeout(() => { window.stub('outer'); void Promise.resolve().then(() => setTimeout(() => window.stub('inner'), 0)); }, 1000); }"); + page.clock().runFor(1000); + assertEquals(1, calls.size()); + assertEquals("outer", ((Object[]) calls.get(0))[0]); + page.clock().runFor(1); + assertEquals(2, calls.size()); + assertEquals("inner", ((Object[]) calls.get(1))[0]); + } +} diff --git a/playwright/src/test/java/com/microsoft/playwright/TestPageNetworkRequest.java b/playwright/src/test/java/com/microsoft/playwright/TestPageNetworkRequest.java index a8c611f7..bef24972 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestPageNetworkRequest.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestPageNetworkRequest.java @@ -62,12 +62,18 @@ public class TestPageNetworkRequest extends TestBase { }); responseWritten.acquire(); + List expectedHeaders = serverHeaders; if (isWebKit() && isWindows) { expectedHeaders = expectedHeaders.stream() .filter(h -> !"accept-encoding".equals(h.name.toLowerCase()) && !"accept-language".equals(h.name.toLowerCase())) .collect(Collectors.toList()); } + if (isFirefox()) { + expectedHeaders = expectedHeaders.stream() + .filter(h -> !"priority".equals(h.name.toLowerCase())) + .collect(Collectors.toList()); + } List headers = request.headersArray(); // Java HTTP server normalizes header names, work around that: diff --git a/playwright/src/test/java/com/microsoft/playwright/TestPageSetInputFiles.java b/playwright/src/test/java/com/microsoft/playwright/TestPageSetInputFiles.java index f2cfbed2..a96de9ab 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestPageSetInputFiles.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestPageSetInputFiles.java @@ -21,7 +21,6 @@ import com.microsoft.playwright.options.FilePayload; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; -import org.junit.platform.commons.support.AnnotationSupport; import java.io.*; import java.nio.file.Files; @@ -429,5 +428,85 @@ public class TestPageSetInputFiles extends TestBase { // rounds it to seconds in WebKit: 1696272058110 -> 1696272058000. assertTrue(Math.abs(timestamps.get(0) - expected.toMillis()) < 1000, "expected: " + expected.toMillis() + "; actual: " + timestamps.get(0)); } + + static void writeFile(Path file, String content) throws IOException { + try (Writer stream = new OutputStreamWriter(Files.newOutputStream(file))) { + stream.write(content); + } + } + + @Test + void shouldUploadAFolder(@TempDir Path tmpDir) throws IOException { + page.navigate(server.PREFIX + "/input/folderupload.html"); + Locator input = page.locator("input[name=\"file1\"]"); + Path dir = tmpDir.resolve("file-upload-test"); + Files.createDirectories(dir); + writeFile(dir.resolve("file1.txt"), "file1 content"); + writeFile(dir.resolve("file2"), "file2 content"); + Files.createDirectories(dir.resolve("sub-dir")); + writeFile(dir.resolve("sub-dir").resolve("really.txt"), "sub-dir file content"); + input.setInputFiles(dir); + List webkitRelativePaths = (List) input.evaluate("e => [...e.files].map(f => f.webkitRelativePath)"); + + List relativePathsSorted = new ArrayList<>(webkitRelativePaths); + relativePathsSorted.sort(String::compareTo); + // https://issues.chromium.org/issues/345393164 + if (isChromium() && !isHeadful() && chromiumVersionLessThan(browser.version(), "127.0.6533.0")) { + assertEquals(asList("file-upload-test/file1.txt", "file-upload-test/file2"), relativePathsSorted); + } else { + assertEquals(asList("file-upload-test/file1.txt", "file-upload-test/file2", "file-upload-test/sub-dir/really.txt"), relativePathsSorted); + } + + for (int i = 0; i < webkitRelativePaths.size(); i++) { + String relativePath = webkitRelativePaths.get(i); + Object content = input.evaluate("(e, i) => {\n" + + " const reader = new FileReader();\n" + + " const promise = new Promise(fulfill => reader.onload = fulfill);\n" + + " reader.readAsText(e.files[i]);\n" + + " return promise.then(() => reader.result);\n" + + " }", i); + String expectedContent = new String(Files.readAllBytes(tmpDir.resolve(relativePath))); + assertEquals(expectedContent, content); + } + } + + @Test + void shouldUploadAFolderAndThrowForMultipleDirectories() throws IOException { + page.navigate(server.PREFIX + "/input/folderupload.html"); + Locator input = page.locator("input[name=\"file1\"]"); + Path dir = Paths.get("file-upload-test"); // Adjust path as necessary + Files.createDirectories(dir.resolve("folder1")); + writeFile(dir.resolve("folder1").resolve("file1.txt"), "file1 content"); + Files.createDirectories(dir.resolve("folder2")); + writeFile(dir.resolve("folder2").resolve("file2.txt"), "file2 content"); + PlaywrightException e = assertThrows(PlaywrightException.class, + () -> input.setInputFiles(new Path[]{dir.resolve("folder1"), dir.resolve("folder2")})); + assertTrue(e.getMessage().contains("Multiple directories are not supported"), e.getMessage()); + } + + @Test + void shouldThrowIfADirectoryAndFilesArePassed() throws IOException { + // Skipping conditions based on environment not directly translatable to Java; needs custom implementation + page.navigate(server.PREFIX + "/input/folderupload.html"); + Locator input = page.locator("input[name=\"file1\"]"); + Path dir = Paths.get("file-upload-test"); // Adjust path as necessary + Files.createDirectories(dir.resolve("folder1")); + writeFile(dir.resolve("folder1").resolve("file1.txt"), "file1 content"); + PlaywrightException e = assertThrows(PlaywrightException.class, + () -> input.setInputFiles(new Path[]{ dir.resolve("folder1"), dir.resolve("folder1").resolve("file1.txt") })); + assertTrue(e.getMessage().contains("File paths must be all files or a single directory"), e.getMessage()); + } + + @Test + void shouldThrowWhenUploadingAFolderInANormalFileUploadInput() throws IOException { + page.navigate(server.PREFIX + "/input/fileupload.html"); + Locator input = page.locator("input[name=\"file1\"]"); + Path dir = Paths.get("file-upload-test"); // Adjust path as necessary + Files.createDirectories(dir); + writeFile(dir.resolve("file1.txt"), "file1 content"); + PlaywrightException e = assertThrows(PlaywrightException.class, + () -> input.setInputFiles(dir)); + assertTrue(e.getMessage().contains("File input does not support directories, pass individual files instead"), e.getMessage()); + } } diff --git a/playwright/src/test/resources/input/folderupload.html b/playwright/src/test/resources/input/folderupload.html new file mode 100644 index 00000000..16c7e2c3 --- /dev/null +++ b/playwright/src/test/resources/input/folderupload.html @@ -0,0 +1,12 @@ + + + + Folder upload test + + +
    + + +
    + + \ No newline at end of file diff --git a/scripts/CLI_VERSION b/scripts/CLI_VERSION index 372cf402..a0fdd66f 100644 --- a/scripts/CLI_VERSION +++ b/scripts/CLI_VERSION @@ -1 +1 @@ -1.44.0 +1.45.0-beta-1718733727000 diff --git a/tools/api-generator/src/main/java/com/microsoft/playwright/tools/ApiGenerator.java b/tools/api-generator/src/main/java/com/microsoft/playwright/tools/ApiGenerator.java index 23bb8258..e08b8b5f 100644 --- a/tools/api-generator/src/main/java/com/microsoft/playwright/tools/ApiGenerator.java +++ b/tools/api-generator/src/main/java/com/microsoft/playwright/tools/ApiGenerator.java @@ -978,6 +978,9 @@ class Interface extends TypeDefinition { if (asList("Page", "Frame", "ElementHandle", "Locator", "FormData", "APIRequest", "APIRequestContext", "FileChooser", "Browser", "BrowserContext", "BrowserType", "Download", "Route", "Selectors", "Tracing", "Video").contains(jsonName)) { output.add("import java.nio.file.Path;"); } + if ("Clock".equals(jsonName)) { + output.add("import java.util.Date;"); + } if (asList("Page", "Frame", "ElementHandle", "Locator", "APIRequest", "Browser", "BrowserContext", "BrowserType", "Route", "Request", "Response", "JSHandle", "ConsoleMessage", "APIResponse", "Playwright").contains(jsonName)) { output.add("import java.util.*;"); }