From 9f8ff0e7a16284d4ce20d0960b203797809b11fb Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Mon, 15 Nov 2021 17:30:09 -0800 Subject: [PATCH] feat: APIRequest & co (#704) --- .../com/microsoft/playwright/APIRequest.java | 178 ++++ .../playwright/APIRequestContext.java | 950 ++++++++++++++++++ .../com/microsoft/playwright/APIResponse.java | 65 ++ .../microsoft/playwright/BrowserContext.java | 4 + .../microsoft/playwright/ElementHandle.java | 6 +- .../java/com/microsoft/playwright/Frame.java | 31 +- .../java/com/microsoft/playwright/Page.java | 57 +- .../com/microsoft/playwright/Playwright.java | 4 + .../com/microsoft/playwright/Selectors.java | 16 +- .../impl/APIRequestContextImpl.java | 207 ++++ .../playwright/impl/APIRequestImpl.java | 53 + .../playwright/impl/APIResponseImpl.java | 114 +++ .../playwright/impl/BrowserContextImpl.java | 7 + .../microsoft/playwright/impl/Connection.java | 2 +- .../microsoft/playwright/impl/PageImpl.java | 9 +- .../playwright/impl/PlaywrightImpl.java | 8 + .../playwright/impl/Serialization.java | 22 +- .../com/microsoft/playwright/impl/Utils.java | 51 +- .../java/com/microsoft/playwright/Server.java | 2 + .../TestBrowserContextAddCookies.java | 1 - .../playwright/TestBrowserContextFetch.java | 689 +++++++++++++ .../microsoft/playwright/TestGlobalFetch.java | 304 ++++++ scripts/CLI_VERSION | 2 +- .../playwright/tools/ApiGenerator.java | 6 +- 24 files changed, 2739 insertions(+), 49 deletions(-) create mode 100644 playwright/src/main/java/com/microsoft/playwright/APIRequest.java create mode 100644 playwright/src/main/java/com/microsoft/playwright/APIRequestContext.java create mode 100644 playwright/src/main/java/com/microsoft/playwright/APIResponse.java create mode 100644 playwright/src/main/java/com/microsoft/playwright/impl/APIRequestContextImpl.java create mode 100644 playwright/src/main/java/com/microsoft/playwright/impl/APIRequestImpl.java create mode 100644 playwright/src/main/java/com/microsoft/playwright/impl/APIResponseImpl.java create mode 100644 playwright/src/test/java/com/microsoft/playwright/TestBrowserContextFetch.java create mode 100644 playwright/src/test/java/com/microsoft/playwright/TestGlobalFetch.java diff --git a/playwright/src/main/java/com/microsoft/playwright/APIRequest.java b/playwright/src/main/java/com/microsoft/playwright/APIRequest.java new file mode 100644 index 00000000..2f0e31ae --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/APIRequest.java @@ -0,0 +1,178 @@ +/* + * 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 com.microsoft.playwright.options.*; +import java.nio.file.Path; +import java.util.*; + +/** + * Exposes API that can be used for the Web API testing. + */ +public interface APIRequest { + class NewContextOptions { + /** + * Methods like {@link APIRequestContext#get APIRequestContext.get()} take the base URL into consideration by using the {@code URL()} constructor for building the corresponding + * URL. Examples: + * + */ + public String baseURL; + /** + * An object containing additional HTTP headers to be sent with every request. + */ + public Map extraHTTPHeaders; + /** + * Credentials for HTTP authentication. + */ + public HttpCredentials httpCredentials; + /** + * Whether to ignore HTTPS errors when sending network requests. Defaults to {@code false}. + */ + public Boolean ignoreHTTPSErrors; + /** + * Network proxy settings. + */ + public Proxy proxy; + /** + * Populates context with given storage state. This option can be used to initialize context with logged-in information + * obtained via {@link BrowserContext#storageState BrowserContext.storageState()} or {@link APIRequestContext#storageState + * APIRequestContext.storageState()}. Either a path to the file with saved storage, or the value returned by one of {@link + * BrowserContext#storageState BrowserContext.storageState()} or {@link APIRequestContext#storageState + * APIRequestContext.storageState()} methods. + */ + public String storageState; + /** + * Populates context with given storage state. This option can be used to initialize context with logged-in information + * obtained via {@link BrowserContext#storageState BrowserContext.storageState()}. Path to the file with saved storage + * state. + */ + public Path storageStatePath; + /** + * Maximum time in milliseconds to wait for the response. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. + */ + public Double timeout; + /** + * Specific user agent to use in this context. + */ + public String userAgent; + + /** + * Methods like {@link APIRequestContext#get APIRequestContext.get()} take the base URL into consideration by using the {@code URL()} constructor for building the corresponding + * URL. Examples: + * + */ + public NewContextOptions setBaseURL(String baseURL) { + this.baseURL = baseURL; + return this; + } + /** + * An object containing additional HTTP headers to be sent with every request. + */ + public NewContextOptions setExtraHTTPHeaders(Map extraHTTPHeaders) { + this.extraHTTPHeaders = extraHTTPHeaders; + return this; + } + /** + * Credentials for HTTP authentication. + */ + public NewContextOptions setHttpCredentials(String username, String password) { + return setHttpCredentials(new HttpCredentials(username, password)); + } + /** + * Credentials for HTTP authentication. + */ + public NewContextOptions setHttpCredentials(HttpCredentials httpCredentials) { + this.httpCredentials = httpCredentials; + return this; + } + /** + * Whether to ignore HTTPS errors when sending network requests. Defaults to {@code false}. + */ + public NewContextOptions setIgnoreHTTPSErrors(boolean ignoreHTTPSErrors) { + this.ignoreHTTPSErrors = ignoreHTTPSErrors; + return this; + } + /** + * Network proxy settings. + */ + public NewContextOptions setProxy(String server) { + return setProxy(new Proxy(server)); + } + /** + * Network proxy settings. + */ + public NewContextOptions setProxy(Proxy proxy) { + this.proxy = proxy; + return this; + } + /** + * Populates context with given storage state. This option can be used to initialize context with logged-in information + * obtained via {@link BrowserContext#storageState BrowserContext.storageState()} or {@link APIRequestContext#storageState + * APIRequestContext.storageState()}. Either a path to the file with saved storage, or the value returned by one of {@link + * BrowserContext#storageState BrowserContext.storageState()} or {@link APIRequestContext#storageState + * APIRequestContext.storageState()} methods. + */ + public NewContextOptions setStorageState(String storageState) { + this.storageState = storageState; + return this; + } + /** + * Populates context with given storage state. This option can be used to initialize context with logged-in information + * obtained via {@link BrowserContext#storageState BrowserContext.storageState()}. Path to the file with saved storage + * state. + */ + public NewContextOptions setStorageStatePath(Path storageStatePath) { + this.storageStatePath = storageStatePath; + return this; + } + /** + * Maximum time in milliseconds to wait for the response. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. + */ + public NewContextOptions setTimeout(double timeout) { + this.timeout = timeout; + return this; + } + /** + * Specific user agent to use in this context. + */ + public NewContextOptions setUserAgent(String userAgent) { + this.userAgent = userAgent; + return this; + } + } + /** + * Creates new instances of {@code APIRequestContext}. + */ + default APIRequestContext newContext() { + return newContext(null); + } + /** + * Creates new instances of {@code APIRequestContext}. + */ + APIRequestContext newContext(NewContextOptions options); +} + diff --git a/playwright/src/main/java/com/microsoft/playwright/APIRequestContext.java b/playwright/src/main/java/com/microsoft/playwright/APIRequestContext.java new file mode 100644 index 00000000..e568b817 --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/APIRequestContext.java @@ -0,0 +1,950 @@ +/* + * 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.nio.file.Path; +import java.util.*; + +/** + * This API is used for the Web API testing. You can use it to trigger API endpoints, configure micro-services, prepare + * environment or the service to your e2e test. When used on {@code Page} or a {@code BrowserContext}, this API will automatically use + * the cookies from the corresponding {@code BrowserContext}. This means that if you log in using this API, your e2e test will be + * logged in and vice versa. + */ +public interface APIRequestContext { + class DeleteOptions { + /** + * Allows to set post data of the request. If the data parameter is an object, it will be serialized to json string and + * {@code content-type} header will be set to {@code application/json} if not explicitly set. Otherwise the {@code content-type} header will + * be set to {@code application/octet-stream} if not explicitly set. + */ + public Object data; + /** + * Whether to throw on response codes other than 2xx and 3xx. By default response object is returned for all status codes. + */ + public Boolean failOnStatusCode; + /** + * Provides an object that will be serialized as html form using {@code application/x-www-form-urlencoded} encoding and sent as + * this request body. If this parameter is specified {@code content-type} header will be set to + * {@code application/x-www-form-urlencoded} unless explicitly provided. + */ + public Map form; + /** + * Allows to set HTTP headers. + */ + public Map headers; + /** + * Whether to ignore HTTPS errors when sending network requests. Defaults to {@code false}. + */ + public Boolean ignoreHTTPSErrors; + /** + * Provides an object that will be serialized as html form using {@code multipart/form-data} encoding and sent as this request + * body. If this parameter is specified {@code content-type} header will be set to {@code multipart/form-data} unless explicitly + * provided. File values can be passed either as [File] or as file-like object [FilePayload] containing file name, + * mime-type and its content. + */ + public Map multipart; + /** + * Query parameters to be sent with the URL. + */ + public Map params; + /** + * Request timeout in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. + */ + public Double timeout; + + /** + * Allows to set post data of the request. If the data parameter is an object, it will be serialized to json string and + * {@code content-type} header will be set to {@code application/json} if not explicitly set. Otherwise the {@code content-type} header will + * be set to {@code application/octet-stream} if not explicitly set. + */ + public DeleteOptions setData(String data) { + this.data = data; + return this; + } + /** + * Allows to set post data of the request. If the data parameter is an object, it will be serialized to json string and + * {@code content-type} header will be set to {@code application/json} if not explicitly set. Otherwise the {@code content-type} header will + * be set to {@code application/octet-stream} if not explicitly set. + */ + public DeleteOptions setData(byte[] data) { + this.data = data; + return this; + } + /** + * Allows to set post data of the request. If the data parameter is an object, it will be serialized to json string and + * {@code content-type} header will be set to {@code application/json} if not explicitly set. Otherwise the {@code content-type} header will + * be set to {@code application/octet-stream} if not explicitly set. + */ + public DeleteOptions setData(Object data) { + this.data = data; + return this; + } + /** + * Whether to throw on response codes other than 2xx and 3xx. By default response object is returned for all status codes. + */ + public DeleteOptions setFailOnStatusCode(boolean failOnStatusCode) { + this.failOnStatusCode = failOnStatusCode; + return this; + } + /** + * Provides an object that will be serialized as html form using {@code application/x-www-form-urlencoded} encoding and sent as + * this request body. If this parameter is specified {@code content-type} header will be set to + * {@code application/x-www-form-urlencoded} unless explicitly provided. + */ + public DeleteOptions setForm(Map form) { + this.form = form; + return this; + } + /** + * Allows to set HTTP headers. + */ + public DeleteOptions setHeaders(Map headers) { + this.headers = headers; + return this; + } + /** + * Whether to ignore HTTPS errors when sending network requests. Defaults to {@code false}. + */ + public DeleteOptions setIgnoreHTTPSErrors(boolean ignoreHTTPSErrors) { + this.ignoreHTTPSErrors = ignoreHTTPSErrors; + return this; + } + /** + * Provides an object that will be serialized as html form using {@code multipart/form-data} encoding and sent as this request + * body. If this parameter is specified {@code content-type} header will be set to {@code multipart/form-data} unless explicitly + * provided. File values can be passed either as [File] or as file-like object [FilePayload] containing file name, + * mime-type and its content. + */ + public DeleteOptions setMultipart(Map multipart) { + this.multipart = multipart; + return this; + } + /** + * Query parameters to be sent with the URL. + */ + public DeleteOptions setParams(Map params) { + this.params = params; + return this; + } + /** + * Request timeout in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. + */ + public DeleteOptions setTimeout(double timeout) { + this.timeout = timeout; + return this; + } + } + class FetchOptions { + /** + * Allows to set post data of the request. If the data parameter is an object, it will be serialized to json string and + * {@code content-type} header will be set to {@code application/json} if not explicitly set. Otherwise the {@code content-type} header will + * be set to {@code application/octet-stream} if not explicitly set. + */ + public Object data; + /** + * Whether to throw on response codes other than 2xx and 3xx. By default response object is returned for all status codes. + */ + public Boolean failOnStatusCode; + /** + * Provides an object that will be serialized as html form using {@code application/x-www-form-urlencoded} encoding and sent as + * this request body. If this parameter is specified {@code content-type} header will be set to + * {@code application/x-www-form-urlencoded} unless explicitly provided. + */ + public Map form; + /** + * Allows to set HTTP headers. + */ + public Map headers; + /** + * Whether to ignore HTTPS errors when sending network requests. Defaults to {@code false}. + */ + public Boolean ignoreHTTPSErrors; + /** + * If set changes the fetch method (e.g. PUT or + * POST). If not specified, GET method is + * used. + */ + public String method; + /** + * Provides an object that will be serialized as html form using {@code multipart/form-data} encoding and sent as this request + * body. If this parameter is specified {@code content-type} header will be set to {@code multipart/form-data} unless explicitly + * provided. File values can be passed either as [File] or as file-like object [FilePayload] containing file name, + * mime-type and its content. + */ + public Map multipart; + /** + * Query parameters to be sent with the URL. + */ + public Map params; + /** + * Request timeout in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. + */ + public Double timeout; + + /** + * Allows to set post data of the request. If the data parameter is an object, it will be serialized to json string and + * {@code content-type} header will be set to {@code application/json} if not explicitly set. Otherwise the {@code content-type} header will + * be set to {@code application/octet-stream} if not explicitly set. + */ + public FetchOptions setData(String data) { + this.data = data; + return this; + } + /** + * Allows to set post data of the request. If the data parameter is an object, it will be serialized to json string and + * {@code content-type} header will be set to {@code application/json} if not explicitly set. Otherwise the {@code content-type} header will + * be set to {@code application/octet-stream} if not explicitly set. + */ + public FetchOptions setData(byte[] data) { + this.data = data; + return this; + } + /** + * Allows to set post data of the request. If the data parameter is an object, it will be serialized to json string and + * {@code content-type} header will be set to {@code application/json} if not explicitly set. Otherwise the {@code content-type} header will + * be set to {@code application/octet-stream} if not explicitly set. + */ + public FetchOptions setData(Object data) { + this.data = data; + return this; + } + /** + * Whether to throw on response codes other than 2xx and 3xx. By default response object is returned for all status codes. + */ + public FetchOptions setFailOnStatusCode(boolean failOnStatusCode) { + this.failOnStatusCode = failOnStatusCode; + return this; + } + /** + * Provides an object that will be serialized as html form using {@code application/x-www-form-urlencoded} encoding and sent as + * this request body. If this parameter is specified {@code content-type} header will be set to + * {@code application/x-www-form-urlencoded} unless explicitly provided. + */ + public FetchOptions setForm(Map form) { + this.form = form; + return this; + } + /** + * Allows to set HTTP headers. + */ + public FetchOptions setHeaders(Map headers) { + this.headers = headers; + return this; + } + /** + * Whether to ignore HTTPS errors when sending network requests. Defaults to {@code false}. + */ + public FetchOptions setIgnoreHTTPSErrors(boolean ignoreHTTPSErrors) { + this.ignoreHTTPSErrors = ignoreHTTPSErrors; + return this; + } + /** + * If set changes the fetch method (e.g. PUT or + * POST). If not specified, GET method is + * used. + */ + public FetchOptions setMethod(String method) { + this.method = method; + return this; + } + /** + * Provides an object that will be serialized as html form using {@code multipart/form-data} encoding and sent as this request + * body. If this parameter is specified {@code content-type} header will be set to {@code multipart/form-data} unless explicitly + * provided. File values can be passed either as [File] or as file-like object [FilePayload] containing file name, + * mime-type and its content. + */ + public FetchOptions setMultipart(Map multipart) { + this.multipart = multipart; + return this; + } + /** + * Query parameters to be sent with the URL. + */ + public FetchOptions setParams(Map params) { + this.params = params; + return this; + } + /** + * Request timeout in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. + */ + public FetchOptions setTimeout(double timeout) { + this.timeout = timeout; + return this; + } + } + class GetOptions { + /** + * Whether to throw on response codes other than 2xx and 3xx. By default response object is returned for all status codes. + */ + public Boolean failOnStatusCode; + /** + * Allows to set HTTP headers. + */ + public Map headers; + /** + * Whether to ignore HTTPS errors when sending network requests. Defaults to {@code false}. + */ + public Boolean ignoreHTTPSErrors; + /** + * Query parameters to be sent with the URL. + */ + public Map params; + /** + * Request timeout in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. + */ + public Double timeout; + + /** + * Whether to throw on response codes other than 2xx and 3xx. By default response object is returned for all status codes. + */ + public GetOptions setFailOnStatusCode(boolean failOnStatusCode) { + this.failOnStatusCode = failOnStatusCode; + return this; + } + /** + * Allows to set HTTP headers. + */ + public GetOptions setHeaders(Map headers) { + this.headers = headers; + return this; + } + /** + * Whether to ignore HTTPS errors when sending network requests. Defaults to {@code false}. + */ + public GetOptions setIgnoreHTTPSErrors(boolean ignoreHTTPSErrors) { + this.ignoreHTTPSErrors = ignoreHTTPSErrors; + return this; + } + /** + * Query parameters to be sent with the URL. + */ + public GetOptions setParams(Map params) { + this.params = params; + return this; + } + /** + * Request timeout in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. + */ + public GetOptions setTimeout(double timeout) { + this.timeout = timeout; + return this; + } + } + class HeadOptions { + /** + * Whether to throw on response codes other than 2xx and 3xx. By default response object is returned for all status codes. + */ + public Boolean failOnStatusCode; + /** + * Allows to set HTTP headers. + */ + public Map headers; + /** + * Whether to ignore HTTPS errors when sending network requests. Defaults to {@code false}. + */ + public Boolean ignoreHTTPSErrors; + /** + * Query parameters to be sent with the URL. + */ + public Map params; + /** + * Request timeout in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. + */ + public Double timeout; + + /** + * Whether to throw on response codes other than 2xx and 3xx. By default response object is returned for all status codes. + */ + public HeadOptions setFailOnStatusCode(boolean failOnStatusCode) { + this.failOnStatusCode = failOnStatusCode; + return this; + } + /** + * Allows to set HTTP headers. + */ + public HeadOptions setHeaders(Map headers) { + this.headers = headers; + return this; + } + /** + * Whether to ignore HTTPS errors when sending network requests. Defaults to {@code false}. + */ + public HeadOptions setIgnoreHTTPSErrors(boolean ignoreHTTPSErrors) { + this.ignoreHTTPSErrors = ignoreHTTPSErrors; + return this; + } + /** + * Query parameters to be sent with the URL. + */ + public HeadOptions setParams(Map params) { + this.params = params; + return this; + } + /** + * Request timeout in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. + */ + public HeadOptions setTimeout(double timeout) { + this.timeout = timeout; + return this; + } + } + class PatchOptions { + /** + * Allows to set post data of the request. If the data parameter is an object, it will be serialized to json string and + * {@code content-type} header will be set to {@code application/json} if not explicitly set. Otherwise the {@code content-type} header will + * be set to {@code application/octet-stream} if not explicitly set. + */ + public Object data; + /** + * Whether to throw on response codes other than 2xx and 3xx. By default response object is returned for all status codes. + */ + public Boolean failOnStatusCode; + /** + * Provides an object that will be serialized as html form using {@code application/x-www-form-urlencoded} encoding and sent as + * this request body. If this parameter is specified {@code content-type} header will be set to + * {@code application/x-www-form-urlencoded} unless explicitly provided. + */ + public Map form; + /** + * Allows to set HTTP headers. + */ + public Map headers; + /** + * Whether to ignore HTTPS errors when sending network requests. Defaults to {@code false}. + */ + public Boolean ignoreHTTPSErrors; + /** + * Provides an object that will be serialized as html form using {@code multipart/form-data} encoding and sent as this request + * body. If this parameter is specified {@code content-type} header will be set to {@code multipart/form-data} unless explicitly + * provided. File values can be passed either as [File] or as file-like object [FilePayload] containing file name, + * mime-type and its content. + */ + public Map multipart; + /** + * Query parameters to be sent with the URL. + */ + public Map params; + /** + * Request timeout in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. + */ + public Double timeout; + + /** + * Allows to set post data of the request. If the data parameter is an object, it will be serialized to json string and + * {@code content-type} header will be set to {@code application/json} if not explicitly set. Otherwise the {@code content-type} header will + * be set to {@code application/octet-stream} if not explicitly set. + */ + public PatchOptions setData(String data) { + this.data = data; + return this; + } + /** + * Allows to set post data of the request. If the data parameter is an object, it will be serialized to json string and + * {@code content-type} header will be set to {@code application/json} if not explicitly set. Otherwise the {@code content-type} header will + * be set to {@code application/octet-stream} if not explicitly set. + */ + public PatchOptions setData(byte[] data) { + this.data = data; + return this; + } + /** + * Allows to set post data of the request. If the data parameter is an object, it will be serialized to json string and + * {@code content-type} header will be set to {@code application/json} if not explicitly set. Otherwise the {@code content-type} header will + * be set to {@code application/octet-stream} if not explicitly set. + */ + public PatchOptions setData(Object data) { + this.data = data; + return this; + } + /** + * Whether to throw on response codes other than 2xx and 3xx. By default response object is returned for all status codes. + */ + public PatchOptions setFailOnStatusCode(boolean failOnStatusCode) { + this.failOnStatusCode = failOnStatusCode; + return this; + } + /** + * Provides an object that will be serialized as html form using {@code application/x-www-form-urlencoded} encoding and sent as + * this request body. If this parameter is specified {@code content-type} header will be set to + * {@code application/x-www-form-urlencoded} unless explicitly provided. + */ + public PatchOptions setForm(Map form) { + this.form = form; + return this; + } + /** + * Allows to set HTTP headers. + */ + public PatchOptions setHeaders(Map headers) { + this.headers = headers; + return this; + } + /** + * Whether to ignore HTTPS errors when sending network requests. Defaults to {@code false}. + */ + public PatchOptions setIgnoreHTTPSErrors(boolean ignoreHTTPSErrors) { + this.ignoreHTTPSErrors = ignoreHTTPSErrors; + return this; + } + /** + * Provides an object that will be serialized as html form using {@code multipart/form-data} encoding and sent as this request + * body. If this parameter is specified {@code content-type} header will be set to {@code multipart/form-data} unless explicitly + * provided. File values can be passed either as [File] or as file-like object [FilePayload] containing file name, + * mime-type and its content. + */ + public PatchOptions setMultipart(Map multipart) { + this.multipart = multipart; + return this; + } + /** + * Query parameters to be sent with the URL. + */ + public PatchOptions setParams(Map params) { + this.params = params; + return this; + } + /** + * Request timeout in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. + */ + public PatchOptions setTimeout(double timeout) { + this.timeout = timeout; + return this; + } + } + class PostOptions { + /** + * Allows to set post data of the request. If the data parameter is an object, it will be serialized to json string and + * {@code content-type} header will be set to {@code application/json} if not explicitly set. Otherwise the {@code content-type} header will + * be set to {@code application/octet-stream} if not explicitly set. + */ + public Object data; + /** + * Whether to throw on response codes other than 2xx and 3xx. By default response object is returned for all status codes. + */ + public Boolean failOnStatusCode; + /** + * Provides an object that will be serialized as html form using {@code application/x-www-form-urlencoded} encoding and sent as + * this request body. If this parameter is specified {@code content-type} header will be set to + * {@code application/x-www-form-urlencoded} unless explicitly provided. + */ + public Map form; + /** + * Allows to set HTTP headers. + */ + public Map headers; + /** + * Whether to ignore HTTPS errors when sending network requests. Defaults to {@code false}. + */ + public Boolean ignoreHTTPSErrors; + /** + * Provides an object that will be serialized as html form using {@code multipart/form-data} encoding and sent as this request + * body. If this parameter is specified {@code content-type} header will be set to {@code multipart/form-data} unless explicitly + * provided. File values can be passed either as [File] or as file-like object [FilePayload] containing file name, + * mime-type and its content. + */ + public Map multipart; + /** + * Query parameters to be sent with the URL. + */ + public Map params; + /** + * Request timeout in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. + */ + public Double timeout; + + /** + * Allows to set post data of the request. If the data parameter is an object, it will be serialized to json string and + * {@code content-type} header will be set to {@code application/json} if not explicitly set. Otherwise the {@code content-type} header will + * be set to {@code application/octet-stream} if not explicitly set. + */ + public PostOptions setData(String data) { + this.data = data; + return this; + } + /** + * Allows to set post data of the request. If the data parameter is an object, it will be serialized to json string and + * {@code content-type} header will be set to {@code application/json} if not explicitly set. Otherwise the {@code content-type} header will + * be set to {@code application/octet-stream} if not explicitly set. + */ + public PostOptions setData(byte[] data) { + this.data = data; + return this; + } + /** + * Allows to set post data of the request. If the data parameter is an object, it will be serialized to json string and + * {@code content-type} header will be set to {@code application/json} if not explicitly set. Otherwise the {@code content-type} header will + * be set to {@code application/octet-stream} if not explicitly set. + */ + public PostOptions setData(Object data) { + this.data = data; + return this; + } + /** + * Whether to throw on response codes other than 2xx and 3xx. By default response object is returned for all status codes. + */ + public PostOptions setFailOnStatusCode(boolean failOnStatusCode) { + this.failOnStatusCode = failOnStatusCode; + return this; + } + /** + * Provides an object that will be serialized as html form using {@code application/x-www-form-urlencoded} encoding and sent as + * this request body. If this parameter is specified {@code content-type} header will be set to + * {@code application/x-www-form-urlencoded} unless explicitly provided. + */ + public PostOptions setForm(Map form) { + this.form = form; + return this; + } + /** + * Allows to set HTTP headers. + */ + public PostOptions setHeaders(Map headers) { + this.headers = headers; + return this; + } + /** + * Whether to ignore HTTPS errors when sending network requests. Defaults to {@code false}. + */ + public PostOptions setIgnoreHTTPSErrors(boolean ignoreHTTPSErrors) { + this.ignoreHTTPSErrors = ignoreHTTPSErrors; + return this; + } + /** + * Provides an object that will be serialized as html form using {@code multipart/form-data} encoding and sent as this request + * body. If this parameter is specified {@code content-type} header will be set to {@code multipart/form-data} unless explicitly + * provided. File values can be passed either as [File] or as file-like object [FilePayload] containing file name, + * mime-type and its content. + */ + public PostOptions setMultipart(Map multipart) { + this.multipart = multipart; + return this; + } + /** + * Query parameters to be sent with the URL. + */ + public PostOptions setParams(Map params) { + this.params = params; + return this; + } + /** + * Request timeout in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. + */ + public PostOptions setTimeout(double timeout) { + this.timeout = timeout; + return this; + } + } + class PutOptions { + /** + * Allows to set post data of the request. If the data parameter is an object, it will be serialized to json string and + * {@code content-type} header will be set to {@code application/json} if not explicitly set. Otherwise the {@code content-type} header will + * be set to {@code application/octet-stream} if not explicitly set. + */ + public Object data; + /** + * Whether to throw on response codes other than 2xx and 3xx. By default response object is returned for all status codes. + */ + public Boolean failOnStatusCode; + /** + * Provides an object that will be serialized as html form using {@code application/x-www-form-urlencoded} encoding and sent as + * this request body. If this parameter is specified {@code content-type} header will be set to + * {@code application/x-www-form-urlencoded} unless explicitly provided. + */ + public Map form; + /** + * Allows to set HTTP headers. + */ + public Map headers; + /** + * Whether to ignore HTTPS errors when sending network requests. Defaults to {@code false}. + */ + public Boolean ignoreHTTPSErrors; + /** + * Provides an object that will be serialized as html form using {@code multipart/form-data} encoding and sent as this request + * body. If this parameter is specified {@code content-type} header will be set to {@code multipart/form-data} unless explicitly + * provided. File values can be passed either as [File] or as file-like object [FilePayload] containing file name, + * mime-type and its content. + */ + public Map multipart; + /** + * Query parameters to be sent with the URL. + */ + public Map params; + /** + * Request timeout in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. + */ + public Double timeout; + + /** + * Allows to set post data of the request. If the data parameter is an object, it will be serialized to json string and + * {@code content-type} header will be set to {@code application/json} if not explicitly set. Otherwise the {@code content-type} header will + * be set to {@code application/octet-stream} if not explicitly set. + */ + public PutOptions setData(String data) { + this.data = data; + return this; + } + /** + * Allows to set post data of the request. If the data parameter is an object, it will be serialized to json string and + * {@code content-type} header will be set to {@code application/json} if not explicitly set. Otherwise the {@code content-type} header will + * be set to {@code application/octet-stream} if not explicitly set. + */ + public PutOptions setData(byte[] data) { + this.data = data; + return this; + } + /** + * Allows to set post data of the request. If the data parameter is an object, it will be serialized to json string and + * {@code content-type} header will be set to {@code application/json} if not explicitly set. Otherwise the {@code content-type} header will + * be set to {@code application/octet-stream} if not explicitly set. + */ + public PutOptions setData(Object data) { + this.data = data; + return this; + } + /** + * Whether to throw on response codes other than 2xx and 3xx. By default response object is returned for all status codes. + */ + public PutOptions setFailOnStatusCode(boolean failOnStatusCode) { + this.failOnStatusCode = failOnStatusCode; + return this; + } + /** + * Provides an object that will be serialized as html form using {@code application/x-www-form-urlencoded} encoding and sent as + * this request body. If this parameter is specified {@code content-type} header will be set to + * {@code application/x-www-form-urlencoded} unless explicitly provided. + */ + public PutOptions setForm(Map form) { + this.form = form; + return this; + } + /** + * Allows to set HTTP headers. + */ + public PutOptions setHeaders(Map headers) { + this.headers = headers; + return this; + } + /** + * Whether to ignore HTTPS errors when sending network requests. Defaults to {@code false}. + */ + public PutOptions setIgnoreHTTPSErrors(boolean ignoreHTTPSErrors) { + this.ignoreHTTPSErrors = ignoreHTTPSErrors; + return this; + } + /** + * Provides an object that will be serialized as html form using {@code multipart/form-data} encoding and sent as this request + * body. If this parameter is specified {@code content-type} header will be set to {@code multipart/form-data} unless explicitly + * provided. File values can be passed either as [File] or as file-like object [FilePayload] containing file name, + * mime-type and its content. + */ + public PutOptions setMultipart(Map multipart) { + this.multipart = multipart; + return this; + } + /** + * Query parameters to be sent with the URL. + */ + public PutOptions setParams(Map params) { + this.params = params; + return this; + } + /** + * Request timeout in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. + */ + public PutOptions setTimeout(double timeout) { + this.timeout = timeout; + 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 + * working directory. If no path is provided, storage state is still returned, but won't be saved to the disk. + */ + public Path path; + + /** + * The file path to save the storage state to. If {@code path} is a relative path, then it is resolved relative to current + * working directory. If no path is provided, storage state is still returned, but won't be saved to the disk. + */ + public StorageStateOptions setPath(Path path) { + this.path = path; + return this; + } + } + /** + * Sends HTTP(S) DELETE 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. + * + * @param url Target URL. + */ + default APIResponse delete(String url) { + return delete(url, null); + } + /** + * Sends HTTP(S) DELETE 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. + * + * @param url Target URL. + */ + APIResponse delete(String url, DeleteOptions options); + /** + * All responses returned by {@link APIRequestContext#get APIRequestContext.get()} and similar methods are stored in the + * memory, so that you can later call {@link APIResponse#body APIResponse.body()}. This method discards all stored + * responses, and makes {@link APIResponse#body APIResponse.body()} throw "Response disposed" error. + */ + void dispose(); + /** + * 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. + * + * @param urlOrRequest Target URL or Request to get all parameters from. + */ + default APIResponse fetch(String urlOrRequest) { + return fetch(urlOrRequest, null); + } + /** + * 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. + * + * @param urlOrRequest Target URL or Request to get all parameters from. + */ + APIResponse fetch(String urlOrRequest, FetchOptions 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. + * + * @param urlOrRequest Target URL or Request to get all parameters from. + */ + default APIResponse fetch(Request urlOrRequest) { + return fetch(urlOrRequest, null); + } + /** + * 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. + * + * @param urlOrRequest Target URL or Request to get all parameters from. + */ + APIResponse fetch(Request urlOrRequest, FetchOptions options); + /** + * Sends HTTP(S) GET 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. + * + * @param url Target URL. + */ + default APIResponse get(String url) { + return get(url, null); + } + /** + * Sends HTTP(S) GET 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. + * + * @param url Target URL. + */ + APIResponse get(String url, GetOptions options); + /** + * Sends HTTP(S) HEAD 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. + * + * @param url Target URL. + */ + default APIResponse head(String url) { + return head(url, null); + } + /** + * Sends HTTP(S) HEAD 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. + * + * @param url Target URL. + */ + APIResponse head(String url, HeadOptions options); + /** + * Sends HTTP(S) PATCH 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. + * + * @param url Target URL. + */ + default APIResponse patch(String url) { + return patch(url, null); + } + /** + * Sends HTTP(S) PATCH 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. + * + * @param url Target URL. + */ + APIResponse patch(String url, PatchOptions options); + /** + * Sends HTTP(S) POST 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. + * + * @param url Target URL. + */ + default APIResponse post(String url) { + return post(url, null); + } + /** + * Sends HTTP(S) POST 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. + * + * @param url Target URL. + */ + APIResponse post(String url, PostOptions options); + /** + * Sends HTTP(S) PUT 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. + * + * @param url Target URL. + */ + default APIResponse put(String url) { + return put(url, null); + } + /** + * Sends HTTP(S) PUT 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. + * + * @param url Target URL. + */ + APIResponse put(String url, PutOptions options); + /** + * Returns storage state for this request context, contains current cookies and local storage snapshot if it was passed to + * the constructor. + */ + default String storageState() { + return storageState(null); + } + /** + * Returns storage state for this request context, contains current cookies and local storage snapshot if it was passed to + * the constructor. + */ + String storageState(StorageStateOptions options); +} + diff --git a/playwright/src/main/java/com/microsoft/playwright/APIResponse.java b/playwright/src/main/java/com/microsoft/playwright/APIResponse.java new file mode 100644 index 00000000..5330fcad --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/APIResponse.java @@ -0,0 +1,65 @@ +/* + * 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 com.microsoft.playwright.options.*; +import java.util.*; + +/** + * {@code APIResponse} class represents responses returned by {@link APIRequestContext#get APIRequestContext.get()} and similar + * methods. + */ +public interface APIResponse { + /** + * Returns the buffer with response body. + */ + byte[] body(); + /** + * Disposes the body of this response. If not called then the body will stay in memory until the context closes. + */ + void dispose(); + /** + * An object with all the response HTTP headers associated with this response. + */ + Map headers(); + /** + * An array with all the request HTTP headers associated with this response. Header names are not lower-cased. Headers with + * multiple entries, such as {@code Set-Cookie}, appear in the array multiple times. + */ + List headersArray(); + /** + * Contains a boolean stating whether the response was successful (status in the range 200-299) or not. + */ + boolean ok(); + /** + * Contains the status code of the response (e.g., 200 for a success). + */ + int status(); + /** + * Contains the status text of the response (e.g. usually an "OK" for a success). + */ + String statusText(); + /** + * Returns the text representation of response body. + */ + String text(); + /** + * Contains the URL of the response. + */ + String url(); +} + diff --git a/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java b/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java index 7a01d1c8..0ba13ce4 100644 --- a/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java +++ b/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java @@ -547,6 +547,10 @@ public interface BrowserContext extends AutoCloseable { * Returns all open pages in the context. */ List pages(); + /** + * API testing helper associated with this context. Requests made with this API will use context cookies. + */ + APIRequestContext request(); /** * Routing provides the capability to modify network requests that are made by any page in the browser context. Once route * is enabled, every request matching the url pattern will stall unless it's continued, fulfilled or aborted. diff --git a/playwright/src/main/java/com/microsoft/playwright/ElementHandle.java b/playwright/src/main/java/com/microsoft/playwright/ElementHandle.java index eef4a2ca..c20b13fd 100644 --- a/playwright/src/main/java/com/microsoft/playwright/ElementHandle.java +++ b/playwright/src/main/java/com/microsoft/playwright/ElementHandle.java @@ -23,6 +23,8 @@ import java.util.*; /** * ElementHandle represents an in-page DOM element. ElementHandles can be created with the {@link Page#querySelector * Page.querySelector()} method. + * + *

NOTE: The use of ElementHandle is discouraged, use {@code Locator} objects and web-first assertions instead. *

{@code
  * ElementHandle hrefElement = page.querySelector("a");
  * hrefElement.click();
@@ -34,10 +36,6 @@ import java.util.*;
  * 

ElementHandle instances can be used as an argument in {@link Page#evalOnSelector Page.evalOnSelector()} and {@link * Page#evaluate Page.evaluate()} methods. * - *

NOTE: In most cases, you would want to use the {@code Locator} object instead. You should only use {@code ElementHandle} if you want to - * retain a handle to a particular DOM Node that you intend to pass into {@link Page#evaluate Page.evaluate()} as an - * argument. - * *

The difference between the {@code Locator} and ElementHandle is that the ElementHandle points to a particular element, while * {@code Locator} captures the logic of how to retrieve an element. * diff --git a/playwright/src/main/java/com/microsoft/playwright/Frame.java b/playwright/src/main/java/com/microsoft/playwright/Frame.java index 65bd6de1..b803e8db 100644 --- a/playwright/src/main/java/com/microsoft/playwright/Frame.java +++ b/playwright/src/main/java/com/microsoft/playwright/Frame.java @@ -2329,6 +2329,9 @@ public interface Frame { /** * Returns the return value of {@code expression}. * + *

NOTE: This method does not wait for the element to pass actionability checks and therefore can lead to the flaky tests. Use + * {@link Locator#evaluate Locator.evaluate()}, other {@code Locator} helper methods or web-first assertions instead. + * *

The method finds an element matching the specified selector within the frame and passes it as a first argument to * {@code expression}. See Working with selectors for more details. If * no elements match the selector, the method throws an error. @@ -2356,6 +2359,9 @@ public interface Frame { /** * Returns the return value of {@code expression}. * + *

NOTE: This method does not wait for the element to pass actionability checks and therefore can lead to the flaky tests. Use + * {@link Locator#evaluate Locator.evaluate()}, other {@code Locator} helper methods or web-first assertions instead. + * *

The method finds an element matching the specified selector within the frame and passes it as a first argument to * {@code expression}. See Working with selectors for more details. If * no elements match the selector, the method throws an error. @@ -2382,6 +2388,9 @@ public interface Frame { /** * Returns the return value of {@code expression}. * + *

NOTE: This method does not wait for the element to pass actionability checks and therefore can lead to the flaky tests. Use + * {@link Locator#evaluate Locator.evaluate()}, other {@code Locator} helper methods or web-first assertions instead. + * *

The method finds an element matching the specified selector within the frame and passes it as a first argument to * {@code expression}. See Working with selectors for more details. If * no elements match the selector, the method throws an error. @@ -2407,6 +2416,9 @@ public interface Frame { /** * Returns the return value of {@code expression}. * + *

NOTE: In most cases, {@link Locator#evaluateAll Locator.evaluateAll()}, other {@code Locator} helper methods and web-first + * assertions do a better job. + * *

The method finds all elements matching the specified selector within the frame and passes an array of matched elements * as a first argument to {@code expression}. See Working with * selectors for more details. @@ -2431,6 +2443,9 @@ public interface Frame { /** * Returns the return value of {@code expression}. * + *

NOTE: In most cases, {@link Locator#evaluateAll Locator.evaluateAll()}, other {@code Locator} helper methods and web-first + * assertions do a better job. + * *

The method finds all elements matching the specified selector within the frame and passes an array of matched elements * as a first argument to {@code expression}. See Working with * selectors for more details. @@ -2475,7 +2490,7 @@ public interface Frame { * *

{@code ElementHandle} instances can be passed as an argument to the {@link Frame#evaluate Frame.evaluate()}: *

{@code
-   * ElementHandle bodyHandle = frame.querySelector("body");
+   * ElementHandle bodyHandle = frame.evaluate("document.body");
    * String html = (String) frame.evaluate("([body, suffix]) => body.innerHTML + suffix", Arrays.asList(bodyHandle, "hello"));
    * bodyHandle.dispose();
    * }
@@ -2510,7 +2525,7 @@ public interface Frame { * *

{@code ElementHandle} instances can be passed as an argument to the {@link Frame#evaluate Frame.evaluate()}: *

{@code
-   * ElementHandle bodyHandle = frame.querySelector("body");
+   * ElementHandle bodyHandle = frame.evaluate("document.body");
    * String html = (String) frame.evaluate("([body, suffix]) => body.innerHTML + suffix", Arrays.asList(bodyHandle, "hello"));
    * bodyHandle.dispose();
    * }
@@ -3010,6 +3025,8 @@ public interface Frame { /** * Returns the ElementHandle pointing to the frame element. * + *

NOTE: The use of {@code ElementHandle} is discouraged, use {@code Locator} objects and web-first assertions instead. + * *

The method finds an element matching the specified selector within the frame. See Working with selectors for more details. If no elements match the * selector, returns {@code null}. @@ -3023,6 +3040,8 @@ public interface Frame { /** * Returns the ElementHandle pointing to the frame element. * + *

NOTE: The use of {@code ElementHandle} is discouraged, use {@code Locator} objects and web-first assertions instead. + * *

The method finds an element matching the specified selector within the frame. See Working with selectors for more details. If no elements match the * selector, returns {@code null}. @@ -3034,6 +3053,8 @@ public interface Frame { /** * Returns the ElementHandles pointing to the frame elements. * + *

NOTE: The use of {@code ElementHandle} is discouraged, use {@code Locator} objects instead. + * *

The method finds all elements matching the specified selector within the frame. See Working with selectors for more details. If no elements match the * selector, returns empty array. @@ -3904,6 +3925,9 @@ public interface Frame { * Returns when element specified by selector satisfies {@code state} option. Returns {@code null} if waiting for {@code hidden} or * {@code detached}. * + *

NOTE: Playwright automatically waits for element to be ready before performing an action. Using {@code Locator} objects and + * web-first assertions make the code wait-for-selector-free. + * *

Wait for the {@code selector} to satisfy {@code state} option (either appear/disappear from dom, or become visible/hidden). If at * the moment of calling the method {@code selector} already satisfies the condition, the method will return immediately. If the * selector doesn't satisfy the condition for the {@code timeout} milliseconds, the function will throw. @@ -3939,6 +3963,9 @@ public interface Frame { * Returns when element specified by selector satisfies {@code state} option. Returns {@code null} if waiting for {@code hidden} or * {@code detached}. * + *

NOTE: Playwright automatically waits for element to be ready before performing an action. Using {@code Locator} objects and + * web-first assertions make the code wait-for-selector-free. + * *

Wait for the {@code selector} to satisfy {@code state} option (either appear/disappear from dom, or become visible/hidden). If at * the moment of calling the method {@code selector} already satisfies the condition, the method will return immediately. If the * selector doesn't satisfy the condition for the {@code timeout} milliseconds, the function will throw. diff --git a/playwright/src/main/java/com/microsoft/playwright/Page.java b/playwright/src/main/java/com/microsoft/playwright/Page.java index d5002845..9e5232ab 100644 --- a/playwright/src/main/java/com/microsoft/playwright/Page.java +++ b/playwright/src/main/java/com/microsoft/playwright/Page.java @@ -3467,7 +3467,10 @@ public interface Page extends AutoCloseable { */ void emulateMedia(EmulateMediaOptions options); /** - * The method finds an element matching the specified selector within the page and passes it as a first argument to + * NOTE: This method does not wait for the element to pass actionability checks and therefore can lead to the flaky tests. Use + * {@link Locator#evaluate Locator.evaluate()}, other {@code Locator} helper methods or web-first assertions instead. + * + *

The method finds an element matching the specified selector within the page and passes it as a first argument to * {@code expression}. If no elements match the selector, the method throws an error. Returns the value of {@code expression}. * *

If {@code expression} returns a NOTE: This method does not wait for the element to pass actionability checks and therefore can lead to the flaky tests. Use + * {@link Locator#evaluate Locator.evaluate()}, other {@code Locator} helper methods or web-first assertions instead. + * + *

The method finds an element matching the specified selector within the page and passes it as a first argument to * {@code expression}. If no elements match the selector, the method throws an error. Returns the value of {@code expression}. * *

If {@code expression} returns a NOTE: This method does not wait for the element to pass actionability checks and therefore can lead to the flaky tests. Use + * {@link Locator#evaluate Locator.evaluate()}, other {@code Locator} helper methods or web-first assertions instead. + * + *

The method finds an element matching the specified selector within the page and passes it as a first argument to * {@code expression}. If no elements match the selector, the method throws an error. Returns the value of {@code expression}. * *

If {@code expression} returns a NOTE: In most cases, {@link Locator#evaluateAll Locator.evaluateAll()}, other {@code Locator} helper methods and web-first + * assertions do a better job. + * + *

The method finds all elements matching the specified selector within the page and passes an array of matched elements as * a first argument to {@code expression}. Returns the result of {@code expression} invocation. * *

If {@code expression} returns a NOTE: In most cases, {@link Locator#evaluateAll Locator.evaluateAll()}, other {@code Locator} helper methods and web-first + * assertions do a better job. + * + *

The method finds all elements matching the specified selector within the page and passes an array of matched elements as * a first argument to {@code expression}. Returns the result of {@code expression} invocation. * *

If {@code expression} returns a {@code ElementHandle} instances can be passed as an argument to the {@link Page#evaluate Page.evaluate()}: *

{@code
-   * ElementHandle bodyHandle = page.querySelector("body");
+   * ElementHandle bodyHandle = page.evaluate("document.body");
    * String html = (String) page.evaluate("([body, suffix]) => body.innerHTML + suffix", Arrays.asList(bodyHandle, "hello"));
    * bodyHandle.dispose();
    * }
@@ -3647,7 +3662,7 @@ public interface Page extends AutoCloseable { * *

{@code ElementHandle} instances can be passed as an argument to the {@link Page#evaluate Page.evaluate()}: *

{@code
-   * ElementHandle bodyHandle = page.querySelector("body");
+   * ElementHandle bodyHandle = page.evaluate("document.body");
    * String html = (String) page.evaluate("([body, suffix]) => body.innerHTML + suffix", Arrays.asList(bodyHandle, "hello"));
    * bodyHandle.dispose();
    * }
@@ -4535,9 +4550,10 @@ public interface Page extends AutoCloseable { */ void press(String selector, String key, PressOptions options); /** - * The method finds an element matching the specified selector within the page. If no elements match the selector, the - * return value resolves to {@code null}. To wait for an element on the page, use {@link Page#waitForSelector - * Page.waitForSelector()}. + * NOTE: The use of {@code ElementHandle} is discouraged, use {@code Locator} objects and web-first assertions instead. + * + *

The method finds an element matching the specified selector within the page. If no elements match the selector, the + * return value resolves to {@code null}. To wait for an element on the page, use {@link Locator#waitFor Locator.waitFor()}. * *

Shortcut for main frame's {@link Frame#querySelector Frame.querySelector()}. * @@ -4548,9 +4564,10 @@ public interface Page extends AutoCloseable { return querySelector(selector, null); } /** - * The method finds an element matching the specified selector within the page. If no elements match the selector, the - * return value resolves to {@code null}. To wait for an element on the page, use {@link Page#waitForSelector - * Page.waitForSelector()}. + * NOTE: The use of {@code ElementHandle} is discouraged, use {@code Locator} objects and web-first assertions instead. + * + *

The method finds an element matching the specified selector within the page. If no elements match the selector, the + * return value resolves to {@code null}. To wait for an element on the page, use {@link Locator#waitFor Locator.waitFor()}. * *

Shortcut for main frame's {@link Frame#querySelector Frame.querySelector()}. * @@ -4559,7 +4576,9 @@ public interface Page extends AutoCloseable { */ ElementHandle querySelector(String selector, QuerySelectorOptions options); /** - * The method finds all elements matching the specified selector within the page. If no elements match the selector, the + * NOTE: The use of {@code ElementHandle} is discouraged, use {@code Locator} objects and web-first assertions instead. + * + *

The method finds all elements matching the specified selector within the page. If no elements match the selector, the * return value resolves to {@code []}. * *

Shortcut for main frame's {@link Frame#querySelectorAll Frame.querySelectorAll()}. @@ -4580,6 +4599,10 @@ public interface Page extends AutoCloseable { * resource response. In case of multiple redirects, the navigation will resolve with the response of the last redirect. */ Response reload(ReloadOptions options); + /** + * API testing helper associated with this page. Requests made with this API will use page cookies. + */ + APIRequestContext request(); /** * Routing provides the capability to modify network requests that are made by a page. * @@ -6343,6 +6366,9 @@ public interface Page extends AutoCloseable { * Returns when element specified by selector satisfies {@code state} option. Returns {@code null} if waiting for {@code hidden} or * {@code detached}. * + *

NOTE: Playwright automatically waits for element to be ready before performing an action. Using {@code Locator} objects and + * web-first assertions make the code wait-for-selector-free. + * *

Wait for the {@code selector} to satisfy {@code state} option (either appear/disappear from dom, or become visible/hidden). If at * the moment of calling the method {@code selector} already satisfies the condition, the method will return immediately. If the * selector doesn't satisfy the condition for the {@code timeout} milliseconds, the function will throw. @@ -6378,6 +6404,9 @@ public interface Page extends AutoCloseable { * Returns when element specified by selector satisfies {@code state} option. Returns {@code null} if waiting for {@code hidden} or * {@code detached}. * + *

NOTE: Playwright automatically waits for element to be ready before performing an action. Using {@code Locator} objects and + * web-first assertions make the code wait-for-selector-free. + * *

Wait for the {@code selector} to satisfy {@code state} option (either appear/disappear from dom, or become visible/hidden). If at * the moment of calling the method {@code selector} already satisfies the condition, the method will return immediately. If the * selector doesn't satisfy the condition for the {@code timeout} milliseconds, the function will throw. diff --git a/playwright/src/main/java/com/microsoft/playwright/Playwright.java b/playwright/src/main/java/com/microsoft/playwright/Playwright.java index 0b7efa48..d92d8524 100644 --- a/playwright/src/main/java/com/microsoft/playwright/Playwright.java +++ b/playwright/src/main/java/com/microsoft/playwright/Playwright.java @@ -64,6 +64,10 @@ public interface Playwright extends AutoCloseable { * This object can be used to launch or connect to Firefox, returning instances of {@code Browser}. */ BrowserType firefox(); + /** + * Exposes API that can be used for the Web API testing. + */ + APIRequest request(); /** * Selectors can be used to install custom selector engines. See Working with selectors for more information. diff --git a/playwright/src/main/java/com/microsoft/playwright/Selectors.java b/playwright/src/main/java/com/microsoft/playwright/Selectors.java index c69b4e85..f4a63c2c 100644 --- a/playwright/src/main/java/com/microsoft/playwright/Selectors.java +++ b/playwright/src/main/java/com/microsoft/playwright/Selectors.java @@ -62,11 +62,11 @@ public interface Selectors { * Page page = browser.newPage(); * page.setContent("

"); * // Use the selector prefixed with its name. - * ElementHandle button = page.querySelector("tag=button"); + * Locator button = page.locator("tag=button"); * // Combine it with other selector engines. * page.click("tag=div >> text=\"Click me\""); * // Can use it in any methods supporting selectors. - * int buttonCount = (int) page.evalOnSelectorAll("tag=button", "buttons => buttons.length"); + * int buttonCount = (int) page.locator("tag=button").count(); * browser.close(); * }
* @@ -97,11 +97,11 @@ public interface Selectors { * Page page = browser.newPage(); * page.setContent("
"); * // Use the selector prefixed with its name. - * ElementHandle button = page.querySelector("tag=button"); + * Locator button = page.locator("tag=button"); * // Combine it with other selector engines. * page.click("tag=div >> text=\"Click me\""); * // Can use it in any methods supporting selectors. - * int buttonCount = (int) page.evalOnSelectorAll("tag=button", "buttons => buttons.length"); + * int buttonCount = (int) page.locator("tag=button").count(); * browser.close(); * } * @@ -130,11 +130,11 @@ public interface Selectors { * Page page = browser.newPage(); * page.setContent("
"); * // Use the selector prefixed with its name. - * ElementHandle button = page.querySelector("tag=button"); + * Locator button = page.locator("tag=button"); * // Combine it with other selector engines. * page.click("tag=div >> text=\"Click me\""); * // Can use it in any methods supporting selectors. - * int buttonCount = (int) page.evalOnSelectorAll("tag=button", "buttons => buttons.length"); + * int buttonCount = (int) page.locator("tag=button").count(); * browser.close(); * } * @@ -165,11 +165,11 @@ public interface Selectors { * Page page = browser.newPage(); * page.setContent("
"); * // Use the selector prefixed with its name. - * ElementHandle button = page.querySelector("tag=button"); + * Locator button = page.locator("tag=button"); * // Combine it with other selector engines. * page.click("tag=div >> text=\"Click me\""); * // Can use it in any methods supporting selectors. - * int buttonCount = (int) page.evalOnSelectorAll("tag=button", "buttons => buttons.length"); + * int buttonCount = (int) page.locator("tag=button").count(); * browser.close(); * } * diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/APIRequestContextImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/APIRequestContextImpl.java new file mode 100644 index 00000000..a4cae831 --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/impl/APIRequestContextImpl.java @@ -0,0 +1,207 @@ +package com.microsoft.playwright.impl; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.microsoft.playwright.APIRequestContext; +import com.microsoft.playwright.APIResponse; +import com.microsoft.playwright.PlaywrightException; +import com.microsoft.playwright.Request; +import com.microsoft.playwright.options.FilePayload; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.Base64; +import java.util.LinkedHashMap; +import java.util.Map; + +import static com.microsoft.playwright.impl.Serialization.*; +import static com.microsoft.playwright.impl.Utils.convertViaReflection; +import static com.microsoft.playwright.impl.Utils.toFilePayload; + +class APIRequestContextImpl extends ChannelOwner implements APIRequestContext { + APIRequestContextImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) { + super(parent, type, guid, initializer); + } + + @Override + public APIResponse delete(String url, DeleteOptions options) { + FetchOptions fetchOptions = toFetchOptions(options); + fetchOptions.method = "DELETE"; + return fetch(url, fetchOptions); + } + + @Override + public void dispose() { + withLogging("APIRequestContext.dispose", () -> sendMessage("dispose")); + } + + @Override + public APIResponse fetch(String urlOrRequest, FetchOptions options) { + return withLogging("APIRequestContext.fetch", () -> fetchImpl(urlOrRequest, options)); + } + + @Override + public APIResponse fetch(Request request, FetchOptions options) { + if (options == null) { + options = new FetchOptions(); + } + if (options.method == null) { + options.method = request.method(); + } + if (options.headers == null) { + options.headers = request.headers(); + } + if (options.data == null && options.form == null && options.multipart == null) { + options.data = request.postDataBuffer(); + } + return fetch(request.url(), options); + } + + private APIResponse fetchImpl(String url, FetchOptions options) { + if (options == null) { + options = new FetchOptions(); + } + JsonObject params = new JsonObject(); + params.addProperty("url", url); + if (options.params != null) { + Map queryParams = new LinkedHashMap<>(); + for (Map.Entry e : options.params.entrySet()) { + queryParams.put(e.getKey(), "" + e.getValue()); + } + params.add("params", toNameValueArray(queryParams)); + } + if (options.method != null) { + params.addProperty("method", options.method); + } + if (options.headers != null) { + params.add("headers", toProtocol(options.headers)); + } + + if (options.data != null) { + byte[] bytes = null; + if (options.data instanceof byte[]) { + bytes = (byte[]) options.data; + } else if (options.data instanceof String && !isJsonContentType(options.headers)) { + bytes = ((String) options.data).getBytes(StandardCharsets.UTF_8); + } + if (bytes == null) { + params.add("jsonData", gson().toJsonTree(options.data)); + } else { + String base64 = Base64.getEncoder().encodeToString(bytes); + params.addProperty("postData", base64); + } + } + if (options.form != null) { + params.add("formData", toNameValueArray(options.form)); + } + if (options.multipart != null) { + params.add("multipartData", serializeMultipartData(options.multipart)); + } + if (options.timeout != null) { + params.addProperty("timeout", options.timeout); + } + if (options.failOnStatusCode != null) { + params.addProperty("failOnStatusCode", options.failOnStatusCode); + } + if (options.ignoreHTTPSErrors != null) { + params.addProperty("ignoreHTTPSErrors", options.ignoreHTTPSErrors); + } + JsonObject json = sendMessage("fetch", params).getAsJsonObject(); + if (json.has("error")) { + throw new PlaywrightException(json.get("error").getAsString()); + } + return new APIResponseImpl(this, json.getAsJsonObject("response")); + } + + private static boolean isJsonContentType(Map headers) { + if (headers == null) { + return false; + } + for (Map.Entry e : headers.entrySet()) { + if ("content-type".equalsIgnoreCase(e.getKey())) { + return "application/json".equals(e.getValue()); + } + } + return false; + } + + private static JsonArray serializeMultipartData(Map data) { + JsonArray result = new JsonArray(); + for (Map.Entry e : data.entrySet()) { + FilePayload filePayload = null; + if (e.getValue() instanceof FilePayload) { + filePayload = (FilePayload) e.getValue(); + } else if (e.getValue() instanceof Path) { + filePayload = toFilePayload((Path) e.getValue()); + } else if (e.getValue() instanceof File) { + filePayload = toFilePayload(((File) e.getValue()).toPath()); + } + JsonObject item = new JsonObject(); + item.addProperty("name", e.getKey()); + if (filePayload == null) { + item.addProperty("value", "" + e.getValue()); + } else { + item.add("file", toProtocol(filePayload)); + } + result.add(item); + } + return result; + } + + @Override + public APIResponse get(String url, GetOptions options) { + FetchOptions fetchOptions = toFetchOptions(options); + fetchOptions.method = "GET"; + return fetch(url, fetchOptions); + } + + @Override + public APIResponse head(String url, HeadOptions options) { + FetchOptions fetchOptions = toFetchOptions(options); + fetchOptions.method = "HEAD"; + return fetch(url, fetchOptions); + } + + @Override + public APIResponse patch(String url, PatchOptions options) { + FetchOptions fetchOptions = toFetchOptions(options); + fetchOptions.method = "PATCH"; + return fetch(url, fetchOptions); + } + + @Override + public APIResponse post(String url, PostOptions options) { + FetchOptions fetchOptions = toFetchOptions(options); + fetchOptions.method = "POST"; + return fetch(url, fetchOptions); + } + + @Override + public APIResponse put(String url, PutOptions options) { + FetchOptions fetchOptions = toFetchOptions(options); + fetchOptions.method = "PUT"; + return fetch(url, fetchOptions); + } + + @Override + public String storageState(StorageStateOptions options) { + return withLogging("APIRequestContext.storageState", () -> { + JsonElement json = sendMessage("storageState"); + String storageState = json.toString(); + if (options != null && options.path != null) { + Utils.writeToFile(storageState.getBytes(StandardCharsets.UTF_8), options.path); + } + return storageState; + }); + } + + private static FetchOptions toFetchOptions(T options) { + FetchOptions fetchOptions = convertViaReflection(options, FetchOptions.class); + if (fetchOptions == null) { + fetchOptions = new FetchOptions(); + } + return fetchOptions; + } +} diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/APIRequestImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/APIRequestImpl.java new file mode 100644 index 00000000..d6060b9f --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/impl/APIRequestImpl.java @@ -0,0 +1,53 @@ +package com.microsoft.playwright.impl; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.microsoft.playwright.APIRequest; +import com.microsoft.playwright.PlaywrightException; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; + +import static com.microsoft.playwright.impl.Serialization.gson; + +class APIRequestImpl implements APIRequest { + private final PlaywrightImpl playwright; + + APIRequestImpl(PlaywrightImpl playwright) { + this.playwright = playwright; + } + + @Override + public APIRequestContextImpl newContext(NewContextOptions options) { + return playwright.withLogging("APIRequest.newContext", () -> newContextImpl(options)); + } + + private APIRequestContextImpl newContextImpl(NewContextOptions options) { + if (options == null) { + options = new NewContextOptions(); + } + if (options.storageStatePath != null) { + try { + byte[] bytes = Files.readAllBytes(options.storageStatePath); + options.storageState = new String(bytes, StandardCharsets.UTF_8); + options.storageStatePath = null; + } catch (IOException e) { + throw new PlaywrightException("Failed to read storage state from file", e); + } + } + JsonObject storageState = null; + if (options.storageState != null) { + storageState = new Gson().fromJson(options.storageState, JsonObject.class); + options.storageState = null; + } + JsonObject params = gson().toJsonTree(options).getAsJsonObject(); + if (storageState != null) { + params.add("storageState", storageState); + } + + JsonObject result = playwright.sendMessage("newRequest", params).getAsJsonObject(); + APIRequestContextImpl context = playwright.connection.getExistingObject(result.getAsJsonObject("request").get("guid").getAsString()); + return context; + } +} diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/APIResponseImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/APIResponseImpl.java new file mode 100644 index 00000000..73dac5e2 --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/impl/APIResponseImpl.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.microsoft.playwright.impl; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.microsoft.playwright.APIResponse; +import com.microsoft.playwright.PlaywrightException; +import com.microsoft.playwright.options.HttpHeader; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.List; +import java.util.Map; + +import static com.microsoft.playwright.impl.Serialization.gson; +import static com.microsoft.playwright.impl.Utils.isSafeCloseError; +import static java.util.Arrays.asList; + +class APIResponseImpl implements APIResponse { + private final APIRequestContextImpl context; + private final JsonObject initializer; + private final RawHeaders headers; + + APIResponseImpl(APIRequestContextImpl apiRequestContext, JsonObject response) { + context = apiRequestContext; + initializer = response; + headers = new RawHeaders(asList(gson().fromJson(initializer.getAsJsonArray("headers"), HttpHeader[].class))); + } + + @Override + public byte[] body() { + return context.withLogging("APIResponse.body", () -> { + try { + JsonObject params = new JsonObject(); + params.addProperty("fetchUid", fetchUid()); + JsonObject json = context.sendMessage("fetchResponseBody", params).getAsJsonObject(); + if (!json.has("binary")) { + throw new PlaywrightException("Response has been disposed"); + } + return Base64.getDecoder().decode(json.get("binary").getAsString()); + } catch (PlaywrightException e) { + if (isSafeCloseError(e)) { + throw new PlaywrightException("Response has been disposed"); + } + throw e; + } + }); + } + + @Override + public void dispose() { + context.withLogging("APIResponse.dispose", () -> { + JsonObject params = new JsonObject(); + params.addProperty("fetchUid", fetchUid()); + context.sendMessage("disposeAPIResponse", params); + }); + } + + @Override + public Map headers() { + return headers.headers(); + } + + @Override + public List headersArray() { + return headers.headersArray(); + } + + @Override + public boolean ok() { + int status = status(); + return status == 0 || (status >= 200 && status <= 299); + } + + @Override + public int status() { + return initializer.get("status").getAsInt(); + } + + @Override + public String statusText() { + return initializer.get("statusText").getAsString(); + } + + @Override + public String text() { + return new String(body(), StandardCharsets.UTF_8); + } + + @Override + public String url() { + return initializer.get("url").getAsString(); + } + + private String fetchUid() { + return initializer.get("fetchUid").getAsString(); + } + +} 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 d68f871c..5554d8d4 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java @@ -47,6 +47,7 @@ import static java.util.Arrays.asList; class BrowserContextImpl extends ChannelOwner implements BrowserContext { private final BrowserImpl browser; private final TracingImpl tracing; + private final APIRequestContextImpl request; final List pages = new ArrayList<>(); final Router routes = new Router(); private boolean isClosedOrClosing; @@ -75,6 +76,7 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext { browser = null; } this.tracing = new TracingImpl(this); + this.request = connection.getExistingObject(initializer.getAsJsonObject("APIRequestContext").get("guid").getAsString()); } void setBaseUrl(String spec) { @@ -329,6 +331,11 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext { return new ArrayList<>(pages); } + @Override + public APIRequestContextImpl request() { + return request; + } + @Override public void route(String url, Consumer handler, RouteOptions options) { route(new UrlMatcher(this.baseUrl, url), handler, options); diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java b/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java index 2e3e7925..b11c3f6c 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java @@ -303,7 +303,7 @@ public class Connection { break; case "APIRequestContext": // Create fake object as this API is experimental an only exposed in Node.js. - result = new ChannelOwner(parent, type, guid, initializer); + result = new APIRequestContextImpl(parent, type, guid, initializer); break; case "Frame": result = new FrameImpl(parent, type, guid, initializer); 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 15ea55c0..28185215 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java @@ -170,7 +170,9 @@ public class PageImpl extends ChannelOwner implements Page { try { bindingCall.call(binding); } catch (RuntimeException e) { - e.printStackTrace(); + if (!isSafeCloseError(e.getMessage())) { + logWithTimestamp(e.getMessage()); + } } } } else if ("load".equals(event)) { @@ -941,6 +943,11 @@ public class PageImpl extends ChannelOwner implements Page { return withLogging("Page.reload", () -> reloadImpl(options)); } + @Override + public APIRequestContextImpl request() { + return browserContext.request(); + } + private Response reloadImpl(ReloadOptions options) { if (options == null) { options = new ReloadOptions(); diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/PlaywrightImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/PlaywrightImpl.java index d975fedb..b70b9498 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/PlaywrightImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/PlaywrightImpl.java @@ -17,6 +17,7 @@ package com.microsoft.playwright.impl; import com.google.gson.JsonObject; +import com.microsoft.playwright.APIRequest; import com.microsoft.playwright.Playwright; import com.microsoft.playwright.PlaywrightException; import com.microsoft.playwright.Selectors; @@ -55,6 +56,7 @@ public class PlaywrightImpl extends ChannelOwner implements Playwright { private final BrowserTypeImpl firefox; private final BrowserTypeImpl webkit; private final SelectorsImpl selectors; + private final APIRequestImpl apiRequest; private SharedSelectors sharedSelectors;; PlaywrightImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) { @@ -64,6 +66,7 @@ public class PlaywrightImpl extends ChannelOwner implements Playwright { webkit = parent.connection.getExistingObject(initializer.getAsJsonObject("webkit").get("guid").getAsString()); selectors = connection.getExistingObject(initializer.getAsJsonObject("selectors").get("guid").getAsString()); + apiRequest = new APIRequestImpl(this); } void initSharedSelectors(PlaywrightImpl parent) { @@ -90,6 +93,11 @@ public class PlaywrightImpl extends ChannelOwner implements Playwright { return firefox; } + @Override + public APIRequest request() { + return apiRequest; + } + @Override public BrowserTypeImpl webkit() { return webkit; 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 dcb65ff7..d857dd91 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/Serialization.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/Serialization.java @@ -212,15 +212,19 @@ class Serialization { static JsonArray toJsonArray(FilePayload[] files) { JsonArray jsonFiles = new JsonArray(); for (FilePayload p : files) { - JsonObject jsonFile = new JsonObject(); - jsonFile.addProperty("name", p.name); - jsonFile.addProperty("mimeType", p.mimeType); - jsonFile.addProperty("buffer", Base64.getEncoder().encodeToString(p.buffer)); - jsonFiles.add(jsonFile); + jsonFiles.add(toProtocol(p)); } return jsonFiles; } + static JsonObject toProtocol(FilePayload p) { + JsonObject jsonFile = new JsonObject(); + jsonFile.addProperty("name", p.name); + jsonFile.addProperty("mimeType", p.mimeType); + jsonFile.addProperty("buffer", Base64.getEncoder().encodeToString(p.buffer)); + return jsonFile; + } + static JsonArray toProtocol(ElementHandle[] handles) { JsonArray jsonElements = new JsonArray(); for (ElementHandle handle : handles) { @@ -232,11 +236,15 @@ class Serialization { } static JsonArray toProtocol(Map map) { + return toNameValueArray(map); + } + + static JsonArray toNameValueArray(Map map) { JsonArray array = new JsonArray(); - for (Map.Entry e : map.entrySet()) { + for (Map.Entry e : map.entrySet()) { JsonObject item = new JsonObject(); item.addProperty("name", e.getKey()); - item.addProperty("value", e.getValue()); + item.add("value", gson().toJsonTree(e.getValue())); array.add(item); } return array; 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 ddd0ae5e..ba7e2051 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/Utils.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/Utils.java @@ -24,6 +24,9 @@ import com.microsoft.playwright.options.HttpHeader; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Modifier; import java.lang.reflect.Type; import java.nio.file.Files; import java.nio.file.Path; @@ -31,6 +34,34 @@ import java.nio.file.Paths; import java.util.*; class Utils { + static T convertViaReflection(F f, Class t) { + if (f == null) { + return null; + } + + try { + T result = t.getDeclaredConstructor().newInstance(); + for (Field toField : t.getDeclaredFields()) { + if (Modifier.isStatic(toField.getModifiers()) || + !Modifier.isPublic(toField.getModifiers())) { + continue; + } + try { + Field fromField = f.getClass().getDeclaredField(toField.getName()); + Object value = fromField.get(f); + if (value != null) { + toField.set(result, value); + } + } catch (NoSuchFieldException e) { + continue; + } + } + return result; + } catch (Exception e) { + throw new PlaywrightException("Internal error", e); + } + } + // TODO: generate converter. static T convertViaJson(F f, Class t) { Gson gson = new GsonBuilder() @@ -126,17 +157,21 @@ class Utils { static FilePayload[] toFilePayloads(Path[] files) { List payloads = new ArrayList<>(); for (Path file : files) { - byte[] buffer; - try { - buffer = Files.readAllBytes(file); - } catch (IOException e) { - throw new PlaywrightException("Failed to read from file", e); - } - payloads.add(new FilePayload(file.getFileName().toString(), mimeType(file), buffer)); + payloads.add(toFilePayload(file)); } return payloads.toArray(new FilePayload[0]); } + static FilePayload toFilePayload(Path file) { + byte[] buffer; + try { + buffer = Files.readAllBytes(file); + } catch (IOException e) { + throw new PlaywrightException("Failed to read from file", e); + } + return new FilePayload(file.getFileName().toString(), mimeType(file), buffer); + } + static void mkParentDirs(Path file) { Path dir = file.getParent(); if (dir != null) { @@ -177,7 +212,7 @@ class Utils { } static boolean isSafeCloseError(String error) { - return error.endsWith("Browser has been closed") || error.endsWith("Target page, context or browser has been closed"); + return error.contains("Browser has been closed") || error.contains("Target page, context or browser has been closed"); } static String createGuid() { diff --git a/playwright/src/test/java/com/microsoft/playwright/Server.java b/playwright/src/test/java/com/microsoft/playwright/Server.java index 1a7e2c51..ca751d20 100644 --- a/playwright/src/test/java/com/microsoft/playwright/Server.java +++ b/playwright/src/test/java/com/microsoft/playwright/Server.java @@ -98,12 +98,14 @@ public class Server implements HttpHandler { } static class Request { + public final String url; public final String method; // TODO: make a copy to ensure thread safety? public final Headers headers; public final byte[] postBody; Request(HttpExchange exchange) throws IOException { + url = exchange.getRequestURI().toString(); method = exchange.getRequestMethod(); headers = exchange.getRequestHeaders(); ByteArrayOutputStream out = new ByteArrayOutputStream(); diff --git a/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextAddCookies.java b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextAddCookies.java index 59bc3631..68b05b71 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextAddCookies.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextAddCookies.java @@ -26,7 +26,6 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import static com.microsoft.playwright.Utils.assertJsonEquals; -import static com.microsoft.playwright.Utils.getOS; import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; diff --git a/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextFetch.java b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextFetch.java new file mode 100644 index 00000000..36fcd32e --- /dev/null +++ b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextFetch.java @@ -0,0 +1,689 @@ +package com.microsoft.playwright; + +import com.google.gson.Gson; +import com.microsoft.playwright.options.Cookie; +import com.microsoft.playwright.options.FilePayload; +import com.microsoft.playwright.options.HttpHeader; +import com.microsoft.playwright.options.SameSiteAttribute; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.*; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +import static com.microsoft.playwright.Utils.*; +import static java.util.Arrays.asList; +import static org.junit.jupiter.api.Assertions.*; + +public class TestBrowserContextFetch extends TestBase { + @Test + void getShouldWork() { + APIResponse response = context.request().get(server.PREFIX + "/simple.json"); + assertEquals(server.PREFIX + "/simple.json", response.url()); + assertEquals(200, response.status()); + assertEquals("OK", response.statusText()); + assertNotNull(response.ok()); + assertEquals(server.PREFIX + "/simple.json", response.url()); + assertEquals("application/json", response.headers().get("content-type")); + Optional contentType = response.headersArray().stream().filter(h -> "content-type".equals(h.name.toLowerCase())).findFirst(); + assertTrue(contentType.isPresent()); + assertEquals("application/json", contentType.get().value); + assertEquals("{\"foo\": \"bar\"}\n", response.text()); + } + + @Test + void fetchShouldWork() { + APIResponse response = context.request().fetch(server.PREFIX + "/simple.json"); + assertEquals(server.PREFIX + "/simple.json", response.url()); + assertEquals(200, response.status()); + assertEquals("OK", response.statusText()); + assertNotNull(response.ok()); + assertEquals(server.PREFIX + "/simple.json", response.url()); + Optional contentType = response.headersArray().stream().filter(h -> "content-type".equals(h.name.toLowerCase())).findFirst(); + assertTrue(contentType.isPresent()); + assertEquals("application/json", contentType.get().value); + assertEquals("{\"foo\": \"bar\"}\n", response.text()); + } + + // server.setRoute("/one-style.css", exchange -> exchange.getResponseBody().close()); + @Test + void shouldThrowOnNetworkError() { + server.setRoute("/test", exchange -> exchange.getResponseBody().close()); + try { + context.request().get(server.PREFIX + "/test"); + fail("did not throw"); + } catch (PlaywrightException e) { + assertTrue(e.getMessage().contains("socket hang up"), e.getMessage()); + } + } + + @Test + void shouldThrowOnNetworkErrorAfterRedirect() { + server.setRedirect("/redirect", "/test"); + server.setRoute("/test", exchange -> exchange.getResponseBody().close()); + try { + context.request().get(server.PREFIX + "/redirect"); + fail("did not throw"); + } catch (PlaywrightException e) { + assertTrue(e.getMessage().contains("socket hang up"), e.getMessage()); + } + } + + @Test + void shouldThrowOnNetworkErrorWhenSendingBody() { + server.setRoute("/test", exchange -> { + exchange.getResponseHeaders().add("content-type", "text/html"); + exchange.sendResponseHeaders(200, 4096); + try (OutputStreamWriter writer = new OutputStreamWriter(exchange.getResponseBody())) { + writer.write("A"); + } + }); + try { + context.request().get(server.PREFIX + "/test"); + fail("did not throw"); + } catch (PlaywrightException e) { + assertTrue(e.getMessage().contains("aborted"), e.getMessage()); + } + } + + @Test + void shouldThrowOnNetworkErrorWhenSendingBodyAfterRedirect() { + server.setRedirect("/redirect", "/test"); + server.setRoute("/test", exchange -> { + exchange.getResponseHeaders().add("content-type", "text/html"); + exchange.sendResponseHeaders(200, 4096); + try (OutputStreamWriter writer = new OutputStreamWriter(exchange.getResponseBody())) { + writer.write("<title>A"); + } + }); + try { + context.request().get(server.PREFIX + "/redirect"); + fail("did not throw"); + } catch (PlaywrightException e) { + assertTrue(e.getMessage().contains("aborted"), e.getMessage()); + } + } + + @Test + void shouldAddSessionCookiesToRequest() throws ExecutionException, InterruptedException { + Cookie cookie = new Cookie("username", "John Doe"); + cookie.domain = "localhost"; + cookie.path = "/"; + cookie.expires = -1.0; + cookie.httpOnly = false; + cookie.secure = false; + cookie.sameSite = SameSiteAttribute.LAX; + context.addCookies(asList(cookie)); + Future<Server.Request> req = server.futureRequest("/simple.json"); + context.request().get(server.PREFIX + "/simple.json"); + assertEquals(asList("username=John Doe"), req.get().headers.get("cookie")); + } + + @Test + void getShouldSupportQueryParams() throws ExecutionException, InterruptedException { + Future<Server.Request> req = server.futureRequest("/empty.html"); + context.request().get(server.EMPTY_PAGE + "?p1=foo", + new APIRequestContext.GetOptions().setParams(mapOf("p1", "v1", "парам2", "знач2"))); + assertNotNull(req.get()); + assertEquals("/empty.html?p1=v1&%D0%BF%D0%B0%D1%80%D0%B0%D0%BC2=%D0%B7%D0%BD%D0%B0%D1%872", req.get().url); + } + + ; + + @Test + void getShouldSupportFailOnStatusCode() { + try { + context.request().get(server.PREFIX + "/does-not-exist.html", new APIRequestContext.GetOptions().setFailOnStatusCode(true)); + fail("did not throw"); + } catch (PlaywrightException e) { + assertTrue(e.getMessage().contains("404 Not Found"), e.getMessage()); + } + } + + @Test + @Disabled("Error: socket hang up") + void getShouldSupportIgnoreHTTPSErrorsOption() { + APIResponse response = context.request().get(httpsServer.EMPTY_PAGE, new APIRequestContext.GetOptions().setIgnoreHTTPSErrors(true)); + assertEquals(200, response.status()); + } + + @Test + void shouldNotAddContextCookieIfCookieHeaderPassedAsAParameter() throws ExecutionException, InterruptedException { + Cookie cookie = new Cookie("username", "John Doe"); + cookie.domain = "localhost"; + cookie.path = "/"; + cookie.expires = -1.0; + cookie.httpOnly = false; + cookie.secure = false; + cookie.sameSite = SameSiteAttribute.LAX; + context.addCookies(asList(cookie)); + Future<Server.Request> req = server.futureRequest("/empty.html"); + context.request().get(server.EMPTY_PAGE, new APIRequestContext.GetOptions().setHeaders(mapOf("Cookie", "foo=bar"))); + assertEquals(asList("foo=bar"), req.get().headers.get("cookie")); + } + + @Test + void shouldFollowRedirects() throws ExecutionException, InterruptedException { + server.setRedirect("/redirect1", "/redirect2"); + server.setRedirect("/redirect2", "/simple.json"); + Cookie cookie = new Cookie("username", "John Doe"); + cookie.domain = "localhost"; + cookie.path = "/"; + cookie.expires = -1.0; + cookie.httpOnly = false; + cookie.secure = false; + cookie.sameSite = SameSiteAttribute.LAX; + context.addCookies(asList(cookie)); + + Future<Server.Request> req = server.futureRequest("/simple.json"); + APIResponse response = context.request().get(server.PREFIX + "/redirect1"); + assertEquals(asList("username=John Doe"), req.get().headers.get("cookie")); + assertEquals(server.PREFIX + "/simple.json", response.url()); + assertEquals("{\"foo\": \"bar\"}\n", response.text()); + } + + @Test + void shouldAddCookiesFromSetCookieHeader() { + server.setRoute("/setcookie.html", exchange -> { + exchange.getResponseHeaders().add("Set-Cookie", "session=value"); + exchange.getResponseHeaders().add("Set-Cookie", "foo=bar; max-age=3600"); + exchange.sendResponseHeaders(200, 0); + exchange.getResponseBody().close(); + }); + + context.request().get(server.PREFIX + "/setcookie.html"); + List<Cookie> cookies = context.cookies(); + assertEquals(2, cookies.size()); + cookies.sort(Comparator.comparing(a -> a.name)); + assertEquals("foo", cookies.get(0).name); + assertEquals("bar", cookies.get(0).value); + assertEquals("session", cookies.get(1).name); + assertEquals("value", cookies.get(1).value); + page.navigate(server.EMPTY_PAGE); + assertEquals(asList("foo=bar", "session=value"), page.evaluate("() => document.cookie.split(';').map(s => s.trim()).sort()")); + } + + @Test + @Disabled("Default Java's HTTP server throws on 'CONNECT non-existent.com:80 HTTP/1.1' because path is null.") + void shouldWorkWithContextLevelProxy() throws ExecutionException, InterruptedException { + server.setRoute("/target.html", exchange -> { + exchange.getResponseHeaders().add("Content-Type", "text/plain; charset=utf-8"); + exchange.sendResponseHeaders(200, 0); + try (OutputStreamWriter writer = new OutputStreamWriter(exchange.getResponseBody())) { + writer.write("<title>Served by the proxy"); + } + }); + try (Browser browser = browserType.launch(new BrowserType.LaunchOptions().setProxy("http://per-context"))) { + BrowserContext context = browser.newContext(new Browser.NewContextOptions().setProxy("localhost:" + server.PORT)); + Future request = server.futureRequest("/target.html"); + APIResponse response = context.request().get("http://non-existent.com/target.html"); + + assertEquals(200, response.status()); + assertEquals("/target.html", request.get().url); + } + } + + + @Test + void shouldWorkWithHttpCredentials() throws ExecutionException, InterruptedException { + server.setAuth("/empty.html", "user", "pass"); + + String base64 = Base64.getEncoder().encodeToString("user:pass".getBytes(StandardCharsets.UTF_8));; + Future request = server.futureRequest("/empty.html"); + APIResponse response = context.request().get(server.EMPTY_PAGE, new APIRequestContext.GetOptions().setHeaders( + mapOf("authorization", "Basic " + base64) + )); + assertEquals(200, response.status()); + assertEquals("/empty.html", request.get().url); + } + + @Test + void shouldWorkWithSetHTTPCredentials() { + server.setAuth("/empty.html", "user", "pass"); + APIResponse response1 = context.request().get(server.EMPTY_PAGE); + assertEquals(401, response1.status()); + + try (BrowserContext context2 = browser.newContext( + new Browser.NewContextOptions().setHttpCredentials("user", "pass"))) { + APIResponse response2 = context2.request().get(server.EMPTY_PAGE); + assertEquals(200, response2.status()); + } + } + + @Test + void shouldReturnErrorWithWrongCredentials() { + server.setAuth("/empty.html", "user", "pass"); + try (BrowserContext context = browser.newContext( + new Browser.NewContextOptions().setHttpCredentials("user", "wrong"))) { + APIResponse response = context.request().get(server.EMPTY_PAGE); + assertEquals(401, response.status()); + } + } + + @Test + void postShouldSupportPostData() throws ExecutionException, InterruptedException { + Future request = server.futureRequest("/simple.json"); + APIResponse response = context.request().post(server.PREFIX + "/simple.json", + new APIRequestContext.PostOptions().setData("My request")); + assertEquals("POST", request.get().method); + assertEquals("My request", new String(request.get().postBody)); + assertEquals(200, response.status()); + assertEquals("/simple.json", request.get().url); + } + + @Test + void deleteShouldSupportPostData() throws ExecutionException, InterruptedException { + Future request = server.futureRequest("/simple.json"); + APIResponse response = context.request().delete(server.PREFIX + "/simple.json", + new APIRequestContext.DeleteOptions().setData("My request")); + assertEquals("DELETE", request.get().method); + assertEquals("My request", new String(request.get().postBody)); + assertEquals(200, response.status()); + assertEquals("/simple.json", request.get().url); + } + + @Test + void patchShouldSupportPostData() throws ExecutionException, InterruptedException { + Future request = server.futureRequest("/simple.json"); + APIResponse response = context.request().patch(server.PREFIX + "/simple.json", + new APIRequestContext.PatchOptions().setData("My request")); + assertEquals("PATCH", request.get().method); + assertEquals("My request", new String(request.get().postBody)); + assertEquals(200, response.status()); + assertEquals("/simple.json", request.get().url); + } + + @Test + void putShouldSupportPostData() throws ExecutionException, InterruptedException { + Future request = server.futureRequest("/simple.json"); + APIResponse response = context.request().put(server.PREFIX + "/simple.json", + new APIRequestContext.PutOptions().setData("My request")); + assertEquals("PUT", request.get().method); + assertEquals("My request", new String(request.get().postBody)); + assertEquals(200, response.status()); + assertEquals("/simple.json", request.get().url); + } + + + @Test + void shouldAddDefaultHeaders() throws ExecutionException, InterruptedException { + Future request = server.futureRequest("/empty.html"); + context.request().get(server.EMPTY_PAGE); + + assertEquals(asList("*/*"), request.get().headers.get("accept")); + Object userAgent = page.evaluate("() => navigator.userAgent"); + assertEquals(asList(userAgent), request.get().headers.get("user-agent")); + assertEquals(asList("gzip,deflate,br"), request.get().headers.get("accept-encoding")); + } + + @Test + void shouldSendContentLength() throws ExecutionException, InterruptedException { + byte[] bytes = new byte[256]; + for (int i = 0; i < 256; i++) { + bytes[i] = (byte) i; + } + Future request = server.futureRequest("/empty.html"); + context.request().post(server.EMPTY_PAGE, new APIRequestContext.PostOptions().setData(bytes)); + assertEquals(asList("256"), request.get().headers.get("content-length")); + assertEquals(asList("application/octet-stream"), request.get().headers.get("content-type")); + } + + @Test + void shouldAddDefaultHeadersToRedirects() throws ExecutionException, InterruptedException { + server.setRedirect("/redirect", "/empty.html"); + Future request = server.futureRequest("/empty.html"); + context.request().get(server.PREFIX + "/redirect"); + + assertEquals(asList("*/*"), request.get().headers.get("accept")); + Object userAgent = page.evaluate("() => navigator.userAgent"); + assertEquals(asList(userAgent), request.get().headers.get("user-agent")); + assertEquals(asList("gzip,deflate,br"), request.get().headers.get("accept-encoding")); + } + + @Test + void shouldAllowToOverrideDefaultHeaders() throws ExecutionException, InterruptedException { + Future request = server.futureRequest("/empty.html"); + context.request().get(server.EMPTY_PAGE, new APIRequestContext.GetOptions().setHeaders( + mapOf( + "User-Agent", "Playwright", + "Accept", "text/html", + "Accept-Encoding", "br" + ))); + assertEquals(asList("text/html"), request.get().headers.get("accept")); + assertEquals(asList("Playwright"), request.get().headers.get("user-agent")); + assertEquals(asList("br"), request.get().headers.get("accept-encoding")); + } + + @Test + void shouldPropagateCustomHeadersWithRedirects() throws ExecutionException, InterruptedException { + server.setRedirect("/a/redirect1", "/b/c/redirect2"); + server.setRedirect("/b/c/redirect2", "/simple.json"); + Future req1 = server.futureRequest("/a/redirect1"); + Future req2 = server.futureRequest("/b/c/redirect2"); + Future req3 = server.futureRequest("/simple.json"); + context.request().get(server.PREFIX + "/a/redirect1", new APIRequestContext.GetOptions().setHeaders( + mapOf("foo", "bar") + )); + assertEquals(asList("bar"), req1.get().headers.get("foo")); + assertEquals(asList("bar"), req2.get().headers.get("foo")); + assertEquals(asList("bar"), req3.get().headers.get("foo")); + } + + @Test + void shouldPropagateExtraHttpHeadersWithRedirects() throws ExecutionException, InterruptedException { + server.setRedirect("/a/redirect1", "/b/c/redirect2"); + server.setRedirect("/b/c/redirect2", "/simple.json"); + context.setExtraHTTPHeaders(mapOf("My-Secret", "Value")); + Future req1 = server.futureRequest("/a/redirect1"); + Future req2 = server.futureRequest("/b/c/redirect2"); + Future req3 = server.futureRequest("/simple.json"); + context.request().get(server.PREFIX + "/a/redirect1"); + + assertEquals(asList("Value"), req1.get().headers.get("my-secret")); + assertEquals(asList("Value"), req2.get().headers.get("my-secret")); + assertEquals(asList("Value"), req3.get().headers.get("my-secret")); + } + + @Test + void shouldThrowOnInvalidHeaderValue() { + try { + context.request().get(server.EMPTY_PAGE, new APIRequestContext.GetOptions() + .setHeaders(mapOf("foo", "недопустимое значение"))); + fail("did not throw"); + } catch (PlaywrightException e) { + assertTrue(e.getMessage().contains("Invalid character in header content"), e.getMessage()); + } + } + + @Test + void shouldThrowOnNonHttpSProtocol() { + try { + context.request().get("data:text/plain,test"); + fail("did not throw"); + } catch (PlaywrightException e) { + assertTrue(e.getMessage().contains("Protocol \"data:\" not supported"), e.getMessage()); + } + try { + context.request().get("file:///tmp/foo"); + fail("did not throw"); + } catch (PlaywrightException e) { + assertTrue(e.getMessage().contains("Protocol \"file:\" not supported"), e.getMessage()); + } + } + + @Test + void shouldSupportTimeoutOption() { + server.setRoute("/slow", exchange -> { + exchange.getResponseHeaders().add("content-type", "text/html"); + exchange.sendResponseHeaders(200, 4096); + }); + + try { + context.request().get(server.PREFIX + "/slow", new APIRequestContext.GetOptions().setTimeout(100)); + fail("did not throw"); + } catch (PlaywrightException e) { + assertTrue(e.getMessage().contains("Request timed out after 100ms"), e.getMessage()); + } + } + + @Test + void shouldSupportATimeoutOf0() { + server.setRoute("/slow", exchange -> { + exchange.getResponseHeaders().add("content-type", "text/html"); + exchange.sendResponseHeaders(200, 4); + try { + Thread.sleep(100); + } catch (InterruptedException e) { + e.printStackTrace(); + } + try (OutputStreamWriter writer = new OutputStreamWriter(exchange.getResponseBody())) { + writer.write("done"); + } + }); + APIResponse response = context.request().get(server.PREFIX + "/slow", + new APIRequestContext.GetOptions().setTimeout(0)); + assertEquals("done", response.text()); + } + + @Test + void shouldRespectTimeoutAfterRedirects() { + server.setRedirect("/redirect", "/slow"); + server.setRoute("/slow", exchange -> { + exchange.getResponseHeaders().add("content-type", "text/html"); + exchange.sendResponseHeaders(200, 4096); + }); + + context.setDefaultTimeout(100); + try { + context.request().get(server.PREFIX + "/redirect"); + fail("did not throw"); + } catch (PlaywrightException e) { + assertTrue(e.getMessage().contains("Request timed out after 100ms"), e.getMessage()); + } + } + + @Test + void shouldDispose() { + APIResponse response = context.request().get(server.PREFIX + "/simple.json"); + assertEquals("{\"foo\": \"bar\"}\n", response.text()); + response.dispose(); + try { + response.body(); + fail("did not throw"); + } catch (PlaywrightException e) { + assertTrue(e.getMessage().contains("Response has been disposed"), e.getMessage()); + } + } + + @Test + void shouldDisposeWhenContextCloses() { + APIResponse response = context.request().get(server.PREFIX + "/simple.json"); + assertEquals("{\"foo\": \"bar\"}\n", response.text()); + context.close(); + try { + response.body(); + fail("did not throw"); + } catch (PlaywrightException e) { + assertTrue(e.getMessage().contains("Response has been disposed") || + e.getMessage().contains("Target page, context or browser has been closed"), e.getMessage()); + } + } + @Test + void shouldOverrideRequestParameters() throws ExecutionException, InterruptedException { + Request pageReq = page.waitForRequest("**/*", () -> page.navigate(server.EMPTY_PAGE)); + Future req = server.futureRequest("/empty.html"); + context.request().fetch(pageReq, new APIRequestContext.FetchOptions().setMethod("POST") + .setHeaders(mapOf("foo", "bar")) + .setData("data")); + assertEquals("POST", req.get().method); + assertEquals(asList("bar"), req.get().headers.get("foo")); + assertEquals("data", new String(req.get().postBody)); + } + + @Test + void shouldSupportApplicationXWwwFormUrlencoded() throws ExecutionException, InterruptedException { + Future req = server.futureRequest("/empty.html"); + context.request().post(server.EMPTY_PAGE, new APIRequestContext.PostOptions().setForm( + mapOf("firstName", "John", + "lastName", "Doe", + "file", "f.js"))); + + assertEquals("POST", req.get().method); + assertEquals(asList("application/x-www-form-urlencoded"), req.get().headers.get("content-type")); + String body = new String(req.get().postBody); + assertTrue(body.contains("firstName=John")); + assertTrue(body.contains("lastName=Doe")); + assertTrue(body.contains("file=f.js")); + } + + @Test + void shouldEncodeToApplicationJsonByDefault() throws ExecutionException, InterruptedException { + Map data = mapOf( + "firstName", "John", + "lastName", "Doe", + "file", mapOf("name", "f.js") + ); + Future req = server.futureRequest("/empty.html"); + context.request().post(server.EMPTY_PAGE, new APIRequestContext.PostOptions().setData(data)); + assertEquals("POST", req.get().method); + assertEquals(asList("application/json"), req.get().headers.get("content-type")); + String body = new String(req.get().postBody); + assertEquals(new Gson().toJson(data), body); + } + + @Test + void shouldSupportMultipartFormData() throws ExecutionException, InterruptedException { + Future serverRequest = server.futureRequest("/empty.html"); + + FilePayload file = new FilePayload("f.js", "text/javascript", + "var x = 10;\r\n;console.log(x);".getBytes(StandardCharsets.UTF_8)); + APIResponse response = context.request().post(server.EMPTY_PAGE, new APIRequestContext.PostOptions().setMultipart( + mapOf("firstName", "John", + "lastName", "Doe", + "file", file + ))); + + assertEquals("POST", serverRequest.get().method); + List contentType = serverRequest.get().headers.get("content-type"); + assertNotNull(contentType); + assertEquals(1, contentType.size()); + assertTrue(contentType.get(0).contains("multipart/form-data"), contentType.get(0)); + + String body = new String(serverRequest.get().postBody); + assertTrue(body.contains("content-disposition: form-data; name=\"firstName\"\r\n" + + "\r\n" + + "John"), body); + assertTrue(body.contains("content-disposition: form-data; name=\"lastName\"\r\n" + + "\r\n" + + "Doe"), body); + assertTrue(body.contains("content-disposition: form-data; name=\"file\"; filename=\"f.js\"\r\n" + + "content-type: text/javascript\r\n" + + "\r\n" + + "var x = 10;\r\n" + + ";console.log(x);"), body); + assertEquals(200, response.status()); + } + + @Test + void shouldSupportMultipartFormDataWithPathValues(@TempDir Path tmp) throws ExecutionException, InterruptedException, IOException { + Future serverRequest = server.futureRequest("/empty.html"); + + Path path = tmp.resolve("simplezip.json"); + try (FileOutputStream output = new FileOutputStream(path.toFile())) { + output.write("{\"foo\":\"bar\"}".getBytes(StandardCharsets.UTF_8)); + } + APIResponse response = context.request().post(server.EMPTY_PAGE, new APIRequestContext.PostOptions().setMultipart( + mapOf("firstName", "John", + "lastName", "Doe", + "file", path + ))); + + assertEquals("POST", serverRequest.get().method); + List contentType = serverRequest.get().headers.get("content-type"); + assertNotNull(contentType); + assertEquals(1, contentType.size()); + assertTrue(contentType.get(0).contains("multipart/form-data"), contentType.get(0)); + + String body = new String(serverRequest.get().postBody); + assertTrue(body.contains("content-disposition: form-data; name=\"firstName\"\r\n" + + "\r\n" + + "John"), body); + assertTrue(body.contains("content-disposition: form-data; name=\"lastName\"\r\n" + + "\r\n" + + "Doe"), body); + assertTrue(body.contains("content-disposition: form-data; name=\"file\"; filename=\"simplezip.json\"\r\n" + + (isMac ? "content-type: application/octet-stream\r\n" : "content-type: application/json\r\n") + + "\r\n" + + "{\"foo\":\"bar\"}"), body); + assertEquals(200, response.status()); + } + + @Test + void shouldSerializeDataToJsonRegardlessOfContentType() throws ExecutionException, InterruptedException { + Map data = mapOf( + "firstName", "John", + "lastName", "Doe"); + Future req = server.futureRequest("/empty.html"); + context.request().post(server.EMPTY_PAGE, new APIRequestContext.PostOptions() + .setHeaders(mapOf("content-type", "unknown")) + .setData(data)); + assertEquals("POST", req.get().method); + assertEquals(asList("unknown"), req.get().headers.get("content-type")); + String body = new String(req.get().postBody); + assertEquals(new Gson().toJson(data), body); + } + + @Test + void shouldThrowWhenDataPassedForUnsupportedRequest() { + try { + context.request().fetch(server.EMPTY_PAGE, new APIRequestContext.FetchOptions() + .setMethod("GET").setData("bar")); + fail("did not throw"); + } catch (PlaywrightException e) { + assertTrue(e.getMessage().contains("Method GET does not accept post data"), e.getMessage()); + } + } + + + @Test + void contextRequestShouldExportSameStorageStateAsContext() { + server.setRoute("/setcookie.html", exchange -> { + exchange.getResponseHeaders().add("Set-Cookie", "a=b"); + exchange.getResponseHeaders().add("Set-Cookie", "c=d"); + exchange.sendResponseHeaders(200, 0); + exchange.getResponseBody().close(); + }); + context.request().get(server.PREFIX + "/setcookie.html"); + String contextState = context.storageState(); + assertEquals(2, context.cookies().size()); + String requestState = context.request().storageState(); + assertEquals(contextState, requestState); + String pageState = page.request().storageState(); + assertEquals(contextState, pageState); + } + + @Test + void shouldAcceptBoolAndNumericParams() throws ExecutionException, InterruptedException { + Future req = server.futureRequest("/empty.html"); + page.request().get(server.EMPTY_PAGE, new APIRequestContext.GetOptions().setParams(mapOf( + "str", "s", + "num", 10, + "bool", true, + "bool2", false + ))); + assertEquals("/empty.html?str=s&bool2=false&bool=true&num=10", req.get().url); + } + + @Test + void shouldAbortRequestsWhenBrowserContextCloses() { + server.setRoute("/empty.html", exchange -> { + }); + + BrowserContext context = browser.newContext(); + Page page = context.newPage(); + + page.exposeFunction("closeContext", (Object... args) -> { + context.close(); + return null; + }); + page.evaluate("() => setTimeout(closeContext, 1000);"); + try { + context.request().get(server.EMPTY_PAGE); + fail("did not throw"); + } catch (PlaywrightException e) { + assertTrue(e.getMessage().contains("Request context disposed"), e.getMessage()); + } + + try { + context.request().post(server.EMPTY_PAGE); + fail("did not throw"); + } catch (PlaywrightException e) { + assertTrue(e.getMessage().contains("Target page, context or browser has been closed"), e.getMessage()); + } + } +} diff --git a/playwright/src/test/java/com/microsoft/playwright/TestGlobalFetch.java b/playwright/src/test/java/com/microsoft/playwright/TestGlobalFetch.java new file mode 100644 index 00000000..6f138124 --- /dev/null +++ b/playwright/src/test/java/com/microsoft/playwright/TestGlobalFetch.java @@ -0,0 +1,304 @@ +package com.microsoft.playwright; + +import com.google.gson.Gson; +import com.microsoft.playwright.options.HttpHeader; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +import static com.microsoft.playwright.Utils.mapOf; +import static java.util.Arrays.asList; +import static org.junit.jupiter.api.Assertions.*; + +public class TestGlobalFetch extends TestBase { + @Test + void fetchShouldWork() { + APIRequestContext request = playwright.request().newContext(); + APIResponse response = request.fetch(server.PREFIX + "/simple.json"); + assertEquals(server.PREFIX + "/simple.json", response.url()); + assertEquals(200, response.status()); + assertEquals("OK", response.statusText()); + assertTrue(response.ok()); + assertEquals("application/json", response.headers().get("content-type")); + Optional contentType = response.headersArray().stream().filter(h -> "content-type".equals(h.name.toLowerCase())).findFirst(); + assertTrue(contentType.isPresent()); + assertEquals("application/json", contentType.get().value); + assertEquals("{\"foo\": \"bar\"}\n", response.text()); + } + + @Test + void deleteShouldWork() { + APIRequestContext request = playwright.request().newContext(); + APIResponse response = request.delete(server.PREFIX + "/simple.json"); + assertEquals(server.PREFIX + "/simple.json", response.url()); + assertEquals(200, response.status()); + assertEquals("OK", response.statusText()); + assertTrue(response.ok()); + assertEquals("application/json", response.headers().get("content-type")); + Optional contentType = response.headersArray().stream().filter(h -> "content-type".equals(h.name.toLowerCase())).findFirst(); + assertTrue(contentType.isPresent()); + assertEquals("application/json", contentType.get().value); + assertEquals("{\"foo\": \"bar\"}\n", response.text()); + } + + @Test + void getShouldWork() { + APIRequestContext request = playwright.request().newContext(); + APIResponse response = request.get(server.PREFIX + "/simple.json"); + assertEquals(server.PREFIX + "/simple.json", response.url()); + assertEquals(200, response.status()); + assertEquals("OK", response.statusText()); + assertTrue(response.ok()); + assertEquals("application/json", response.headers().get("content-type")); + Optional contentType = response.headersArray().stream().filter(h -> "content-type".equals(h.name.toLowerCase())).findFirst(); + assertTrue(contentType.isPresent()); + assertEquals("application/json", contentType.get().value); + assertEquals("{\"foo\": \"bar\"}\n", response.text()); + } + + @Test + void headShouldWork() { + APIRequestContext request = playwright.request().newContext(); + APIResponse response = request.head(server.PREFIX + "/simple.json"); + assertEquals(server.PREFIX + "/simple.json", response.url()); + assertEquals(200, response.status()); + assertEquals("OK", response.statusText()); + assertTrue(response.ok()); + assertEquals("application/json", response.headers().get("content-type")); + Optional contentType = response.headersArray().stream().filter(h -> "content-type".equals(h.name.toLowerCase())).findFirst(); + assertTrue(contentType.isPresent()); + assertEquals("application/json", contentType.get().value); + assertEquals("", response.text()); + } + + @Test + void patchShouldWork() { + APIRequestContext request = playwright.request().newContext(); + APIResponse response = request.patch(server.PREFIX + "/simple.json"); + assertEquals(server.PREFIX + "/simple.json", response.url()); + assertEquals(200, response.status()); + assertEquals("OK", response.statusText()); + assertTrue(response.ok()); + assertEquals("application/json", response.headers().get("content-type")); + Optional contentType = response.headersArray().stream().filter(h -> "content-type".equals(h.name.toLowerCase())).findFirst(); + assertTrue(contentType.isPresent()); + assertEquals("application/json", contentType.get().value); + assertEquals("{\"foo\": \"bar\"}\n", response.text()); + } + + @Test + void postShouldWork() { + APIRequestContext request = playwright.request().newContext(); + APIResponse response = request.post(server.PREFIX + "/simple.json"); + assertEquals(server.PREFIX + "/simple.json", response.url()); + assertEquals(200, response.status()); + assertEquals("OK", response.statusText()); + assertTrue(response.ok()); + assertEquals("application/json", response.headers().get("content-type")); + Optional contentType = response.headersArray().stream().filter(h -> "content-type".equals(h.name.toLowerCase())).findFirst(); + assertTrue(contentType.isPresent()); + assertEquals("application/json", contentType.get().value); + assertEquals("{\"foo\": \"bar\"}\n", response.text()); + } + + @Test + void putShouldWork() { + APIRequestContext request = playwright.request().newContext(); + APIResponse response = request.put(server.PREFIX + "/simple.json"); + assertEquals(server.PREFIX + "/simple.json", response.url()); + assertEquals(200, response.status()); + assertEquals("OK", response.statusText()); + assertTrue(response.ok()); + assertEquals("application/json", response.headers().get("content-type")); + Optional contentType = response.headersArray().stream().filter(h -> "content-type".equals(h.name.toLowerCase())).findFirst(); + assertTrue(contentType.isPresent()); + assertEquals("application/json", contentType.get().value); + assertEquals("{\"foo\": \"bar\"}\n", response.text()); + } + + @Test + void shouldDisposeGlobalRequest() { + APIRequestContext request = playwright.request().newContext(); + APIResponse response = request.get(server.PREFIX + "/simple.json"); + assertEquals("{\"foo\": \"bar\"}\n", response.text()); + request.dispose(); + try { + response.body(); + fail("did not throw"); + } catch (PlaywrightException e) { + assertTrue(e.getMessage().contains("Response has been disposed"), e.getMessage()); + } + } + + @Test + void shouldSupportGlobalUserAgentOption() throws ExecutionException, InterruptedException { + APIRequestContext request = playwright.request().newContext(new APIRequest.NewContextOptions().setUserAgent("My Agent")); + Future serverRequest = server.futureRequest("/empty.html"); + APIResponse response = request.get(server.EMPTY_PAGE); + assertTrue(response.ok()); + assertEquals(server.EMPTY_PAGE, response.url()); + assertEquals(asList("My Agent"), serverRequest.get().headers.get("user-agent")); + } + + @Test + void shouldSupportGlobalTimeoutOption() { + APIRequestContext request = playwright.request().newContext(new APIRequest.NewContextOptions().setTimeout(1)); + server.setRoute("/empty.html", exchange -> {}); + try { + request.get(server.EMPTY_PAGE); + fail("did not throw"); + } catch (PlaywrightException e) { + assertTrue(e.getMessage().contains("Request timed out after 1ms"), e.getMessage()); + } + } + + + @Test + void shouldPropagateExtraHttpHeadersWithRedirects() throws ExecutionException, InterruptedException { + server.setRedirect("/a/redirect1", "/b/c/redirect2"); + server.setRedirect("/b/c/redirect2", "/simple.json"); + APIRequestContext request = playwright.request().newContext(new APIRequest.NewContextOptions().setExtraHTTPHeaders(mapOf("My-Secret", "Value"))); + Future req1 = server.futureRequest("/a/redirect1"); + Future req2 = server.futureRequest("/b/c/redirect2"); + Future req3 = server.futureRequest("/simple.json"); + request.get(server.PREFIX + "/a/redirect1"); + assertEquals(asList("Value"), req1.get().headers.get("my-secret")); + assertEquals(asList("Value"), req2.get().headers.get("my-secret")); + assertEquals(asList("Value"), req3.get().headers.get("my-secret")); + } + + @Test + void shouldSupportGlobalHttpCredentialsOption() { + server.setAuth("/empty.html", "user", "pass"); + APIRequestContext request1 = playwright.request().newContext(); + APIResponse response1 = request1.get(server.EMPTY_PAGE); + assertEquals(401, response1.status()); + request1.dispose(); + + APIRequestContext request2 = playwright.request().newContext(new APIRequest.NewContextOptions().setHttpCredentials("user", "pass")); + APIResponse response2 = request2.get(server.EMPTY_PAGE); + assertEquals(200, response2.status()); + request2.dispose(); + } + + @Test + void shouldReturnErrorWithWrongCredentials() { + server.setAuth("/empty.html", "user", "pass"); + APIRequestContext request = playwright.request().newContext(new APIRequest.NewContextOptions().setHttpCredentials("user", "wrong")); + APIResponse response = request.get(server.EMPTY_PAGE); + assertEquals(401, response.status()); + } + + void shouldUseSocksProxy() { + } + + void shouldPassProxyCredentials() { + } + + @Test + @Disabled("Error: socket hang up") + void shouldSupportGlobalIgnoreHTTPSErrorsOption() { + APIRequestContext request = playwright.request().newContext(new APIRequest.NewContextOptions().setIgnoreHTTPSErrors(true)); + APIResponse response = request.get(httpsServer.EMPTY_PAGE); + assertEquals(200, response.status()); + } + + @Test + @Disabled("Error: socket hang up") + void shouldPropagateIgnoreHTTPSErrorsOnRedirects() { + httpsServer.setRedirect("/redir", "/empty.html"); + APIRequestContext request = playwright.request().newContext(); + APIResponse response = request.get(httpsServer.PREFIX + "/redir", new APIRequestContext.GetOptions().setIgnoreHTTPSErrors(true)); + assertEquals(200, response.status()); + } + + @Test + void shouldResolveUrlRelativeToGobalBaseURLOption() { + APIRequestContext request = playwright.request().newContext(new APIRequest.NewContextOptions().setBaseURL(server.PREFIX)); + APIResponse response = request.get("/empty.html"); + assertEquals(server.EMPTY_PAGE, response.url()); + } + + @Test + void shouldSetPlaywrightAsUserAgent() throws ExecutionException, InterruptedException { + APIRequestContext request = playwright.request().newContext(); + Future serverRequest = server.futureRequest("/empty.html"); + request.get(server.EMPTY_PAGE); + List headers = serverRequest.get().headers.get("user-agent"); + assertNotNull(headers); + assertEquals(1, headers.size()); + assertTrue(headers.get(0).startsWith("Playwright/"), headers.get(0)); + } + + void shouldBeAbleToConstructWithContextOptions() { + } + + @Test + void shouldReturnEmptyBody() { + APIRequestContext request = playwright.request().newContext(); + APIResponse response = request.get(server.EMPTY_PAGE); + byte[] body = response.body(); + assertEquals(0, body.length); + assertEquals("", response.text()); + request.dispose(); + try { + response.body(); + fail("did not throw"); + } catch (PlaywrightException e) { + assertTrue(e.getMessage().contains("Response has been disposed"), e.getMessage()); + } + } + + @Test + void shouldRemoveContentLengthFromReidrectedPostRequests() throws ExecutionException, InterruptedException { + server.setRedirect("/redirect", "/empty.html"); + APIRequestContext request = playwright.request().newContext(); + Future req1 = server.futureRequest("/redirect"); + Future req2 = server.futureRequest("/empty.html"); + APIResponse result = request.post(server.PREFIX + "/redirect", new APIRequestContext.PostOptions().setData(mapOf("foo", "bar"))); + + assertEquals(200, result.status()); + assertEquals(asList("13"), req1.get().headers.get("content-length")); + assertNull(req2.get().headers.get("content-length")); + request.dispose(); + } + + private static final List values = asList( + mapOf("foo", "bar"), + new Object[] {"foo", "bar", 2021}, + "foo", + true, + 2021 + ); + + @Test + void shouldJsonStringifyTypeBodyWhenContentTypeIsApplicationJson() throws ExecutionException, InterruptedException { + APIRequestContext request = playwright.request().newContext(); + for (Object value : values) { + Future req = server.futureRequest("/empty.html"); + request.post(server.EMPTY_PAGE, new APIRequestContext.PostOptions().setHeaders(mapOf("content-type", "application/json")).setData(value)); + byte[] body = req.get().postBody; + assertEquals(new Gson().toJson(value), new String(body)); + } + request.dispose(); + } + + @Test + void shouldNotDoubleStringifyTypeBodyWhenContentTypeIsApplicationJson() throws ExecutionException, InterruptedException { + APIRequestContext request = playwright.request().newContext(); + for (Object value : values) { + String stringifiedValue = new Gson().toJson(value); + Future req = server.futureRequest("/empty.html"); + request.post(server.EMPTY_PAGE, new APIRequestContext.PostOptions() + .setHeaders(mapOf("content-type", "application/json")) + .setData(stringifiedValue)); + byte[] body = req.get().postBody; + assertEquals(stringifiedValue, new String(body)); + } + request.dispose(); + } +} diff --git a/scripts/CLI_VERSION b/scripts/CLI_VERSION index 5bab889c..18f7f568 100644 --- a/scripts/CLI_VERSION +++ b/scripts/CLI_VERSION @@ -1 +1 @@ -1.17.0-next-1636496060000 +1.18.0-next-1637016847000 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 bbc48b66..578524a3 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 @@ -255,6 +255,7 @@ class TypeRef extends Element { customTypeNames.put("Request.headersArray", "HttpHeader"); customTypeNames.put("Response.headersArray", "HttpHeader"); + customTypeNames.put("APIResponse.headersArray", "HttpHeader"); customTypeNames.put("Locator.selectOption.values", "SelectOption"); customTypeNames.put("ElementHandle.selectOption.values", "SelectOption"); @@ -266,6 +267,7 @@ class TypeRef extends Element { customTypeNames.put("FileChooser.setFiles.files", "FilePayload"); customTypeNames.put("Frame.setInputFiles.files", "FilePayload"); customTypeNames.put("Page.setInputFiles.files", "FilePayload"); + customTypeNames.put("Page.setInputFiles.files", "FilePayload"); customTypeNames.put("Page.dragAndDrop.options.sourcePosition", "Position"); customTypeNames.put("Frame.dragAndDrop.options.sourcePosition", "Position"); @@ -897,7 +899,7 @@ class Interface extends TypeDefinition { if ("Playwright".equals(jsonName)) { output.add("import com.microsoft.playwright.impl.PlaywrightImpl;"); } - if (asList("Page", "Request", "Response", "FileChooser", "Frame", "ElementHandle", "Locator", "Browser", "BrowserContext", "BrowserType", "Mouse", "Keyboard").contains(jsonName)) { + if (asList("Page", "Request", "Response", "APIRequest", "APIResponse", "FileChooser", "Frame", "ElementHandle", "Locator", "Browser", "BrowserContext", "BrowserType", "Mouse", "Keyboard").contains(jsonName)) { output.add("import com.microsoft.playwright.options.*;"); } if (jsonName.equals("Route")) { @@ -906,7 +908,7 @@ class Interface extends TypeDefinition { if ("Download".equals(jsonName)) { output.add("import java.io.InputStream;"); } - if (asList("Page", "Frame", "ElementHandle", "Locator", "FileChooser", "Browser", "BrowserContext", "BrowserType", "Download", "Route", "Selectors", "Tracing", "Video").contains(jsonName)) { + if (asList("Page", "Frame", "ElementHandle", "Locator", "APIRequest", "APIRequestContext", "FileChooser", "Browser", "BrowserContext", "BrowserType", "Download", "Route", "Selectors", "Tracing", "Video").contains(jsonName)) { output.add("import java.nio.file.Path;"); } output.add("import java.util.*;");