diff --git a/.github/workflows/test_cli.yml b/.github/workflows/test_cli.yml
index 9b877de15..d04887be9 100644
--- a/.github/workflows/test_cli.yml
+++ b/.github/workflows/test_cli.yml
@@ -15,7 +15,7 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Cache Maven packages
- uses: actions/cache@v2
+ uses: actions/cache@v4
with:
path: ~/.m2
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
diff --git a/README.md b/README.md
index 812f25ba1..e81930130 100644
--- a/README.md
+++ b/README.md
@@ -10,9 +10,9 @@ Playwright is a Java library to automate [Chromium](https://www.chromium.org/Hom
| | Linux | macOS | Windows |
| :--- | :---: | :---: | :---: |
-| Chromium 133.0.6943.16 | :white_check_mark: | :white_check_mark: | :white_check_mark: |
-| WebKit 18.2 | ✅ | ✅ | ✅ |
-| Firefox 134.0 | :white_check_mark: | :white_check_mark: | :white_check_mark: |
+| Chromium 134.0.6998.35 | :white_check_mark: | :white_check_mark: | :white_check_mark: |
+| WebKit 18.4 | ✅ | ✅ | ✅ |
+| Firefox 135.0 | :white_check_mark: | :white_check_mark: | :white_check_mark: |
Headless execution is supported for all the browsers on all platforms. Check out [system requirements](https://playwright.dev/java/docs/intro#system-requirements) for details.
diff --git a/examples/pom.xml b/examples/pom.xml
index 7dfb7dc6f..86f330fec 100644
--- a/examples/pom.xml
+++ b/examples/pom.xml
@@ -10,7 +10,7 @@
Playwright Client ExamplesUTF-8
- 1.49.0
+ 1.51.0
diff --git a/playwright/src/main/java/com/microsoft/playwright/APIRequest.java b/playwright/src/main/java/com/microsoft/playwright/APIRequest.java
index 48b522640..85833f084 100644
--- a/playwright/src/main/java/com/microsoft/playwright/APIRequest.java
+++ b/playwright/src/main/java/com/microsoft/playwright/APIRequest.java
@@ -59,6 +59,10 @@ class NewContextOptions {
* An object containing additional HTTP headers to be sent with every request. Defaults to none.
*/
public Map extraHTTPHeaders;
+ /**
+ * Whether to throw on response codes other than 2xx and 3xx. By default response object is returned for all status codes.
+ */
+ public Boolean failOnStatusCode;
/**
* Credentials for HTTP authentication. If
* no origin is specified, the username and password are sent to any servers upon unauthorized responses.
@@ -138,6 +142,13 @@ public NewContextOptions setExtraHTTPHeaders(Map extraHTTPHeader
this.extraHTTPHeaders = extraHTTPHeaders;
return this;
}
+ /**
+ * Whether to throw on response codes other than 2xx and 3xx. By default response object is returned for all status codes.
+ */
+ public NewContextOptions setFailOnStatusCode(boolean failOnStatusCode) {
+ this.failOnStatusCode = failOnStatusCode;
+ return this;
+ }
/**
* Credentials for HTTP authentication. If
* no origin is specified, the username and password are sent to any servers upon unauthorized responses.
diff --git a/playwright/src/main/java/com/microsoft/playwright/APIRequestContext.java b/playwright/src/main/java/com/microsoft/playwright/APIRequestContext.java
index a2f0d7432..34b25000d 100644
--- a/playwright/src/main/java/com/microsoft/playwright/APIRequestContext.java
+++ b/playwright/src/main/java/com/microsoft/playwright/APIRequestContext.java
@@ -58,12 +58,23 @@ public DisposeOptions setReason(String reason) {
}
}
class StorageStateOptions {
+ /**
+ * Set to {@code true} to include IndexedDB in the storage state snapshot.
+ */
+ public Boolean indexedDB;
/**
* 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;
+ /**
+ * Set to {@code true} to include IndexedDB in the storage state snapshot.
+ */
+ public StorageStateOptions setIndexedDB(boolean indexedDB) {
+ this.indexedDB = indexedDB;
+ return this;
+ }
/**
* 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.
diff --git a/playwright/src/main/java/com/microsoft/playwright/Browser.java b/playwright/src/main/java/com/microsoft/playwright/Browser.java
index 3b32211ed..265e02fdb 100644
--- a/playwright/src/main/java/com/microsoft/playwright/Browser.java
+++ b/playwright/src/main/java/com/microsoft/playwright/Browser.java
@@ -118,6 +118,12 @@ class NewContextOptions {
* "light"}.
*/
public Optional colorScheme;
+ /**
+ * Emulates {@code "prefers-contrast"} media feature, supported values are {@code "no-preference"}, {@code "more"}. See
+ * {@link com.microsoft.playwright.Page#emulateMedia Page.emulateMedia()} for more details. Passing {@code null} resets
+ * emulation to system defaults. Defaults to {@code "no-preference"}.
+ */
+ public Optional contrast;
/**
* Specify device scale factor (can be thought of as dpr). Defaults to {@code 1}. Learn more about emulating devices with device scale factor.
@@ -335,6 +341,15 @@ public NewContextOptions setColorScheme(ColorScheme colorScheme) {
this.colorScheme = Optional.ofNullable(colorScheme);
return this;
}
+ /**
+ * Emulates {@code "prefers-contrast"} media feature, supported values are {@code "no-preference"}, {@code "more"}. See
+ * {@link com.microsoft.playwright.Page#emulateMedia Page.emulateMedia()} for more details. Passing {@code null} resets
+ * emulation to system defaults. Defaults to {@code "no-preference"}.
+ */
+ public NewContextOptions setContrast(Contrast contrast) {
+ this.contrast = Optional.ofNullable(contrast);
+ return this;
+ }
/**
* Specify device scale factor (can be thought of as dpr). Defaults to {@code 1}. Learn more about emulating devices with device scale factor.
@@ -671,6 +686,12 @@ class NewPageOptions {
* "light"}.
*/
public Optional colorScheme;
+ /**
+ * Emulates {@code "prefers-contrast"} media feature, supported values are {@code "no-preference"}, {@code "more"}. See
+ * {@link com.microsoft.playwright.Page#emulateMedia Page.emulateMedia()} for more details. Passing {@code null} resets
+ * emulation to system defaults. Defaults to {@code "no-preference"}.
+ */
+ public Optional contrast;
/**
* Specify device scale factor (can be thought of as dpr). Defaults to {@code 1}. Learn more about emulating devices with device scale factor.
@@ -888,6 +909,15 @@ public NewPageOptions setColorScheme(ColorScheme colorScheme) {
this.colorScheme = Optional.ofNullable(colorScheme);
return this;
}
+ /**
+ * Emulates {@code "prefers-contrast"} media feature, supported values are {@code "no-preference"}, {@code "more"}. See
+ * {@link com.microsoft.playwright.Page#emulateMedia Page.emulateMedia()} for more details. Passing {@code null} resets
+ * emulation to system defaults. Defaults to {@code "no-preference"}.
+ */
+ public NewPageOptions setContrast(Contrast contrast) {
+ this.contrast = Optional.ofNullable(contrast);
+ return this;
+ }
/**
* Specify device scale factor (can be thought of as dpr). Defaults to {@code 1}. Learn more about emulating devices with device scale factor.
diff --git a/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java b/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java
index d0ef27190..be6b18654 100644
--- a/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java
+++ b/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java
@@ -407,12 +407,31 @@ public RouteFromHAROptions setUrl(Pattern url) {
}
}
class StorageStateOptions {
+ /**
+ * Set to {@code true} to include IndexedDB in
+ * the storage state snapshot. If your application uses IndexedDB to store authentication tokens, like Firebase
+ * Authentication, enable this.
+ *
+ *
NOTE: IndexedDBs with typed arrays are currently not supported.
+ */
+ public Boolean indexedDB;
/**
* 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;
+ /**
+ * Set to {@code true} to include IndexedDB in
+ * the storage state snapshot. If your application uses IndexedDB to store authentication tokens, like Firebase
+ * Authentication, enable this.
+ *
+ *
NOTE: IndexedDBs with typed arrays are currently not supported.
+ */
+ public StorageStateOptions setIndexedDB(boolean indexedDB) {
+ this.indexedDB = indexedDB;
+ return this;
+ }
/**
* 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.
@@ -1435,7 +1454,7 @@ default void routeFromHAR(Path har) {
*/
void setOffline(boolean offline);
/**
- * Returns storage state for this browser context, contains current cookies and local storage snapshot.
+ * Returns storage state for this browser context, contains current cookies, local storage snapshot and IndexedDB snapshot.
*
* @since v1.8
*/
@@ -1443,7 +1462,7 @@ default String storageState() {
return storageState(null);
}
/**
- * Returns storage state for this browser context, contains current cookies and local storage snapshot.
+ * Returns storage state for this browser context, contains current cookies, local storage snapshot and IndexedDB snapshot.
*
* @since v1.8
*/
diff --git a/playwright/src/main/java/com/microsoft/playwright/BrowserType.java b/playwright/src/main/java/com/microsoft/playwright/BrowserType.java
index 63dc1284c..b100387de 100644
--- a/playwright/src/main/java/com/microsoft/playwright/BrowserType.java
+++ b/playwright/src/main/java/com/microsoft/playwright/BrowserType.java
@@ -497,6 +497,12 @@ class LaunchPersistentContextOptions {
* "light"}.
*/
public Optional colorScheme;
+ /**
+ * Emulates {@code "prefers-contrast"} media feature, supported values are {@code "no-preference"}, {@code "more"}. See
+ * {@link com.microsoft.playwright.Page#emulateMedia Page.emulateMedia()} for more details. Passing {@code null} resets
+ * emulation to system defaults. Defaults to {@code "no-preference"}.
+ */
+ public Optional contrast;
/**
* Specify device scale factor (can be thought of as dpr). Defaults to {@code 1}. Learn more about emulating devices with device scale factor.
@@ -816,6 +822,15 @@ public LaunchPersistentContextOptions setColorScheme(ColorScheme colorScheme) {
this.colorScheme = Optional.ofNullable(colorScheme);
return this;
}
+ /**
+ * Emulates {@code "prefers-contrast"} media feature, supported values are {@code "no-preference"}, {@code "more"}. See
+ * {@link com.microsoft.playwright.Page#emulateMedia Page.emulateMedia()} for more details. Passing {@code null} resets
+ * emulation to system defaults. Defaults to {@code "no-preference"}.
+ */
+ public LaunchPersistentContextOptions setContrast(Contrast contrast) {
+ this.contrast = Optional.ofNullable(contrast);
+ return this;
+ }
/**
* Specify device scale factor (can be thought of as dpr). Defaults to {@code 1}. Learn more about emulating devices with device scale factor.
@@ -1197,22 +1212,24 @@ public LaunchPersistentContextOptions setViewportSize(ViewportSize viewportSize)
}
}
/**
- * This method attaches Playwright to an existing browser instance. When connecting to another browser launched via {@code
- * BrowserType.launchServer} in Node.js, the major and minor version needs to match the client version (1.2.3 → is
- * compatible with 1.2.x).
+ * This method attaches Playwright to an existing browser instance created via {@code BrowserType.launchServer} in Node.js.
+ *
+ *
NOTE: The major and minor version of the Playwright instance that connects needs to match the version of Playwright that
+ * launches the browser (1.2.3 → is compatible with 1.2.x).
*
- * @param wsEndpoint A browser websocket endpoint to connect to.
+ * @param wsEndpoint A Playwright browser websocket endpoint to connect to. You obtain this endpoint via {@code BrowserServer.wsEndpoint}.
* @since v1.8
*/
default Browser connect(String wsEndpoint) {
return connect(wsEndpoint, null);
}
/**
- * This method attaches Playwright to an existing browser instance. When connecting to another browser launched via {@code
- * BrowserType.launchServer} in Node.js, the major and minor version needs to match the client version (1.2.3 → is
- * compatible with 1.2.x).
+ * This method attaches Playwright to an existing browser instance created via {@code BrowserType.launchServer} in Node.js.
*
- * @param wsEndpoint A browser websocket endpoint to connect to.
+ *
NOTE: The major and minor version of the Playwright instance that connects needs to match the version of Playwright that
+ * launches the browser (1.2.3 → is compatible with 1.2.x).
+ *
+ * @param wsEndpoint A Playwright browser websocket endpoint to connect to. You obtain this endpoint via {@code BrowserServer.wsEndpoint}.
* @since v1.8
*/
Browser connect(String wsEndpoint, ConnectOptions options);
@@ -1223,6 +1240,11 @@ default Browser connect(String wsEndpoint) {
*
*
NOTE: Connecting over the Chrome DevTools Protocol is only supported for Chromium-based browsers.
*
+ *
NOTE: This connection is significantly lower fidelity than the Playwright protocol connection via {@link
+ * com.microsoft.playwright.BrowserType#connect BrowserType.connect()}. If you are experiencing issues or attempting to use
+ * advanced functionality, you probably want to use {@link com.microsoft.playwright.BrowserType#connect
+ * BrowserType.connect()}.
+ *
*
NOTE: Connecting over the Chrome DevTools Protocol is only supported for Chromium-based browsers.
*
+ *
NOTE: This connection is significantly lower fidelity than the Playwright protocol connection via {@link
+ * com.microsoft.playwright.BrowserType#connect BrowserType.connect()}. If you are experiencing issues or attempting to use
+ * advanced functionality, you probably want to use {@link com.microsoft.playwright.BrowserType#connect
+ * BrowserType.connect()}.
+ *
*
Usage
*
{@code
* Browser browser = playwright.chromium().connectOverCDP("http://localhost:9222");
diff --git a/playwright/src/main/java/com/microsoft/playwright/ElementHandle.java b/playwright/src/main/java/com/microsoft/playwright/ElementHandle.java
index 59b92da37..cc7720101 100644
--- a/playwright/src/main/java/com/microsoft/playwright/ElementHandle.java
+++ b/playwright/src/main/java/com/microsoft/playwright/ElementHandle.java
@@ -599,7 +599,9 @@ class ScreenshotOptions {
public ScreenshotCaret caret;
/**
* Specify locators that should be masked when the screenshot is taken. Masked elements will be overlaid with a pink box
- * {@code #FF00FF} (customized by {@code maskColor}) that completely covers its bounding box.
+ * {@code #FF00FF} (customized by {@code maskColor}) that completely covers its bounding box. The mask is also applied to
+ * invisible elements, see Matching only
+ * visible elements to disable that.
*/
public List mask;
/**
@@ -673,7 +675,9 @@ public ScreenshotOptions setCaret(ScreenshotCaret caret) {
}
/**
* Specify locators that should be masked when the screenshot is taken. Masked elements will be overlaid with a pink box
- * {@code #FF00FF} (customized by {@code maskColor}) that completely covers its bounding box.
+ * {@code #FF00FF} (customized by {@code maskColor}) that completely covers its bounding box. The mask is also applied to
+ * invisible elements, see Matching only
+ * visible elements to disable that.
*/
public ScreenshotOptions setMask(List mask) {
this.mask = mask;
diff --git a/playwright/src/main/java/com/microsoft/playwright/Locator.java b/playwright/src/main/java/com/microsoft/playwright/Locator.java
index 7173b0432..1fdaea343 100644
--- a/playwright/src/main/java/com/microsoft/playwright/Locator.java
+++ b/playwright/src/main/java/com/microsoft/playwright/Locator.java
@@ -715,6 +715,10 @@ class FilterOptions {
*
Playwright
}.
*/
public Object hasText;
+ /**
+ * Only matches visible or invisible elements.
+ */
+ public Boolean visible;
/**
* Narrows down the results of the method to those which contain elements matching this relative locator. For example,
@@ -777,6 +781,13 @@ public FilterOptions setHasText(Pattern hasText) {
this.hasText = hasText;
return this;
}
+ /**
+ * Only matches visible or invisible elements.
+ */
+ public FilterOptions setVisible(boolean visible) {
+ this.visible = visible;
+ return this;
+ }
}
class FocusOptions {
/**
@@ -1519,7 +1530,9 @@ class ScreenshotOptions {
public ScreenshotCaret caret;
/**
* Specify locators that should be masked when the screenshot is taken. Masked elements will be overlaid with a pink box
- * {@code #FF00FF} (customized by {@code maskColor}) that completely covers its bounding box.
+ * {@code #FF00FF} (customized by {@code maskColor}) that completely covers its bounding box. The mask is also applied to
+ * invisible elements, see Matching only
+ * visible elements to disable that.
*/
public List mask;
/**
@@ -1593,7 +1606,9 @@ public ScreenshotOptions setCaret(ScreenshotCaret caret) {
}
/**
* Specify locators that should be masked when the screenshot is taken. Masked elements will be overlaid with a pink box
- * {@code #FF00FF} (customized by {@code maskColor}) that completely covers its bounding box.
+ * {@code #FF00FF} (customized by {@code maskColor}) that completely covers its bounding box. The mask is also applied to
+ * invisible elements, see Matching only
+ * visible elements to disable that.
*/
public ScreenshotOptions setMask(List mask) {
this.mask = mask;
@@ -2840,10 +2855,6 @@ default ElementHandle elementHandle() {
*
If {@code expression} throws or rejects, this method throws.
*
*
*
* @param expression JavaScript expression to be evaluated in the browser context. If the expression evaluates to a function, the function is
* automatically invoked.
@@ -2868,10 +2879,6 @@ default Object evaluate(String expression, Object arg) {
*
If {@code expression} throws or rejects, this method throws.
*
*
*
* @param expression JavaScript expression to be evaluated in the browser context. If the expression evaluates to a function, the function is
* automatically invoked.
@@ -2895,10 +2902,6 @@ default Object evaluate(String expression) {
*
If {@code expression} throws or rejects, this method throws.
*
*
*
* @param expression JavaScript expression to be evaluated in the browser context. If the expression evaluates to a function, the function is
* automatically invoked.
@@ -5212,7 +5215,9 @@ default void setInputFiles(FilePayload[] files) {
*/
void setInputFiles(FilePayload[] files, SetInputFilesOptions options);
/**
- * Perform a tap gesture on the element matching the locator.
+ * Perform a tap gesture on the element matching the locator. For examples of emulating other gestures by manually
+ * dispatching touch events, see the emulating legacy touch
+ * events page.
*
*
Details
*
@@ -5238,7 +5243,9 @@ default void tap() {
tap(null);
}
/**
- * Perform a tap gesture on the element matching the locator.
+ * Perform a tap gesture on the element matching the locator. For examples of emulating other gestures by manually
+ * dispatching touch events, see the emulating legacy touch
+ * events page.
*
*
Details
*
diff --git a/playwright/src/main/java/com/microsoft/playwright/Page.java b/playwright/src/main/java/com/microsoft/playwright/Page.java
index d5d111532..4a3cd7314 100644
--- a/playwright/src/main/java/com/microsoft/playwright/Page.java
+++ b/playwright/src/main/java/com/microsoft/playwright/Page.java
@@ -965,6 +965,11 @@ class EmulateMediaOptions {
* {@code "no-preference"} is deprecated.
*/
public Optional colorScheme;
+ /**
+ * Emulates {@code "prefers-contrast"} media feature, supported values are {@code "no-preference"}, {@code "more"}. Passing
+ * {@code null} disables contrast emulation.
+ */
+ public Optional contrast;
/**
* Emulates {@code "forced-colors"} media feature, supported values are {@code "active"} and {@code "none"}. Passing {@code
* null} disables forced colors emulation.
@@ -991,6 +996,14 @@ public EmulateMediaOptions setColorScheme(ColorScheme colorScheme) {
this.colorScheme = Optional.ofNullable(colorScheme);
return this;
}
+ /**
+ * Emulates {@code "prefers-contrast"} media feature, supported values are {@code "no-preference"}, {@code "more"}. Passing
+ * {@code null} disables contrast emulation.
+ */
+ public EmulateMediaOptions setContrast(Contrast contrast) {
+ this.contrast = Optional.ofNullable(contrast);
+ return this;
+ }
/**
* Emulates {@code "forced-colors"} media feature, supported values are {@code "active"} and {@code "none"}. Passing {@code
* null} disables forced colors emulation.
@@ -2522,7 +2535,9 @@ class ScreenshotOptions {
public Boolean fullPage;
/**
* Specify locators that should be masked when the screenshot is taken. Masked elements will be overlaid with a pink box
- * {@code #FF00FF} (customized by {@code maskColor}) that completely covers its bounding box.
+ * {@code #FF00FF} (customized by {@code maskColor}) that completely covers its bounding box. The mask is also applied to
+ * invisible elements, see Matching only
+ * visible elements to disable that.
*/
public List mask;
/**
@@ -2617,7 +2632,9 @@ public ScreenshotOptions setFullPage(boolean fullPage) {
}
/**
* Specify locators that should be masked when the screenshot is taken. Masked elements will be overlaid with a pink box
- * {@code #FF00FF} (customized by {@code maskColor}) that completely covers its bounding box.
+ * {@code #FF00FF} (customized by {@code maskColor}) that completely covers its bounding box. The mask is also applied to
+ * invisible elements, see Matching only
+ * visible elements to disable that.
*/
public ScreenshotOptions setMask(List mask) {
this.mask = mask;
diff --git a/playwright/src/main/java/com/microsoft/playwright/Touchscreen.java b/playwright/src/main/java/com/microsoft/playwright/Touchscreen.java
index f59a32e01..2024016c1 100644
--- a/playwright/src/main/java/com/microsoft/playwright/Touchscreen.java
+++ b/playwright/src/main/java/com/microsoft/playwright/Touchscreen.java
@@ -20,6 +20,9 @@
/**
* The Touchscreen class operates in main-frame CSS pixels relative to the top-left corner of the viewport. Methods on the
* touchscreen can only be used in browser contexts that have been initialized with {@code hasTouch} set to true.
+ *
+ *
This class is limited to emulating tap gestures. For examples of other gestures simulated by manually dispatching touch
+ * events, see the emulating legacy touch events page.
*/
public interface Touchscreen {
/**
diff --git a/playwright/src/main/java/com/microsoft/playwright/WebSocket.java b/playwright/src/main/java/com/microsoft/playwright/WebSocket.java
index 7a60b1923..ab777879c 100644
--- a/playwright/src/main/java/com/microsoft/playwright/WebSocket.java
+++ b/playwright/src/main/java/com/microsoft/playwright/WebSocket.java
@@ -20,7 +20,10 @@
import java.util.function.Predicate;
/**
- * The {@code WebSocket} class represents websocket connections in the page.
+ * The {@code WebSocket} class represents WebSocket connections within a page. It provides the ability to inspect and
+ * manipulate the data being transmitted and received.
+ *
+ *
");
+ Locator locator = page.locator(".item").filter(new Locator.FilterOptions().setVisible(true)).nth(1);
+ assertThat(locator).hasText("visible data2");
+ assertThat(page.locator(".item").filter(new Locator.FilterOptions().setVisible(true)).getByText("data3")).hasText("visible data3");
+ assertThat(page.locator(".item").filter(new Locator.FilterOptions().setVisible(false)).getByText("data1")).hasText("Hidden data1");
+ }
}
diff --git a/playwright/src/test/java/com/microsoft/playwright/TestPageEmulateMedia.java b/playwright/src/test/java/com/microsoft/playwright/TestPageEmulateMedia.java
index e1a3ce610..28facfd14 100644
--- a/playwright/src/test/java/com/microsoft/playwright/TestPageEmulateMedia.java
+++ b/playwright/src/test/java/com/microsoft/playwright/TestPageEmulateMedia.java
@@ -16,6 +16,7 @@
package com.microsoft.playwright;
+import com.microsoft.playwright.options.Contrast;
import com.microsoft.playwright.options.ForcedColors;
import com.microsoft.playwright.options.ReducedMotion;
import org.junit.jupiter.api.Test;
@@ -171,4 +172,17 @@ void shouldEmulateForcedColors() {
page.emulateMedia(new Page.EmulateMediaOptions().setForcedColors(null));
assertEquals(true, page.evaluate("() => matchMedia('(forced-colors: none)').matches"));
}
+
+ @Test
+ void shouldEmulateContrast() {
+ assertEquals(true, page.evaluate("matchMedia('(prefers-contrast: no-preference)').matches"));
+ page.emulateMedia(new Page.EmulateMediaOptions().setContrast(Contrast.NO_PREFERENCE));
+ assertEquals(true, page.evaluate("matchMedia('(prefers-contrast: no-preference)').matches"));
+ assertEquals(false, page.evaluate("matchMedia('(prefers-contrast: more)').matches"));
+ page.emulateMedia(new Page.EmulateMediaOptions().setContrast(Contrast.MORE));
+ assertEquals(false, page.evaluate("matchMedia('(prefers-contrast: no-preference)').matches"));
+ assertEquals(true, page.evaluate("matchMedia('(prefers-contrast: more)').matches"));
+ page.emulateMedia(new Page.EmulateMediaOptions().setContrast(null));
+ assertEquals(true, page.evaluate("matchMedia('(prefers-contrast: no-preference)').matches"));
+ }
}
diff --git a/playwright/src/test/java/com/microsoft/playwright/TestPageScreenshot.java b/playwright/src/test/java/com/microsoft/playwright/TestPageScreenshot.java
index 795ed35fc..a756476b3 100644
--- a/playwright/src/test/java/com/microsoft/playwright/TestPageScreenshot.java
+++ b/playwright/src/test/java/com/microsoft/playwright/TestPageScreenshot.java
@@ -253,7 +253,7 @@ void shouldWorkWhenMaskColorIsNotPinkF0F() {
}
static boolean isScreenshotTestDisabled() {
- if (isWebKit()) {
+ if (isWebKit() || isChromium()) {
// Array lengths differ.
return true;
}
diff --git a/playwright/src/test/resources/to-do-notifications/LICENSE b/playwright/src/test/resources/to-do-notifications/LICENSE
new file mode 100644
index 000000000..3bbbc1ee9
--- /dev/null
+++ b/playwright/src/test/resources/to-do-notifications/LICENSE
@@ -0,0 +1,116 @@
+CC0 1.0 Universal
+
+Statement of Purpose
+
+The laws of most jurisdictions throughout the world automatically confer
+exclusive Copyright and Related Rights (defined below) upon the creator and
+subsequent owner(s) (each and all, an "owner") of an original work of
+authorship and/or a database (each, a "Work").
+
+Certain owners wish to permanently relinquish those rights to a Work for the
+purpose of contributing to a commons of creative, cultural and scientific
+works ("Commons") that the public can reliably and without fear of later
+claims of infringement build upon, modify, incorporate in other works, reuse
+and redistribute as freely as possible in any form whatsoever and for any
+purposes, including without limitation commercial purposes. These owners may
+contribute to the Commons to promote the ideal of a free culture and the
+further production of creative, cultural and scientific works, or to gain
+reputation or greater distribution for their Work in part through the use and
+efforts of others.
+
+For these and/or other purposes and motivations, and without any expectation
+of additional consideration or compensation, the person associating CC0 with a
+Work (the "Affirmer"), to the extent that he or she is an owner of Copyright
+and Related Rights in the Work, voluntarily elects to apply CC0 to the Work
+and publicly distribute the Work under its terms, with knowledge of his or her
+Copyright and Related Rights in the Work and the meaning and intended legal
+effect of CC0 on those rights.
+
+1. Copyright and Related Rights. A Work made available under CC0 may be
+protected by copyright and related or neighboring rights ("Copyright and
+Related Rights"). Copyright and Related Rights include, but are not limited
+to, the following:
+
+ i. the right to reproduce, adapt, distribute, perform, display, communicate,
+ and translate a Work;
+
+ ii. moral rights retained by the original author(s) and/or performer(s);
+
+ iii. publicity and privacy rights pertaining to a person's image or likeness
+ depicted in a Work;
+
+ iv. rights protecting against unfair competition in regards to a Work,
+ subject to the limitations in paragraph 4(a), below;
+
+ v. rights protecting the extraction, dissemination, use and reuse of data in
+ a Work;
+
+ vi. database rights (such as those arising under Directive 96/9/EC of the
+ European Parliament and of the Council of 11 March 1996 on the legal
+ protection of databases, and under any national implementation thereof,
+ including any amended or successor version of such directive); and
+
+ vii. other similar, equivalent or corresponding rights throughout the world
+ based on applicable law or treaty, and any national implementations thereof.
+
+2. Waiver. To the greatest extent permitted by, but not in contravention of,
+applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and
+unconditionally waives, abandons, and surrenders all of Affirmer's Copyright
+and Related Rights and associated claims and causes of action, whether now
+known or unknown (including existing as well as future claims and causes of
+action), in the Work (i) in all territories worldwide, (ii) for the maximum
+duration provided by applicable law or treaty (including future time
+extensions), (iii) in any current or future medium and for any number of
+copies, and (iv) for any purpose whatsoever, including without limitation
+commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes
+the Waiver for the benefit of each member of the public at large and to the
+detriment of Affirmer's heirs and successors, fully intending that such Waiver
+shall not be subject to revocation, rescission, cancellation, termination, or
+any other legal or equitable action to disrupt the quiet enjoyment of the Work
+by the public as contemplated by Affirmer's express Statement of Purpose.
+
+3. Public License Fallback. Should any part of the Waiver for any reason be
+judged legally invalid or ineffective under applicable law, then the Waiver
+shall be preserved to the maximum extent permitted taking into account
+Affirmer's express Statement of Purpose. In addition, to the extent the Waiver
+is so judged Affirmer hereby grants to each affected person a royalty-free,
+non transferable, non sublicensable, non exclusive, irrevocable and
+unconditional license to exercise Affirmer's Copyright and Related Rights in
+the Work (i) in all territories worldwide, (ii) for the maximum duration
+provided by applicable law or treaty (including future time extensions), (iii)
+in any current or future medium and for any number of copies, and (iv) for any
+purpose whatsoever, including without limitation commercial, advertising or
+promotional purposes (the "License"). The License shall be deemed effective as
+of the date CC0 was applied by Affirmer to the Work. Should any part of the
+License for any reason be judged legally invalid or ineffective under
+applicable law, such partial invalidity or ineffectiveness shall not
+invalidate the remainder of the License, and in such case Affirmer hereby
+affirms that he or she will not (i) exercise any of his or her remaining
+Copyright and Related Rights in the Work or (ii) assert any associated claims
+and causes of action with respect to the Work, in either case contrary to
+Affirmer's express Statement of Purpose.
+
+4. Limitations and Disclaimers.
+
+ a. No trademark or patent rights held by Affirmer are waived, abandoned,
+ surrendered, licensed or otherwise affected by this document.
+
+ b. Affirmer offers the Work as-is and makes no representations or warranties
+ of any kind concerning the Work, express, implied, statutory or otherwise,
+ including without limitation warranties of title, merchantability, fitness
+ for a particular purpose, non infringement, or the absence of latent or
+ other defects, accuracy, or the present or absence of errors, whether or not
+ discoverable, all to the greatest extent permissible under applicable law.
+
+ c. Affirmer disclaims responsibility for clearing rights of other persons
+ that may apply to the Work or any use thereof, including without limitation
+ any person's Copyright and Related Rights in the Work. Further, Affirmer
+ disclaims responsibility for obtaining any necessary consents, permissions
+ or other rights required for any use of the Work.
+
+ d. Affirmer understands and acknowledges that Creative Commons is not a
+ party to this document and has no duty or obligation with respect to this
+ CC0 or use of the Work.
+
+For more information, please see
+
\ No newline at end of file
diff --git a/playwright/src/test/resources/to-do-notifications/README.md b/playwright/src/test/resources/to-do-notifications/README.md
new file mode 100644
index 000000000..5bd8b1d38
--- /dev/null
+++ b/playwright/src/test/resources/to-do-notifications/README.md
@@ -0,0 +1 @@
+Source: https://github.com/mdn/dom-examples/tree/main/to-do-notifications
diff --git a/playwright/src/test/resources/to-do-notifications/index.html b/playwright/src/test/resources/to-do-notifications/index.html
new file mode 100644
index 000000000..e9cd52c7b
--- /dev/null
+++ b/playwright/src/test/resources/to-do-notifications/index.html
@@ -0,0 +1,108 @@
+
+
+
+
+
+
+ To-do list with Notifications
+
+
+
+
+
To-do list
+
+
+
+
+
+
+
+
+
+
+
Add new to-do item.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/playwright/src/test/resources/to-do-notifications/manifest.webapp b/playwright/src/test/resources/to-do-notifications/manifest.webapp
new file mode 100644
index 000000000..fe48e2e42
--- /dev/null
+++ b/playwright/src/test/resources/to-do-notifications/manifest.webapp
@@ -0,0 +1,18 @@
+{
+ "version": "0.1",
+ "name": "To-do list",
+ "description": "Store to-do items on your device, and be notified when the deadlines are up.",
+ "launch_path": "/to-do-notifications/index.html",
+ "icons": {
+ "128": "/to-do-notifications/img/icon-128.png"
+ },
+ "developer": {
+ "name": "Chris Mills",
+ "url": "http://chrisdavidmills.github.io/to-do-notifications/"
+ },
+ "permissions": {
+ "desktop-notification": {
+ "description": "Needed for creating system notifications."
+ }
+ }
+}
\ No newline at end of file
diff --git a/playwright/src/test/resources/to-do-notifications/scripts/todo.js b/playwright/src/test/resources/to-do-notifications/scripts/todo.js
new file mode 100644
index 000000000..2e3cfae21
--- /dev/null
+++ b/playwright/src/test/resources/to-do-notifications/scripts/todo.js
@@ -0,0 +1,354 @@
+window.onload = () => {
+ const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
+
+ // Hold an instance of a db object for us to store the IndexedDB data in
+ let db;
+
+ // Create a reference to the notifications list in the bottom of the app; we will write database messages into this list by
+ // appending list items as children of this element
+ const note = document.getElementById('notifications');
+
+ // All other UI elements we need for the app
+ const taskList = document.getElementById('task-list');
+ const taskForm = document.getElementById('task-form');
+ const title = document.getElementById('title');
+ const hours = document.getElementById('deadline-hours');
+ const minutes = document.getElementById('deadline-minutes');
+ const day = document.getElementById('deadline-day');
+ const month = document.getElementById('deadline-month');
+ const year = document.getElementById('deadline-year');
+ const notificationBtn = document.getElementById('enable');
+
+ // Do an initial check to see what the notification permission state is
+ if (Notification.permission === 'denied' || Notification.permission === 'default') {
+ notificationBtn.style.display = 'block';
+ } else {
+ notificationBtn.style.display = 'none';
+ }
+
+ note.appendChild(createListItem('App initialised.'));
+
+ // Let us open our database
+ const DBOpenRequest = window.indexedDB.open('toDoList', 4);
+
+ // Register two event handlers to act on the database being opened successfully, or not
+ DBOpenRequest.onerror = (event) => {
+ note.appendChild(createListItem('Error loading database.'));
+ };
+
+ DBOpenRequest.onsuccess = (event) => {
+ note.appendChild(createListItem('Database initialised.'));
+
+ // Store the result of opening the database in the db variable. This is used a lot below
+ db = DBOpenRequest.result;
+
+ // Run the displayData() function to populate the task list with all the to-do list data already in the IndexedDB
+ displayData();
+ };
+
+ // This event handles the event whereby a new version of the database needs to be created
+ // Either one has not been created before, or a new version number has been submitted via the
+ // window.indexedDB.open line above
+ //it is only implemented in recent browsers
+ DBOpenRequest.onupgradeneeded = (event) => {
+ db = event.target.result;
+
+ db.onerror = (event) => {
+ note.appendChild(createListItem('Error loading database.'));
+ };
+
+ // Create an objectStore for this database
+ const objectStore = db.createObjectStore('toDoList', { keyPath: 'taskTitle' });
+
+ // Define what data items the objectStore will contain
+ objectStore.createIndex('hours', 'hours', { unique: false });
+ objectStore.createIndex('minutes', 'minutes', { unique: false });
+ objectStore.createIndex('day', 'day', { unique: false });
+ objectStore.createIndex('month', 'month', { unique: false });
+ objectStore.createIndex('year', 'year', { unique: false });
+
+ objectStore.createIndex('notified', 'notified', { unique: false });
+
+ note.appendChild(createListItem('Object store created.'));
+ };
+
+ function displayData() {
+ // First clear the content of the task list so that you don't get a huge long list of duplicate stuff each time
+ // the display is updated.
+ while (taskList.firstChild) {
+ taskList.removeChild(taskList.lastChild);
+ }
+
+ // Open our object store and then get a cursor list of all the different data items in the IDB to iterate through
+ const objectStore = db.transaction('toDoList').objectStore('toDoList');
+ objectStore.openCursor().onsuccess = (event) => {
+ const cursor = event.target.result;
+ // Check if there are no (more) cursor items to iterate through
+ if (!cursor) {
+ // No more items to iterate through, we quit.
+ note.appendChild(createListItem('Entries all displayed.'));
+ return;
+ }
+
+ // Check which suffix the deadline day of the month needs
+ const { hours, minutes, day, month, year, notified, taskTitle } = cursor.value;
+ const ordDay = ordinal(day);
+
+ // Build the to-do list entry and put it into the list item.
+ const toDoText = `${taskTitle} — ${hours}:${minutes}, ${month} ${ordDay} ${year}.`;
+ const listItem = createListItem(toDoText);
+
+ if (notified === 'yes') {
+ listItem.style.textDecoration = 'line-through';
+ listItem.style.color = 'rgba(255, 0, 0, 0.5)';
+ }
+
+ // Put the item item inside the task list
+ taskList.appendChild(listItem);
+
+ // Create a delete button inside each list item,
+ const deleteButton = document.createElement('button');
+ listItem.appendChild(deleteButton);
+ deleteButton.textContent = 'X';
+
+ // Set a data attribute on our delete button to associate the task it relates to.
+ deleteButton.setAttribute('data-task', taskTitle);
+
+ // Associate action (deletion) when clicked
+ deleteButton.onclick = (event) => {
+ deleteItem(event);
+ };
+
+ // continue on to the next item in the cursor
+ cursor.continue();
+ };
+ };
+
+ // Add listener for clicking the submit button
+ taskForm.addEventListener('submit', addData, false);
+
+ function addData(e) {
+ // Prevent default, as we don't want the form to submit in the conventional way
+ e.preventDefault();
+
+ // Stop the form submitting if any values are left empty.
+ // This should never happen as there is the required attribute
+ if (title.value === '' || hours.value === null || minutes.value === null || day.value === '' || month.value === '' || year.value === null) {
+ note.appendChild(createListItem('Data not submitted — form incomplete.'));
+ return;
+ }
+
+ // Grab the values entered into the form fields and store them in an object ready for being inserted into the IndexedDB
+ const newItem = [
+ { taskTitle: title.value, hours: hours.value, minutes: minutes.value, day: day.value, month: month.value, year: year.value, notified: 'no' },
+ ];
+
+ // Open a read/write DB transaction, ready for adding the data
+ const transaction = db.transaction(['toDoList'], 'readwrite');
+
+ // Report on the success of the transaction completing, when everything is done
+ transaction.oncomplete = () => {
+ note.appendChild(createListItem('Transaction completed: database modification finished.'));
+
+ // Update the display of data to show the newly added item, by running displayData() again.
+ displayData();
+ };
+
+ // Handler for any unexpected error
+ transaction.onerror = () => {
+ note.appendChild(createListItem(`Transaction not opened due to error: ${transaction.error}`));
+ };
+
+ // Call an object store that's already been added to the database
+ const objectStore = transaction.objectStore('toDoList');
+ console.log(objectStore.indexNames);
+ console.log(objectStore.keyPath);
+ console.log(objectStore.name);
+ console.log(objectStore.transaction);
+ console.log(objectStore.autoIncrement);
+
+ // Make a request to add our newItem object to the object store
+ const objectStoreRequest = objectStore.add(newItem[0]);
+ objectStoreRequest.onsuccess = (event) => {
+
+ // Report the success of our request
+ // (to detect whether it has been succesfully
+ // added to the database, you'd look at transaction.oncomplete)
+ note.appendChild(createListItem('Request successful.'));
+
+ // Clear the form, ready for adding the next entry
+ title.value = '';
+ hours.value = null;
+ minutes.value = null;
+ day.value = 01;
+ month.value = 'January';
+ year.value = 2020;
+ };
+ };
+
+ function deleteItem(event) {
+ // Retrieve the name of the task we want to delete
+ const dataTask = event.target.getAttribute('data-task');
+
+ // Open a database transaction and delete the task, finding it by the name we retrieved above
+ const transaction = db.transaction(['toDoList'], 'readwrite');
+ transaction.objectStore('toDoList').delete(dataTask);
+
+ // Report that the data item has been deleted
+ transaction.oncomplete = () => {
+ // Delete the parent of the button, which is the list item, so it is no longer displayed
+ event.target.parentNode.parentNode.removeChild(event.target.parentNode);
+ note.appendChild(createListItem(`Task "${dataTask}" deleted.`));
+ };
+ };
+
+ // Check whether the deadline for each task is up or not, and responds appropriately
+ function checkDeadlines() {
+ // First of all check whether notifications are enabled or denied
+ if (Notification.permission === 'denied' || Notification.permission === 'default') {
+ notificationBtn.style.display = 'block';
+ } else {
+ notificationBtn.style.display = 'none';
+ }
+
+ // Grab the current time and date
+ const now = new Date();
+
+ // From the now variable, store the current minutes, hours, day of the month, month, year and seconds
+ const minuteCheck = now.getMinutes();
+ const hourCheck = now.getHours();
+ const dayCheck = now.getDate(); // Do not use getDay() that returns the day of the week, 1 to 7
+ const monthCheck = now.getMonth();
+ const yearCheck = now.getFullYear(); // Do not use getYear() that is deprecated.
+
+ // Open a new transaction
+ const objectStore = db.transaction(['toDoList'], 'readwrite').objectStore('toDoList');
+
+ // Open a cursor to iterate through all the data items in the IndexedDB
+ objectStore.openCursor().onsuccess = (event) => {
+ const cursor = event.target.result;
+ if (!cursor) return;
+ const { hours, minutes, day, month, year, notified, taskTitle } = cursor.value;
+
+ // convert the month names we have installed in the IDB into a month number that JavaScript will understand.
+ // The JavaScript date object creates month values as a number between 0 and 11.
+ const monthNumber = MONTHS.indexOf(month);
+ if (monthNumber === -1) throw new Error('Incorrect month entered in database.');
+
+ // Check if the current hours, minutes, day, month and year values match the stored values for each task.
+ // The parseInt() function transforms the value from a string to a number for comparison
+ // (taking care of leading zeros, and removing spaces and underscores from the string).
+ let matched = parseInt(hours) === hourCheck;
+ matched &&= parseInt(minutes) === minuteCheck;
+ matched &&= parseInt(day) === dayCheck;
+ matched &&= parseInt(monthNumber) === monthCheck;
+ matched &&= parseInt(year) === yearCheck;
+ if (matched && notified === 'no') {
+ // If the numbers all do match, run the createNotification() function to create a system notification
+ // but only if the permission is set
+ if (Notification.permission === 'granted') {
+ createNotification(taskTitle);
+ }
+ }
+
+ // Move on to the next cursor item
+ cursor.continue();
+ };
+ };
+
+ // Ask for permission when the 'Enable notifications' button is clicked
+ function askNotificationPermission() {
+ // Function to actually ask the permissions
+ function handlePermission(permission) {
+ // Whatever the user answers, we make sure Chrome stores the information
+ if (!Reflect.has(Notification, 'permission')) {
+ Notification.permission = permission;
+ }
+
+ // Set the button to shown or hidden, depending on what the user answers
+ if (Notification.permission === 'denied' || Notification.permission === 'default') {
+ notificationBtn.style.display = 'block';
+ } else {
+ notificationBtn.style.display = 'none';
+ }
+ };
+
+ // Check if the browser supports notifications
+ if (!Reflect.has(window, 'Notification')) {
+ console.log('This browser does not support notifications.');
+ } else {
+ if (checkNotificationPromise()) {
+ Notification.requestPermission().then(handlePermission);
+ } else {
+ Notification.requestPermission(handlePermission);
+ }
+ }
+ };
+
+ // Check whether browser supports the promise version of requestPermission()
+ // Safari only supports the old callback-based version
+ function checkNotificationPromise() {
+ try {
+ Notification.requestPermission().then();
+ } catch(e) {
+ return false;
+ }
+
+ return true;
+ };
+
+ // Wire up notification permission functionality to 'Enable notifications' button
+ notificationBtn.addEventListener('click', askNotificationPermission);
+
+ function createListItem(contents) {
+ const listItem = document.createElement('li');
+ listItem.textContent = contents;
+ return listItem;
+ };
+
+ // Create a notification with the given title
+ function createNotification(title) {
+ // Create and show the notification
+ const img = '/to-do-notifications/img/icon-128.png';
+ const text = `HEY! Your task "${title}" is now overdue.`;
+ const notification = new Notification('To do list', { body: text, icon: img });
+
+ // We need to update the value of notified to 'yes' in this particular data object, so the
+ // notification won't be set off on it again
+
+ // First open up a transaction
+ const objectStore = db.transaction(['toDoList'], 'readwrite').objectStore('toDoList');
+
+ // Get the to-do list object that has this title as its title
+ const objectStoreTitleRequest = objectStore.get(title);
+
+ objectStoreTitleRequest.onsuccess = () => {
+ // Grab the data object returned as the result
+ const data = objectStoreTitleRequest.result;
+
+ // Update the notified value in the object to 'yes'
+ data.notified = 'yes';
+
+ // Create another request that inserts the item back into the database
+ const updateTitleRequest = objectStore.put(data);
+
+ // When this new request succeeds, run the displayData() function again to update the display
+ updateTitleRequest.onsuccess = () => {
+ displayData();
+ };
+ };
+ };
+
+ // Using a setInterval to run the checkDeadlines() function every second
+ setInterval(checkDeadlines, 1000);
+}
+
+// Helper function returning the day of the month followed by an ordinal (st, nd, or rd)
+function ordinal(day) {
+ const n = day.toString();
+ const last = n.slice(-1);
+ if (last === '1' && n !== '11') return `${n}st`;
+ if (last === '2' && n !== '12') return `${n}nd`;
+ if (last === '3' && n !== '13') return `${n}rd`;
+ return `${n}th`;
+};
\ No newline at end of file
diff --git a/playwright/src/test/resources/to-do-notifications/style/style.css b/playwright/src/test/resources/to-do-notifications/style/style.css
new file mode 100644
index 000000000..c08ff299a
--- /dev/null
+++ b/playwright/src/test/resources/to-do-notifications/style/style.css
@@ -0,0 +1,248 @@
+/* Basic set up + sizing for containers */
+
+html,
+body {
+ margin: 0;
+}
+
+html {
+ width: 100%;
+ height: 100%;
+ font-size: 10px;
+ font-family: Georgia, "Times New Roman", Times, serif;
+ background: #111;
+}
+
+body {
+ width: 50rem;
+ position: relative;
+ background: #d88;
+ margin: 0 auto;
+ border-left: 2px solid #d33;
+ border-right: 2px solid #d33;
+}
+
+h1,
+h2 {
+ text-align: center;
+ background: #d88;
+ font-family: Arial, Helvetica, sans-serif;
+}
+
+h1 {
+ font-size: 6rem;
+ margin: 0;
+ background: #d66;
+}
+
+h2 {
+ font-size: 2.4rem;
+}
+
+/* Bottom toolbar styling */
+
+#toolbar {
+ position: relative;
+ height: 6rem;
+ width: 100%;
+ background: #d66;
+ border-top: 2px solid #d33;
+ border-bottom: 2px solid #d33;
+}
+
+#enable,
+input[type="submit"] {
+ line-height: 1.8;
+ font-size: 1.3rem;
+ border-radius: 5px;
+ border: 1px solid black;
+ color: black;
+ text-shadow: 1px 1px 1px black;
+ border: 1px solid rgba(0, 0, 0, 0.1);
+ box-shadow:
+ inset 0px 5px 3px rgba(255, 255, 255, 0.2),
+ inset 0px -5px 3px rgba(0, 0, 0, 0.2);
+}
+
+#enable {
+ position: absolute;
+ bottom: 0.3rem;
+ right: 0.3rem;
+}
+
+#notifications {
+ margin: 0;
+ position: relative;
+ padding: 0.3rem;
+ background: #ddd;
+ position: absolute;
+ top: 0rem;
+ left: 0rem;
+ height: 5.4rem;
+ width: 50%;
+ overflow: auto;
+ line-height: 1.2;
+}
+
+#notifications li {
+ margin-left: 1.5rem;
+}
+
+/* New item form styling */
+
+.form-box {
+ background: #d66;
+ width: 85%;
+ padding: 1rem;
+ margin: 2rem auto;
+ box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.7);
+}
+
+form div {
+ margin-bottom: 1rem;
+}
+
+form .full-width {
+ margin: 1rem auto 2rem;
+ width: 100%;
+}
+
+form .half-width {
+ width: 50%;
+ float: left;
+}
+
+form .third-width {
+ width: 33%;
+ float: left;
+}
+
+form div label {
+ width: 10rem;
+ float: left;
+ padding-right: 1rem;
+ font-size: 1.6rem;
+ line-height: 1.6;
+}
+
+form .full-width input {
+ width: 30rem;
+}
+
+form .half-width input {
+ width: 8.75rem;
+}
+
+form .third-width select {
+ width: 13.5rem;
+}
+
+form div input[type="submit"] {
+ clear: both;
+ width: 20rem;
+ display: block;
+ height: 3rem;
+ margin: 0 auto;
+ position: relative;
+ top: 0.5rem;
+}
+
+/* || tasks box */
+
+.task-box {
+ width: 85%;
+ padding: 1rem;
+ margin: 2rem auto;
+ font-size: 1.8rem;
+}
+
+.task-box ul {
+ margin: 0;
+ padding: 0;
+}
+
+.task-box li {
+ list-style-type: none;
+ padding: 1rem;
+ border-bottom: 2px solid #d33;
+}
+
+.task-box li:last-child {
+ border-bottom: none;
+}
+
+.task-box li:last-child {
+ margin-bottom: 0rem;
+}
+
+.task-box button {
+ margin-left: 2rem;
+ font-size: 1.6rem;
+ border: 1px solid #eee;
+ border-radius: 5px;
+ box-shadow: inset 0 -2px 5px rgba(0, 0, 0, 0.5) 1px 1px 1px black;
+}
+
+/* setting cursor for interactive controls */
+
+button,
+input[type="submit"],
+select {
+ cursor: pointer;
+}
+
+/* media query for small screens */
+
+@media (max-width: 32rem) {
+ body {
+ width: 100%;
+ border-left: none;
+ border-right: none;
+ }
+
+ form div {
+ clear: both;
+ }
+
+ form .full-width {
+ margin: 1rem auto;
+ }
+
+ form .half-width {
+ width: 100%;
+ float: none;
+ }
+
+ form .third-width {
+ width: 100%;
+ float: none;
+ }
+
+ form div label {
+ width: 36%;
+ padding-left: 1rem;
+ }
+
+ form input,
+ form select,
+ form label {
+ line-height: 2.5rem;
+ font-size: 2rem;
+ }
+
+ form .full-width input {
+ width: 50%;
+ }
+
+ form .half-width input {
+ width: 50%;
+ }
+
+ form .third-width select {
+ width: 50%;
+ }
+
+ #enable {
+ right: 1rem;
+ }
+}
\ No newline at end of file
diff --git a/scripts/DRIVER_VERSION b/scripts/DRIVER_VERSION
index 41fda7934..ba0a71911 100644
--- a/scripts/DRIVER_VERSION
+++ b/scripts/DRIVER_VERSION
@@ -1 +1 @@
-1.50.1-beta-1738592302000
+1.51.0