chore: roll 1.51.0 driver, implement new features (#1757)
This commit is contained in:
@@ -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') }}
|
||||
|
||||
@@ -10,9 +10,9 @@ Playwright is a Java library to automate [Chromium](https://www.chromium.org/Hom
|
||||
|
||||
| | Linux | macOS | Windows |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| Chromium <!-- GEN:chromium-version -->133.0.6943.16<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| WebKit <!-- GEN:webkit-version -->18.2<!-- GEN:stop --> | ✅ | ✅ | ✅ |
|
||||
| Firefox <!-- GEN:firefox-version -->134.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| Chromium <!-- GEN:chromium-version -->134.0.6998.35<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| WebKit <!-- GEN:webkit-version -->18.4<!-- GEN:stop --> | ✅ | ✅ | ✅ |
|
||||
| Firefox <!-- GEN:firefox-version -->135.0<!-- GEN:stop --> | :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.
|
||||
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
<name>Playwright Client Examples</name>
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<playwright.version>1.49.0</playwright.version>
|
||||
<playwright.version>1.51.0</playwright.version>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
|
||||
@@ -59,6 +59,10 @@ public interface APIRequest {
|
||||
* An object containing additional HTTP headers to be sent with every request. Defaults to none.
|
||||
*/
|
||||
public Map<String, String> 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 <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication">HTTP authentication</a>. If
|
||||
* no origin is specified, the username and password are sent to any servers upon unauthorized responses.
|
||||
@@ -138,6 +142,13 @@ public interface APIRequest {
|
||||
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 <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication">HTTP authentication</a>. If
|
||||
* no origin is specified, the username and password are sent to any servers upon unauthorized responses.
|
||||
|
||||
@@ -58,12 +58,23 @@ public interface APIRequestContext {
|
||||
}
|
||||
}
|
||||
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.
|
||||
|
||||
@@ -118,6 +118,12 @@ public interface Browser extends AutoCloseable {
|
||||
* "light"}.
|
||||
*/
|
||||
public Optional<ColorScheme> 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> contrast;
|
||||
/**
|
||||
* Specify device scale factor (can be thought of as dpr). Defaults to {@code 1}. Learn more about <a
|
||||
* href="https://playwright.dev/java/docs/emulation#devices">emulating devices with device scale factor</a>.
|
||||
@@ -335,6 +341,15 @@ public interface Browser extends AutoCloseable {
|
||||
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 <a
|
||||
* href="https://playwright.dev/java/docs/emulation#devices">emulating devices with device scale factor</a>.
|
||||
@@ -671,6 +686,12 @@ public interface Browser extends AutoCloseable {
|
||||
* "light"}.
|
||||
*/
|
||||
public Optional<ColorScheme> 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> contrast;
|
||||
/**
|
||||
* Specify device scale factor (can be thought of as dpr). Defaults to {@code 1}. Learn more about <a
|
||||
* href="https://playwright.dev/java/docs/emulation#devices">emulating devices with device scale factor</a>.
|
||||
@@ -888,6 +909,15 @@ public interface Browser extends AutoCloseable {
|
||||
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 <a
|
||||
* href="https://playwright.dev/java/docs/emulation#devices">emulating devices with device scale factor</a>.
|
||||
|
||||
@@ -407,12 +407,31 @@ public interface BrowserContext extends AutoCloseable {
|
||||
}
|
||||
}
|
||||
class StorageStateOptions {
|
||||
/**
|
||||
* Set to {@code true} to include <a href="https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API">IndexedDB</a> in
|
||||
* the storage state snapshot. If your application uses IndexedDB to store authentication tokens, like Firebase
|
||||
* Authentication, enable this.
|
||||
*
|
||||
* <p> <strong>NOTE:</strong> 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 <a href="https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API">IndexedDB</a> in
|
||||
* the storage state snapshot. If your application uses IndexedDB to store authentication tokens, like Firebase
|
||||
* Authentication, enable this.
|
||||
*
|
||||
* <p> <strong>NOTE:</strong> 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 @@ public interface BrowserContext extends AutoCloseable {
|
||||
*/
|
||||
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 @@ public interface BrowserContext extends AutoCloseable {
|
||||
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
|
||||
*/
|
||||
|
||||
@@ -497,6 +497,12 @@ public interface BrowserType {
|
||||
* "light"}.
|
||||
*/
|
||||
public Optional<ColorScheme> 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> contrast;
|
||||
/**
|
||||
* Specify device scale factor (can be thought of as dpr). Defaults to {@code 1}. Learn more about <a
|
||||
* href="https://playwright.dev/java/docs/emulation#devices">emulating devices with device scale factor</a>.
|
||||
@@ -816,6 +822,15 @@ public interface BrowserType {
|
||||
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 <a
|
||||
* href="https://playwright.dev/java/docs/emulation#devices">emulating devices with device scale factor</a>.
|
||||
@@ -1197,22 +1212,24 @@ public interface BrowserType {
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 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.
|
||||
* <p> <strong>NOTE:</strong> 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
|
||||
*/
|
||||
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.
|
||||
* <p> <strong>NOTE:</strong> 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 @@ public interface BrowserType {
|
||||
*
|
||||
* <p> <strong>NOTE:</strong> Connecting over the Chrome DevTools Protocol is only supported for Chromium-based browsers.
|
||||
*
|
||||
* <p> <strong>NOTE:</strong> 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()}.
|
||||
*
|
||||
* <p> <strong>Usage</strong>
|
||||
* <pre>{@code
|
||||
* Browser browser = playwright.chromium().connectOverCDP("http://localhost:9222");
|
||||
@@ -1244,6 +1266,11 @@ public interface BrowserType {
|
||||
*
|
||||
* <p> <strong>NOTE:</strong> Connecting over the Chrome DevTools Protocol is only supported for Chromium-based browsers.
|
||||
*
|
||||
* <p> <strong>NOTE:</strong> 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()}.
|
||||
*
|
||||
* <p> <strong>Usage</strong>
|
||||
* <pre>{@code
|
||||
* Browser browser = playwright.chromium().connectOverCDP("http://localhost:9222");
|
||||
|
||||
@@ -599,7 +599,9 @@ public interface ElementHandle extends JSHandle {
|
||||
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 <a href="https://playwright.dev/java/docs/locators#matching-only-visible-elements">Matching only
|
||||
* visible elements</a> to disable that.
|
||||
*/
|
||||
public List<Locator> mask;
|
||||
/**
|
||||
@@ -673,7 +675,9 @@ public interface ElementHandle extends JSHandle {
|
||||
}
|
||||
/**
|
||||
* 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 <a href="https://playwright.dev/java/docs/locators#matching-only-visible-elements">Matching only
|
||||
* visible elements</a> to disable that.
|
||||
*/
|
||||
public ScreenshotOptions setMask(List<Locator> mask) {
|
||||
this.mask = mask;
|
||||
|
||||
@@ -715,6 +715,10 @@ public interface Locator {
|
||||
* <article><div>Playwright</div></article>}.
|
||||
*/
|
||||
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 interface Locator {
|
||||
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 @@ public interface Locator {
|
||||
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 <a href="https://playwright.dev/java/docs/locators#matching-only-visible-elements">Matching only
|
||||
* visible elements</a> to disable that.
|
||||
*/
|
||||
public List<Locator> mask;
|
||||
/**
|
||||
@@ -1593,7 +1606,9 @@ public interface Locator {
|
||||
}
|
||||
/**
|
||||
* 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 <a href="https://playwright.dev/java/docs/locators#matching-only-visible-elements">Matching only
|
||||
* visible elements</a> to disable that.
|
||||
*/
|
||||
public ScreenshotOptions setMask(List<Locator> mask) {
|
||||
this.mask = mask;
|
||||
@@ -2840,10 +2855,6 @@ public interface Locator {
|
||||
* <p> If {@code expression} throws or rejects, this method throws.
|
||||
*
|
||||
* <p> <strong>Usage</strong>
|
||||
* <pre>{@code
|
||||
* Locator tweets = page.locator(".tweet .retweets");
|
||||
* assertEquals("10 retweets", tweets.evaluate("node => node.innerText"));
|
||||
* }</pre>
|
||||
*
|
||||
* @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 @@ public interface Locator {
|
||||
* <p> If {@code expression} throws or rejects, this method throws.
|
||||
*
|
||||
* <p> <strong>Usage</strong>
|
||||
* <pre>{@code
|
||||
* Locator tweets = page.locator(".tweet .retweets");
|
||||
* assertEquals("10 retweets", tweets.evaluate("node => node.innerText"));
|
||||
* }</pre>
|
||||
*
|
||||
* @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 @@ public interface Locator {
|
||||
* <p> If {@code expression} throws or rejects, this method throws.
|
||||
*
|
||||
* <p> <strong>Usage</strong>
|
||||
* <pre>{@code
|
||||
* Locator tweets = page.locator(".tweet .retweets");
|
||||
* assertEquals("10 retweets", tweets.evaluate("node => node.innerText"));
|
||||
* }</pre>
|
||||
*
|
||||
* @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 @@ public interface Locator {
|
||||
*/
|
||||
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 <a href="https://playwright.dev/java/docs/touch-events">emulating legacy touch
|
||||
* events</a> page.
|
||||
*
|
||||
* <p> <strong>Details</strong>
|
||||
*
|
||||
@@ -5238,7 +5243,9 @@ public interface Locator {
|
||||
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 <a href="https://playwright.dev/java/docs/touch-events">emulating legacy touch
|
||||
* events</a> page.
|
||||
*
|
||||
* <p> <strong>Details</strong>
|
||||
*
|
||||
|
||||
@@ -965,6 +965,11 @@ public interface Page extends AutoCloseable {
|
||||
* {@code "no-preference"} is deprecated.
|
||||
*/
|
||||
public Optional<ColorScheme> colorScheme;
|
||||
/**
|
||||
* Emulates {@code "prefers-contrast"} media feature, supported values are {@code "no-preference"}, {@code "more"}. Passing
|
||||
* {@code null} disables contrast emulation.
|
||||
*/
|
||||
public Optional<Contrast> 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 interface Page extends AutoCloseable {
|
||||
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 @@ public interface Page extends AutoCloseable {
|
||||
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 <a href="https://playwright.dev/java/docs/locators#matching-only-visible-elements">Matching only
|
||||
* visible elements</a> to disable that.
|
||||
*/
|
||||
public List<Locator> mask;
|
||||
/**
|
||||
@@ -2617,7 +2632,9 @@ public interface Page extends AutoCloseable {
|
||||
}
|
||||
/**
|
||||
* 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 <a href="https://playwright.dev/java/docs/locators#matching-only-visible-elements">Matching only
|
||||
* visible elements</a> to disable that.
|
||||
*/
|
||||
public ScreenshotOptions setMask(List<Locator> mask) {
|
||||
this.mask = mask;
|
||||
|
||||
@@ -20,6 +20,9 @@ package com.microsoft.playwright;
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* <p> This class is limited to emulating tap gestures. For examples of other gestures simulated by manually dispatching touch
|
||||
* events, see the <a href="https://playwright.dev/java/docs/touch-events">emulating legacy touch events</a> page.
|
||||
*/
|
||||
public interface Touchscreen {
|
||||
/**
|
||||
|
||||
@@ -20,7 +20,10 @@ import java.util.function.Consumer;
|
||||
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.
|
||||
*
|
||||
* <p> If you want to intercept or modify WebSocket frames, consider using {@code WebSocketRoute}.
|
||||
*/
|
||||
public interface WebSocket {
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ public interface PageAssertions {
|
||||
class HasURLOptions {
|
||||
/**
|
||||
* Whether to perform case-insensitive match. {@code ignoreCase} option takes precedence over the corresponding regular
|
||||
* expression flag if specified.
|
||||
* expression parameter if specified. A provided predicate ignores this flag.
|
||||
*/
|
||||
public Boolean ignoreCase;
|
||||
/**
|
||||
@@ -64,7 +64,7 @@ public interface PageAssertions {
|
||||
|
||||
/**
|
||||
* Whether to perform case-insensitive match. {@code ignoreCase} option takes precedence over the corresponding regular
|
||||
* expression flag if specified.
|
||||
* expression parameter if specified. A provided predicate ignores this flag.
|
||||
*/
|
||||
public HasURLOptions setIgnoreCase(boolean ignoreCase) {
|
||||
this.ignoreCase = ignoreCase;
|
||||
|
||||
@@ -46,22 +46,20 @@ class APIResponseImpl implements APIResponse {
|
||||
|
||||
@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;
|
||||
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
|
||||
|
||||
@@ -623,14 +623,22 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
|
||||
|
||||
@Override
|
||||
public String storageState(StorageStateOptions options) {
|
||||
return withLogging("BrowserContext.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;
|
||||
});
|
||||
return withLogging("BrowserContext.storageState", () -> storageStateImpl(options));
|
||||
}
|
||||
|
||||
private String storageStateImpl(StorageStateOptions options) {
|
||||
if (options == null) {
|
||||
options = new StorageStateOptions();
|
||||
}
|
||||
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
|
||||
params.remove("path");
|
||||
JsonElement json = sendMessage("storageState", params);
|
||||
|
||||
String storageState = json.toString();
|
||||
if (options.path != null) {
|
||||
Utils.writeToFile(storageState.getBytes(StandardCharsets.UTF_8), options.path);
|
||||
}
|
||||
return storageState;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -26,6 +26,7 @@ import com.microsoft.playwright.options.HarContentPolicy;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static com.microsoft.playwright.impl.Serialization.addHarUrlFilter;
|
||||
@@ -196,6 +197,10 @@ class BrowserTypeImpl extends ChannelOwner implements BrowserType {
|
||||
}
|
||||
|
||||
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
|
||||
if (!userDataDir.isAbsolute() && !userDataDir.toString().isEmpty()) {
|
||||
Path cwd = Paths.get("").toAbsolutePath();
|
||||
userDataDir = cwd.resolve(userDataDir);
|
||||
}
|
||||
params.addProperty("userDataDir", userDataDir.toString());
|
||||
if (recordHar != null) {
|
||||
params.add("recordHar", recordHar);
|
||||
|
||||
@@ -136,6 +136,6 @@ class FrameLocatorImpl implements FrameLocator {
|
||||
|
||||
@Override
|
||||
public Locator owner() {
|
||||
return new LocatorImpl(frame, frameSelector);
|
||||
return new LocatorImpl(frame, frameSelector, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,29 +21,25 @@ import com.google.gson.JsonObject;
|
||||
import com.microsoft.playwright.*;
|
||||
import com.microsoft.playwright.options.*;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static com.microsoft.playwright.impl.LocatorUtils.*;
|
||||
import static com.microsoft.playwright.impl.Serialization.gson;
|
||||
import static com.microsoft.playwright.impl.Utils.convertType;
|
||||
import static com.microsoft.playwright.impl.Utils.toJsRegexFlags;
|
||||
|
||||
class LocatorImpl implements Locator {
|
||||
final FrameImpl frame;
|
||||
final String selector;
|
||||
|
||||
LocatorImpl(FrameImpl frame, String frameSelector) {
|
||||
this(frame, frameSelector, null);
|
||||
LocatorImpl(FrameImpl frame, String selector, LocatorOptions options) {
|
||||
this(frame, selector, options, null);
|
||||
}
|
||||
|
||||
public LocatorImpl(FrameImpl frame, String selector, LocatorOptions options) {
|
||||
private LocatorImpl(FrameImpl frame, String selector, LocatorOptions options, Boolean visible) {
|
||||
this.frame = frame;
|
||||
if (options != null) {
|
||||
if (options.hasText != null) {
|
||||
@@ -65,6 +61,9 @@ class LocatorImpl implements Locator {
|
||||
selector += " >> internal:has-not=" + gson().toJson(locator.selector);
|
||||
}
|
||||
}
|
||||
if (visible != null) {
|
||||
selector += " >> visible=" + visible;
|
||||
}
|
||||
this.selector = selector;
|
||||
}
|
||||
|
||||
@@ -252,7 +251,8 @@ class LocatorImpl implements Locator {
|
||||
|
||||
@Override
|
||||
public Locator filter(FilterOptions options) {
|
||||
return new LocatorImpl(frame, selector, convertType(options,LocatorOptions.class));
|
||||
Boolean visible = (options == null) ? null : options.visible;
|
||||
return new LocatorImpl(frame, selector, convertType(options, LocatorOptions.class), visible);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -47,6 +47,7 @@ class Serialization {
|
||||
.registerTypeAdapter(SameSiteAttribute.class, new SameSiteAdapter().nullSafe())
|
||||
.registerTypeAdapter(BrowserChannel.class, new ToLowerCaseAndDashSerializer<BrowserChannel>())
|
||||
.registerTypeAdapter(ColorScheme.class, new ToLowerCaseAndDashSerializer<ColorScheme>())
|
||||
.registerTypeAdapter(Contrast.class, new ToLowerCaseAndDashSerializer<Contrast>())
|
||||
.registerTypeAdapter(Media.class, new ToLowerCaseSerializer<Media>())
|
||||
.registerTypeAdapter(ForcedColors.class, new ToLowerCaseSerializer<ForcedColors>())
|
||||
.registerTypeAdapter(HttpCredentialsSend.class, new ToLowerCaseSerializer<HttpCredentialsSend>())
|
||||
@@ -425,6 +426,7 @@ class Serialization {
|
||||
private static boolean isSupported(Type type) {
|
||||
return new TypeToken<Optional<Media>>() {}.getType().getTypeName().equals(type.getTypeName()) ||
|
||||
new TypeToken<Optional<ColorScheme>>() {}.getType().getTypeName().equals(type.getTypeName()) ||
|
||||
new TypeToken<Optional<Contrast>>() {}.getType().getTypeName().equals(type.getTypeName()) ||
|
||||
new TypeToken<Optional<ForcedColors>>() {}.getType().getTypeName().equals(type.getTypeName()) ||
|
||||
new TypeToken<Optional<ReducedMotion>>() {}.getType().getTypeName().equals(type.getTypeName()) ||
|
||||
new TypeToken<Optional<ViewportSize>>() {}.getType().getTypeName().equals(type.getTypeName());
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.microsoft.playwright.options;
|
||||
|
||||
public enum Contrast {
|
||||
NO_PREFERENCE,
|
||||
MORE
|
||||
}
|
||||
@@ -22,8 +22,9 @@ 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.Charset;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import java.text.ParseException;
|
||||
@@ -34,7 +35,7 @@ import java.util.*;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import static com.microsoft.playwright.Utils.*;
|
||||
import static com.microsoft.playwright.Utils.mapOf;
|
||||
import static java.util.Arrays.asList;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ import java.nio.file.Path;
|
||||
|
||||
import static com.microsoft.playwright.Utils.assertJsonEquals;
|
||||
import static com.microsoft.playwright.Utils.mapOf;
|
||||
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
public class TestBrowserContextStorageState extends TestBase {
|
||||
@@ -169,4 +170,97 @@ public class TestBrowserContextStorageState extends TestBase {
|
||||
" }]\n" +
|
||||
"}]}", new Gson().fromJson(storageState, JsonObject.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSupportIndexedDB() {
|
||||
page.navigate(server.PREFIX + "/to-do-notifications/index.html");
|
||||
page.locator("label:has-text('Task title')").fill("Pet the cat");
|
||||
page.locator("label:has-text('Hours')").fill("1");
|
||||
page.locator("label:has-text('Mins')").fill("1");
|
||||
page.locator("text=Add Task").click();
|
||||
|
||||
String storageState = page.context().storageState(new BrowserContext.StorageStateOptions().setIndexedDB(true));
|
||||
assertJsonEquals("{\"cookies\":[],\"origins\":[\n" +
|
||||
" {\n" +
|
||||
" \"origin\": \"" + server.PREFIX + "\",\n" +
|
||||
" \"localStorage\": [],\n" +
|
||||
" \"indexedDB\": [\n" +
|
||||
" {\n" +
|
||||
" \"name\": \"toDoList\",\n" +
|
||||
" \"version\": 4,\n" +
|
||||
" \"stores\": [\n" +
|
||||
" {\n" +
|
||||
" \"name\": \"toDoList\",\n" +
|
||||
" \"autoIncrement\": false,\n" +
|
||||
" \"keyPath\": \"taskTitle\",\n" +
|
||||
" \"records\": [\n" +
|
||||
" {\n" +
|
||||
" \"value\": {\n" +
|
||||
" \"day\": \"01\",\n" +
|
||||
" \"hours\": \"1\",\n" +
|
||||
" \"minutes\": \"1\",\n" +
|
||||
" \"month\": \"January\",\n" +
|
||||
" \"notified\": \"no\",\n" +
|
||||
" \"taskTitle\": \"Pet the cat\",\n" +
|
||||
" \"year\": \"2025\"\n" +
|
||||
" }\n" +
|
||||
" }\n" +
|
||||
" ],\n" +
|
||||
" \"indexes\": [\n" +
|
||||
" {\n" +
|
||||
" \"name\": \"day\",\n" +
|
||||
" \"keyPath\": \"day\",\n" +
|
||||
" \"multiEntry\": false,\n" +
|
||||
" \"unique\": false\n" +
|
||||
" },\n" +
|
||||
" {\n" +
|
||||
" \"name\": \"hours\",\n" +
|
||||
" \"keyPath\": \"hours\",\n" +
|
||||
" \"multiEntry\": false,\n" +
|
||||
" \"unique\": false\n" +
|
||||
" },\n" +
|
||||
" {\n" +
|
||||
" \"name\": \"minutes\",\n" +
|
||||
" \"keyPath\": \"minutes\",\n" +
|
||||
" \"multiEntry\": false,\n" +
|
||||
" \"unique\": false\n" +
|
||||
" },\n" +
|
||||
" {\n" +
|
||||
" \"name\": \"month\",\n" +
|
||||
" \"keyPath\": \"month\",\n" +
|
||||
" \"multiEntry\": false,\n" +
|
||||
" \"unique\": false\n" +
|
||||
" },\n" +
|
||||
" {\n" +
|
||||
" \"name\": \"notified\",\n" +
|
||||
" \"keyPath\": \"notified\",\n" +
|
||||
" \"multiEntry\": false,\n" +
|
||||
" \"unique\": false\n" +
|
||||
" },\n" +
|
||||
" {\n" +
|
||||
" \"name\": \"year\",\n" +
|
||||
" \"keyPath\": \"year\",\n" +
|
||||
" \"multiEntry\": false,\n" +
|
||||
" \"unique\": false\n" +
|
||||
" }\n" +
|
||||
" ]\n" +
|
||||
" }\n" +
|
||||
" ]\n" +
|
||||
" }\n" +
|
||||
" ]\n" +
|
||||
" }\n" +
|
||||
"]}", new Gson().fromJson(storageState, JsonObject.class));
|
||||
|
||||
BrowserContext context = browser.newContext(new Browser.NewContextOptions().setStorageState(storageState));
|
||||
assertEquals(storageState, context.storageState(new BrowserContext.StorageStateOptions().setIndexedDB(true)));
|
||||
|
||||
Page recreatedPage = context.newPage();
|
||||
recreatedPage.navigate(server.PREFIX + "/to-do-notifications/index.html");
|
||||
assertThat(recreatedPage.locator("#task-list")).matchesAriaSnapshot("\n" +
|
||||
" - list:\n" +
|
||||
" - listitem:\n" +
|
||||
" - text: /Pet the cat/\n");
|
||||
assertEquals("{\"cookies\":[],\"origins\":[]}", context.storageState(
|
||||
new BrowserContext.StorageStateOptions().setIndexedDB(false)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package com.microsoft.playwright;
|
||||
|
||||
import com.microsoft.playwright.options.Contrast;
|
||||
import com.microsoft.playwright.options.Geolocation;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Assumptions;
|
||||
@@ -28,6 +29,7 @@ import java.io.OutputStreamWriter;
|
||||
import java.io.Writer;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
@@ -282,4 +284,27 @@ public class TestDefaultBrowserContext2 extends TestBase {
|
||||
assertEquals(1, fields.size());
|
||||
assertEquals("200MB.zip", fields.get(0).filename);
|
||||
assertEquals(200 * 1024 * 1024, fields.get(0).content.length());
|
||||
}}
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSupportContrastOption() {
|
||||
Page page = launchPersistent(new BrowserType.LaunchPersistentContextOptions().setContrast(Contrast.MORE));
|
||||
assertEquals(true, page.evaluate("() => matchMedia('(prefers-contrast: more)').matches"));
|
||||
assertEquals(false, page.evaluate("() => matchMedia('(prefers-contrast: no-preference)').matches"));
|
||||
}
|
||||
|
||||
static boolean tempDirCanBeOnDifferentRoot() {
|
||||
return isWindows;
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisabledIf(value="tempDirCanBeOnDifferentRoot", disabledReason="IllegalArgument 'other' has different root on GitHub Actions.")
|
||||
void shouldAcceptRelativeUserDataDir(@TempDir Path tmpDir) throws Exception {
|
||||
Path userDataDir = tempDir.resolve("user-data-dir");
|
||||
Path cwd = Paths.get("").toAbsolutePath();
|
||||
Path relativePath = cwd.relativize(userDataDir);
|
||||
BrowserContext context = browserType.launchPersistentContext(relativePath);
|
||||
assertTrue(Files.list(userDataDir).count() > 0);
|
||||
context.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -548,4 +548,36 @@ public class TestGlobalFetch extends TestBase {
|
||||
assertEquals(4, requestCount[0]);
|
||||
requestContext.dispose();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldThrowWhenFailOnStatusCodeIsSetToTrue() {
|
||||
APIRequestContext request = playwright.request().newContext(new APIRequest.NewContextOptions().setFailOnStatusCode(true));
|
||||
server.setRoute("/empty.html", exchange -> {
|
||||
exchange.getResponseHeaders().set("Content-Length", "10");
|
||||
exchange.getResponseHeaders().set("Content-type", "text/plain");
|
||||
exchange.sendResponseHeaders(404, 10);
|
||||
try (Writer writer = new OutputStreamWriter(exchange.getResponseBody())) {
|
||||
writer.write("Not found.");
|
||||
}
|
||||
});
|
||||
PlaywrightException error = assertThrows(PlaywrightException.class, () -> {
|
||||
request.get(server.EMPTY_PAGE);
|
||||
});
|
||||
assertTrue(error.getMessage().contains("404 Not Found"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotThrowWhenFailOnStatusCodeIsSetToFalse() {
|
||||
APIRequestContext request = playwright.request().newContext(new APIRequest.NewContextOptions().setFailOnStatusCode(false));
|
||||
server.setRoute("/empty.html", exchange -> {
|
||||
exchange.getResponseHeaders().set("Content-Length", "10");
|
||||
exchange.getResponseHeaders().set("Content-type", "text/plain");
|
||||
exchange.sendResponseHeaders(404, 10);
|
||||
try (Writer writer = new OutputStreamWriter(exchange.getResponseBody())) {
|
||||
writer.write("Not found.");
|
||||
}
|
||||
});
|
||||
APIResponse response = request.get(server.EMPTY_PAGE);
|
||||
assertEquals(404, response.status());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class TestLocatorMisc extends TestBase{
|
||||
@@ -123,4 +124,20 @@ public class TestLocatorMisc extends TestBase{
|
||||
page.locator("input").pressSequentially("hello");
|
||||
assertEquals("hello", page.evalOnSelector("input", "input => input.value"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldSupportFilterVisible() {
|
||||
page.setContent("<div>\n" +
|
||||
" <div class=\"item\" style=\"display: none\">Hidden data0</div>\n" +
|
||||
" <div class=\"item\">visible data1</div>\n" +
|
||||
" <div class=\"item\" style=\"display: none\">Hidden data1</div>\n" +
|
||||
" <div class=\"item\">visible data2</div>\n" +
|
||||
" <div class=\"item\" style=\"display: none\">Hidden data2</div>\n" +
|
||||
" <div class=\"item\">visible data3</div>\n" +
|
||||
"</div>");
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 @@ public class TestPageEmulateMedia extends TestBase {
|
||||
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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,7 +253,7 @@ public class TestPageScreenshot extends TestBase {
|
||||
}
|
||||
|
||||
static boolean isScreenshotTestDisabled() {
|
||||
if (isWebKit()) {
|
||||
if (isWebKit() || isChromium()) {
|
||||
// Array lengths differ.
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
<http://creativecommons.org/publicdomain/zero/1.0/>
|
||||
@@ -0,0 +1 @@
|
||||
Source: https://github.com/mdn/dom-examples/tree/main/to-do-notifications
|
||||
@@ -0,0 +1,108 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=380">
|
||||
<script src="scripts/todo.js"></script>
|
||||
<title>To-do list with Notifications</title>
|
||||
<link href="style/style.css" type="text/css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>To-do list</h1>
|
||||
|
||||
<div class="task-box">
|
||||
|
||||
<ul id="task-list">
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="form-box">
|
||||
<h2>Add new to-do item.</h2>
|
||||
|
||||
<form id="task-form" action="index.html">
|
||||
<div class="full-width"><label for="title">Task title:</label><input type="text" id="title" required></div>
|
||||
<div class="half-width"><label for="deadline-hours">Hours (hh):</label><input type="number" id="deadline-hours" required></div>
|
||||
<div class="half-width"><label for="deadline-minutes">Mins (mm):</label><input type="number" id="deadline-minutes" required></div>
|
||||
<div class="third-width"><label for="deadline-day">Day:</label>
|
||||
<select id="deadline-day" required>
|
||||
<option value="01">01</option>
|
||||
<option value="02">02</option>
|
||||
<option value="03">03</option>
|
||||
<option value="04">04</option>
|
||||
<option value="05">05</option>
|
||||
<option value="06">06</option>
|
||||
<option value="07">07</option>
|
||||
<option value="08">08</option>
|
||||
<option value="09">09</option>
|
||||
<option value="10">10</option>
|
||||
<option value="11">11</option>
|
||||
<option value="12">12</option>
|
||||
<option value="13">13</option>
|
||||
<option value="14">14</option>
|
||||
<option value="15">15</option>
|
||||
<option value="16">16</option>
|
||||
<option value="17">17</option>
|
||||
<option value="18">18</option>
|
||||
<option value="19">19</option>
|
||||
<option value="20">20</option>
|
||||
<option value="21">21</option>
|
||||
<option value="22">22</option>
|
||||
<option value="23">23</option>
|
||||
<option value="24">24</option>
|
||||
<option value="25">25</option>
|
||||
<option value="26">26</option>
|
||||
<option value="27">27</option>
|
||||
<option value="28">28</option>
|
||||
<option value="29">29</option>
|
||||
<option value="30">30</option>
|
||||
<option value="31">31</option>
|
||||
</select></div>
|
||||
|
||||
<div class="third-width"><label for="deadline-month">Month:</label>
|
||||
<select id="deadline-month" required>
|
||||
<option value="January">January</option>
|
||||
<option value="February">February</option>
|
||||
<option value="March">March</option>
|
||||
<option value="April">April</option>
|
||||
<option value="May">May</option>
|
||||
<option value="June">June</option>
|
||||
<option value="July">July</option>
|
||||
<option value="August">August</option>
|
||||
<option value="September">September</option>
|
||||
<option value="October">October</option>
|
||||
<option value="November">November</option>
|
||||
<option value="December">December</option>
|
||||
</select></div>
|
||||
|
||||
<div class="third-width"><label for="deadline-year">Year:</label>
|
||||
<select id="deadline-year" required>
|
||||
<option value="2025">2025</option>
|
||||
<option value="2024">2024</option>
|
||||
<option value="2023">2023</option>
|
||||
<option value="2022">2022</option>
|
||||
<option value="2021">2021</option>
|
||||
<option value="2020">2020</option>
|
||||
<option value="2019">2019</option>
|
||||
<option value="2018">2018</option>
|
||||
</select></div>
|
||||
|
||||
<div><input type="submit" id="submit" value="Add Task"></div>
|
||||
<div></div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="toolbar">
|
||||
<ul id="notifications">
|
||||
|
||||
</ul>
|
||||
|
||||
<button id="enable">
|
||||
Enable notifications
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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`;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
1.50.1-beta-1738592302000
|
||||
1.51.0
|
||||
|
||||
Reference in New Issue
Block a user