├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .rustfmt.toml ├── Cargo.toml ├── Makefile ├── README.md ├── scripts ├── Cargo.toml ├── Makefile ├── src │ ├── diff.rs │ └── gen.rs └── types.json ├── src ├── api.rs ├── api │ ├── accessibility.rs │ ├── api.json │ ├── browser.rs │ ├── browser_context.rs │ ├── browser_type.rs │ ├── console_message.rs │ ├── dialog.rs │ ├── download.rs │ ├── element_handle.rs │ ├── file_chooser.rs │ ├── frame.rs │ ├── generated.rs │ ├── input_device.rs │ ├── js_handle.rs │ ├── page.rs │ ├── playwright.rs │ ├── request.rs │ ├── response.rs │ ├── route.rs │ ├── selectors.rs │ ├── video.rs │ ├── websocket.rs │ └── worker.rs ├── build.rs ├── imp.rs ├── imp │ ├── artifact.rs │ ├── binding_call.rs │ ├── browser.rs │ ├── browser_context.rs │ ├── browser_type.rs │ ├── console_message.rs │ ├── core │ │ ├── connection.rs │ │ ├── driver.rs │ │ ├── event_emitter.rs │ │ ├── message.rs │ │ ├── message │ │ │ ├── de.rs │ │ │ └── ser.rs │ │ ├── remote_object.rs │ │ └── transport.rs │ ├── dialog.rs │ ├── download.rs │ ├── element_handle.rs │ ├── file_hooser.rs │ ├── frame.rs │ ├── js_handle.rs │ ├── page.rs │ ├── playwright.rs │ ├── request.rs │ ├── response.rs │ ├── route.rs │ ├── selectors.rs │ ├── stream.rs │ ├── utils.rs │ ├── video.rs │ ├── websocket.rs │ └── worker.rs ├── lib.rs └── main.rs └── tests ├── browser └── mod.rs ├── browser_context └── mod.rs ├── browser_type └── mod.rs ├── connect └── mod.rs ├── devices └── mod.rs ├── hello.rs ├── page └── mod.rs ├── selectors └── mod.rs ├── server ├── empty.html ├── empty2.html ├── form.html ├── worker.html └── worker.js └── test.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | RUST_LOG: trace 12 | 13 | jobs: 14 | test: 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, windows-latest] 18 | runs-on: ${{ matrix.os }} 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Install latest nightly 23 | uses: actions-rs/toolchain@v1 24 | with: 25 | toolchain: stable 26 | override: true 27 | components: rustfmt, clippy 28 | 29 | - name: Lint 30 | run: cargo clippy --all-targets 31 | - name: Prepare driver and browsers 32 | run: cargo test hello 33 | - name: Test 34 | run: cargo test --all-targets -- --nocapture 35 | 36 | - name: Coverage 37 | if: success() && matrix.os == 'ubuntu-latest' && github.ref == 'refs/heads/master' 38 | run: | 39 | cargo install cargo-tarpaulin 40 | cargo tarpaulin --out Xml --verbose --exclude-files scripts/ tests/ src/build.rs src/main.rs src/generated.rs 41 | - name: Upload to codecov.io 42 | if: success() && matrix.os == 'ubuntu-latest' && github.ref == 'refs/heads/master' 43 | uses: codecov/codecov-action@v1.0.2 44 | with: 45 | file: cobertura.xml 46 | token: ${{secrets.CODECOV}} 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | driver 4 | tarpaulin-report.html 5 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2018" 2 | unstable_features = true 3 | fn_single_line = true 4 | trailing_comma = "Never" 5 | imports_granularity = "Crate" 6 | normalize_comments = true 7 | normalize_doc_attributes = true 8 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "playwright" 3 | version = "0.0.20" 4 | authors = ["octaltree "] 5 | description = "Playwright port to Rust" 6 | license = "MIT OR Apache-2.0" 7 | documentation = "https://docs.rs/playwright/" 8 | repository = "https://github.com/octaltree/playwright-rust" 9 | categories = ["web-programming"] 10 | keywords = ["testing", "headless", "web", "browser", "automation"] 11 | edition = "2018" 12 | build = "src/build.rs" 13 | 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 | 16 | [build-dependencies] 17 | reqwest = { version = "0.11.4", features = ["blocking"] } 18 | 19 | [dependencies] 20 | log = "0.4.14" 21 | serde = { version = "1.0.126", features = ["derive"] } 22 | serde_json = "1.0.66" 23 | zip = "0.5.13" 24 | thiserror = "1.0.26" 25 | strong = { version = "0.3.4", features = ["serde", "shorthand"] } 26 | tokio = { version = "1.9.0", features = ["sync", "rt-multi-thread", "macros"] } 27 | actix-rt = { version = "2.2.0", optional = true } 28 | async-std = { version = "1.9.0", features = ["attributes"], optional = true } 29 | dirs = "3.0.2" 30 | paste = "1.0.5" 31 | base64 = "0.13.0" 32 | itertools = "0.10.1" 33 | chrono = { version = "0.4.19", optional = true, features = ["serde"] } 34 | tokio-stream = { version = "0.1.7", features = ["sync"] } 35 | futures = "0.3.16" 36 | serde_with = { version = "1.9.4", default-features = false, features = ["macros"] } 37 | 38 | [dev-dependencies] 39 | env_logger = "0.9.0" 40 | tempdir = "0.3.7" 41 | tide = "0.16.0" 42 | warp = "0.3.1" 43 | 44 | [features] 45 | default = ["chrono", "rt-tokio"] 46 | rt-tokio = [] 47 | rt-actix = ["actix-rt"] 48 | rt-async-std = ["async-std"] 49 | only-for-docs-rs = [] 50 | 51 | [package.metadata.docs.rs] 52 | features = ["only-for-docs-rs"] 53 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: format lint test doc 2 | cargo build --release 3 | 4 | dev: 5 | cargo fmt 6 | cargo clippy --all-targets 7 | cargo test 8 | cargo doc 9 | 10 | d: 11 | cargo watch -c -s 'make dev' 12 | 13 | format: 14 | cargo fmt 15 | 16 | lint: 17 | cargo clippy --all-targets 18 | cargo clippy --no-default-features --features chrono --features rt-actix --all-targets 19 | cargo clippy --no-default-features --features chrono --features rt-async-std --all-targets 20 | 21 | test: 22 | cargo test hello 23 | cargo test --all-targets 24 | cargo test --no-default-features --features chrono --features rt-actix --all-targets 25 | cargo test --no-default-features --features chrono --features rt-async-std --all-targets 26 | 27 | doc: 28 | cargo doc 29 | 30 | cov: 31 | cargo tarpaulin --out html --exclude-files scripts/ tests/ src/build.rs src/main.rs src/generated.rs 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🎭 [Playwright](https://playwright.dev) for Rust 2 | 3 | [![crates.io](https://img.shields.io/crates/v/playwright)](https://crates.io/crates/playwright) 4 | [![docs.rs](https://docs.rs/playwright/badge.svg)](https://docs.rs/playwright/) 5 | ![MIT OR Apache-2.0](https://img.shields.io/crates/l/playwright) 6 | [![CI](https://github.com/octaltree/playwright-rust/actions/workflows/ci.yml/badge.svg)](https://github.com/octaltree/playwright-rust/actions/workflows/ci.yml) 7 | [![codecov](https://codecov.io/gh/octaltree/playwright-rust/branch/master/graph/badge.svg)](https://codecov.io/gh/octaltree/playwright-rust) 8 | 9 | Playwright is a rust library to automate [Chromium](https://www.chromium.org/Home), [Firefox](https://www.mozilla.org/en-US/firefox/new/) and [WebKit](https://webkit.org/) built on top of Node.js library. 10 | 11 | ## Installation 12 | ``` 13 | [dependencies] 14 | playwright = "0.0.20" 15 | ``` 16 | 17 | ## Usage 18 | ```rust 19 | use playwright::Playwright; 20 | 21 | #[tokio::main] 22 | async fn main() -> Result<(), playwright::Error> { 23 | let playwright = Playwright::initialize().await?; 24 | playwright.prepare()?; // Install browsers 25 | let chromium = playwright.chromium(); 26 | let browser = chromium.launcher().headless(true).launch().await?; 27 | let context = browser.context_builder().build().await?; 28 | let page = context.new_page().await?; 29 | page.goto_builder("https://example.com/").goto().await?; 30 | 31 | // Exec in browser and Deserialize with serde 32 | let s: String = page.eval("() => location.href").await?; 33 | assert_eq!(s, "https://example.com/"); 34 | page.click_builder("a").click().await?; 35 | Ok(()) 36 | } 37 | ``` 38 | 39 | ## Async runtimes 40 | * [tokio](https://crates.io/crates/tokio) 41 | * [actix-rt](https://crates.io/crates/actix-rt) 42 | * [async-std](https://crates.io/crates/async-std) 43 | 44 | These runtimes have passed tests. You can disable tokio, the default feature, and then choose another. 45 | 46 | ## Incompatibility 47 | Functions do not have default arguments in rust. 48 | Functions with two or more optional arguments are now passed with the builder pattern. 49 | 50 | ## Playwright Driver 51 | Playwright is designed as a server-client. All playwright client dependent on the driver: zip of core js library and Node.js. 52 | Application uses this library will be bundled the driver into rust binary at build time. There is an overhead of unzipping on the first run. 53 | 54 | ### NOTICE 55 | ``` 56 | playwright-rust redistributes Playwright licensed under the Apache 2.0. 57 | Playwright has NOTICE: 58 | """ 59 | Playwright 60 | Copyright (c) Microsoft Corporation 61 | 62 | This software contains code derived from the Puppeteer project (https://github.com/puppeteer/puppeteer), 63 | available under the Apache 2.0 license (https://github.com/puppeteer/puppeteer/blob/master/LICENSE). 64 | """ 65 | ``` 66 | 67 | ## Browser automation in rust 68 | - [atroche/rust-headless-chrome](https://github.com/atroche/rust-headless-chrome) 69 | - [saresend/selenium-rs](https://github.com/saresend/selenium-rs) 70 | - [https://crates.io/crates/webdriver](https://crates.io/crates/webdriver) 71 | - [mattsse/chromiumoxide](https://github.com/mattsse/chromiumoxide) 72 | 73 | ## Other languages 74 | - [microsoft/playwright](https://github.com/microsoft/playwright) 75 | * [Documentation](https://playwright.dev/docs/intro/) 76 | * [API Reference](https://playwright.dev/docs/api/class-playwright/) 77 | - [microsoft/playwright-python](https://github.com/microsoft/playwright-python) 78 | - [microsoft/playwright-sharp](https://github.com/microsoft/playwright-sharp) 79 | - [microsoft/playwright-java](https://github.com/microsoft/playwright-java) 80 | - [mxschmitt/playwright-go](https://github.com/mxschmitt/playwright-go) 81 | - [YusukeIwaki/playwright-ruby-client](https://github.com/YusukeIwaki/playwright-ruby-client) 82 | - [teodesian/playwright-perl](https://github.com/teodesian/playwright-perl) 83 | - [luka-dev/playwright-php](https://github.com/luka-dev/playwright-php) 84 | - [naqvis/playwright-cr](https://github.com/naqvis/playwright-cr) 85 | - [geometerio/playwright-elixir](https://github.com/geometerio/playwright-elixir) 86 | -------------------------------------------------------------------------------- /scripts/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "scripts" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow = "1.0.41" 10 | case = "1.0.0" 11 | proc-macro2 = "1.0.27" 12 | quote = "1.0.9" 13 | serde = { version = "1.0.126", features = ["derive"] } 14 | serde_json = "1.0.64" 15 | 16 | [[bin]] 17 | name = "gen" 18 | path = "src/gen.rs" 19 | 20 | [[bin]] 21 | name = "diff" 22 | path = "src/diff.rs" 23 | -------------------------------------------------------------------------------- /scripts/Makefile: -------------------------------------------------------------------------------- 1 | ../src/api/generated.rs: ../src/api/api.json 2 | rustfmt --emit stdout <(cargo run --bin gen < $^)| tail +3 > $@ 3 | sed -i 's/\[actionability\](.\/actionability.md)/[actionability](https:\/\/playwright.dev\/docs\/actionability\/)/g' $@ 4 | 5 | ../src/api/api.json: ../src/build.rs 6 | cd .. && cargo run print-api-json |jq > src/api/api.json 7 | 8 | diff: 9 | cargo run --bin diff <(git show master:src/api/api.json) <(git show HEAD:src/api/api.json) 10 | -------------------------------------------------------------------------------- /scripts/src/diff.rs: -------------------------------------------------------------------------------- 1 | use serde_json::Value; 2 | use std::{ 3 | collections::{HashMap, HashSet}, 4 | env, fmt, fs 5 | }; 6 | 7 | fn main() -> anyhow::Result<()> { 8 | let mut args = env::args_os(); 9 | args.next().unwrap(); 10 | let (fst, snd) = (args.next().unwrap(), args.next().unwrap()); 11 | let files = (fs::File::open(fst)?, fs::File::open(snd)?); 12 | let values: (Value, Value) = ( 13 | serde_json::from_reader(files.0)?, 14 | serde_json::from_reader(files.1)? 15 | ); 16 | let diffs = diff_interfaces(values); 17 | for diff in diffs { 18 | println!("{}", diff); 19 | } 20 | Ok(()) 21 | } 22 | 23 | #[derive(Debug)] 24 | enum DiffInterface { 25 | Add(String), 26 | Remove(String), 27 | Change { 28 | name: String, 29 | members: Vec, 30 | others: bool 31 | } 32 | } 33 | 34 | #[derive(Debug)] 35 | enum DiffMember { 36 | Add(String), 37 | Remove(String), 38 | Change(String) 39 | } 40 | 41 | fn diff_interfaces((fst, snd): (Value, Value)) -> Vec { 42 | let dic = (collect_dic(&fst), collect_dic(&snd)); 43 | let names: (HashSet<&str>, HashSet<&str>) = ( 44 | dic.0.keys().cloned().collect(), 45 | dic.1.keys().cloned().collect() 46 | ); 47 | let (added, removed, intersections) = ( 48 | &names.1 - &names.0, 49 | &names.0 - &names.1, 50 | names.0.intersection(&names.1).cloned() 51 | ); 52 | let (added, removed, changes) = ( 53 | added.into_iter().map(|s| DiffInterface::Add(s.into())), 54 | removed.into_iter().map(|s| DiffInterface::Remove(s.into())), 55 | intersections 56 | .into_iter() 57 | .filter(|s| dic.0.get(s).unwrap() != dic.1.get(s).unwrap()) 58 | .map(|s| { 59 | let (mut fst, mut snd) = ( 60 | (*dic.0.get(s).unwrap()).clone(), 61 | (*dic.1.get(s).unwrap()).clone() 62 | ); 63 | *fst.as_object_mut().unwrap().get_mut("members").unwrap() = Vec::::new().into(); 64 | *snd.as_object_mut().unwrap().get_mut("members").unwrap() = Vec::::new().into(); 65 | let others = fst != snd; 66 | let members = diff_members((dic.0.get(s).unwrap(), dic.1.get(s).unwrap())); 67 | DiffInterface::Change { 68 | name: s.to_owned(), 69 | members, 70 | others 71 | } 72 | }) 73 | //.filter(|c| matches!(c, DiffInterface::Change { members, others, .. } if !members.is_empty() || *others)) 74 | ); 75 | added.chain(removed).chain(changes).collect() 76 | } 77 | 78 | fn diff_members((fst, snd): (&Value, &Value)) -> Vec { 79 | let members = ( 80 | collect_dic(fst.as_object().unwrap().get("members").unwrap()), 81 | collect_dic(snd.as_object().unwrap().get("members").unwrap()) 82 | ); 83 | let names: (HashSet<&str>, HashSet<&str>) = ( 84 | members.0.keys().cloned().collect(), 85 | members.1.keys().cloned().collect() 86 | ); 87 | let (added, removed, intersections) = ( 88 | &names.1 - &names.0, 89 | &names.0 - &names.1, 90 | names.0.intersection(&names.1).cloned() 91 | ); 92 | let (added, removed, changes) = ( 93 | added.into_iter().map(|s| DiffMember::Add(s.into())), 94 | removed.into_iter().map(|s| DiffMember::Remove(s.into())), 95 | intersections 96 | .into_iter() 97 | .filter(|s| members.0.get(s) != members.1.get(s)) 98 | .map(|s| DiffMember::Change(s.into())) 99 | ); 100 | added.chain(removed).chain(changes).collect() 101 | } 102 | 103 | fn collect_dic<'a>(v: &'a Value) -> HashMap<&'a str, &'a Value> { 104 | v.as_array() 105 | .unwrap() 106 | .into_iter() 107 | .map(|x| { 108 | ( 109 | x.as_object() 110 | .unwrap() 111 | .get("name") 112 | .unwrap() 113 | .as_str() 114 | .unwrap(), 115 | x 116 | ) 117 | }) 118 | .collect() 119 | } 120 | 121 | impl fmt::Display for DiffInterface { 122 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 123 | match self { 124 | Self::Add(name) => write!(f, "Add {}", name), 125 | Self::Remove(name) => write!(f, "Remove {}", name), 126 | Self::Change { 127 | name, 128 | members, 129 | others 130 | } => { 131 | let mut first_line_exists = false; 132 | if *others { 133 | write!(f, "Change {}", name)?; 134 | first_line_exists = true; 135 | } 136 | let br = |x: bool| x.then(|| "\n").unwrap_or_default(); 137 | for c in members { 138 | write!(f, "{}Change {} {}", br(first_line_exists), name, c)?; 139 | first_line_exists = true; 140 | } 141 | Ok(()) 142 | } 143 | } 144 | } 145 | } 146 | 147 | impl fmt::Display for DiffMember { 148 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 149 | match self { 150 | Self::Add(name) => write!(f, "Add {}", name), 151 | Self::Remove(name) => write!(f, "Remove {}", name), 152 | Self::Change(name) => write!(f, "Change {}", name) 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/api.rs: -------------------------------------------------------------------------------- 1 | macro_rules! setter { 2 | ( 3 | $( 4 | $(#[$meta:ident $($args:tt)*])* 5 | $field:ident : Option<$t:ty> 6 | ),+ 7 | ) => { 8 | $( 9 | paste::paste! { 10 | #[allow(clippy::wrong_self_convention)] 11 | $(#[$meta $($args)*])* 12 | pub fn [<$field>](mut self, x:$t) -> Self { 13 | self.args.$field = Some(x); 14 | self 15 | } 16 | } 17 | )* 18 | $( 19 | paste::paste! { 20 | pub fn [](mut self) -> Self { 21 | self.args.$field = None; 22 | self 23 | } 24 | } 25 | )* 26 | }; 27 | } 28 | 29 | #[doc(hidden)] 30 | #[macro_export] 31 | macro_rules! subscribe_event { 32 | () => { 33 | // TODO: FusedStream + Sink 34 | pub fn subscribe_event( 35 | &self 36 | ) -> Result< 37 | impl futures::stream::Stream< 38 | Item = Result 39 | >, 40 | Error 41 | > { 42 | use futures::stream::StreamExt; 43 | use tokio_stream::wrappers::BroadcastStream; 44 | let stream = BroadcastStream::new(upgrade(&self.inner)?.subscribe_event()); 45 | Ok(stream.map(|e| e.map(Event::from))) 46 | } 47 | }; 48 | } 49 | 50 | pub mod input_device; 51 | pub mod playwright; 52 | 53 | pub mod accessibility; 54 | pub mod browser; 55 | pub mod browser_context; 56 | pub mod browser_type; 57 | pub mod console_message; 58 | pub mod dialog; 59 | pub mod download; 60 | pub mod element_handle; 61 | pub mod file_chooser; 62 | pub mod frame; 63 | pub mod js_handle; 64 | pub mod page; 65 | pub mod request; 66 | pub mod response; 67 | pub mod route; 68 | pub mod selectors; 69 | pub mod video; 70 | pub mod websocket; 71 | pub mod worker; 72 | 73 | pub use crate::imp::{core::DateTime, utils::*}; 74 | 75 | pub use self::playwright::Playwright; 76 | pub use accessibility::Accessibility; 77 | pub use browser::Browser; 78 | pub use browser_context::BrowserContext; 79 | pub use browser_type::BrowserType; 80 | pub use console_message::ConsoleMessage; 81 | pub use dialog::Dialog; 82 | pub use download::Download; 83 | pub use element_handle::ElementHandle; 84 | pub use file_chooser::FileChooser; 85 | pub use frame::Frame; 86 | pub use input_device::{Keyboard, Mouse, TouchScreen}; 87 | pub use js_handle::JsHandle; 88 | pub use page::Page; 89 | pub use request::Request; 90 | pub use response::Response; 91 | pub use route::Route; 92 | pub use selectors::Selectors; 93 | pub use video::Video; 94 | pub use websocket::WebSocket; 95 | pub use worker::Worker; 96 | 97 | // Artifact 98 | // BindingCall 99 | // Stream 100 | 101 | // Android 102 | // AndroidDevice 103 | // androidinput 104 | // androidsocket 105 | // androidwebview 106 | // browserserver 107 | // cdpsession 108 | // coverage 109 | // electron 110 | // electronapplication 111 | // logger 112 | // websocketframe 113 | -------------------------------------------------------------------------------- /src/api/accessibility.rs: -------------------------------------------------------------------------------- 1 | pub use crate::imp::page::{AccessibilitySnapshotResponse as SnapshotResponse, Mixed, Val}; 2 | use crate::{ 3 | api::ElementHandle, 4 | imp::{ 5 | core::*, 6 | page::{AccessibilitySnapshotArgs as SnapshotArgs, Page as PageImpl}, 7 | prelude::* 8 | } 9 | }; 10 | 11 | /// The Accessibility class provides methods for inspecting Chromium's accessibility tree. The accessibility tree is used by 12 | /// assistive technology such as [screen readers](https://en.wikipedia.org/wiki/Screen_reader) or 13 | /// [switches](https://en.wikipedia.org/wiki/Switch_access). 14 | /// 15 | /// Accessibility is a very platform-specific thing. On different platforms, there are different screen readers that might 16 | /// have wildly different output. 17 | /// 18 | /// Rendering engines of Chromium, Firefox and WebKit have a concept of "accessibility tree", which is then translated into 19 | /// different platform-specific APIs. Accessibility namespace gives access to this Accessibility Tree. 20 | /// 21 | /// Most of the accessibility tree gets filtered out when converting from internal browser AX Tree to Platform-specific 22 | /// AX-Tree or by assistive technologies themselves. By default, Playwright tries to approximate this filtering, exposing 23 | /// only the "interesting" nodes of the tree. 24 | #[derive(Debug, Clone)] 25 | pub struct Accessibility { 26 | inner: Weak 27 | } 28 | 29 | impl Accessibility { 30 | pub(crate) fn new(inner: Weak) -> Self { Self { inner } } 31 | 32 | /// Captures the current state of the accessibility tree. The returned object represents the root accessible node of the 33 | /// page. 34 | /// 35 | /// > NOTE: The Chromium accessibility tree contains nodes that go unused on most platforms and by most screen readers. 36 | /// Playwright will discard them as well for an easier to process tree, unless `interestingOnly` is set to `false`. 37 | /// 38 | /// An example of dumping the entire accessibility tree: 39 | /// 40 | /// ```js 41 | /// const snapshot = await page.accessibility.snapshot(); 42 | /// console.log(snapshot); 43 | /// ``` 44 | /// 45 | /// An example of logging the focused node's name: 46 | /// 47 | /// ```js 48 | /// const snapshot = await page.accessibility.snapshot(); 49 | /// const node = findFocusedNode(snapshot); 50 | /// console.log(node && node.name); 51 | /// 52 | /// function findFocusedNode(node) { 53 | /// if (node.focused) 54 | /// return node; 55 | /// for (const child of node.children || []) { 56 | /// const foundNode = findFocusedNode(child); 57 | /// return foundNode; 58 | /// } 59 | /// return null; 60 | /// } 61 | /// var accessibilitySnapshot = await Page.Accessibility.SnapshotAsync(); 62 | /// var focusedNode = findFocusedNode(accessibilitySnapshot); 63 | /// if(focusedNode != null) 64 | /// Console.WriteLine(focusedNode.Name); 65 | /// ``` 66 | pub fn snapshot_builder(&self) -> SnapshotBuilder { SnapshotBuilder::new(self.inner.clone()) } 67 | } 68 | 69 | pub struct SnapshotBuilder { 70 | inner: Weak, 71 | args: SnapshotArgs 72 | } 73 | 74 | impl SnapshotBuilder { 75 | fn new(inner: Weak) -> Self { 76 | let args = SnapshotArgs::default(); 77 | Self { inner, args } 78 | } 79 | 80 | pub async fn snapshot(self) -> ArcResult> { 81 | let Self { inner, args } = self; 82 | upgrade(&inner)?.accessibility_snapshot(args).await 83 | } 84 | 85 | /// The root DOM element for the snapshot. Defaults to the whole page. 86 | pub fn try_root(mut self, x: ElementHandle) -> Result { 87 | let guid = x.guid()?; 88 | self.args.root = Some(OnlyGuid { guid }); 89 | Ok(self) 90 | } 91 | 92 | setter!( 93 | /// Prune uninteresting nodes from the tree. Defaults to `true`. 94 | interesting_only: Option 95 | ); 96 | 97 | pub fn clear_root(mut self) -> Self { 98 | self.args.root = None; 99 | self 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/api/browser.rs: -------------------------------------------------------------------------------- 1 | pub use crate::imp::browser_type::{RecordHar, RecordVideo}; 2 | use crate::{ 3 | api::BrowserContext, 4 | imp::{ 5 | self, 6 | browser::NewContextArgs, 7 | core::*, 8 | playwright::DeviceDescriptor, 9 | prelude::*, 10 | utils::{ColorScheme, Geolocation, HttpCredentials, ProxySettings, StorageState, Viewport} 11 | }, 12 | Error 13 | }; 14 | 15 | #[derive(Debug)] 16 | pub struct Browser { 17 | inner: Weak 18 | } 19 | 20 | impl PartialEq for Browser { 21 | fn eq(&self, other: &Self) -> bool { 22 | let a = self.inner.upgrade(); 23 | let b = other.inner.upgrade(); 24 | a.and_then(|a| b.map(|b| (a, b))) 25 | .map(|(a, b)| a.guid() == b.guid()) 26 | .unwrap_or_default() 27 | } 28 | } 29 | 30 | impl Browser { 31 | pub(crate) fn new(inner: Weak) -> Self { Self { inner } } 32 | 33 | /// Returns an array of all open browser contexts. In a newly created browser, this will return zero browser contexts. 34 | /// 35 | /// ```js 36 | /// const browser = await pw.webkit.launch(); 37 | /// console.log(browser.contexts().length); // prints `0` 38 | /// 39 | /// const context = await browser.newContext(); 40 | /// console.log(browser.contexts().length); // prints `1` 41 | /// ``` 42 | pub fn contexts(&self) -> Result, Error> { 43 | Ok(upgrade(&self.inner)? 44 | .contexts() 45 | .iter() 46 | .cloned() 47 | .map(BrowserContext::new) 48 | .collect()) 49 | } 50 | 51 | /// Returns the browser version. 52 | pub fn version(&self) -> Result { 53 | Ok(upgrade(&self.inner)?.version().to_owned()) 54 | } 55 | 56 | pub fn exists(&self) -> bool { self.inner.upgrade().is_some() } 57 | 58 | /// new_context [`BrowserContext`] 59 | /// Creates a new browser context. It won't share cookies/cache with other browser contexts. 60 | pub fn context_builder(&self) -> ContextBuilder<'_, '_, '_, '_, '_, '_, '_> { 61 | ContextBuilder::new(self.inner.clone()) 62 | } 63 | 64 | /// All temporary browsers will be closed when the connection is terminated, but 65 | /// it needs to be called explicitly to close it at any given time. 66 | pub async fn close(&self) -> Result<(), Arc> { 67 | let inner = match self.inner.upgrade() { 68 | None => return Ok(()), 69 | Some(inner) => inner 70 | }; 71 | inner.close().await 72 | } 73 | 74 | // new_browser_cdp_session 75 | // start_tracing 76 | // stop_tracing 77 | } 78 | 79 | // TODO: async drop 80 | 81 | /// [`Browser::context_builder`] 82 | pub struct ContextBuilder<'e, 'f, 'g, 'h, 'i, 'j, 'k> { 83 | inner: Weak, 84 | args: NewContextArgs<'e, 'f, 'g, 'h, 'i, 'j, 'k> 85 | } 86 | 87 | impl<'e, 'f, 'g, 'h, 'i, 'j, 'k> ContextBuilder<'e, 'f, 'g, 'h, 'i, 'j, 'k> { 88 | pub async fn build(self) -> Result> { 89 | let Self { inner, args } = self; 90 | let r = upgrade(&inner)?.new_context(args).await?; 91 | Ok(BrowserContext::new(r)) 92 | } 93 | 94 | fn new(inner: Weak) -> Self { 95 | Self { 96 | inner, 97 | args: NewContextArgs::default() 98 | } 99 | } 100 | 101 | pub fn set_device(self, device: &'e DeviceDescriptor) -> Self { 102 | DeviceDescriptor::set_context(device, self) 103 | } 104 | 105 | setter! { 106 | /// Whether to automatically download all the attachments. Defaults to `false` where all the downloads are canceled. 107 | accept_downloads: Option, 108 | /// Toggles bypassing page's Content-Security-Policy. 109 | bypass_csp: Option, 110 | /// Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. See 111 | /// [`method: Page.emulateMedia`] for more details. Defaults to `'light'`. 112 | color_scheme: Option, 113 | /// Specify device scale factor (can be thought of as dpr). Defaults to `1`. 114 | device_scale_factor: Option, 115 | /// An object containing additional HTTP headers to be sent with every request. All header values must be strings. 116 | extra_http_headers: Option>, 117 | geolocation: Option, 118 | has_touch: Option, 119 | /// Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). 120 | http_credentials: Option<&'i HttpCredentials>, 121 | /// Whether to ignore HTTPS errors during navigation. Defaults to `false`. 122 | ignore_https_errors: Option, 123 | /// Whether the `meta viewport` tag is taken into account and touch events are enabled. Defaults to `false`. Not supported 124 | /// in Firefox. 125 | is_mobile: Option, 126 | /// Whether or not to enable JavaScript in the context. Defaults to `true`. 127 | js_enabled: Option, 128 | /// Specify user locale, for example `en-GB`, `de-DE`, etc. Locale will affect `navigator.language` value, `Accept-Language` 129 | /// request header value as well as number and date formatting rules. 130 | locale: Option<&'f str>, 131 | /// Does not enforce fixed viewport, allows resizing window in the headed mode. 132 | no_viewport: Option, 133 | /// Whether to emulate network being offline. Defaults to `false`. 134 | offline: Option, 135 | /// A list of permissions to grant to all pages in this context. See [BrowserContext::grant_permissions] for more details. 136 | permissions: Option<&'h [String]>, 137 | /// Network proxy settings to use with this context. Note that browser needs to be launched with the global proxy for this 138 | /// option to work. If all contexts override the proxy, global proxy will be never used and can be any string, for example 139 | /// `launch({ proxy: { server: 'per-context' } })`. 140 | proxy: Option, 141 | /// Enables [HAR](http://www.softwareishard.com/blog/har-12-spec) recording for all pages into `recordHar.path` file. If not 142 | /// specified, the HAR is not recorded. Make sure to await [`method: BrowserContext.close`] for the HAR to be saved. 143 | record_har: Option>, 144 | /// Enables video recording for all pages into `recordVideo.dir` directory. If not specified videos are not recorded. Make 145 | /// sure to await [`method: BrowserContext.close`] for videos to be saved. 146 | record_video: Option>, 147 | /// Emulates consistent window screen size available inside web page via `window.screen`. Is only used when the `viewport` 148 | /// is set. 149 | screen: Option, 150 | /// Populates context with given storage state. This option can be used to initialize context with logged-in information 151 | /// obtained via [`method: BrowserContext.storageState`]. Either a path to the file with saved storage, or an object with 152 | /// the following fields: 153 | storage_state: Option, 154 | /// Changes the timezone of the context. See 155 | /// [ICU's metaZones.txt](https://cs.chromium.org/chromium/src/third_party/icu/source/data/misc/metaZones.txt?rcl=faee8bc70570192d82d2978a71e2a615788597d1) 156 | /// for a list of supported timezone IDs. 157 | timezone_id: Option<&'g str>, 158 | /// Specific user agent to use in this context. 159 | user_agent: Option<&'e str>, 160 | /// Emulates consistent viewport for each page. Defaults to an 1280x720 viewport. `null` disables the default viewport. 161 | viewport: Option> 162 | } 163 | ///// Logger sink for Playwright logging. 164 | // logger: Option, 165 | } 166 | -------------------------------------------------------------------------------- /src/api/browser_context.rs: -------------------------------------------------------------------------------- 1 | pub use crate::imp::browser_context::EventType; 2 | use crate::{ 3 | api::{Browser, Page}, 4 | imp::{ 5 | browser_context::{BrowserContext as Impl, Evt}, 6 | core::*, 7 | prelude::*, 8 | utils::{Cookie, Geolocation, StorageState} 9 | }, 10 | Error 11 | }; 12 | 13 | /// BrowserContexts provide a way to operate multiple independent browser sessions. 14 | /// 15 | /// If a page opens another page, e.g. with a `window.open` call, the popup will belong to the parent page's browser 16 | /// context. 17 | /// 18 | /// Playwright allows creation of "incognito" browser contexts with `browser.newContext()` method. "Incognito" browser 19 | /// contexts don't write any browsing data to disk. 20 | #[derive(Debug)] 21 | pub struct BrowserContext { 22 | inner: Weak 23 | } 24 | 25 | impl PartialEq for BrowserContext { 26 | fn eq(&self, other: &Self) -> bool { 27 | let a = self.inner.upgrade(); 28 | let b = other.inner.upgrade(); 29 | a.and_then(|a| b.map(|b| (a, b))) 30 | .map(|(a, b)| a.guid() == b.guid()) 31 | .unwrap_or_default() 32 | } 33 | } 34 | 35 | impl BrowserContext { 36 | pub(crate) fn new(inner: Weak) -> Self { Self { inner } } 37 | 38 | /// Returns all open pages in the context. 39 | pub fn pages(&self) -> Result, Error> { 40 | Ok(upgrade(&self.inner)? 41 | .pages() 42 | .iter() 43 | .cloned() 44 | .map(Page::new) 45 | .collect()) 46 | } 47 | 48 | /// Returns the browser instance of the context. If it was launched as a persistent context None gets returned. 49 | pub fn browser(&self) -> Result, Error> { 50 | Ok(upgrade(&self.inner)?.browser().map(Browser::new)) 51 | } 52 | 53 | /// Creates a new page in the browser context. 54 | pub async fn new_page(&self) -> Result> { 55 | let inner = upgrade(&self.inner)?; 56 | Ok(Page::new(inner.new_page().await?)) 57 | } 58 | 59 | pub async fn set_default_navigation_timeout(&self, timeout: u32) -> ArcResult<()> { 60 | upgrade(&self.inner)? 61 | .set_default_navigation_timeout(timeout) 62 | .await 63 | } 64 | 65 | pub async fn set_default_timeout(&self, timeout: u32) -> ArcResult<()> { 66 | upgrade(&self.inner)?.set_default_timeout(timeout).await 67 | } 68 | 69 | /// If no URLs are specified, this method returns all cookies. If URLs are specified, only cookies that affect those URLs 70 | /// are returned. 71 | pub async fn cookies(&self, urls: &[String]) -> ArcResult> { 72 | upgrade(&self.inner)?.cookies(urls).await 73 | } 74 | 75 | /// Adds cookies into this browser context. All pages within this context will have these cookies installed. 76 | pub async fn add_cookies(&self, cookies: &[Cookie]) -> ArcResult<()> { 77 | upgrade(&self.inner)?.add_cookies(cookies).await 78 | } 79 | 80 | /// Clears context cookies. 81 | pub async fn clear_cookies(&self) -> ArcResult<()> { 82 | upgrade(&self.inner)?.clear_cookies().await 83 | } 84 | 85 | /// Grants specified permissions to the browser context. Only grants corresponding permissions to the given origin if 86 | /// specified. 87 | /// 88 | /// ```js 89 | /// const context = await browser.newContext(); 90 | /// await context.grantPermissions(['clipboard-read']); 91 | /// context.clearPermissions(); 92 | /// ``` 93 | /// # Args 94 | /// ## permissions 95 | /// A permission or an array of permissions to grant. Permissions can be one of the following values: 96 | /// - `'geolocation'` 97 | /// - `'midi'` 98 | /// - `'midi-sysex'` (system-exclusive midi) 99 | /// - `'notifications'` 100 | /// - `'push'` 101 | /// - `'camera'` 102 | /// - `'microphone'` 103 | /// - `'background-sync'` 104 | /// - `'ambient-light-sensor'` 105 | /// - `'accelerometer'` 106 | /// - `'gyroscope'` 107 | /// - `'magnetometer'` 108 | /// - `'accessibility-events'` 109 | /// - `'clipboard-read'` 110 | /// - `'clipboard-write'` 111 | /// - `'payment-handler'` 112 | /// ## origin 113 | /// The origin to grant permissions to, e.g. `"https://example.com"`. 114 | pub async fn grant_permissions( 115 | &self, 116 | permissions: &[String], 117 | origin: Option<&str> 118 | ) -> ArcResult<()> { 119 | upgrade(&self.inner)? 120 | .grant_permissions(permissions, origin) 121 | .await 122 | } 123 | 124 | /// Clears all permission overrides for the browser context. 125 | pub async fn clear_permissions(&self) -> ArcResult<()> { 126 | upgrade(&self.inner)?.clear_permissions().await 127 | } 128 | 129 | /// Sets the context's geolocation. Passing `null` or `undefined` emulates position unavailable. 130 | /// 131 | /// ```js 132 | /// await browserContext.setGeolocation({latitude: 59.95, longitude: 30.31667}); 133 | /// ``` 134 | /// > NOTE: Consider using [`method: BrowserContext.grantPermissions`] to grant permissions for the browser context pages to 135 | /// read its geolocation. 136 | pub async fn set_geolocation(&self, geolocation: Option<&Geolocation>) -> ArcResult<()> { 137 | upgrade(&self.inner)?.set_geolocation(geolocation).await 138 | } 139 | 140 | /// Sets whether to emulate network being offline for the browser context. 141 | pub async fn set_offline(&self, offline: bool) -> ArcResult<()> { 142 | upgrade(&self.inner)?.set_offline(offline).await 143 | } 144 | 145 | /// Adds a script which would be evaluated in one of the following scenarios: 146 | /// - Whenever a page is created in the browser context or is navigated. 147 | /// - Whenever a child frame is attached or navigated in any page in the browser context. In this case, the script is 148 | /// evaluated in the context of the newly attached frame. 149 | /// 150 | /// The script is evaluated after the document was created but before any of its scripts were run. This is useful to amend 151 | /// the JavaScript environment, e.g. to seed `Math.random`. 152 | /// 153 | /// An example of overriding `Math.random` before the page loads: 154 | /// 155 | /// ```js browser 156 | ///// preload.js 157 | /// Math.random = () => 42; 158 | /// ``` 159 | /// ```js 160 | ///// In your playwright script, assuming the preload.js file is in same directory. 161 | /// await browserContext.addInitScript({ 162 | /// path: 'preload.js' 163 | /// }); 164 | /// ``` 165 | /// > NOTE: The order of evaluation of multiple scripts installed via [`method: BrowserContext.addInitScript`] and 166 | /// [`method: Page.addInitScript`] is not defined. 167 | pub async fn add_init_script(&self, script: &str) -> ArcResult<()> { 168 | // arg not supported 169 | upgrade(&self.inner)?.add_init_script(script).await 170 | } 171 | 172 | /// The extra HTTP headers will be sent with every request initiated by any page in the context. These headers are merged 173 | /// with page-specific extra HTTP headers set with [`method: Page.setExtraHTTPHeaders`]. If page overrides a particular 174 | /// header, page-specific header value will be used instead of the browser context header value. 175 | /// 176 | /// > NOTE: [`method: BrowserContext.setExtraHTTPHeaders`] does not guarantee the order of headers in the outgoing requests. 177 | pub async fn set_extra_http_headers(&self, headers: T) -> ArcResult<()> 178 | where 179 | T: IntoIterator 180 | { 181 | upgrade(&self.inner)?.set_extra_http_headers(headers).await 182 | } 183 | 184 | // async fn expose_binding(&mut self) -> Result<(), Error> { unimplemented!() } 185 | 186 | // async fn expose_function(&mut self) -> Result<(), Error> { unimplemented!() } 187 | 188 | // async fn route(&mut self) -> Result<(), Error> { unimplemented!() } 189 | 190 | // async fn unroute(&mut self) -> Result<(), Error> { unimplemented!() } 191 | 192 | pub async fn expect_event(&self, evt: EventType) -> Result { 193 | let stream = upgrade(&self.inner)?.subscribe_event(); 194 | let timeout = upgrade(&self.inner)?.default_timeout(); 195 | expect_event(stream, evt, timeout).await.map(Event::from) 196 | } 197 | 198 | /// Returns storage state for this browser context, contains current cookies and local storage snapshot. 199 | pub async fn storage_state(&self) -> ArcResult { 200 | // path no supported 201 | upgrade(&self.inner)?.storage_state().await 202 | } 203 | 204 | /// All temporary browsers will be closed when the connection is terminated, but 205 | /// this struct has no Drop. it needs to be called explicitly to close it at any given time. 206 | /// > NOTE: The default browser context cannot be closed. 207 | pub async fn close(&self) -> ArcResult<()> { 208 | let inner = match self.inner.upgrade() { 209 | None => return Ok(()), 210 | Some(inner) => inner 211 | }; 212 | inner.close().await 213 | } 214 | 215 | subscribe_event! {} 216 | 217 | // background_page for chromium 218 | // new_cdp_session 219 | // service_workers 220 | } 221 | 222 | #[derive(Debug, PartialEq)] 223 | pub enum Event { 224 | // BackgroundPage for chromium persistent 225 | // ServiceWorker 226 | /// Emitted when Browser context gets closed. This might happen because of one of the following: 227 | /// - Browser context is closed. 228 | /// - Browser application is closed or crashed. 229 | /// - The [`method: Browser.close`] method was called. 230 | Close, 231 | /// The event is emitted when a new Page is created in the BrowserContext. The page may still be loading. The event will 232 | /// also fire for popup pages. See also [`event: Page.popup`] to receive events about popups relevant to a specific page. 233 | /// 234 | /// The earliest moment that page is available is when it has navigated to the initial url. For example, when opening a 235 | /// popup with `window.open('http://example.com')`, this event will fire when the network request to is 236 | /// done and its response has started loading in the popup. 237 | /// 238 | /// ```js 239 | /// const [newPage] = await Promise.all([ 240 | /// context.waitForEvent('page'), 241 | /// page.click('a[target=_blank]'), 242 | /// ]); 243 | /// console.log(await newPage.evaluate('location.href')); 244 | /// ``` 245 | Page(Page) 246 | } 247 | 248 | impl From for Event { 249 | fn from(e: Evt) -> Event { 250 | match e { 251 | Evt::Close => Event::Close, 252 | Evt::Page(w) => Event::Page(Page::new(w)) 253 | } 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /src/api/console_message.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | api::JsHandle, 3 | imp::{console_message::ConsoleMessage as Impl, core::*, prelude::*, utils::SourceLocation} 4 | }; 5 | 6 | /// `ConsoleMessage` objects are dispatched by page via the [page::Event::Console](crate::api::page::Event::Console) event. 7 | #[derive(Clone)] 8 | pub struct ConsoleMessage { 9 | inner: Weak 10 | } 11 | 12 | impl ConsoleMessage { 13 | pub(crate) fn new(inner: Weak) -> Self { Self { inner } } 14 | 15 | /// One of the following values: `'log'`, `'debug'`, `'info'`, `'error'`, `'warning'`, `'dir'`, `'dirxml'`, `'table'`, 16 | /// `'trace'`, `'clear'`, `'startGroup'`, `'startGroupCollapsed'`, `'endGroup'`, `'assert'`, `'profile'`, `'profileEnd'`, 17 | /// `'count'`, `'timeEnd'`. 18 | pub fn r#type(&self) -> Result { Ok(upgrade(&self.inner)?.r#type().into()) } 19 | 20 | /// The text of the console message. 21 | pub fn text(&self) -> Result { Ok(upgrade(&self.inner)?.text().into()) } 22 | 23 | /// URL of the resource followed by 0-based line and column numbers in the resource formatted as `URL:line:column`. 24 | pub fn location(&self) -> Result { 25 | Ok(upgrade(&self.inner)?.location().to_owned()) 26 | } 27 | 28 | /// List of arguments passed to a `console` function call. 29 | pub fn args(&self) -> Result, Error> { 30 | Ok(upgrade(&self.inner)? 31 | .args() 32 | .iter() 33 | .map(|x| JsHandle::new(x.clone())) 34 | .collect()) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/api/dialog.rs: -------------------------------------------------------------------------------- 1 | use crate::imp::{core::*, dialog::Dialog as Impl, prelude::*}; 2 | 3 | /// `Dialog` objects are dispatched by page via the [page::Event::Dialog](crate::api::page::Event::Dialog) event. 4 | /// 5 | /// An example of using `Dialog` class: 6 | /// 7 | /// ```js 8 | /// const { chromium } = require('playwright'); // Or 'firefox' or 'webkit'. 9 | /// 10 | /// (async () => { 11 | /// const browser = await chromium.launch(); 12 | /// const page = await browser.newPage(); 13 | /// page.on('dialog', async dialog => { 14 | /// console.log(dialog.message()); 15 | /// await dialog.dismiss(); 16 | /// }); 17 | /// await page.evaluate(() => alert('1')); 18 | /// await browser.close(); 19 | /// })(); 20 | /// ``` 21 | /// 22 | /// > NOTE: Dialogs are dismissed automatically, unless there is a [`event: Page.dialog`] listener. When listener is 23 | /// present, it **must** either [`method: Dialog.accept`] or [`method: Dialog.dismiss`] the dialog - otherwise the page will 24 | /// [freeze](https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop#never_blocking) waiting for the dialog, and 25 | /// actions like click will never finish. 26 | pub struct Dialog { 27 | inner: Weak 28 | } 29 | 30 | impl Dialog { 31 | pub(crate) fn new(inner: Weak) -> Self { Self { inner } } 32 | 33 | ///// Returns when the dialog has been accepted. 34 | ///// A text to enter in prompt. Does not cause any effects if the dialog's `type` is not prompt. Optional. 35 | // fn accept(&self, prompt_text: Option) -> Result<(), Arc> { todo!() } 36 | ///// If dialog is prompt, returns default prompt value. Otherwise, returns empty string. 37 | // fn default_value(&self) -> Result { todo!() } 38 | ///// Returns when the dialog has been dismissed. 39 | // fn dismiss(&self) -> Result<(), Arc> { todo!() } 40 | ///// A message displayed in the dialog. 41 | // fn message(&self) -> Result { todo!() } 42 | ///// Returns dialog's type, can be one of `alert`, `beforeunload`, `confirm` or `prompt`. 43 | // fn r#type(&self) -> Result { todo!() } 44 | } 45 | -------------------------------------------------------------------------------- /src/api/download.rs: -------------------------------------------------------------------------------- 1 | use crate::imp::{core::*, download::Download as Impl, prelude::*}; 2 | 3 | /// `Download` objects are dispatched by page via the [`event: Page.download`] event. 4 | /// 5 | /// All the downloaded files belonging to the browser context are deleted when the browser context is closed. All downloaded 6 | /// files are deleted when the browser closes. 7 | /// 8 | /// Download event is emitted once the download starts. Download path becomes available once download completes: 9 | /// 10 | /// ```js 11 | /// const [ download ] = await Promise.all([ 12 | /// page.waitForEvent('download'), // wait for download to start 13 | /// page.click('a') 14 | /// ]); 15 | ///// wait for download to complete 16 | /// const path = await download.path(); 17 | /// ``` 18 | /// 19 | /// > NOTE: Browser context **must** be created with the `acceptDownloads` set to `true` when user needs access to the 20 | /// downloaded content. If `acceptDownloads` is not set, download events are emitted, but the actual download is not 21 | /// performed and user has no access to the downloaded files. 22 | #[derive(Clone)] 23 | pub struct Download { 24 | inner: Arc 25 | } 26 | 27 | impl Download { 28 | pub(crate) fn new(inner: Arc) -> Self { Self { inner } } 29 | 30 | /// Returns downloaded url. 31 | pub fn url(&self) -> &str { self.inner.url() } 32 | 33 | /// Returns suggested filename for this download. It is typically computed by the browser from the 34 | /// [`Content-Disposition`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition) response header 35 | /// or the `download` attribute. See the spec on [whatwg](https://html.spec.whatwg.org/#downloading-resources). Different 36 | /// browsers can use different logic for computing it. 37 | pub fn suggested_filename(&self) -> &str { self.inner.suggested_filename() } 38 | 39 | /// Returns path to the downloaded file in case of successful download. The method will wait for the download to finish if 40 | /// necessary. 41 | pub async fn path(&self) -> ArcResult> { self.inner.path().await } 42 | 43 | /// Deletes the downloaded file. Will wait for the download to finish if necessary. 44 | pub async fn delete(&self) -> ArcResult<()> { self.inner.delete().await } 45 | 46 | /// Saves the download to a user-specified path. It is safe to call this method while the download is still in progress. 47 | /// Path where the download should be saved. 48 | pub async fn save_as>(&self, path: P) -> Result<(), Arc> { 49 | self.inner.save_as(path).await 50 | } 51 | 52 | ///// Returns readable stream for current download or `null` if download failed. 53 | // fn create_read_stream(&self) -> Result, Arc> { todo!() } 54 | 55 | /// Returns download error if any. Will wait for the download to finish if necessary. 56 | pub async fn failure(&self) -> Result, Arc> { self.inner.failure().await } 57 | } 58 | -------------------------------------------------------------------------------- /src/api/file_chooser.rs: -------------------------------------------------------------------------------- 1 | pub use crate::imp::file_hooser::FileChooser; 2 | use crate::{ 3 | api::{element_handle::SetInputFilesBuilder, ElementHandle, Page}, 4 | imp::utils::File 5 | }; 6 | 7 | impl FileChooser { 8 | /// Returns input element associated with this file chooser. 9 | fn element(&self) -> ElementHandle { ElementHandle::new(self.element_handle.clone()) } 10 | /// Returns whether this file chooser accepts multiple files. 11 | fn is_multiple(&self) -> bool { self.is_multiple } 12 | /// Returns page this file chooser belongs to. 13 | fn page(&self) -> Page { Page::new(self.page.clone()) } 14 | 15 | /// Sets the value of the file input this chooser is associated with. If some of the `filePaths` are relative paths, then 16 | /// they are resolved relative to the the current working directory. For empty array, clears the selected files. 17 | fn set_input_files_builder(&self, file: File) -> SetInputFilesBuilder { 18 | SetInputFilesBuilder::new(self.element_handle.clone(), file) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/api/input_device.rs: -------------------------------------------------------------------------------- 1 | use crate::imp::{ 2 | core::*, 3 | page::{MouseClickArgs, Page as PageImpl}, 4 | prelude::*, 5 | utils::MouseButton 6 | }; 7 | 8 | /// Keyboard provides an api for managing a virtual keyboard. The high level api is [`method: Keyboard.type`], which takes 9 | /// raw characters and generates proper keydown, keypress/input, and keyup events on your page. 10 | /// 11 | /// For finer control, you can use [`method: Keyboard.down`], [`method: Keyboard.up`], and [`method: Keyboard.insertText`] 12 | /// to manually fire events as if they were generated from a real keyboard. 13 | /// 14 | /// An example of holding down `Shift` in order to select and delete some text: 15 | /// 16 | /// ```js 17 | /// await page.keyboard.type('Hello World!'); 18 | /// await page.keyboard.press('ArrowLeft'); 19 | /// 20 | /// await page.keyboard.down('Shift'); 21 | /// for (let i = 0; i < ' World'.length; i++) 22 | /// await page.keyboard.press('ArrowLeft'); 23 | /// await page.keyboard.up('Shift'); 24 | /// 25 | /// await page.keyboard.press('Backspace'); 26 | ///// Result text will end up saying 'Hello!' 27 | /// ``` 28 | /// 29 | /// An example of pressing uppercase `A` 30 | /// ```js 31 | /// await page.keyboard.press('Shift+KeyA'); 32 | ///// or 33 | /// await page.keyboard.press('Shift+A'); 34 | /// ``` 35 | /// 36 | /// An example to trigger select-all with the keyboard 37 | /// ```js 38 | ///// on Windows and Linux 39 | /// await page.keyboard.press('Control+A'); 40 | ///// on macOS 41 | /// await page.keyboard.press('Meta+A'); 42 | /// ``` 43 | #[derive(Debug, Clone)] 44 | pub struct Keyboard { 45 | inner: Weak 46 | } 47 | 48 | /// The Mouse class operates in main-frame CSS pixels relative to the top-left corner of the viewport. 49 | /// 50 | /// Every `page` object has its own Mouse, accessible with [`property: Page.mouse`]. 51 | /// 52 | /// ```js 53 | ///// Using ‘page.mouse’ to trace a 100x100 square. 54 | /// await page.mouse.move(0, 0); 55 | /// await page.mouse.down(); 56 | /// await page.mouse.move(0, 100); 57 | /// await page.mouse.move(100, 100); 58 | /// await page.mouse.move(100, 0); 59 | /// await page.mouse.move(0, 0); 60 | /// await page.mouse.up(); 61 | /// ``` 62 | #[derive(Debug, Clone)] 63 | pub struct Mouse { 64 | inner: Weak 65 | } 66 | 67 | /// The Touchscreen class operates in main-frame CSS pixels relative to the top-left corner of the viewport. Methods on the 68 | /// touchscreen can only be used in browser contexts that have been initialized with `hasTouch` set to true. 69 | #[derive(Debug, Clone)] 70 | pub struct TouchScreen { 71 | inner: Weak 72 | } 73 | 74 | impl Keyboard { 75 | pub(crate) fn new(inner: Weak) -> Self { Self { inner } } 76 | 77 | /// Dispatches a `keydown` event. 78 | /// 79 | /// `key` can specify the intended [keyboardEvent.key](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key) 80 | /// value or a single character to generate the text for. A superset of the `key` values can be found 81 | /// [here](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values). Examples of the keys are: 82 | /// 83 | /// `F1` - `F12`, `Digit0`- `Digit9`, `KeyA`- `KeyZ`, `Backquote`, `Minus`, `Equal`, `Backslash`, `Backspace`, `Tab`, 84 | /// `Delete`, `Escape`, `ArrowDown`, `End`, `Enter`, `Home`, `Insert`, `PageDown`, `PageUp`, `ArrowRight`, `ArrowUp`, etc. 85 | /// 86 | /// Following modification shortcuts are also supported: `Shift`, `Control`, `Alt`, `Meta`, `ShiftLeft`. 87 | /// 88 | /// Holding down `Shift` will type the text that corresponds to the `key` in the upper case. 89 | /// 90 | /// If `key` is a single character, it is case-sensitive, so the values `a` and `A` will generate different respective 91 | /// texts. 92 | /// 93 | /// If `key` is a modifier key, `Shift`, `Meta`, `Control`, or `Alt`, subsequent key presses will be sent with that modifier 94 | /// active. To release the modifier key, use [`method: Keyboard.up`]. 95 | /// 96 | /// After the key is pressed once, subsequent calls to [`method: Keyboard.down`] will have 97 | /// [repeat](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/repeat) set to true. To release the key, use 98 | /// [`method: Keyboard.up`]. 99 | /// 100 | /// > NOTE: Modifier keys DO influence `keyboard.down`. Holding down `Shift` will type the text in upper case. 101 | pub async fn down(&self, key: &str) -> Result<(), Arc> { 102 | let inner = upgrade(&self.inner)?; 103 | inner.key_down(key).await 104 | } 105 | 106 | pub async fn up(&self, key: &str) -> Result<(), Arc> { 107 | let inner = upgrade(&self.inner)?; 108 | inner.key_up(key).await 109 | } 110 | 111 | /// Dispatches only `input` event, does not emit the `keydown`, `keyup` or `keypress` events. 112 | /// 113 | /// ```js 114 | /// page.keyboard.insertText('嗨'); 115 | /// ``` 116 | /// 117 | /// 118 | /// > NOTE: Modifier keys DO NOT effect `keyboard.insertText`. Holding down `Shift` will not type the text in upper case. 119 | pub async fn input_text(&self, text: &str) -> Result<(), Arc> { 120 | let inner = upgrade(&self.inner)?; 121 | inner.key_input_text(text).await 122 | } 123 | 124 | /// Sends a `keydown`, `keypress`/`input`, and `keyup` event for each character in the text. 125 | /// 126 | /// To press a special key, like `Control` or `ArrowDown`, use [`method: Keyboard.press`]. 127 | /// 128 | /// ```js 129 | /// await page.keyboard.type('Hello'); // Types instantly 130 | /// await page.keyboard.type('World', {delay: 100}); // Types slower, like a user 131 | /// ``` 132 | /// 133 | /// > NOTE: Modifier keys DO NOT effect `keyboard.type`. Holding down `Shift` will not type the text in upper case. 134 | /// > NOTE: For characters that are not on a US keyboard, only an `input` event will be sent. 135 | pub async fn r#type(&self, text: &str, delay: Option) -> Result<(), Arc> { 136 | let inner = upgrade(&self.inner)?; 137 | inner.key_type(text, delay).await 138 | } 139 | 140 | /// Shortcut for [`method: Keyboard.down`] and [`method: Keyboard.up`]. 141 | pub async fn press(&self, key: &str, delay: Option) -> Result<(), Arc> { 142 | let inner = upgrade(&self.inner)?; 143 | inner.key_press(key, delay).await 144 | } 145 | } 146 | 147 | impl Mouse { 148 | pub(crate) fn new(inner: Weak) -> Self { Self { inner } } 149 | 150 | pub async fn r#move(&self, x: f64, y: f64, steps: Option) -> Result<(), Arc> { 151 | let inner = upgrade(&self.inner)?; 152 | inner.mouse_move(x, y, steps).await 153 | } 154 | 155 | pub async fn down( 156 | &self, 157 | button: Option, 158 | click_count: Option 159 | ) -> Result<(), Arc> { 160 | let inner = upgrade(&self.inner)?; 161 | inner.mouse_down(button, click_count).await 162 | } 163 | 164 | pub async fn up( 165 | &self, 166 | button: Option, 167 | click_count: Option 168 | ) -> Result<(), Arc> { 169 | let inner = upgrade(&self.inner)?; 170 | inner.mouse_up(button, click_count).await 171 | } 172 | 173 | /// Shortcut for [`method: Mouse.move`], [`method: Mouse.down`], [`method: Mouse.up`]. 174 | pub fn click_builder(&self, x: f64, y: f64) -> ClickBuilder { 175 | ClickBuilder::new(self.inner.clone(), x, y) 176 | } 177 | 178 | /// Shortcut for [`method: Mouse.move`], [`method: Mouse.down`], [`method: Mouse.up`], [`method: Mouse.down`] and 179 | /// [`method: Mouse.up`]. 180 | pub fn dblclick_builder(&self, x: f64, y: f64) -> DblClickBuilder { 181 | DblClickBuilder::new(self.inner.clone(), x, y) 182 | } 183 | } 184 | 185 | impl TouchScreen { 186 | pub(crate) fn new(inner: Weak) -> Self { Self { inner } } 187 | 188 | pub async fn tap(&self, x: f64, y: f64) -> Result<(), Arc> { 189 | let inner = upgrade(&self.inner)?; 190 | inner.screen_tap(x, y).await 191 | } 192 | } 193 | 194 | macro_rules! clicker { 195 | ($t: ident, $f: ident, $mf: ident) => { 196 | pub struct $t { 197 | inner: Weak, 198 | args: MouseClickArgs 199 | } 200 | 201 | impl $t { 202 | pub(crate) fn new(inner: Weak, x: f64, y: f64) -> Self { 203 | let args = MouseClickArgs::new(x, y); 204 | Self { inner, args } 205 | } 206 | 207 | pub async fn $f(self) -> Result<(), Arc> { 208 | let Self { inner, args } = self; 209 | let _ = upgrade(&inner)?.$mf(args).await?; 210 | Ok(()) 211 | } 212 | 213 | setter! { 214 | /// Defaults to `left`. 215 | button: Option, 216 | /// defaults to 1. See [UIEvent.detail]. 217 | click_count: Option, 218 | /// Time to wait between `mousedown` and `mouseup` in milliseconds. Defaults to 0. 219 | delay: Option 220 | } 221 | } 222 | }; 223 | } 224 | 225 | clicker!(ClickBuilder, click, mouse_click); 226 | clicker!(DblClickBuilder, dblclick, mouse_dblclick); 227 | -------------------------------------------------------------------------------- /src/api/js_handle.rs: -------------------------------------------------------------------------------- 1 | use crate::imp::{core::*, js_handle::JsHandle as Impl, prelude::*}; 2 | use std::fmt; 3 | 4 | /// JsHandle represents an in-page JavaScript object. JsHandles can be created with the [`method: Page.evaluateHandle`] 5 | /// method. 6 | /// 7 | /// ```js 8 | /// const windowHandle = await page.evaluateHandle(() => window); 9 | ///// ... 10 | /// ``` 11 | /// 12 | /// JsHandle prevents the referenced JavaScript object being garbage collected unless the handle is exposed with 13 | /// [`method: JsHandle.dispose`]. JsHandles are auto-disposed when their origin frame gets navigated or the parent context 14 | /// gets destroyed. 15 | /// 16 | /// JsHandle instances can be used as an argument in [`method: Page.evalOnSelector`], [`method: Page.evaluate`] and 17 | /// [`method: Page.evaluateHandle`] methods. 18 | pub struct JsHandle { 19 | inner: Weak 20 | } 21 | 22 | impl PartialEq for JsHandle { 23 | fn eq(&self, other: &Self) -> bool { 24 | let a = self.inner.upgrade(); 25 | let b = other.inner.upgrade(); 26 | a.and_then(|a| b.map(|b| (a, b))) 27 | .map(|(a, b)| a.guid() == b.guid()) 28 | .unwrap_or_default() 29 | } 30 | } 31 | 32 | impl JsHandle { 33 | pub(crate) fn new(inner: Weak) -> Self { Self { inner } } 34 | 35 | pub(crate) fn guid(&self) -> Result, Error> { 36 | Ok(upgrade(&self.inner)?.guid().to_owned()) 37 | } 38 | 39 | /// Fetches a single property from the referenced object. 40 | pub async fn get_property(&mut self, name: &str) -> ArcResult { 41 | upgrade(&self.inner)? 42 | .get_property(name) 43 | .await 44 | .map(JsHandle::new) 45 | } 46 | 47 | /// The method returns a map with **own property names** as keys and JsHandle instances for the property values. 48 | /// 49 | /// ```js 50 | /// const handle = await page.evaluateHandle(() => ({window, document})); 51 | /// const properties = await handle.getProperties(); 52 | /// const windowHandle = properties.get('window'); 53 | /// const documentHandle = properties.get('document'); 54 | /// await handle.dispose(); 55 | /// ``` 56 | pub async fn get_properties(&mut self) -> ArcResult> { 57 | let m = upgrade(&self.inner)?.get_properties().await?; 58 | Ok(m.into_iter().map(|(k, v)| (k, JsHandle::new(v))).collect()) 59 | } 60 | 61 | pub async fn dispose(&mut self) -> ArcResult<()> { upgrade(&self.inner)?.dispose().await } 62 | 63 | /// Returns a JSON representation of the object. If the object has a `toJSON` function, it **will not be called**. 64 | /// 65 | /// > NOTE: The method will return an empty JSON object if the referenced object is not stringifiable. It will throw an 66 | /// error if the object has circular references. 67 | pub async fn json_value(&mut self) -> ArcResult 68 | where 69 | U: DeserializeOwned 70 | { 71 | upgrade(&self.inner)?.json_value().await 72 | } 73 | 74 | // evaluate 75 | } 76 | 77 | impl fmt::Display for JsHandle { 78 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 79 | if let Some(inner) = self.inner.upgrade() { 80 | inner.fmt(f) 81 | } else { 82 | write!(f, "") 83 | } 84 | } 85 | } 86 | 87 | mod ser { 88 | use super::*; 89 | use serde::{ser, ser::SerializeStruct}; 90 | 91 | impl Serialize for JsHandle { 92 | fn serialize(&self, serializer: S) -> Result 93 | where 94 | S: ser::Serializer 95 | { 96 | let mut s = serializer.serialize_struct("4a9c3811-6f00-49e5-8a81-939f932d9061", 1)?; 97 | let guid = &self.guid().map_err(::custom)?; 98 | s.serialize_field("guid", &guid)?; 99 | s.end() 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/api/playwright.rs: -------------------------------------------------------------------------------- 1 | pub use crate::imp::playwright::DeviceDescriptor; 2 | use crate::{ 3 | api::{browser_type::BrowserType, selectors::Selectors}, 4 | imp::{core::*, playwright::Playwright as Impl, prelude::*}, 5 | Error 6 | }; 7 | use std::{io, process::Command}; 8 | 9 | /// Entry point 10 | pub struct Playwright { 11 | driver: Driver, 12 | _conn: Connection, 13 | inner: Weak 14 | } 15 | 16 | fn run(driver: &Driver, args: &'static [&'static str]) -> io::Result<()> { 17 | let status = Command::new(driver.executable()).args(args).status()?; 18 | if !status.success() { 19 | return Err(io::Error::new( 20 | io::ErrorKind::Other, 21 | format!("Exit with {}", status) 22 | )); 23 | } 24 | Ok(()) 25 | } 26 | 27 | impl Playwright { 28 | /// Installs playwright driver to "$CACHE_DIR/.ms-playwright/playwright-rust/driver" 29 | pub async fn initialize() -> Result { 30 | let driver = Driver::install()?; 31 | Self::with_driver(driver).await 32 | } 33 | 34 | /// Constructs from installed playwright driver 35 | pub async fn with_driver(driver: Driver) -> Result { 36 | let conn = Connection::run(&driver.executable())?; 37 | let p = Impl::wait_initial_object(&conn).await?; 38 | Ok(Self { 39 | driver, 40 | _conn: conn, 41 | inner: p 42 | }) 43 | } 44 | 45 | /// Runs $ playwright install 46 | pub fn prepare(&self) -> io::Result<()> { run(&self.driver, &["install"]) } 47 | 48 | /// Runs $ playwright install chromium 49 | pub fn install_chromium(&self) -> io::Result<()> { run(&self.driver, &["install", "chromium"]) } 50 | 51 | pub fn install_firefox(&self) -> io::Result<()> { run(&self.driver, &["install", "firefox"]) } 52 | 53 | pub fn install_webkit(&self) -> io::Result<()> { run(&self.driver, &["install", "webkit"]) } 54 | 55 | /// Launcher 56 | pub fn chromium(&self) -> BrowserType { 57 | let inner = weak_and_then(&self.inner, |rc| rc.chromium()); 58 | BrowserType::new(inner) 59 | } 60 | 61 | /// Launcher 62 | pub fn firefox(&self) -> BrowserType { 63 | let inner = weak_and_then(&self.inner, |rc| rc.firefox()); 64 | BrowserType::new(inner) 65 | } 66 | 67 | /// Launcher 68 | pub fn webkit(&self) -> BrowserType { 69 | let inner = weak_and_then(&self.inner, |rc| rc.webkit()); 70 | BrowserType::new(inner) 71 | } 72 | 73 | pub fn driver(&mut self) -> &mut Driver { &mut self.driver } 74 | 75 | pub fn selectors(&self) -> Selectors { 76 | let inner = weak_and_then(&self.inner, |rc| rc.selectors()); 77 | Selectors::new(inner) 78 | } 79 | 80 | /// Returns a dictionary of devices to be used with [`method: Browser.newContext`] or [`method: Browser.newPage`]. 81 | /// 82 | /// ```js 83 | /// const { webkit, devices } = require('playwright'); 84 | /// const iPhone = devices['iPhone 6']; 85 | /// 86 | /// (async () => { 87 | /// const browser = await webkit.launch(); 88 | /// const context = await browser.newContext({ 89 | /// ...iPhone 90 | /// }); 91 | /// const page = await context.newPage(); 92 | /// await page.goto('http://example.com'); 93 | /// // other actions... 94 | /// await browser.close(); 95 | /// })(); 96 | /// ``` 97 | pub fn devices(&self) -> Vec { 98 | upgrade(&self.inner) 99 | .map(|x| x.devices().to_vec()) 100 | .unwrap_or_default() 101 | } 102 | 103 | pub fn device(&self, name: &str) -> Option { 104 | let inner = self.inner.upgrade()?; 105 | let device = inner.device(name)?; 106 | Some(device.to_owned()) 107 | } 108 | } 109 | 110 | #[cfg(test)] 111 | mod tests { 112 | use super::*; 113 | 114 | crate::runtime_test!(failure_status_code, { 115 | let mut p = Playwright::initialize().await.unwrap(); 116 | let err = run(p.driver(), &["nonExistentArg"]); 117 | assert!(err.is_err()); 118 | if let Some(e) = err.err() { 119 | assert_eq!(e.kind(), io::ErrorKind::Other); 120 | } 121 | }); 122 | } 123 | -------------------------------------------------------------------------------- /src/api/request.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | api::{Frame, Response}, 3 | imp::{core::*, prelude::*, request::Request as Impl, utils::ResponseTiming} 4 | }; 5 | 6 | /// Whenever the page sends a request for a network resource the following sequence of events are emitted by `Page`: 7 | /// - [`event: Page.request`] emitted when the request is issued by the page. 8 | /// - [`event: Page.response`] emitted when/if the response status and headers are received for the request. 9 | /// - [`event: Page.requestFinished`] emitted when the response body is downloaded and the request is complete. 10 | /// 11 | /// If request fails at some point, then instead of `'requestfinished'` event (and possibly instead of 'response' event), 12 | /// the [`event: Page.requestFailed`] event is emitted. 13 | /// 14 | /// > NOTE: HTTP Error responses, such as 404 or 503, are still successful responses from HTTP standpoint, so request will 15 | /// complete with `'requestfinished'` event. 16 | /// 17 | /// If request gets a 'redirect' response, the request is successfully finished with the 'requestfinished' event, and a new 18 | /// request is issued to a redirected url. 19 | #[derive(Clone)] 20 | pub struct Request { 21 | inner: Weak 22 | } 23 | 24 | impl PartialEq for Request { 25 | fn eq(&self, other: &Self) -> bool { 26 | let a = self.inner.upgrade(); 27 | let b = other.inner.upgrade(); 28 | a.and_then(|a| b.map(|b| (a, b))) 29 | .map(|(a, b)| a.guid() == b.guid()) 30 | .unwrap_or_default() 31 | } 32 | } 33 | 34 | impl Request { 35 | pub(crate) fn new(inner: Weak) -> Self { Self { inner } } 36 | 37 | /// Request's method (GET, POST, etc.) 38 | pub fn method(&self) -> Result { Ok(upgrade(&self.inner)?.method().into()) } 39 | 40 | /// Contains the request's resource type as it was perceived by the rendering engine. ResourceType will be one of the 41 | /// following: `document`, `stylesheet`, `image`, `media`, `font`, `script`, `texttrack`, `xhr`, `fetch`, `eventsource`, 42 | /// `websocket`, `manifest`, `other`. 43 | pub fn resource_type(&self) -> Result { 44 | Ok(upgrade(&self.inner)?.resource_type().into()) 45 | } 46 | 47 | pub fn url(&self) -> Result { Ok(upgrade(&self.inner)?.url().into()) } 48 | 49 | /// Whether this request is driving frame's navigation. 50 | pub fn is_navigation_request(&self) -> Result { 51 | Ok(upgrade(&self.inner)?.is_navigation_request()) 52 | } 53 | 54 | /// Returns the `Frame` that initiated this request. 55 | pub fn frame(&self) -> Frame { 56 | let inner = weak_and_then(&self.inner, |rc| rc.frame()); 57 | Frame::new(inner) 58 | } 59 | 60 | pub fn post_data(&self) -> Result>, Error> { 61 | Ok(upgrade(&self.inner)?.post_data()) 62 | } 63 | 64 | pub fn post_post_as_string(&self) -> Result, Error> { 65 | Ok(upgrade(&self.inner)?.post_data_as_string()) 66 | } 67 | 68 | /// An object with HTTP headers associated with the request. All header names are lower-case. 69 | pub fn headers(&self) -> Result, Error> { 70 | Ok(upgrade(&self.inner)?.headers().clone()) 71 | } 72 | 73 | /// Request that was redirected by the server to this one, if any. 74 | /// 75 | /// When the server responds with a redirect, Playwright creates a new `Request` object. The two requests are connected by 76 | /// `redirectedFrom()` and `redirectedTo()` methods. When multiple server redirects has happened, it is possible to 77 | /// construct the whole redirect chain by repeatedly calling `redirectedFrom()`. 78 | /// 79 | /// For example, if the website `http://example.com` redirects to `https://example.com`: 80 | /// 81 | /// ```js 82 | /// const response = await page.goto('http://example.com'); 83 | /// console.log(response.request().redirectedFrom().url()); // 'http://example.com' 84 | /// ``` 85 | /// 86 | /// If the website `https://google.com` has no redirects: 87 | /// 88 | /// ```js 89 | /// const response = await page.goto('https://google.com'); 90 | /// console.log(response.request().redirectedFrom()); // null 91 | /// ``` 92 | pub fn redirected_from(&self) -> Result, Error> { 93 | Ok(upgrade(&self.inner)?.redirected_from().map(Request::new)) 94 | } 95 | 96 | pub async fn redirected_to(&self) -> Result, Error> { 97 | Ok(upgrade(&self.inner)?.redirected_to().map(Request::new)) 98 | } 99 | 100 | /// Returns the matching `Response` object, or `null` if the response was not received due to error. 101 | pub async fn response(&self) -> Result, Arc> { 102 | Ok(upgrade(&self.inner)?.response().await?.map(Response::new)) 103 | } 104 | 105 | /// The method returns `null` unless this request has failed, as reported by `requestfailed` event. 106 | /// 107 | /// Example of logging of all the failed requests: 108 | /// 109 | /// ```js 110 | /// page.on('requestfailed', request => { 111 | /// console.log(request.url() + ' ' + request.failure().errorText); 112 | /// }); 113 | /// ``` 114 | pub fn failure(&self) -> Result, Error> { Ok(upgrade(&self.inner)?.failure()) } 115 | 116 | /// Returns resource timing information for given request. Most of the timing values become available upon the response, 117 | /// `responseEnd` becomes available when request finishes. Find more information at 118 | /// [Resource Timing API](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming). 119 | /// 120 | /// ```js 121 | /// const [request] = await Promise.all([ 122 | /// page.waitForEvent('requestfinished'), 123 | /// page.goto('http://example.com') 124 | /// ]); 125 | /// console.log(request.timing()); 126 | /// ``` 127 | pub fn timing(&self) -> Result, Error> { 128 | Ok(upgrade(&self.inner)?.timing()) 129 | } 130 | 131 | pub fn response_end(&self) -> Result, Error> { 132 | Ok(upgrade(&self.inner)?.response_end()) 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/api/response.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | api::{Frame, Request}, 3 | imp::{core::*, prelude::*, response::Response as Impl, utils::Header} 4 | }; 5 | 6 | #[derive(Debug, Clone)] 7 | pub struct Response { 8 | inner: Weak 9 | } 10 | 11 | impl PartialEq for Response { 12 | fn eq(&self, other: &Self) -> bool { 13 | let a = self.inner.upgrade(); 14 | let b = other.inner.upgrade(); 15 | a.and_then(|a| b.map(|b| (a, b))) 16 | .map(|(a, b)| a.guid() == b.guid()) 17 | .unwrap_or_default() 18 | } 19 | } 20 | 21 | impl Response { 22 | pub(crate) fn new(inner: Weak) -> Self { Self { inner } } 23 | 24 | pub fn url(&self) -> Result { Ok(upgrade(&self.inner)?.url().into()) } 25 | /// Contains the status code of the response (e.g., 200 for a success). 26 | pub fn status(&self) -> Result { Ok(upgrade(&self.inner)?.status()) } 27 | /// Contains the status text of the response (e.g. usually an "OK" for a success). 28 | pub fn status_text(&self) -> Result { 29 | Ok(upgrade(&self.inner)?.status_text().into()) 30 | } 31 | 32 | /// Contains a boolean stating whether the response was successful (status in the range 200-299) or not. 33 | pub fn ok(&self) -> Result { Ok(upgrade(&self.inner)?.ok()) } 34 | 35 | pub fn request(&self) -> Request { 36 | let inner = weak_and_then(&self.inner, |rc| rc.request()); 37 | Request::new(inner) 38 | } 39 | 40 | /// Waits for this response to finish, returns failure error if request failed. 41 | pub async fn finished(&self) -> ArcResult> { 42 | upgrade(&self.inner)?.finished().await 43 | } 44 | 45 | pub async fn body(&self) -> ArcResult> { upgrade(&self.inner)?.body().await } 46 | 47 | /// Returns the text representation of response body. 48 | pub async fn text(&self) -> ArcResult { upgrade(&self.inner)?.text().await } 49 | 50 | /// Returns the object with HTTP headers associated with the response. All header names are lower-case. 51 | pub async fn headers(&self) -> ArcResult> { upgrade(&self.inner)?.headers().await } 52 | 53 | /// Shortcut for [`Response::request`]'s [`Request::frame`] 54 | pub fn frame(&self) -> Frame { self.request().frame() } 55 | } 56 | -------------------------------------------------------------------------------- /src/api/route.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | api::{Header, Request}, 3 | imp::{ 4 | core::*, 5 | prelude::*, 6 | route::{ContinueArgs, FulfillArgs, Route as Impl} 7 | } 8 | }; 9 | 10 | /// Whenever a network route is set up with [`method: Page.route`] or [`method: BrowserContext.route`], the `Route` object 11 | /// allows to handle the route. 12 | pub struct Route { 13 | inner: Weak 14 | } 15 | 16 | impl PartialEq for Route { 17 | fn eq(&self, other: &Self) -> bool { 18 | let a = self.inner.upgrade(); 19 | let b = other.inner.upgrade(); 20 | a.and_then(|a| b.map(|b| (a, b))) 21 | .map(|(a, b)| a.guid() == b.guid()) 22 | .unwrap_or_default() 23 | } 24 | } 25 | 26 | impl Route { 27 | fn new(inner: Weak) -> Self { Self { inner } } 28 | 29 | /// A request to be routed. 30 | pub fn request(&self) -> Request { 31 | let inner = weak_and_then(&self.inner, |rc| rc.request()); 32 | Request::new(inner) 33 | } 34 | 35 | /// Aborts the route's request. 36 | /// Optional error code. Defaults to `failed`, could be one of the following: 37 | /// - `'aborted'` - An operation was aborted (due to user action) 38 | /// - `'accessdenied'` - Permission to access a resource, other than the network, was denied 39 | /// - `'addressunreachable'` - The IP address is unreachable. This usually means that there is no route to the specified 40 | /// host or network. 41 | /// - `'blockedbyclient'` - The client chose to block the request. 42 | /// - `'blockedbyresponse'` - The request failed because the response was delivered along with requirements which are not 43 | /// met ('X-Frame-Options' and 'Content-Security-Policy' ancestor checks, for instance). 44 | /// - `'connectionaborted'` - A connection timed out as a result of not receiving an ACK for data sent. 45 | /// - `'connectionclosed'` - A connection was closed (corresponding to a TCP FIN). 46 | /// - `'connectionfailed'` - A connection attempt failed. 47 | /// - `'connectionrefused'` - A connection attempt was refused. 48 | /// - `'connectionreset'` - A connection was reset (corresponding to a TCP RST). 49 | /// - `'internetdisconnected'` - The Internet connection has been lost. 50 | /// - `'namenotresolved'` - The host name could not be resolved. 51 | /// - `'timedout'` - An operation timed out. 52 | /// - `'failed'` - A generic failure occurred. 53 | pub async fn abort(&self, err_code: Option<&str>) -> Result<(), Arc> { 54 | let inner = upgrade(&self.inner)?; 55 | inner.abort(err_code).await 56 | } 57 | 58 | /// Fulfills route's request with given response. 59 | /// 60 | /// An example of fulfilling all requests with 404 responses: 61 | /// 62 | /// ```js 63 | /// await page.route('**/*', route => { 64 | /// route.fulfill({ 65 | /// status: 404, 66 | /// contentType: 'text/plain', 67 | /// body: 'Not Found!' 68 | /// }); 69 | /// }); 70 | pub async fn fulfill_builder<'a>( 71 | &self, 72 | body: &'a str, 73 | is_base64: bool 74 | ) -> FulfillBuilder<'a, '_> { 75 | FulfillBuilder::new(self.inner.clone(), body, is_base64) 76 | } 77 | 78 | /// Continues route's request with optional overrides. 79 | /// 80 | /// ```js 81 | /// await page.route('**/*', (route, request) => { 82 | /// // Override headers 83 | /// const headers = { 84 | /// ...request.headers(), 85 | /// foo: 'bar', // set "foo" header 86 | /// origin: undefined, // remove "origin" header 87 | /// }; 88 | /// route.continue({headers}); 89 | /// }); 90 | /// ``` 91 | pub async fn continue_builder(&self) -> ContinueBuilder<'_, '_, '_> { 92 | ContinueBuilder::new(self.inner.clone()) 93 | } 94 | } 95 | 96 | pub struct FulfillBuilder<'a, 'b> { 97 | inner: Weak, 98 | args: FulfillArgs<'a, 'b> 99 | } 100 | 101 | impl<'a, 'b> FulfillBuilder<'a, 'b> { 102 | pub(crate) fn new(inner: Weak, body: &'a str, is_base64: bool) -> Self { 103 | let args = FulfillArgs::new(body, is_base64); 104 | Self { inner, args } 105 | } 106 | 107 | pub async fn fulfill(self) -> Result<(), Arc> { 108 | let Self { inner, args } = self; 109 | upgrade(&inner)?.fulfill(args).await 110 | } 111 | 112 | /// Response headers. Header values will be converted to a string. 113 | pub fn headers(mut self, x: T) -> Self 114 | where 115 | T: IntoIterator 116 | { 117 | self.args.headers = Some(x.into_iter().map(Header::from).collect()); 118 | self 119 | } 120 | 121 | setter! { 122 | /// If set, equals to setting `Content-Type` response header. 123 | content_type: Option<&'b str>, 124 | /// Response status code, defaults to `200`. 125 | status: Option 126 | } 127 | 128 | pub fn clear_headers(mut self) -> Self { 129 | self.args.headers = None; 130 | self 131 | } 132 | } 133 | 134 | pub struct ContinueBuilder<'a, 'b, 'c> { 135 | inner: Weak, 136 | args: ContinueArgs<'a, 'b, 'c> 137 | } 138 | 139 | impl<'a, 'b, 'c> ContinueBuilder<'a, 'b, 'c> { 140 | pub(crate) fn new(inner: Weak) -> Self { 141 | let args = ContinueArgs::default(); 142 | Self { inner, args } 143 | } 144 | 145 | pub async fn r#continue(self) -> Result<(), Arc> { 146 | let Self { inner, args } = self; 147 | upgrade(&inner)?.r#continue(args).await 148 | } 149 | 150 | /// If set changes the request HTTP headers. Header values will be converted to a string. 151 | pub fn headers(mut self, x: T) -> Self 152 | where 153 | T: IntoIterator 154 | { 155 | self.args.headers = Some(x.into_iter().map(Header::from).collect()); 156 | self 157 | } 158 | 159 | setter! { 160 | /// If set changes the request method (e.g. GET or POST) 161 | method: Option<&'b str>, 162 | /// If set changes the post data of request 163 | post_data: Option<&'c str>, 164 | /// If set changes the request URL. New URL must have same protocol as original one. 165 | url: Option<&'a str> 166 | } 167 | 168 | pub fn clear_headers(mut self) -> Self { 169 | self.args.headers = None; 170 | self 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/api/selectors.rs: -------------------------------------------------------------------------------- 1 | use crate::imp::{core::*, prelude::*, selectors::Selectors as Impl}; 2 | 3 | /// Selectors can be used to install custom selector engines. 4 | #[derive(Debug, Clone)] 5 | pub struct Selectors { 6 | inner: Weak 7 | } 8 | 9 | impl Selectors { 10 | pub(crate) fn new(inner: Weak) -> Self { Self { inner } } 11 | 12 | /// An example of registering selector engine that queries elements based on a tag name: 13 | /// 14 | /// ```js 15 | /// const { selectors, firefox } = require('playwright'); // Or 'chromium' or 'webkit'. 16 | /// 17 | /// (async () => { 18 | /// // Must be a function that evaluates to a selector engine instance. 19 | /// const createTagNameEngine = () => ({ 20 | /// // Returns the first element matching given selector in the root's subtree. 21 | /// query(root, selector) { 22 | /// return root.querySelector(selector); 23 | /// }, 24 | /// 25 | /// // Returns all elements matching given selector in the root's subtree. 26 | /// queryAll(root, selector) { 27 | /// return Array.from(root.querySelectorAll(selector)); 28 | /// } 29 | /// }); 30 | /// 31 | /// // Register the engine. Selectors will be prefixed with "tag=". 32 | /// await selectors.register('tag', createTagNameEngine); 33 | /// 34 | /// const browser = await firefox.launch(); 35 | /// const page = await browser.newPage(); 36 | /// await page.setContent(`
`); 37 | /// 38 | /// // Use the selector prefixed with its name. 39 | /// const button = await page.$('tag=button'); 40 | /// // Combine it with other selector engines. 41 | /// await page.click('tag=div >> text="Click me"'); 42 | /// // Can use it in any methods supporting selectors. 43 | /// const buttonCount = await page.$$eval('tag=button', buttons => buttons.length); 44 | /// 45 | /// await browser.close(); 46 | /// })(); 47 | /// ``` 48 | /// # Args 49 | /// ## name 50 | /// Name that is used in selectors as a prefix, e.g. `{name: 'foo'}` enables `foo=myselectorbody` selectors. 51 | /// May only contain `[a-zA-Z0-9_]` characters. 52 | /// ## script 53 | /// Script that evaluates to a selector engine instance. 54 | /// ## content_script 55 | /// Whether to run this selector engine in isolated JavaScript environment. This environment 56 | /// has access to the same DOM, but not any JavaScript objects from the frame's scripts. 57 | /// Defaults to `false`. Note that running as a content script is not 58 | /// guaranteed when this engine is used together with other registered engines. 59 | pub async fn register( 60 | &self, 61 | name: &str, 62 | script: &str, 63 | content_script: bool 64 | ) -> Result<(), Arc> { 65 | let inner = upgrade(&self.inner)?; 66 | inner.register(name, script, content_script).await 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/api/video.rs: -------------------------------------------------------------------------------- 1 | use crate::imp::{core::*, prelude::*, video::Video as Impl}; 2 | 3 | #[derive(Debug, Clone)] 4 | pub struct Video { 5 | inner: Impl 6 | } 7 | 8 | impl Video { 9 | pub(crate) fn new(inner: Impl) -> Self { Self { inner } } 10 | 11 | pub fn path(&self) -> Result { self.inner.path() } 12 | 13 | // doesn't work with this version 14 | async fn save_as>(&self, path: P) -> ArcResult<()> { 15 | self.inner.save_as(path).await 16 | } 17 | 18 | // doesn't work with this version 19 | async fn delete(&self) -> ArcResult<()> { self.inner.delete().await } 20 | } 21 | -------------------------------------------------------------------------------- /src/api/websocket.rs: -------------------------------------------------------------------------------- 1 | pub use crate::imp::websocket::Buffer; 2 | use crate::imp::{ 3 | core::*, 4 | prelude::*, 5 | websocket::{Evt, WebSocket as Impl} 6 | }; 7 | 8 | #[derive(Clone)] 9 | pub struct WebSocket { 10 | inner: Weak 11 | } 12 | 13 | impl PartialEq for WebSocket { 14 | fn eq(&self, other: &Self) -> bool { 15 | let a = self.inner.upgrade(); 16 | let b = other.inner.upgrade(); 17 | a.and_then(|a| b.map(|b| (a, b))) 18 | .map(|(a, b)| a.guid() == b.guid()) 19 | .unwrap_or_default() 20 | } 21 | } 22 | 23 | impl WebSocket { 24 | pub(crate) fn new(inner: Weak) -> Self { Self { inner } } 25 | 26 | /// Contains the URL of the WebSocket. 27 | pub fn url(&self) -> Result { Ok(upgrade(&self.inner)?.url().to_owned()) } 28 | 29 | pub fn is_closed(&self) -> Result { Ok(upgrade(&self.inner)?.is_closed()) } 30 | 31 | subscribe_event! {} 32 | } 33 | 34 | #[derive(Debug)] 35 | pub enum Event { 36 | FrameSent(Buffer), 37 | FrameReceived(Buffer), 38 | Error(Value), 39 | Close 40 | } 41 | 42 | impl From for Event { 43 | fn from(e: Evt) -> Self { 44 | match e { 45 | Evt::FrameSent(x) => Self::FrameSent(x), 46 | Evt::FrameReceived(x) => Self::FrameReceived(x), 47 | Evt::Error(x) => Self::Error(x), 48 | Evt::Close => Self::Close 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/api/worker.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | api::JsHandle, 3 | imp::{ 4 | core::*, 5 | prelude::*, 6 | worker::{Evt, Worker as Impl} 7 | } 8 | }; 9 | 10 | /// The Worker class represents a [WebWorker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API). `worker` 11 | /// event is emitted on the page object to signal a worker creation. `close` event is emitted on the worker object when the 12 | /// worker is gone. 13 | /// 14 | /// ```js 15 | /// page.on('worker', worker => { 16 | /// console.log('Worker created: ' + worker.url()); 17 | /// worker.on('close', worker => console.log('Worker destroyed: ' + worker.url())); 18 | /// }); 19 | /// 20 | /// console.log('Current workers:'); 21 | /// for (const worker of page.workers()) 22 | /// console.log(' ' + worker.url()); 23 | /// ``` 24 | #[derive(Clone)] 25 | pub struct Worker { 26 | inner: Weak 27 | } 28 | 29 | impl PartialEq for Worker { 30 | fn eq(&self, other: &Self) -> bool { 31 | let a = self.inner.upgrade(); 32 | let b = other.inner.upgrade(); 33 | a.and_then(|a| b.map(|b| (a, b))) 34 | .map(|(a, b)| a.guid() == b.guid()) 35 | .unwrap_or_default() 36 | } 37 | } 38 | 39 | impl Worker { 40 | pub(crate) fn new(inner: Weak) -> Self { Self { inner } } 41 | 42 | pub fn url(&self) -> Result { Ok(upgrade(&self.inner)?.url().to_owned()) } 43 | 44 | pub async fn eval_handle(&self, expression: &str) -> ArcResult { 45 | upgrade(&self.inner)? 46 | .eval_handle(expression) 47 | .await 48 | .map(JsHandle::new) 49 | } 50 | 51 | pub async fn evaluate_handle(&self, expression: &str, arg: Option) -> ArcResult 52 | where 53 | T: Serialize 54 | { 55 | upgrade(&self.inner)? 56 | .evaluate_handle(expression, arg) 57 | .await 58 | .map(JsHandle::new) 59 | } 60 | 61 | pub async fn eval(&self, expression: &str) -> ArcResult 62 | where 63 | U: DeserializeOwned 64 | { 65 | upgrade(&self.inner)?.eval(expression).await 66 | } 67 | 68 | pub async fn evaluate(&self, expression: &str, arg: Option) -> ArcResult 69 | where 70 | T: Serialize, 71 | U: DeserializeOwned 72 | { 73 | upgrade(&self.inner)?.evaluate(expression, arg).await 74 | } 75 | 76 | subscribe_event! {} 77 | } 78 | 79 | #[derive(Debug)] 80 | pub enum Event { 81 | Close 82 | } 83 | 84 | impl From for Event { 85 | fn from(e: Evt) -> Self { 86 | match e { 87 | Evt::Close => Self::Close 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/build.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | env, fmt, fs, 3 | fs::File, 4 | path::{Path, PathBuf, MAIN_SEPARATOR} 5 | }; 6 | 7 | const DRIVER_VERSION: &str = "1.11.0-1620331022000"; 8 | 9 | fn main() { 10 | let out_dir: PathBuf = env::var_os("OUT_DIR").unwrap().into(); 11 | let dest = out_dir.join("driver.zip"); 12 | let platform = PlaywrightPlatform::default(); 13 | fs::write(out_dir.join("platform"), platform.to_string()).unwrap(); 14 | download(&url(platform), &dest); 15 | println!("cargo:rerun-if-changed=src/build.rs"); 16 | println!("cargo:rustc-env=SEP={}", MAIN_SEPARATOR); 17 | } 18 | 19 | #[cfg(all(not(feature = "only-for-docs-rs"), not(unix)))] 20 | fn download(url: &str, dest: &Path) { 21 | let mut resp = reqwest::blocking::get(url).unwrap(); 22 | let mut dest = File::create(dest).unwrap(); 23 | resp.copy_to(&mut dest).unwrap(); 24 | } 25 | 26 | #[cfg(all(not(feature = "only-for-docs-rs"), unix))] 27 | fn download(url: &str, dest: &Path) { 28 | let cache_dir: &Path = "/tmp/build-playwright-rust".as_ref(); 29 | let cached = cache_dir.join("driver.zip"); 30 | if cfg!(debug_assertions) { 31 | let maybe_metadata = cached.metadata().ok(); 32 | let cache_is_file = || { 33 | maybe_metadata 34 | .as_ref() 35 | .map(fs::Metadata::is_file) 36 | .unwrap_or_default() 37 | }; 38 | let cache_size = || { 39 | maybe_metadata 40 | .as_ref() 41 | .map(fs::Metadata::len) 42 | .unwrap_or_default() 43 | }; 44 | if cache_is_file() && cache_size() > 10000000 { 45 | fs::copy(cached, dest).unwrap(); 46 | check_size(dest); 47 | return; 48 | } 49 | } 50 | let mut resp = reqwest::blocking::get(url).unwrap(); 51 | let mut dest_file = File::create(dest).unwrap(); 52 | resp.copy_to(&mut dest_file).unwrap(); 53 | if cfg!(debug_assertions) { 54 | fs::create_dir_all(cache_dir).unwrap(); 55 | fs::copy(dest, cached).unwrap(); 56 | } 57 | check_size(dest); 58 | } 59 | 60 | fn size(p: &Path) -> u64 { 61 | let maybe_metadata = p.metadata().ok(); 62 | let size = maybe_metadata 63 | .as_ref() 64 | .map(fs::Metadata::len) 65 | .unwrap_or_default(); 66 | size 67 | } 68 | 69 | fn check_size(p: &Path) { 70 | assert!(size(p) > 10_000_000, "file size is smaller than the driver"); 71 | } 72 | 73 | // No network access 74 | #[cfg(feature = "only-for-docs-rs")] 75 | fn download(_url: &str, dest: &Path) { File::create(dest).unwrap(); } 76 | 77 | fn url(platform: PlaywrightPlatform) -> String { 78 | // let next = DRIVER_VERSION 79 | // .contains("next") 80 | // .then(|| "/next") 81 | // .unwrap_or_default(); 82 | let next = "/next"; 83 | format!( 84 | "https://playwright.azureedge.net/builds/driver{}/playwright-{}-{}.zip", 85 | next, DRIVER_VERSION, platform 86 | ) 87 | } 88 | 89 | #[derive(Clone, Copy)] 90 | enum PlaywrightPlatform { 91 | Linux, 92 | Win32, 93 | Win32x64, 94 | Mac 95 | } 96 | 97 | impl fmt::Display for PlaywrightPlatform { 98 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 99 | match self { 100 | Self::Linux => write!(f, "linux"), 101 | Self::Win32 => write!(f, "win32"), 102 | Self::Win32x64 => write!(f, "win32_x64"), 103 | Self::Mac => write!(f, "mac") 104 | } 105 | } 106 | } 107 | 108 | impl Default for PlaywrightPlatform { 109 | fn default() -> Self { 110 | match env::var("CARGO_CFG_TARGET_OS").as_deref() { 111 | Ok("linux") => return PlaywrightPlatform::Linux, 112 | Ok("macos") => return PlaywrightPlatform::Mac, 113 | _ => () 114 | }; 115 | if env::var("CARGO_CFG_WINDOWS").is_ok() { 116 | if env::var("CARGO_CFG_TARGET_POINTER_WIDTH").as_deref() == Ok("64") { 117 | PlaywrightPlatform::Win32x64 118 | } else { 119 | PlaywrightPlatform::Win32 120 | } 121 | } else if env::var("CARGO_CFG_UNIX").is_ok() { 122 | PlaywrightPlatform::Linux 123 | } else { 124 | panic!("Unsupported plaform"); 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/imp.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod impl_future { 2 | pub use std::{future::Future, pin::Pin, task}; 3 | } 4 | pub(crate) mod prelude { 5 | pub use serde::{de::DeserializeOwned, Deserialize, Serialize}; 6 | pub use serde_json::{ 7 | map::Map, 8 | value::{to_value, Value} 9 | }; 10 | pub use std::{ 11 | collections::HashMap, 12 | convert::{TryFrom, TryInto}, 13 | future::Future, 14 | path::{Path, PathBuf}, 15 | pin::Pin, 16 | sync::{Arc, Mutex, MutexGuard, Weak}, 17 | task::{Poll, Waker}, 18 | time::Duration 19 | }; 20 | pub use strong::*; 21 | pub type Wm = Weak>; 22 | pub type Am = Arc>; 23 | 24 | #[cfg(feature = "rt-async-std")] 25 | #[derive(Debug, thiserror::Error)] 26 | pub enum JoinError {} 27 | #[cfg(feature = "rt-async-std")] 28 | pub use async_std::{task::sleep, task::spawn}; 29 | #[cfg(feature = "rt-tokio")] 30 | pub use tokio::{task::spawn, task::JoinError, time::sleep}; 31 | #[cfg(feature = "rt-actix")] 32 | pub use tokio::{task::spawn, task::JoinError, time::sleep}; 33 | 34 | pub(crate) trait RemoveOne { 35 | fn remove_one(&mut self, f: F) 36 | where 37 | F: Fn(&T) -> bool; 38 | } 39 | 40 | impl RemoveOne for Vec { 41 | fn remove_one(&mut self, f: F) 42 | where 43 | F: Fn(&T) -> bool 44 | { 45 | let index = match self.iter().position(f) { 46 | Some(i) => i, 47 | None => return 48 | }; 49 | self.remove(index); 50 | } 51 | } 52 | } 53 | 54 | #[macro_use] 55 | mod macros { 56 | #[doc(hidden)] 57 | #[macro_export] 58 | macro_rules! get_object { 59 | ($c:expr, $guid:expr, $t:ident) => { 60 | match $c.find_object($guid) { 61 | Some(RemoteWeak::$t(x)) => Ok(x), 62 | _ => Err(Error::ObjectNotFound) 63 | } 64 | }; 65 | } 66 | 67 | #[doc(hidden)] 68 | #[macro_export] 69 | macro_rules! send_message { 70 | ($r: expr, $method:literal, $args: expr) => {{ 71 | let m: Str = $method.to_owned().try_into().unwrap(); 72 | let r = $r.channel().create_request(m).set_args($args)?; 73 | let fut = $r.channel().send_message(r).await?; 74 | let res = fut.await?; 75 | let res = res.map_err(Error::ErrorResponded)?; 76 | res 77 | }}; 78 | } 79 | } 80 | 81 | pub(crate) mod core { 82 | mod connection; 83 | mod driver; 84 | mod event_emitter; 85 | mod message; 86 | mod remote_object; 87 | mod transport; 88 | pub use connection::*; 89 | pub use driver::*; 90 | pub use event_emitter::*; 91 | pub use message::*; 92 | pub(crate) use remote_object::*; 93 | pub use transport::*; 94 | } 95 | 96 | pub(crate) mod browser_type; 97 | pub(crate) mod playwright; 98 | pub(crate) mod selectors; 99 | pub(crate) mod utils; 100 | 101 | pub(crate) mod artifact; 102 | pub(crate) mod binding_call; 103 | pub(crate) mod browser; 104 | pub(crate) mod browser_context; 105 | pub(crate) mod console_message; 106 | pub(crate) mod dialog; 107 | pub(crate) mod download; 108 | pub(crate) mod element_handle; 109 | pub(crate) mod file_hooser; 110 | pub(crate) mod frame; 111 | pub(crate) mod js_handle; 112 | pub(crate) mod page; 113 | pub(crate) mod request; 114 | pub(crate) mod response; 115 | pub(crate) mod route; 116 | pub(crate) mod stream; 117 | pub(crate) mod video; 118 | pub(crate) mod websocket; 119 | pub(crate) mod worker; 120 | 121 | //_accessibility.py 122 | //_api_structures.py 123 | //_api_types.py 124 | //_element_handle.py 125 | //_event_context_manager.py 126 | //_helper.py 127 | //_impl_to_api_mapping.py 128 | //_input.py 129 | //_path_utils.py 130 | //_sync_base.py 131 | //_video.py 132 | //_wait_helper.py 133 | // ChromiumBrowserContext CdpSession 134 | -------------------------------------------------------------------------------- /src/imp/artifact.rs: -------------------------------------------------------------------------------- 1 | use crate::imp::{core::*, prelude::*}; 2 | 3 | #[derive(Debug)] 4 | pub(crate) struct Artifact { 5 | channel: ChannelOwner, 6 | pub(crate) absolute_path: String, 7 | var: Mutex 8 | } 9 | 10 | #[derive(Debug, Default)] 11 | pub(crate) struct Variable { 12 | is_remote: bool 13 | } 14 | 15 | impl Artifact { 16 | pub(crate) fn try_new(channel: ChannelOwner) -> Result { 17 | let Initializer { absolute_path } = serde_json::from_value(channel.initializer.clone())?; 18 | Ok(Self { 19 | channel, 20 | absolute_path, 21 | var: Mutex::default() 22 | }) 23 | } 24 | 25 | pub(crate) async fn path_after_finished(&self) -> ArcResult> { 26 | if self.is_remote() { 27 | return Err(Error::RemoteArtifact.into()); 28 | } 29 | let v = send_message!(self, "pathAfterFinished", Map::new()); 30 | let p: Option = maybe_only_str(&*v)?.map(|s| s.into()); 31 | Ok(p) 32 | } 33 | 34 | pub(crate) async fn delete(&self) -> ArcResult<()> { 35 | let _ = send_message!(self, "delete", Map::new()); 36 | Ok(()) 37 | } 38 | 39 | pub(crate) async fn save_as>(&self, path: P) -> ArcResult<()> { 40 | let path = path.as_ref(); 41 | let dir = path 42 | .parent() 43 | .ok_or_else(|| Error::ResolvePath(path.into()))?; 44 | let res = send_message!(self, "saveAsStream", Map::new()); 45 | let guid = only_guid(&res)?; 46 | let stream = get_object!(self.context()?.lock().unwrap(), guid, Stream)?; 47 | std::fs::create_dir_all(dir).map_err(Error::from)?; 48 | upgrade(&stream)?.save_as(path).await?; 49 | Ok(()) 50 | } 51 | 52 | pub(crate) async fn failure(&self) -> ArcResult> { 53 | let v = send_message!(self, "failure", Map::new()); 54 | let msg = maybe_only_str(&v)?; 55 | Ok(msg.map(ToOwned::to_owned)) 56 | } 57 | } 58 | 59 | // mutable 60 | impl Artifact { 61 | fn set_is_remote(&self, x: bool) { self.var.lock().unwrap().is_remote = x; } 62 | 63 | fn is_remote(&self) -> bool { self.var.lock().unwrap().is_remote } 64 | } 65 | 66 | impl RemoteObject for Artifact { 67 | fn channel(&self) -> &ChannelOwner { &self.channel } 68 | fn channel_mut(&mut self) -> &mut ChannelOwner { &mut self.channel } 69 | } 70 | 71 | #[derive(Debug, Deserialize)] 72 | #[serde(rename_all = "camelCase")] 73 | struct Initializer { 74 | absolute_path: String 75 | } 76 | -------------------------------------------------------------------------------- /src/imp/binding_call.rs: -------------------------------------------------------------------------------- 1 | use crate::imp::core::*; 2 | 3 | #[derive(Debug)] 4 | pub(crate) struct BindingCall { 5 | channel: ChannelOwner 6 | } 7 | 8 | impl BindingCall { 9 | pub(crate) fn new(channel: ChannelOwner) -> Self { Self { channel } } 10 | } 11 | 12 | impl RemoteObject for BindingCall { 13 | fn channel(&self) -> &ChannelOwner { &self.channel } 14 | fn channel_mut(&mut self) -> &mut ChannelOwner { &mut self.channel } 15 | } 16 | -------------------------------------------------------------------------------- /src/imp/browser.rs: -------------------------------------------------------------------------------- 1 | use crate::imp::{ 2 | browser_context::BrowserContext, 3 | browser_type::{RecordHar, RecordVideo}, 4 | core::*, 5 | prelude::*, 6 | utils::{ColorScheme, Geolocation, HttpCredentials, ProxySettings, StorageState, Viewport} 7 | }; 8 | 9 | #[derive(Debug)] 10 | pub(crate) struct Browser { 11 | channel: ChannelOwner, 12 | version: String, 13 | var: Mutex 14 | } 15 | 16 | #[derive(Debug, Default)] 17 | pub(crate) struct Variable { 18 | contexts: Vec>, 19 | is_remote: bool 20 | } 21 | 22 | impl Browser { 23 | pub(crate) fn try_new(channel: ChannelOwner) -> Result { 24 | let Initializer { version } = serde_json::from_value(channel.initializer.clone())?; 25 | Ok(Self { 26 | channel, 27 | version, 28 | var: Mutex::new(Variable { 29 | contexts: Vec::new(), 30 | is_remote: false 31 | }) 32 | }) 33 | } 34 | pub(crate) fn version(&self) -> &str { &self.version } 35 | 36 | pub(crate) async fn close(&self) -> Result<(), Arc> { 37 | let _ = send_message!(self, "close", Map::new()); 38 | Ok(()) 39 | } 40 | 41 | // Responds newtype `OwnerPage` of `SinglePageBrowserContext`. 42 | // There are different behavior in BrowserContext::new_page 43 | // async fn new_page( 44 | } 45 | 46 | // mutable 47 | impl Browser { 48 | pub(crate) fn contexts(&self) -> Vec> { 49 | self.var.lock().unwrap().contexts.to_owned() 50 | } 51 | 52 | pub(crate) fn push_context(&self, c: Weak) { 53 | self.var.lock().unwrap().contexts.push(c); 54 | } 55 | 56 | pub(super) fn remove_context(&self, c: &Weak) { 57 | let contexts = &mut self.var.lock().unwrap().contexts; 58 | contexts.remove_one(|v| v.ptr_eq(c)); 59 | } 60 | 61 | pub(crate) fn is_remote(&self) -> bool { self.var.lock().unwrap().is_remote } 62 | 63 | pub(crate) fn set_is_remote_true(&self) { self.var.lock().unwrap().is_remote = true; } 64 | 65 | pub(crate) async fn new_context( 66 | &self, 67 | args: NewContextArgs<'_, '_, '_, '_, '_, '_, '_> 68 | ) -> Result, Arc> { 69 | let res = send_message!(self, "newContext", args); 70 | let guid = only_guid(&res)?; 71 | let c = get_object!(self.context()?.lock().unwrap(), guid, BrowserContext)?; 72 | self.register_new_context(c.clone())?; 73 | Ok(c) 74 | } 75 | 76 | fn register_new_context(&self, c: Weak) -> Result<(), Arc> { 77 | self.push_context(c); 78 | // TODO: options 79 | // let this = get_object!(self.context()?.lock().unwrap(), &self.guid(), Browser)?; 80 | // let bc = upgrade(&c)?; 81 | // bc._options = params 82 | Ok(()) 83 | } 84 | } 85 | 86 | impl RemoteObject for Browser { 87 | fn channel(&self) -> &ChannelOwner { &self.channel } 88 | fn channel_mut(&mut self) -> &mut ChannelOwner { &mut self.channel } 89 | } 90 | 91 | #[derive(Debug, Deserialize)] 92 | #[serde(rename_all = "camelCase")] 93 | struct Initializer { 94 | version: String 95 | } 96 | 97 | #[skip_serializing_none] 98 | #[derive(Debug, Serialize, Default)] 99 | #[serde(rename_all = "camelCase")] 100 | pub(crate) struct NewContextArgs<'e, 'f, 'g, 'h, 'i, 'j, 'k> { 101 | sdk_language: &'static str, 102 | 103 | pub(crate) proxy: Option, 104 | 105 | pub(crate) viewport: Option>, 106 | pub(crate) screen: Option, 107 | pub(crate) no_viewport: Option, 108 | #[serde(rename = "ignoreHTTPSErrors")] 109 | pub(crate) ignore_https_errors: Option, 110 | #[serde(rename = "javaScriptEnabled")] 111 | pub(crate) js_enabled: Option, 112 | #[serde(rename = "bypassCSP")] 113 | pub(crate) bypass_csp: Option, 114 | pub(crate) user_agent: Option<&'e str>, 115 | pub(crate) locale: Option<&'f str>, 116 | pub(crate) timezone_id: Option<&'g str>, 117 | pub(crate) geolocation: Option, 118 | pub(crate) permissions: Option<&'h [String]>, 119 | #[serde(rename = "extraHTTPHeaders")] 120 | pub(crate) extra_http_headers: Option>, 121 | pub(crate) offline: Option, 122 | pub(crate) http_credentials: Option<&'i HttpCredentials>, 123 | pub(crate) device_scale_factor: Option, 124 | pub(crate) is_mobile: Option, 125 | pub(crate) has_touch: Option, 126 | pub(crate) color_scheme: Option, 127 | pub(crate) accept_downloads: Option, 128 | pub(crate) chromium_sandbox: Option, 129 | pub(crate) record_video: Option>, 130 | pub(crate) record_har: Option>, 131 | 132 | pub(crate) storage_state: Option 133 | } 134 | 135 | #[cfg(test)] 136 | mod tests { 137 | use super::*; 138 | use crate::imp::{browser_type::*, playwright::Playwright}; 139 | 140 | crate::runtime_test!(new_context, { 141 | let driver = Driver::install().unwrap(); 142 | let conn = Connection::run(&driver.executable()).unwrap(); 143 | let p = Playwright::wait_initial_object(&conn).await.unwrap(); 144 | let p = p.upgrade().unwrap(); 145 | let chromium = p.chromium().upgrade().unwrap(); 146 | let b = chromium.launch(LaunchArgs::default()).await.unwrap(); 147 | let b = b.upgrade().unwrap(); 148 | b.new_context(NewContextArgs::default()).await.unwrap(); 149 | }); 150 | } 151 | -------------------------------------------------------------------------------- /src/imp/browser_context.rs: -------------------------------------------------------------------------------- 1 | use crate::imp::{ 2 | browser::Browser, 3 | core::*, 4 | page::Page, 5 | prelude::*, 6 | utils::{Cookie, Geolocation, Header, StorageState} 7 | }; 8 | 9 | #[derive(Debug)] 10 | pub(crate) struct BrowserContext { 11 | channel: ChannelOwner, 12 | var: Mutex, 13 | tx: Mutex>> 14 | } 15 | 16 | #[derive(Debug, Default)] 17 | pub(crate) struct Variable { 18 | browser: Option>, 19 | pages: Vec>, 20 | timeout: Option, 21 | navigation_timeout: Option 22 | } 23 | 24 | impl BrowserContext { 25 | const DEFAULT_TIMEOUT: u32 = 30000; 26 | 27 | pub(crate) fn try_new(channel: ChannelOwner) -> Result { 28 | let Initializer {} = serde_json::from_value(channel.initializer.clone())?; 29 | let browser = match &channel.parent { 30 | Some(RemoteWeak::Browser(b)) => Some(b.clone()), 31 | _ => None 32 | }; 33 | let var = Mutex::new(Variable { 34 | browser, 35 | ..Variable::default() 36 | }); 37 | Ok(Self { 38 | channel, 39 | var, 40 | tx: Mutex::default() 41 | }) 42 | } 43 | 44 | pub(crate) async fn new_page(&self) -> Result, Arc> { 45 | let res = send_message!(self, "newPage", Map::new()); 46 | let guid = only_guid(&res)?; 47 | let p = get_object!(self.context()?.lock().unwrap(), guid, Page)?; 48 | Ok(p) 49 | } 50 | 51 | pub(crate) async fn close(&self) -> Result<(), Arc> { 52 | let _ = send_message!(self, "close", Map::new()); 53 | Ok(()) 54 | } 55 | 56 | pub(crate) async fn storage_state(&self) -> ArcResult { 57 | let v = send_message!(self, "storageState", Map::new()); 58 | let s = serde_json::from_value((*v).clone()).map_err(Error::Serde)?; 59 | Ok(s) 60 | } 61 | 62 | pub(crate) async fn clear_cookies(&self) -> ArcResult<()> { 63 | let _ = send_message!(self, "clearCookies", Map::new()); 64 | Ok(()) 65 | } 66 | 67 | pub(crate) async fn cookies(&self, urls: &[String]) -> ArcResult> { 68 | #[derive(Serialize)] 69 | #[serde(rename_all = "camelCase")] 70 | struct Args<'a> { 71 | urls: &'a [String] 72 | } 73 | let args = Args { urls }; 74 | let v = send_message!(self, "cookies", args); 75 | let cookies = first(&v).ok_or(Error::InvalidParams)?; 76 | let cs: Vec = serde_json::from_value((*cookies).clone()).map_err(Error::Serde)?; 77 | Ok(cs) 78 | } 79 | 80 | pub(crate) async fn add_cookies(&self, cookies: &[Cookie]) -> ArcResult<()> { 81 | #[derive(Serialize)] 82 | #[serde(rename_all = "camelCase")] 83 | struct Args<'a> { 84 | cookies: &'a [Cookie] 85 | } 86 | let args = Args { cookies }; 87 | let _ = send_message!(self, "addCookies", args); 88 | Ok(()) 89 | } 90 | 91 | pub(crate) async fn grant_permissions( 92 | &self, 93 | permissions: &[String], 94 | origin: Option<&str> 95 | ) -> ArcResult<()> { 96 | #[skip_serializing_none] 97 | #[derive(Serialize)] 98 | #[serde(rename_all = "camelCase")] 99 | struct Args<'a, 'b> { 100 | permissions: &'a [String], 101 | origin: Option<&'b str> 102 | } 103 | let args = Args { 104 | permissions, 105 | origin 106 | }; 107 | let _ = send_message!(self, "grantPermissions", args); 108 | Ok(()) 109 | } 110 | 111 | pub(crate) async fn clear_permissions(&self) -> ArcResult<()> { 112 | let _ = send_message!(self, "clearPermissions", Map::new()); 113 | Ok(()) 114 | } 115 | 116 | pub(crate) async fn set_geolocation(&self, geolocation: Option<&Geolocation>) -> ArcResult<()> { 117 | #[skip_serializing_none] 118 | #[derive(Serialize)] 119 | #[serde(rename_all = "camelCase")] 120 | struct Args<'a> { 121 | geolocation: Option<&'a Geolocation> 122 | } 123 | let args = Args { geolocation }; 124 | let _ = send_message!(self, "setGeolocation", args); 125 | Ok(()) 126 | } 127 | 128 | pub(crate) async fn set_offline(&self, offline: bool) -> ArcResult<()> { 129 | let mut args = Map::new(); 130 | args.insert("offline".into(), offline.into()); 131 | let _ = send_message!(self, "setOffline", args); 132 | Ok(()) 133 | } 134 | 135 | pub(crate) async fn add_init_script(&self, script: &str) -> ArcResult<()> { 136 | let mut args = HashMap::new(); 137 | args.insert("source", script); 138 | let _ = send_message!(self, "addInitScript", args); 139 | Ok(()) 140 | } 141 | 142 | pub(crate) async fn set_extra_http_headers(&self, headers: T) -> ArcResult<()> 143 | where 144 | T: IntoIterator 145 | { 146 | #[derive(Serialize)] 147 | #[serde(rename_all = "camelCase")] 148 | struct Args { 149 | headers: Vec
150 | } 151 | let args = Args { 152 | headers: headers.into_iter().map(Header::from).collect() 153 | }; 154 | let _ = send_message!(self, "setExtraHTTPHeaders", args); 155 | Ok(()) 156 | } 157 | 158 | // async def expose_binding( 159 | // async def expose_function(self, name: str, callback: Callable) -> None: 160 | // async def route(self, url: URLMatch, handler: RouteHandler) -> None: 161 | // async def unroute( 162 | 163 | // async fn pause(&self) -> ArcResult<()> { 164 | // let _ = send_message!(self, "pause", Map::new()); 165 | // Ok(()) 166 | //} 167 | } 168 | 169 | // mutable 170 | impl BrowserContext { 171 | pub(crate) fn browser(&self) -> Option> { 172 | self.var.lock().unwrap().browser.clone() 173 | } 174 | 175 | pub(crate) fn set_browser(&self, browser: Weak) { 176 | self.var.lock().unwrap().browser = Some(browser); 177 | } 178 | 179 | pub(crate) fn pages(&self) -> Vec> { self.var.lock().unwrap().pages.clone() } 180 | 181 | pub(super) fn push_page(&self, p: Weak) { self.var.lock().unwrap().pages.push(p); } 182 | 183 | pub(super) fn remove_page(&self, page: &Weak) { 184 | let pages = &mut self.var.lock().unwrap().pages; 185 | pages.remove_one(|p| p.ptr_eq(page)); 186 | } 187 | 188 | pub(crate) fn default_timeout(&self) -> u32 { 189 | self.var 190 | .lock() 191 | .unwrap() 192 | .timeout 193 | .unwrap_or(Self::DEFAULT_TIMEOUT) 194 | } 195 | 196 | pub(crate) fn default_navigation_timeout(&self) -> u32 { 197 | self.var 198 | .lock() 199 | .unwrap() 200 | .navigation_timeout 201 | .unwrap_or(Self::DEFAULT_TIMEOUT) 202 | } 203 | 204 | pub(crate) async fn set_default_timeout(&self, timeout: u32) -> ArcResult<()> { 205 | let mut args = Map::new(); 206 | args.insert("timeout".into(), timeout.into()); 207 | let _ = send_message!(self, "setDefaultTimeoutNoReply", args); 208 | self.var.lock().unwrap().timeout = Some(timeout); 209 | Ok(()) 210 | } 211 | 212 | pub(crate) async fn set_default_navigation_timeout(&self, timeout: u32) -> ArcResult<()> { 213 | let mut args = Map::new(); 214 | args.insert("timeout".into(), timeout.into()); 215 | let _ = send_message!(self, "setDefaultNavigationTimeoutNoReply", args); 216 | self.var.lock().unwrap().navigation_timeout = Some(timeout); 217 | Ok(()) 218 | } 219 | 220 | fn on_close(&self, ctx: &Context) -> Result<(), Error> { 221 | let browser = match self.browser().and_then(|b| b.upgrade()) { 222 | None => return Ok(()), 223 | Some(b) => b 224 | }; 225 | let this = get_object!(ctx, self.guid(), BrowserContext)?; 226 | browser.remove_context(&this); 227 | self.emit_event(Evt::Close); 228 | Ok(()) 229 | } 230 | 231 | fn on_route(&self, _ctx: &Context, _parmas: Map) -> Result<(), Error> { 232 | // TODO: noimplemented 233 | Ok(()) 234 | } 235 | } 236 | 237 | impl RemoteObject for BrowserContext { 238 | fn channel(&self) -> &ChannelOwner { &self.channel } 239 | fn channel_mut(&mut self) -> &mut ChannelOwner { &mut self.channel } 240 | 241 | fn handle_event( 242 | &self, 243 | ctx: &Context, 244 | method: Str, 245 | params: Map 246 | ) -> Result<(), Error> { 247 | match method.as_str() { 248 | "page" => { 249 | let first = first_object(¶ms).ok_or(Error::InvalidParams)?; 250 | let OnlyGuid { guid } = serde_json::from_value((*first).clone())?; 251 | let p = get_object!(ctx, &guid, Page)?; 252 | self.push_page(p.clone()); 253 | self.emit_event(Evt::Page(p)); 254 | } 255 | "close" => self.on_close(ctx)?, 256 | "bindingCall" => {} 257 | "route" => self.on_route(ctx, params)?, 258 | _ => {} 259 | } 260 | Ok(()) 261 | } 262 | } 263 | 264 | #[derive(Debug, Clone)] 265 | pub(crate) enum Evt { 266 | Close, 267 | Page(Weak) 268 | } 269 | 270 | impl EventEmitter for BrowserContext { 271 | type Event = Evt; 272 | 273 | fn tx(&self) -> Option> { self.tx.lock().unwrap().clone() } 274 | 275 | fn set_tx(&self, tx: broadcast::Sender) { *self.tx.lock().unwrap() = Some(tx); } 276 | } 277 | 278 | #[derive(Debug, Clone, Copy, PartialEq)] 279 | pub enum EventType { 280 | Close, 281 | Page 282 | } 283 | 284 | impl IsEvent for Evt { 285 | type EventType = EventType; 286 | 287 | fn event_type(&self) -> Self::EventType { 288 | match self { 289 | Self::Close => EventType::Close, 290 | Self::Page(_) => EventType::Page 291 | } 292 | } 293 | } 294 | 295 | #[derive(Debug, Deserialize)] 296 | #[serde(rename_all = "camelCase")] 297 | struct Initializer {} 298 | 299 | #[cfg(test)] 300 | mod tests { 301 | use super::*; 302 | use crate::imp::{browser::*, browser_type::*, playwright::Playwright}; 303 | 304 | crate::runtime_test!(storage_state, { 305 | let driver = Driver::install().unwrap(); 306 | let conn = Connection::run(&driver.executable()).unwrap(); 307 | let p = Playwright::wait_initial_object(&conn).await.unwrap(); 308 | let p = p.upgrade().unwrap(); 309 | let chromium = p.chromium().upgrade().unwrap(); 310 | let b = chromium.launch(LaunchArgs::default()).await.unwrap(); 311 | let b = b.upgrade().unwrap(); 312 | let c = b.new_context(NewContextArgs::default()).await.unwrap(); 313 | let c = c.upgrade().unwrap(); 314 | c.storage_state().await.unwrap(); 315 | c.cookies(&[]).await.unwrap(); 316 | c.set_default_timeout(30000).await.unwrap(); 317 | }); 318 | } 319 | -------------------------------------------------------------------------------- /src/imp/console_message.rs: -------------------------------------------------------------------------------- 1 | use crate::imp::{core::*, js_handle::JsHandle, prelude::*, utils::SourceLocation}; 2 | 3 | #[derive(Debug)] 4 | pub(crate) struct ConsoleMessage { 5 | channel: ChannelOwner, 6 | location: SourceLocation, 7 | args: Vec> 8 | } 9 | 10 | impl ConsoleMessage { 11 | pub(crate) fn try_new(ctx: &Context, channel: ChannelOwner) -> Result { 12 | #[derive(Deserialize)] 13 | struct De { 14 | location: SourceLocation, 15 | args: Vec 16 | } 17 | let De { location, args } = serde_json::from_value(channel.initializer.clone())?; 18 | let args = args 19 | .iter() 20 | .map(|OnlyGuid { guid }| get_object!(ctx, guid, JsHandle)) 21 | .collect::, _>>()?; 22 | Ok(Self { 23 | channel, 24 | location, 25 | args 26 | }) 27 | } 28 | 29 | pub(crate) fn r#type(&self) -> &str { 30 | self.channel() 31 | .initializer 32 | .get("type") 33 | .and_then(|v| v.as_str()) 34 | .unwrap_or_default() 35 | } 36 | 37 | pub(crate) fn text(&self) -> &str { 38 | self.channel() 39 | .initializer 40 | .get("text") 41 | .and_then(|v| v.as_str()) 42 | .unwrap_or_default() 43 | } 44 | 45 | pub(crate) fn location(&self) -> &SourceLocation { &self.location } 46 | 47 | pub(crate) fn args(&self) -> &[Weak] { &self.args } 48 | } 49 | 50 | impl RemoteObject for ConsoleMessage { 51 | fn channel(&self) -> &ChannelOwner { &self.channel } 52 | fn channel_mut(&mut self) -> &mut ChannelOwner { &mut self.channel } 53 | } 54 | -------------------------------------------------------------------------------- /src/imp/core/connection.rs: -------------------------------------------------------------------------------- 1 | use crate::imp::{core::*, prelude::*}; 2 | use std::{ 3 | io, 4 | process::{Child, Command, Stdio}, 5 | sync::{ 6 | atomic::{AtomicBool, Ordering}, 7 | TryLockError 8 | } 9 | }; 10 | 11 | #[derive(Debug)] 12 | pub(crate) struct Context { 13 | objects: HashMap, RemoteArc>, 14 | ctx: Wm, 15 | id: i32, 16 | callbacks: HashMap>, 17 | writer: Writer 18 | } 19 | 20 | #[derive(Debug)] 21 | pub(crate) struct Connection { 22 | _child: Child, 23 | ctx: Am, 24 | reader: Am, 25 | should_stop: Arc 26 | } 27 | 28 | #[derive(thiserror::Error, Debug)] 29 | pub enum Error { 30 | #[error(transparent)] 31 | Io(#[from] io::Error), 32 | #[error("Failed to initialize")] 33 | InitializationError, 34 | #[error("Disconnected")] 35 | ReceiverClosed, 36 | #[error("Invalid message")] 37 | InvalidParams, 38 | #[error("Object not found")] 39 | ObjectNotFound, 40 | #[error(transparent)] 41 | Serde(#[from] serde_json::Error), 42 | #[error("Failed to send")] 43 | Channel, 44 | #[error(transparent)] 45 | Transport(#[from] TransportError), 46 | #[error("Callback not found")] 47 | CallbackNotFound, 48 | #[error(transparent)] 49 | ErrorResponded(#[from] Arc), 50 | #[error("Value is not Object")] 51 | NotObject, 52 | #[error("guid not found in {0:?}")] 53 | GuidNotFound(Value), 54 | #[error(transparent)] 55 | InvalidBase64(#[from] base64::DecodeError), 56 | #[error(transparent)] 57 | InvalidUtf8(#[from] std::string::FromUtf8Error), 58 | #[error(transparent)] 59 | SerializationPwJson(#[from] ser::Error), 60 | #[error(transparent)] 61 | DeserializationPwJson(#[from] de::Error), 62 | #[error(transparent)] 63 | Arc(#[from] Arc), 64 | #[error(transparent)] 65 | Event(#[from] broadcast::error::RecvError), 66 | #[error("Path is not available when using BrowserType.connect(). Use save_as() to save a local copy.")] 67 | RemoteArtifact, 68 | #[error("Failed to resolve path {0:?}")] 69 | ResolvePath(PathBuf), 70 | #[error("Timed out")] 71 | Timeout, 72 | #[error(transparent)] 73 | Join(#[from] JoinError) 74 | } 75 | 76 | pub(crate) type ArcResult = Result>; 77 | 78 | impl Drop for Connection { 79 | fn drop(&mut self) { 80 | self.notify_closed(Error::ReceiverClosed); 81 | self.should_stop.store(true, Ordering::Relaxed); 82 | } 83 | } 84 | 85 | impl Connection { 86 | fn try_new(exec: &Path) -> io::Result { 87 | let mut child = Command::new(exec) 88 | .args(&["run-driver"]) 89 | .stdin(Stdio::piped()) 90 | .stdout(Stdio::piped()) 91 | .stderr(Stdio::null()) 92 | .spawn()?; 93 | // TODO: env "NODE_OPTIONS" 94 | let stdin = child.stdin.take().unwrap(); 95 | let stdout = child.stdout.take().unwrap(); 96 | let reader = Reader::new(stdout); 97 | let writer = Writer::new(stdin); 98 | let ctx = Context::new(writer); 99 | Ok(Self { 100 | _child: child, 101 | ctx, 102 | should_stop: Arc::new(false.into()), 103 | reader: Arc::new(Mutex::new(reader)) 104 | }) 105 | } 106 | 107 | pub(crate) fn run(exec: &Path) -> io::Result { 108 | let conn = Self::try_new(exec)?; 109 | conn.start(); 110 | Ok(conn) 111 | } 112 | 113 | fn start(&self) { 114 | let c2 = Arc::downgrade(&self.ctx); 115 | let r2 = Arc::downgrade(&self.reader); 116 | let s2 = Arc::downgrade(&self.should_stop); 117 | std::thread::spawn(move || { 118 | let c = c2; 119 | let r = r2; 120 | let s = s2; 121 | log::trace!("succcess starting connection"); 122 | let status = (|| -> Result<(), Error> { 123 | loop { 124 | let response = { 125 | let r = match r.upgrade() { 126 | Some(x) => x, 127 | None => break 128 | }; 129 | let mut reader = match r.try_lock() { 130 | Ok(x) => x, 131 | Err(TryLockError::WouldBlock) => continue, 132 | Err(e) => Err(e).unwrap() 133 | }; 134 | match reader.try_read()? { 135 | Some(x) => x, 136 | None => continue 137 | } 138 | }; 139 | { 140 | let s = match s.upgrade() { 141 | Some(x) => x, 142 | None => break 143 | }; 144 | let should_stop = s.load(Ordering::Relaxed); 145 | if should_stop { 146 | break; 147 | } 148 | } 149 | // dispatch 150 | { 151 | let c = match c.upgrade() { 152 | Some(x) => x, 153 | None => break 154 | }; 155 | let mut ctx = c.lock().unwrap(); 156 | ctx.dispatch(response)?; 157 | // log::debug!("{:?}", ctx.objects.keys()); 158 | } 159 | } 160 | Ok(()) 161 | })(); 162 | if let Err(e) = status { 163 | log::trace!("Failed with {:?}", e); 164 | if let Some(c) = c.upgrade() { 165 | let mut ctx = c.lock().unwrap(); 166 | ctx.notify_closed(e); 167 | } 168 | } else { 169 | log::trace!("Done"); 170 | } 171 | }); 172 | } 173 | 174 | pub(crate) fn context(&self) -> Wm { Arc::downgrade(&self.ctx) } 175 | 176 | fn notify_closed(&mut self, e: Error) { 177 | let ctx = &mut self.ctx.lock().unwrap(); 178 | ctx.notify_closed(e); 179 | } 180 | } 181 | 182 | impl Context { 183 | fn new(writer: Writer) -> Am { 184 | let objects = { 185 | let mut d = HashMap::new(); 186 | let root = RootObject::new(); 187 | d.insert(root.guid().to_owned(), RemoteArc::Root(Arc::new(root))); 188 | d 189 | }; 190 | let ctx = Context { 191 | objects, 192 | ctx: Weak::new(), 193 | id: 0, 194 | callbacks: HashMap::new(), 195 | writer 196 | }; 197 | let am = Arc::new(Mutex::new(ctx)); 198 | am.lock().unwrap().ctx = Arc::downgrade(&am); 199 | am 200 | } 201 | 202 | fn notify_closed(&mut self, e: Error) { 203 | let err = Arc::new(e); 204 | for p in self.callbacks.iter().map(|(_, v)| v) { 205 | Context::respond_wait(p, Err(err.clone())); 206 | } 207 | self.objects = HashMap::new(); 208 | } 209 | 210 | fn dispatch(&mut self, msg: Res) -> Result<(), Error> { 211 | match msg { 212 | Res::Result(msg) => { 213 | let p = self.callbacks.get(&msg.id).ok_or(Error::CallbackNotFound)?; 214 | Self::respond_wait(p, Ok(msg.body.map(Arc::new).map_err(Arc::new))); 215 | return Ok(()); 216 | } 217 | Res::Initial(msg) => { 218 | if Method::is_create(&msg.method) { 219 | self.create_remote_object(&msg.guid, msg.params)?; 220 | //(&**parent).push_child(r.clone()); 221 | return Ok(()); 222 | } 223 | if Method::is_dispose(&msg.method) { 224 | self.dispose(&msg.guid); 225 | return Ok(()); 226 | } 227 | let target = self.objects.get(&msg.guid).ok_or(Error::ObjectNotFound)?; 228 | let ResInitial { method, params, .. } = msg; 229 | target.handle_event(self, method, params)?; 230 | } 231 | } 232 | Ok(()) 233 | } 234 | 235 | fn dispose(&mut self, i: &S) { 236 | let a = match self.objects.get(i) { 237 | None => return, 238 | Some(a) => a 239 | }; 240 | let cs = a.channel().children(); 241 | for c in cs { 242 | let c = match c.upgrade() { 243 | None => continue, 244 | Some(c) => c 245 | }; 246 | self.dispose(&c.channel().guid); 247 | } 248 | self.remove_object(i); 249 | } 250 | 251 | fn respond_wait( 252 | WaitPlaces { value, waker }: &WaitPlaces, 253 | result: WaitMessageResult 254 | ) { 255 | let place = match value.upgrade() { 256 | Some(p) => p, 257 | None => return 258 | }; 259 | let waker = match waker.upgrade() { 260 | Some(x) => x, 261 | None => return 262 | }; 263 | *place.lock().unwrap() = Some(result); 264 | let waker: &Option = &waker.lock().unwrap(); 265 | let waker = match waker { 266 | Some(x) => x.clone(), 267 | None => return 268 | }; 269 | waker.wake(); 270 | } 271 | 272 | fn create_remote_object( 273 | &mut self, 274 | parent: &S, 275 | params: Map 276 | ) -> Result<(), Error> { 277 | let CreateParams { 278 | typ, 279 | guid, 280 | initializer 281 | } = serde_json::from_value(params.into())?; 282 | let parent = self.objects.get(parent).ok_or(Error::ObjectNotFound)?; 283 | let c = ChannelOwner::new( 284 | self.ctx.clone(), 285 | parent.downgrade(), 286 | typ.to_owned(), 287 | guid.to_owned(), 288 | initializer 289 | ); 290 | let r = RemoteArc::try_new(&typ, self, c)?; 291 | parent.channel().push_child(r.downgrade()); 292 | self.objects.insert(guid, r.clone()); 293 | match r { 294 | RemoteArc::Page(p) => { 295 | p.hook_created(Arc::downgrade(&p))?; 296 | } 297 | RemoteArc::Frame(f) => { 298 | f.hook_created(Arc::downgrade(&f))?; 299 | } 300 | _ => () 301 | } 302 | Ok(()) 303 | } 304 | 305 | pub(in crate::imp) fn find_object(&self, k: &S) -> Option { 306 | self.objects.get(k).map(|r| r.downgrade()) 307 | } 308 | 309 | pub(in crate::imp) fn remove_object(&mut self, k: &S) { self.objects.remove(k); } 310 | 311 | pub(in crate::imp::core) fn send_message(&mut self, r: RequestBody) -> Result<(), Error> { 312 | self.id += 1; 313 | let RequestBody { 314 | guid, 315 | method, 316 | params, 317 | place 318 | } = r; 319 | self.callbacks.insert(self.id, place); 320 | let req = Req { 321 | guid: &guid, 322 | method: &method, 323 | params, 324 | id: self.id 325 | }; 326 | self.writer.send(&req)?; 327 | Ok(()) 328 | } 329 | } 330 | 331 | #[cfg(test)] 332 | mod tests { 333 | use crate::imp::core::*; 334 | 335 | crate::runtime_test!(start, { 336 | let driver = Driver::install().unwrap(); 337 | let conn = Connection::try_new(&driver.executable()).unwrap(); 338 | Connection::start(&conn); 339 | }); 340 | } 341 | -------------------------------------------------------------------------------- /src/imp/core/driver.rs: -------------------------------------------------------------------------------- 1 | use crate::imp::prelude::*; 2 | use std::{env, fs, io}; 3 | use zip::{result::ZipError, ZipArchive}; 4 | 5 | #[derive(Debug, Clone, PartialEq)] 6 | pub struct Driver { 7 | path: PathBuf 8 | } 9 | 10 | impl Driver { 11 | const ZIP: &'static [u8] = include_bytes!(concat!(env!("OUT_DIR"), env!("SEP"), "driver.zip")); 12 | const PLATFORM: &'static str = include_str!(concat!(env!("OUT_DIR"), env!("SEP"), "platform")); 13 | 14 | pub fn install() -> io::Result { 15 | let this = Self::new(Self::default_dest()); 16 | if !this.path.is_dir() { 17 | this.prepare()?; 18 | } 19 | Ok(this) 20 | } 21 | 22 | /// Without prepare 23 | pub fn new>(path: P) -> Self { Self { path: path.into() } } 24 | /// 25 | pub fn prepare(&self) -> Result<(), ZipError> { 26 | fs::create_dir_all(&self.path)?; 27 | let mut a = ZipArchive::new(io::Cursor::new(Self::ZIP))?; 28 | a.extract(&self.path) 29 | } 30 | 31 | pub fn default_dest() -> PathBuf { 32 | let base: PathBuf = dirs::cache_dir().unwrap_or_else(env::temp_dir); 33 | let dir: PathBuf = [ 34 | base.as_os_str(), 35 | "ms-playwright".as_ref(), 36 | "playwright-rust".as_ref(), 37 | "driver".as_ref() 38 | ] 39 | .iter() 40 | .collect(); 41 | dir 42 | } 43 | 44 | pub fn platform(&self) -> Platform { 45 | match Self::PLATFORM { 46 | "linux" => Platform::Linux, 47 | "mac" => Platform::Mac, 48 | "win32" => Platform::Win32, 49 | "win32_x64" => Platform::Win32x64, 50 | _ => unreachable!() 51 | } 52 | } 53 | 54 | pub fn executable(&self) -> PathBuf { 55 | match self.platform() { 56 | Platform::Linux => self.path.join("playwright.sh"), 57 | Platform::Mac => self.path.join("playwright.sh"), 58 | Platform::Win32 => self.path.join("playwright.cmd"), 59 | Platform::Win32x64 => self.path.join("playwright.cmd") 60 | } 61 | } 62 | } 63 | 64 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 65 | pub enum Platform { 66 | Linux, 67 | Win32, 68 | Win32x64, 69 | Mac 70 | } 71 | 72 | #[cfg(test)] 73 | mod tests { 74 | use super::*; 75 | 76 | #[test] 77 | fn install() { let _driver = Driver::install().unwrap(); } 78 | } 79 | -------------------------------------------------------------------------------- /src/imp/core/event_emitter.rs: -------------------------------------------------------------------------------- 1 | use crate::imp::{core::*, prelude::*}; 2 | pub(crate) use tokio::sync::{broadcast, broadcast::error::TryRecvError}; 3 | 4 | pub trait EventEmitter { 5 | type Event: Clone; 6 | 7 | fn tx(&self) -> Option>; 8 | 9 | fn set_tx(&self, tx: broadcast::Sender); 10 | 11 | fn new_tx( 12 | &self 13 | ) -> ( 14 | broadcast::Sender, 15 | broadcast::Receiver 16 | ) { 17 | broadcast::channel(64) 18 | } 19 | 20 | fn subscribe_event(&self) -> broadcast::Receiver { 21 | if let Some(tx) = self.tx() { 22 | tx.subscribe() 23 | } else { 24 | let (tx, rx) = self.new_tx(); 25 | self.set_tx(tx); 26 | rx 27 | } 28 | } 29 | 30 | fn emit_event>(&self, e: E) { self.tx().map(|tx| tx.send(e.into()).ok()); } 31 | } 32 | 33 | pub(crate) trait IsEvent: Clone { 34 | type EventType: Clone + Copy + PartialEq; 35 | 36 | fn event_type(&self) -> Self::EventType; 37 | } 38 | 39 | #[cfg(any(feature = "rt-tokio", feature = "rt-actix"))] 40 | pub(crate) async fn expect_event( 41 | mut rx: broadcast::Receiver, 42 | evt: E::EventType, 43 | timeout: u32 44 | ) -> Result 45 | where 46 | E: IsEvent + Send + Sync + 'static, 47 | ::EventType: Send + Sync 48 | { 49 | consume(&mut rx).await?; 50 | let sleep = sleep(Duration::from_millis(timeout as u64)); 51 | let event = spawn(async move { 52 | loop { 53 | match rx.recv().await { 54 | Ok(x) if x.event_type() == evt => break Ok(x), 55 | Ok(_) => continue, 56 | Err(e) => break Err(e) 57 | } 58 | } 59 | }); 60 | tokio::select! { 61 | _ = sleep => Err(Error::Timeout), 62 | x = event => x?.map_err(Error::Event) 63 | } 64 | } 65 | 66 | #[cfg(feature = "rt-async-std")] 67 | pub(crate) async fn expect_event( 68 | mut rx: broadcast::Receiver, 69 | evt: E::EventType, 70 | timeout: u32 71 | ) -> Result 72 | where 73 | E: IsEvent + Send + Sync + 'static, 74 | ::EventType: Send + Sync 75 | { 76 | consume(&mut rx).await?; 77 | let sleep = sleep(Duration::from_millis(timeout as u64)); 78 | let event = spawn(async move { 79 | loop { 80 | match rx.recv().await { 81 | Ok(x) if x.event_type() == evt => break Ok(x), 82 | Ok(_) => continue, 83 | Err(e) => break Err(e) 84 | } 85 | } 86 | }); 87 | tokio::select! { 88 | _ = sleep => Err(Error::Timeout), 89 | x = event => x.map_err(Error::Event) 90 | } 91 | } 92 | 93 | async fn consume(rx: &mut broadcast::Receiver) -> Result<(), Error> 94 | where 95 | E: IsEvent 96 | { 97 | loop { 98 | match rx.try_recv() { 99 | Err(TryRecvError::Empty) | Err(TryRecvError::Closed) => break, 100 | _ => {} 101 | } 102 | } 103 | Ok(()) 104 | } 105 | 106 | #[cfg(test)] 107 | mod tests { 108 | crate::runtime_test!(select, { 109 | use crate::imp::prelude::*; 110 | let first = sleep(Duration::from_millis(200u64)); 111 | let second = sleep(Duration::from_millis(400u64)); 112 | tokio::select! { 113 | _ = first => {}, 114 | _ = second => unreachable!() 115 | } 116 | }); 117 | } 118 | -------------------------------------------------------------------------------- /src/imp/core/message.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod de; 2 | pub(crate) mod ser; 3 | 4 | use crate::imp::core::Error; 5 | use serde::{Deserialize, Deserializer}; 6 | use serde_json::{map::Map, value::Value}; 7 | use strong::*; 8 | 9 | #[derive(Debug, Serialize)] 10 | pub(crate) struct Req<'a, 'b> { 11 | #[serde(default)] 12 | pub(crate) id: i32, 13 | pub(crate) guid: &'a S, 14 | #[serde(default)] 15 | pub(crate) method: &'b S, 16 | #[serde(default)] 17 | pub(crate) params: Map 18 | } 19 | 20 | #[derive(Debug, Deserialize, Clone)] 21 | #[serde(untagged)] 22 | pub(crate) enum Res { 23 | Result(ResResult), 24 | Initial(ResInitial) 25 | } 26 | 27 | #[derive(Debug, Clone)] 28 | pub(crate) struct ResResult { 29 | pub(crate) id: i32, 30 | pub(crate) body: Result 31 | } 32 | 33 | impl<'de> Deserialize<'de> for ResResult { 34 | fn deserialize(deserializer: D) -> Result 35 | where 36 | D: Deserializer<'de> 37 | { 38 | #[derive(Deserialize)] 39 | struct ResponseResultImpl { 40 | id: i32, 41 | result: Option, 42 | error: Option 43 | } 44 | let ResponseResultImpl { id, result, error } = 45 | ResponseResultImpl::deserialize(deserializer)?; 46 | if let Some(ErrorWrap { error }) = error { 47 | Ok(Self { 48 | id, 49 | body: Err(error) 50 | }) 51 | } else if let Some(x) = result { 52 | Ok(Self { id, body: Ok(x) }) 53 | } else { 54 | Ok(Self { 55 | id, 56 | body: Ok(Value::default()) 57 | }) 58 | } 59 | } 60 | } 61 | 62 | #[derive(Debug, Deserialize, Serialize, Clone)] 63 | pub(crate) struct ResInitial { 64 | pub(crate) guid: Str, 65 | pub(crate) method: Str, 66 | pub(crate) params: Map 67 | } 68 | 69 | #[derive(Debug, Deserialize, Serialize)] 70 | pub(crate) struct CreateParams { 71 | #[serde(rename = "type")] 72 | pub(crate) typ: Str, 73 | pub(crate) guid: Str, 74 | #[serde(default)] 75 | pub(crate) initializer: Value 76 | } 77 | 78 | #[derive(Debug, Deserialize, Serialize, Clone)] 79 | pub(crate) struct ErrorWrap { 80 | error: ErrorMessage 81 | } 82 | 83 | #[derive(Debug, Deserialize, Serialize, Clone, thiserror::Error)] 84 | #[error("{name} {message:?}")] 85 | pub struct ErrorMessage { 86 | pub(crate) name: String, 87 | pub(crate) message: String, 88 | pub(crate) stack: String 89 | } 90 | 91 | #[derive(Debug, Deserialize, Serialize)] 92 | pub(crate) struct OnlyGuid { 93 | pub(crate) guid: Str 94 | } 95 | 96 | pub(crate) enum Guid {} 97 | 98 | impl Validator for Guid { 99 | type Err = std::convert::Infallible; 100 | } 101 | 102 | pub(crate) enum Method {} 103 | 104 | #[derive(thiserror::Error, Debug)] 105 | #[error("Method {0:?} validation error")] 106 | pub(crate) struct MethodError(String); 107 | 108 | impl Validator for Method { 109 | type Err = MethodError; 110 | 111 | fn validate(raw: &str) -> Result<(), Self::Err> { 112 | if raw.is_empty() { 113 | Err(MethodError(raw.to_string())) 114 | } else { 115 | Ok(()) 116 | } 117 | } 118 | } 119 | 120 | impl Method { 121 | pub(crate) fn is_create(s: &S) -> bool { s.as_str() == "__create__" } 122 | pub(crate) fn is_dispose(s: &S) -> bool { s.as_str() == "__dispose__" } 123 | } 124 | 125 | pub(crate) enum ObjectType {} 126 | 127 | impl Validator for ObjectType { 128 | type Err = std::convert::Infallible; 129 | } 130 | 131 | pub(crate) fn first(v: &Value) -> Option<&Value> { 132 | let m: &Map = v.as_object()?; 133 | first_object(m) 134 | } 135 | 136 | pub(crate) fn first_object(m: &Map) -> Option<&Value> { 137 | if m.len() != 1 { 138 | return None; 139 | } 140 | let v: &Value = m.values().next()?; 141 | Some(v) 142 | } 143 | 144 | /// If {"": {"guid": str}} then str 145 | pub(crate) fn as_only_guid(v: &Value) -> Option<&S> { 146 | // {"": {"guid": str}} 147 | let v: &Value = first(v)?; 148 | // {"guid": str} 149 | let m: &Map = v.as_object()?; 150 | let v: &Value = m.get("guid")?; 151 | let s: &str = v.as_str()?; 152 | S::validate(s).ok() 153 | } 154 | 155 | pub(crate) fn only_guid(v: &Value) -> Result<&S, Error> { 156 | as_only_guid(v).ok_or_else(|| Error::GuidNotFound(v.clone())) 157 | } 158 | 159 | pub(crate) fn only_str(v: &Value) -> Result<&str, Error> { 160 | let s = first(v) 161 | .ok_or(Error::InvalidParams)? 162 | .as_str() 163 | .ok_or(Error::InvalidParams)?; 164 | Ok(s) 165 | } 166 | 167 | pub(crate) fn maybe_only_str(v: &Value) -> Result, Error> { 168 | let s = match first(v) { 169 | Some(s) => s.as_str().ok_or(Error::InvalidParams)?, 170 | None => return Ok(None) 171 | }; 172 | Ok(Some(s)) 173 | } 174 | 175 | #[derive(Debug, Serialize)] 176 | pub(crate) struct Argument { 177 | pub(crate) value: Map, 178 | pub(crate) handles: Vec 179 | } 180 | 181 | #[derive(Debug, Deserialize)] 182 | pub struct DateTime { 183 | d: String 184 | } 185 | 186 | mod datetime { 187 | use super::*; 188 | #[cfg(feature = "chrono")] 189 | use chrono::Utc; 190 | use serde::{ser, ser::SerializeStruct}; 191 | use std::convert::TryFrom; 192 | 193 | #[cfg(feature = "chrono")] 194 | impl From> for DateTime { 195 | fn from(c: chrono::DateTime) -> DateTime { Self { d: c.to_rfc3339() } } 196 | } 197 | 198 | #[cfg(feature = "chrono")] 199 | impl TryFrom for chrono::DateTime { 200 | type Error = chrono::format::ParseError; 201 | 202 | fn try_from(d: DateTime) -> Result, Self::Error> { 203 | let f = chrono::DateTime::parse_from_rfc3339(&d.d)?; 204 | Ok(f.with_timezone(&chrono::Utc)) 205 | } 206 | } 207 | 208 | impl ser::Serialize for DateTime { 209 | fn serialize(&self, serializer: S) -> Result 210 | where 211 | S: ser::Serializer 212 | { 213 | let mut s = serializer.serialize_struct("e7ee19d3-64cb-4286-8762-6dd8ab78eb89", 1)?; 214 | s.serialize_field("d", &self.d)?; 215 | s.end() 216 | } 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /src/imp/core/transport.rs: -------------------------------------------------------------------------------- 1 | use crate::imp::core::*; 2 | use std::{ 3 | convert::TryInto, 4 | io, 5 | io::{Read, Write}, 6 | process::{ChildStdin, ChildStdout} 7 | }; 8 | use thiserror::Error; 9 | 10 | #[derive(Debug)] 11 | pub(super) struct Reader { 12 | stdout: ChildStdout, 13 | length: Option, 14 | buf: Vec 15 | } 16 | 17 | #[derive(Debug)] 18 | pub(super) struct Writer { 19 | stdin: ChildStdin 20 | } 21 | 22 | #[derive(Error, Debug)] 23 | pub enum TransportError { 24 | #[error(transparent)] 25 | Serde(#[from] serde_json::error::Error), 26 | #[error(transparent)] 27 | Io(#[from] io::Error) 28 | } 29 | 30 | impl Reader { 31 | const BUFSIZE: usize = 30000; 32 | 33 | pub(super) fn new(stdout: ChildStdout) -> Self { 34 | Self { 35 | stdout, 36 | length: None, 37 | buf: Vec::with_capacity(Self::BUFSIZE) 38 | } 39 | } 40 | 41 | // TODO: heap efficiency 42 | pub(super) fn try_read(&mut self) -> Result, TransportError> { 43 | let this = self; 44 | { 45 | if this.length.is_none() && this.buf.len() >= 4 { 46 | let off = this.buf.split_off(4); 47 | let bytes: &[u8] = &this.buf; 48 | this.length = Some(u32::from_le_bytes(bytes.try_into().unwrap())); 49 | this.buf = off; 50 | } 51 | match this.length.map(|u| u as usize) { 52 | None => {} 53 | Some(l) if this.buf.len() < l => {} 54 | Some(l) => { 55 | let bytes: &[u8] = &this.buf[..l]; 56 | log::debug!("RECV {}", unsafe { std::str::from_utf8_unchecked(bytes) }); 57 | let msg: Res = serde_json::from_slice(bytes)?; 58 | this.length = None; 59 | this.buf = this.buf[l..].to_owned(); 60 | return Ok(Some(msg)); 61 | } 62 | } 63 | } 64 | { 65 | let mut buf = [0; Self::BUFSIZE]; 66 | let n = this.stdout.read(&mut buf)?; 67 | this.buf.extend(&buf[..n]); 68 | } 69 | Ok(None) 70 | } 71 | } 72 | 73 | impl Writer { 74 | pub(super) fn new(stdin: ChildStdin) -> Self { Self { stdin } } 75 | 76 | pub(super) fn send(&mut self, req: &Req<'_, '_>) -> Result<(), TransportError> { 77 | log::debug!("SEND {:?}", &req); 78 | let serialized = serde_json::to_vec(&req)?; 79 | let length = serialized.len() as u32; 80 | let mut bytes = length.to_le_bytes().to_vec(); 81 | bytes.extend(serialized); 82 | self.stdin.write_all(&bytes)?; 83 | Ok(()) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/imp/dialog.rs: -------------------------------------------------------------------------------- 1 | use crate::imp::core::*; 2 | 3 | #[derive(Debug)] 4 | pub(crate) struct Dialog { 5 | channel: ChannelOwner 6 | } 7 | 8 | impl Dialog { 9 | pub(crate) fn new(channel: ChannelOwner) -> Self { Self { channel } } 10 | } 11 | 12 | impl RemoteObject for Dialog { 13 | fn channel(&self) -> &ChannelOwner { &self.channel } 14 | fn channel_mut(&mut self) -> &mut ChannelOwner { &mut self.channel } 15 | } 16 | -------------------------------------------------------------------------------- /src/imp/download.rs: -------------------------------------------------------------------------------- 1 | use crate::imp::{artifact::Artifact, core::*, prelude::*}; 2 | 3 | #[derive(Debug)] 4 | pub(crate) struct Download { 5 | url: String, 6 | suggested_filename: String, 7 | artifact: Weak 8 | } 9 | 10 | impl Download { 11 | pub(crate) fn new(artifact: Weak, url: String, suggested_filename: String) -> Self { 12 | Self { 13 | url, 14 | suggested_filename, 15 | artifact 16 | } 17 | } 18 | 19 | pub(crate) fn url(&self) -> &str { &self.url } 20 | 21 | pub(crate) fn suggested_filename(&self) -> &str { &self.suggested_filename } 22 | 23 | pub(crate) async fn path(&self) -> ArcResult> { 24 | upgrade(&self.artifact)?.path_after_finished().await 25 | } 26 | 27 | pub(crate) async fn delete(&self) -> ArcResult<()> { upgrade(&self.artifact)?.delete().await } 28 | 29 | pub(crate) async fn save_as>(&self, path: P) -> Result<(), Arc> { 30 | upgrade(&self.artifact)?.save_as(path).await 31 | } 32 | 33 | pub(crate) async fn failure(&self) -> ArcResult> { 34 | upgrade(&self.artifact)?.failure().await 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/imp/file_hooser.rs: -------------------------------------------------------------------------------- 1 | use crate::imp::{ 2 | core::*, element_handle::ElementHandle as ElementHandleImpl, page::Page as PageImpl, prelude::* 3 | }; 4 | 5 | /// `FileChooser` objects are dispatched by the page in the [page::Event::FileChooser](crate::api::page::Event::FileChooser) event. 6 | /// 7 | /// ```js 8 | /// const [fileChooser] = await Promise.all([ 9 | /// page.waitForEvent('filechooser'), 10 | /// page.click('upload') 11 | /// ]); 12 | /// await fileChooser.setFiles('myfile.pdf'); 13 | /// ``` 14 | #[derive(Debug, Clone)] 15 | pub struct FileChooser { 16 | pub(crate) page: Weak, 17 | pub(crate) element_handle: Weak, 18 | pub(crate) is_multiple: bool 19 | } 20 | 21 | impl FileChooser { 22 | pub(crate) fn new( 23 | page: Weak, 24 | element_handle: Weak, 25 | is_multiple: bool 26 | ) -> Self { 27 | Self { 28 | page, 29 | element_handle, 30 | is_multiple 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/imp/js_handle.rs: -------------------------------------------------------------------------------- 1 | use crate::imp::{core::*, prelude::*}; 2 | use std::fmt; 3 | 4 | #[derive(Debug)] 5 | pub(crate) struct JsHandle { 6 | channel: ChannelOwner, 7 | var: Mutex 8 | } 9 | 10 | #[derive(Debug)] 11 | struct Var { 12 | preview: String 13 | } 14 | 15 | impl JsHandle { 16 | pub(crate) fn try_new(channel: ChannelOwner) -> Result { 17 | let Initializer { preview } = serde_json::from_value(channel.initializer.clone())?; 18 | let var = Mutex::new(Var { preview }); 19 | Ok(Self { channel, var }) 20 | } 21 | 22 | pub(crate) async fn get_property(&self, name: &str) -> ArcResult> { 23 | let mut args = HashMap::new(); 24 | args.insert("name", name); 25 | let v = send_message!(self, "getProperty", args); 26 | let guid = only_guid(&v)?; 27 | let j = get_object!(self.context()?.lock().unwrap(), guid, JsHandle)?; 28 | Ok(j) 29 | } 30 | 31 | pub(crate) async fn get_properties(&self) -> ArcResult>> { 32 | let v = send_message!(self, "getPropertyList", Map::new()); 33 | let first = first(&v).ok_or(Error::InvalidParams)?; 34 | let properties: Vec = 35 | serde_json::from_value((*first).clone()).map_err(Error::Serde)?; 36 | let ps = properties 37 | .into_iter() 38 | .map( 39 | |Property { 40 | name, 41 | value: OnlyGuid { guid } 42 | }| { 43 | get_object!(self.context()?.lock().unwrap(), &guid, JsHandle).map(|o| (name, o)) 44 | } 45 | ) 46 | .collect::, Error>>()?; 47 | Ok(ps) 48 | } 49 | 50 | pub(crate) async fn dispose(&self) -> ArcResult<()> { 51 | let _ = send_message!(self, "dispose", Map::new()); 52 | Ok(()) 53 | } 54 | 55 | pub(crate) async fn json_value(&self) -> ArcResult 56 | where 57 | U: DeserializeOwned 58 | { 59 | let v = send_message!(self, "jsonValue", Map::new()); 60 | let first = first(&v).ok_or(Error::ObjectNotFound)?; 61 | Ok(de::from_value(first).map_err(Error::DeserializationPwJson)?) 62 | } 63 | } 64 | 65 | impl JsHandle { 66 | fn set_preview(&self, preview: String) { 67 | let var = &mut self.var.lock().unwrap(); 68 | var.preview = preview; 69 | } 70 | 71 | fn on_preview_updated(&self, params: Map) -> Result<(), Error> { 72 | #[derive(Deserialize)] 73 | struct De { 74 | preview: String 75 | } 76 | let De { preview } = serde_json::from_value(params.into())?; 77 | self.set_preview(preview); 78 | Ok(()) 79 | } 80 | } 81 | 82 | impl RemoteObject for JsHandle { 83 | fn channel(&self) -> &ChannelOwner { &self.channel } 84 | fn channel_mut(&mut self) -> &mut ChannelOwner { &mut self.channel } 85 | 86 | fn handle_event( 87 | &self, 88 | _ctx: &Context, 89 | method: Str, 90 | params: Map 91 | ) -> Result<(), Error> { 92 | if method.as_str() == "previewUpdated" { 93 | self.on_preview_updated(params)?; 94 | } 95 | Ok(()) 96 | } 97 | } 98 | 99 | impl fmt::Display for JsHandle { 100 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 101 | write!(f, "{}", &self.var.lock().unwrap().preview) 102 | } 103 | } 104 | 105 | #[derive(Debug, Deserialize)] 106 | #[serde(rename_all = "camelCase")] 107 | struct Initializer { 108 | preview: String 109 | } 110 | 111 | #[derive(Deserialize)] 112 | struct Property { 113 | name: String, 114 | value: OnlyGuid 115 | } 116 | -------------------------------------------------------------------------------- /src/imp/playwright.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | api::{browser::ContextBuilder, browser_type::PersistentContextLauncher}, 3 | imp::{ 4 | browser_type::BrowserType, core::*, impl_future::*, prelude::*, selectors::Selectors, 5 | utils::Viewport 6 | } 7 | }; 8 | use serde::Deserialize; 9 | use std::{sync::TryLockError, time::Instant}; 10 | 11 | #[derive(Debug)] 12 | pub(crate) struct Playwright { 13 | channel: ChannelOwner, 14 | chromium: Weak, 15 | firefox: Weak, 16 | webkit: Weak, 17 | selectors: Weak, 18 | devices: Vec 19 | } 20 | 21 | impl Playwright { 22 | pub(crate) fn try_new(ctx: &Context, channel: ChannelOwner) -> Result { 23 | let i: Initializer = serde_json::from_value(channel.initializer.clone())?; 24 | let chromium = get_object!(ctx, &i.chromium.guid, BrowserType)?; 25 | let firefox = get_object!(ctx, &i.firefox.guid, BrowserType)?; 26 | let webkit = get_object!(ctx, &i.webkit.guid, BrowserType)?; 27 | let selectors = get_object!(ctx, &i.selectors.guid, Selectors)?; 28 | let devices = i.device_descriptors; 29 | Ok(Self { 30 | channel, 31 | chromium, 32 | firefox, 33 | webkit, 34 | selectors, 35 | devices 36 | }) 37 | } 38 | 39 | pub(crate) fn devices(&self) -> &[DeviceDescriptor] { &self.devices } 40 | 41 | pub(crate) fn device(&self, name: &str) -> Option<&DeviceDescriptor> { 42 | self.devices.iter().find(|d| d.name == name) 43 | } 44 | 45 | pub(crate) fn chromium(&self) -> Weak { self.chromium.clone() } 46 | 47 | pub(crate) fn firefox(&self) -> Weak { self.firefox.clone() } 48 | 49 | pub(crate) fn webkit(&self) -> Weak { self.webkit.clone() } 50 | 51 | pub(crate) fn selectors(&self) -> Weak { self.selectors.clone() } 52 | 53 | pub(crate) fn wait_initial_object(conn: &Connection) -> WaitInitialObject { 54 | WaitInitialObject::new(conn.context()) 55 | } 56 | } 57 | 58 | impl RemoteObject for Playwright { 59 | fn channel(&self) -> &ChannelOwner { &self.channel } 60 | fn channel_mut(&mut self) -> &mut ChannelOwner { &mut self.channel } 61 | } 62 | 63 | #[derive(Debug, Deserialize)] 64 | #[serde(rename_all = "camelCase")] 65 | struct Initializer { 66 | chromium: OnlyGuid, 67 | firefox: OnlyGuid, 68 | webkit: OnlyGuid, 69 | android: OnlyGuid, 70 | selectors: OnlyGuid, 71 | device_descriptors: Vec 72 | } 73 | 74 | pub(crate) struct WaitInitialObject { 75 | ctx: Wm, 76 | started: Instant 77 | } 78 | 79 | impl WaitInitialObject { 80 | fn new(ctx: Wm) -> Self { 81 | Self { 82 | ctx, 83 | started: Instant::now() 84 | } 85 | } 86 | } 87 | 88 | impl Future for WaitInitialObject { 89 | type Output = Result, Error>; 90 | 91 | fn poll(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll { 92 | let i: &S = S::validate("Playwright").unwrap(); 93 | let this = self.get_mut(); 94 | macro_rules! pending { 95 | () => {{ 96 | cx.waker().wake_by_ref(); 97 | if this.started.elapsed().as_secs() > 10 { 98 | return Poll::Ready(Err(Error::InitializationError)); 99 | } 100 | return Poll::Pending; 101 | }}; 102 | } 103 | let rc = upgrade(&this.ctx)?; 104 | let c = match rc.try_lock() { 105 | Ok(x) => x, 106 | Err(TryLockError::WouldBlock) => pending!(), 107 | Err(e) => Err(e).unwrap() 108 | }; 109 | match get_object!(c, i, Playwright) { 110 | Ok(p) => Poll::Ready(Ok(p)), 111 | Err(_) => pending!() 112 | } 113 | } 114 | } 115 | 116 | #[derive(Debug, Clone, PartialEq)] 117 | pub struct DeviceDescriptor { 118 | pub name: String, 119 | pub user_agent: String, 120 | pub viewport: Viewport, 121 | pub screen: Option, 122 | pub device_scale_factor: f64, 123 | pub is_mobile: bool, 124 | pub has_touch: bool, 125 | pub default_browser_type: String 126 | } 127 | 128 | impl<'de> Deserialize<'de> for DeviceDescriptor { 129 | fn deserialize(deserializer: D) -> Result 130 | where 131 | D: serde::Deserializer<'de> 132 | { 133 | #[derive(Deserialize)] 134 | struct DeviceDescriptorImpl { 135 | name: String, 136 | descriptor: Descriptor 137 | } 138 | #[derive(Deserialize)] 139 | #[serde(rename_all = "camelCase")] 140 | struct Descriptor { 141 | user_agent: String, 142 | viewport: Viewport, 143 | screen: Option, 144 | device_scale_factor: f64, 145 | is_mobile: bool, 146 | has_touch: bool, 147 | default_browser_type: String 148 | } 149 | let DeviceDescriptorImpl { 150 | name, 151 | descriptor: 152 | Descriptor { 153 | user_agent, 154 | viewport, 155 | screen, 156 | device_scale_factor, 157 | is_mobile, 158 | has_touch, 159 | default_browser_type 160 | } 161 | } = DeviceDescriptorImpl::deserialize(deserializer)?; 162 | Ok(DeviceDescriptor { 163 | name, 164 | user_agent, 165 | viewport, 166 | screen, 167 | device_scale_factor, 168 | is_mobile, 169 | has_touch, 170 | default_browser_type 171 | }) 172 | } 173 | } 174 | 175 | macro_rules! impl_set_device { 176 | ($device: expr, $builder:expr) => { 177 | (if let Some(screen) = &$device.screen { 178 | $builder.screen(screen.clone()) 179 | } else { 180 | $builder 181 | }) 182 | .user_agent(&$device.user_agent) 183 | .viewport(Some($device.viewport.clone())) 184 | .device_scale_factor($device.device_scale_factor) 185 | .is_mobile($device.is_mobile) 186 | .has_touch($device.has_touch) 187 | }; 188 | } 189 | 190 | impl DeviceDescriptor { 191 | pub(crate) fn set_persistent_context<'source, 'b, 'c, 'd, 'e, 'g, 'h, 'i, 'j, 'k, 'l>( 192 | device: &'source Self, 193 | builder: PersistentContextLauncher<'b, 'c, 'd, 'e, 'source, 'g, 'h, 'i, 'j, 'k, 'l> 194 | ) -> PersistentContextLauncher<'b, 'c, 'd, 'e, 'source, 'g, 'h, 'i, 'j, 'k, 'l> { 195 | impl_set_device!(device, builder) 196 | } 197 | 198 | pub(crate) fn set_context<'source, 'c, 'd, 'e, 'f, 'g, 'h>( 199 | device: &'source Self, 200 | builder: ContextBuilder<'source, 'c, 'd, 'e, 'f, 'g, 'h> 201 | ) -> ContextBuilder<'source, 'c, 'd, 'e, 'f, 'g, 'h> { 202 | impl_set_device!(device, builder) 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/imp/request.rs: -------------------------------------------------------------------------------- 1 | use crate::imp::{ 2 | core::*, 3 | frame::Frame, 4 | prelude::*, 5 | response::Response, 6 | utils::{Header, ResponseTiming} 7 | }; 8 | 9 | #[derive(Debug)] 10 | pub(crate) struct Request { 11 | channel: ChannelOwner, 12 | url: String, 13 | resource_type: String, 14 | method: String, 15 | is_navigation_request: bool, 16 | post_data: Option, 17 | frame: Weak, 18 | headers: HashMap, 19 | redirected_from: Option>, 20 | var: Mutex 21 | } 22 | 23 | #[derive(Debug, Default)] 24 | pub(crate) struct Variable { 25 | redirected_to: Option>, 26 | failure: Option, 27 | timing: Option, 28 | response_end: Option 29 | } 30 | 31 | impl Request { 32 | pub(crate) fn try_new(ctx: &Context, channel: ChannelOwner) -> Result, Error> { 33 | let Initializer { 34 | url, 35 | resource_type, 36 | method, 37 | frame, 38 | is_navigation_request, 39 | post_data, 40 | headers, 41 | redirected_from 42 | } = serde_json::from_value(channel.initializer.clone())?; 43 | let headers: HashMap<_, _> = headers 44 | .into_iter() 45 | .map(Into::<(_, _)>::into) 46 | .map(|(mut k, v)| { 47 | k.make_ascii_lowercase(); 48 | (k, v) 49 | }) 50 | .collect(); 51 | let frame = get_object!(ctx, &frame.guid, Frame)?; 52 | let redirected_from = 53 | match redirected_from.map(|OnlyGuid { guid }| get_object!(ctx, &guid, Request)) { 54 | None => None, 55 | Some(Ok(x)) => Some(x), 56 | Some(Err(e)) => return Err(e) 57 | }; 58 | let var = Mutex::new(Variable::default()); 59 | let arc = Arc::new(Self { 60 | channel, 61 | url, 62 | resource_type, 63 | method, 64 | is_navigation_request, 65 | post_data, 66 | frame, 67 | headers, 68 | redirected_from, 69 | var 70 | }); 71 | if let Some(from) = arc.redirected_from.as_ref().and_then(|w| w.upgrade()) { 72 | let this = Arc::downgrade(&arc); 73 | from.set_redirected_to(this); 74 | } 75 | Ok(arc) 76 | } 77 | 78 | pub(crate) fn url(&self) -> &str { &self.url } 79 | 80 | pub(crate) fn resource_type(&self) -> &str { &self.resource_type } 81 | 82 | pub(crate) fn method(&self) -> &str { &self.method } 83 | 84 | pub(crate) fn is_navigation_request(&self) -> bool { self.is_navigation_request } 85 | 86 | pub(crate) fn frame(&self) -> Weak { self.frame.clone() } 87 | 88 | pub(crate) fn post_data(&self) -> Option> { 89 | base64::decode(self.post_data.as_ref()?).ok() 90 | } 91 | 92 | pub(crate) fn post_data_as_string(&self) -> Option { 93 | let bytes = self.post_data()?; 94 | let s = String::from_utf8(bytes).ok()?; 95 | Some(s) 96 | } 97 | 98 | pub(crate) fn headers(&self) -> &HashMap { &self.headers } 99 | 100 | pub(crate) fn redirected_from(&self) -> Option> { self.redirected_from.clone() } 101 | 102 | pub(crate) async fn response(&self) -> ArcResult>> { 103 | let v = send_message!(self, "response", Map::new()); 104 | let guid = match as_only_guid(&v) { 105 | Some(g) => g, 106 | None => return Ok(None) 107 | }; 108 | let r = get_object!(self.context()?.lock().unwrap(), guid, Response)?; 109 | Ok(Some(r)) 110 | } 111 | } 112 | 113 | impl Request { 114 | pub(crate) fn timing(&self) -> Option { 115 | self.var.lock().unwrap().timing.clone() 116 | } 117 | 118 | pub(crate) fn response_end(&self) -> Option { self.var.lock().unwrap().response_end } 119 | 120 | pub(crate) fn failure(&self) -> Option { self.var.lock().unwrap().failure.clone() } 121 | 122 | pub(crate) fn redirected_to(&self) -> Option> { 123 | self.var.lock().unwrap().redirected_to.clone() 124 | } 125 | 126 | fn set_redirected_to(&self, to: Weak) { 127 | let var = &mut self.var.lock().unwrap(); 128 | var.redirected_to = Some(to); 129 | } 130 | 131 | pub(crate) fn set_response_timing(&self, timing: ResponseTiming) { 132 | let var = &mut self.var.lock().unwrap(); 133 | var.timing = Some(timing); 134 | } 135 | 136 | pub(crate) fn set_response_end(&self, response_end: f64) { 137 | let var = &mut self.var.lock().unwrap(); 138 | var.response_end = Some(response_end); 139 | } 140 | 141 | pub(crate) fn set_failure(&self, failure: Option) { 142 | let var = &mut self.var.lock().unwrap(); 143 | var.failure = failure; 144 | } 145 | } 146 | 147 | // mutable 148 | impl Request { 149 | // redirected_to 150 | // failure 151 | // timing 152 | } 153 | 154 | impl RemoteObject for Request { 155 | fn channel(&self) -> &ChannelOwner { &self.channel } 156 | fn channel_mut(&mut self) -> &mut ChannelOwner { &mut self.channel } 157 | } 158 | 159 | #[derive(Debug, Deserialize)] 160 | #[serde(rename_all = "camelCase")] 161 | struct Initializer { 162 | url: String, 163 | resource_type: String, 164 | method: String, 165 | frame: OnlyGuid, 166 | is_navigation_request: bool, 167 | // base64 168 | post_data: Option, 169 | headers: Vec
, 170 | redirected_from: Option 171 | } 172 | -------------------------------------------------------------------------------- /src/imp/response.rs: -------------------------------------------------------------------------------- 1 | use crate::imp::{ 2 | core::*, 3 | prelude::*, 4 | request::Request, 5 | utils::{Header, ResponseTiming} 6 | }; 7 | 8 | #[derive(Debug)] 9 | pub(crate) struct Response { 10 | channel: ChannelOwner, 11 | url: String, 12 | status: i32, 13 | status_text: String, 14 | request: Weak 15 | } 16 | 17 | impl Response { 18 | pub(crate) fn try_new(ctx: &Context, channel: ChannelOwner) -> Result { 19 | let Initializer { 20 | url, 21 | status, 22 | status_text, 23 | request, 24 | timing 25 | } = serde_json::from_value(channel.initializer.clone())?; 26 | let request = get_object!(ctx, &request.guid, Request)?; 27 | upgrade(&request)?.set_response_timing(timing); 28 | Ok(Self { 29 | channel, 30 | url, 31 | status, 32 | status_text, 33 | request 34 | }) 35 | } 36 | 37 | pub(crate) fn url(&self) -> &str { &self.url } 38 | pub(crate) fn status(&self) -> i32 { self.status } 39 | pub(crate) fn status_text(&self) -> &str { &self.status_text } 40 | 41 | pub(crate) fn ok(&self) -> bool { self.status == 0 || (200..300).contains(&self.status) } 42 | 43 | pub(crate) async fn finished(&self) -> ArcResult> { 44 | let v = send_message!(self, "finished", Map::new()); 45 | let s = maybe_only_str(&v)?; 46 | Ok(s.map(ToOwned::to_owned)) 47 | } 48 | 49 | pub(crate) async fn body(&self) -> ArcResult> { 50 | let v = send_message!(self, "body", Map::new()); 51 | let s = only_str(&v)?; 52 | let bytes = base64::decode(s).map_err(Error::InvalidBase64)?; 53 | Ok(bytes) 54 | } 55 | 56 | pub(crate) async fn text(&self) -> ArcResult { 57 | Ok(String::from_utf8(self.body().await?).map_err(Error::InvalidUtf8)?) 58 | } 59 | 60 | pub(crate) fn request(&self) -> Weak { self.request.clone() } 61 | 62 | pub(crate) async fn headers(&self) -> ArcResult> { 63 | let v = send_message!(self, "body", Map::new()); 64 | let first = first(&v).ok_or(Error::InvalidParams)?; 65 | let mut headers: Vec
= 66 | serde_json::from_value((*first).clone()).map_err(Error::Serde)?; 67 | for h in headers.iter_mut() { 68 | h.name.make_ascii_lowercase(); 69 | } 70 | Ok(headers) 71 | } 72 | } 73 | 74 | impl RemoteObject for Response { 75 | fn channel(&self) -> &ChannelOwner { &self.channel } 76 | fn channel_mut(&mut self) -> &mut ChannelOwner { &mut self.channel } 77 | } 78 | 79 | #[derive(Debug, Deserialize)] 80 | #[serde(rename_all = "camelCase")] 81 | struct Initializer { 82 | url: String, 83 | status: i32, 84 | status_text: String, 85 | request: OnlyGuid, 86 | timing: ResponseTiming 87 | } 88 | -------------------------------------------------------------------------------- /src/imp/route.rs: -------------------------------------------------------------------------------- 1 | use crate::imp::{core::*, prelude::*, request::Request, utils::Header}; 2 | 3 | #[derive(Debug)] 4 | pub(crate) struct Route { 5 | channel: ChannelOwner, 6 | request: Weak 7 | } 8 | 9 | impl Route { 10 | pub(crate) fn try_new(ctx: &Context, channel: ChannelOwner) -> Result { 11 | let Initializer { request } = serde_json::from_value(channel.initializer.clone())?; 12 | let request = get_object!(ctx, &request.guid, Request)?; 13 | Ok(Self { channel, request }) 14 | } 15 | 16 | pub(crate) fn request(&self) -> Weak { self.request.clone() } 17 | 18 | pub(crate) async fn abort(&self, err_code: Option<&str>) -> Result<(), Arc> { 19 | let mut args = HashMap::new(); 20 | if let Some(x) = err_code { 21 | args.insert("errCode", x); 22 | } 23 | let _ = send_message!(self, "abort", args); 24 | Ok(()) 25 | } 26 | 27 | pub(crate) async fn fulfill(&self, args: FulfillArgs<'_, '_>) -> ArcResult<()> { 28 | let _ = send_message!(self, "fulfill", args); 29 | Ok(()) 30 | } 31 | 32 | pub(crate) async fn r#continue(&self, args: ContinueArgs<'_, '_, '_>) -> ArcResult<()> { 33 | let _ = send_message!(self, "continue", args); 34 | Ok(()) 35 | } 36 | } 37 | 38 | impl RemoteObject for Route { 39 | fn channel(&self) -> &ChannelOwner { &self.channel } 40 | fn channel_mut(&mut self) -> &mut ChannelOwner { &mut self.channel } 41 | } 42 | 43 | #[derive(Debug, Deserialize)] 44 | #[serde(rename_all = "camelCase")] 45 | struct Initializer { 46 | request: OnlyGuid 47 | } 48 | 49 | #[skip_serializing_none] 50 | #[derive(Serialize)] 51 | #[serde(rename_all = "camelCase")] 52 | pub(crate) struct FulfillArgs<'a, 'b> { 53 | body: &'a str, 54 | is_base64: bool, 55 | pub(crate) status: Option, 56 | pub(crate) headers: Option>, 57 | pub(crate) content_type: Option<&'b str> 58 | } 59 | 60 | impl<'a, 'b> FulfillArgs<'a, 'b> { 61 | pub(crate) fn new(body: &'a str, is_base64: bool) -> Self { 62 | Self { 63 | body, 64 | is_base64, 65 | status: None, 66 | headers: None, 67 | content_type: None 68 | } 69 | } 70 | } 71 | 72 | #[skip_serializing_none] 73 | #[derive(Serialize, Default)] 74 | #[serde(rename_all = "camelCase")] 75 | pub(crate) struct ContinueArgs<'a, 'b, 'c> { 76 | pub(crate) url: Option<&'a str>, 77 | pub(crate) method: Option<&'b str>, 78 | pub(crate) headers: Option>, 79 | pub(crate) post_data: Option<&'c str> 80 | } 81 | -------------------------------------------------------------------------------- /src/imp/selectors.rs: -------------------------------------------------------------------------------- 1 | use crate::imp::{core::*, prelude::*}; 2 | 3 | #[derive(Debug)] 4 | pub(crate) struct Selectors { 5 | channel: ChannelOwner 6 | } 7 | 8 | impl Selectors { 9 | pub(crate) fn new(channel: ChannelOwner) -> Self { Self { channel } } 10 | 11 | pub(crate) async fn register( 12 | &self, 13 | name: &str, 14 | script: &str, 15 | content_script: bool 16 | ) -> Result<(), Arc> { 17 | let args = RegisterArgs { 18 | name, 19 | source: script, 20 | content_script 21 | }; 22 | let _ = send_message!(self, "register", args); 23 | Ok(()) 24 | } 25 | } 26 | 27 | impl RemoteObject for Selectors { 28 | fn channel(&self) -> &ChannelOwner { &self.channel } 29 | fn channel_mut(&mut self) -> &mut ChannelOwner { &mut self.channel } 30 | } 31 | 32 | #[derive(Serialize)] 33 | #[serde(rename_all = "camelCase")] 34 | struct RegisterArgs<'a, 'b> { 35 | name: &'a str, 36 | source: &'b str, 37 | #[serde(skip_serializing_if = "std::ops::Not::not")] 38 | content_script: bool 39 | } 40 | 41 | #[cfg(test)] 42 | mod tests { 43 | use super::*; 44 | use crate::imp::playwright::Playwright; 45 | 46 | crate::runtime_test!(register, { 47 | let driver = Driver::install().unwrap(); 48 | let conn = Connection::run(&driver.executable()).unwrap(); 49 | let p = Playwright::wait_initial_object(&conn).await.unwrap(); 50 | let p = p.upgrade().unwrap(); 51 | let s: Arc = p.selectors().upgrade().unwrap(); 52 | let fut = s.register("foo", "()", false); 53 | log::trace!("fut"); 54 | let res = fut.await; 55 | dbg!(&res); 56 | assert!(res.is_ok()); 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /src/imp/stream.rs: -------------------------------------------------------------------------------- 1 | use crate::imp::{core::*, prelude::*}; 2 | use std::{ 3 | fs::File, 4 | io::{BufWriter, Write} 5 | }; 6 | 7 | #[derive(Debug)] 8 | pub(crate) struct Stream { 9 | channel: ChannelOwner 10 | } 11 | 12 | impl Stream { 13 | pub(crate) fn new(channel: ChannelOwner) -> Self { Self { channel } } 14 | 15 | pub(crate) async fn save_as>(&self, path: P) -> ArcResult<()> { 16 | let file = File::create(path).map_err(Error::from)?; 17 | let mut writer = BufWriter::new(file); 18 | loop { 19 | let v = send_message!(self, "read", Map::new()); 20 | let b64 = only_str(&v)?; 21 | if b64.is_empty() { 22 | break; 23 | } else { 24 | let bytes = base64::decode(b64).map_err(Error::InvalidBase64)?; 25 | writer.write(&bytes).map_err(Error::from)?; 26 | } 27 | } 28 | Ok(()) 29 | } 30 | 31 | // with open(path, mode="wb") as file: 32 | // while True: 33 | // binary = await self._channel.send("read") 34 | // if not binary: 35 | // break 36 | // file.write(base64.b64decode(binary)) 37 | } 38 | 39 | impl RemoteObject for Stream { 40 | fn channel(&self) -> &ChannelOwner { &self.channel } 41 | fn channel_mut(&mut self) -> &mut ChannelOwner { &mut self.channel } 42 | } 43 | -------------------------------------------------------------------------------- /src/imp/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::imp::prelude::*; 2 | 3 | #[derive(Debug, Deserialize, Clone, Serialize, PartialEq, Eq)] 4 | pub struct Viewport { 5 | pub width: i32, 6 | pub height: i32 7 | } 8 | 9 | #[skip_serializing_none] 10 | #[derive(Debug, Deserialize, Clone, Serialize)] 11 | pub struct ProxySettings { 12 | /// Proxy to be used for all requests. HTTP and SOCKS proxies are supported, for example `http://myproxy.com:3128` or\n`socks5://myproxy.com:3128`. Short form `myproxy.com:3128` is considered an HTTP proxy. 13 | pub server: String, 14 | /// Optional coma-separated domains to bypass proxy, for example `\".com, chromium.org, .domain.com\"`. 15 | pub bypass: Option, 16 | pub username: Option, 17 | pub password: Option 18 | } 19 | 20 | #[skip_serializing_none] 21 | #[derive(Debug, Deserialize, Clone, Serialize)] 22 | pub struct Geolocation { 23 | /// Latitude between -90 and 90. 24 | pub latitude: f64, 25 | /// Longitude between -180 and 180. 26 | pub longitude: f64, 27 | /// Non-negative accuracy value. Defaults to `0`. 28 | pub accuracy: Option 29 | } 30 | 31 | #[derive(Debug, Deserialize, Clone, Serialize)] 32 | pub struct HttpCredentials { 33 | pub username: String, 34 | pub password: String 35 | } 36 | 37 | #[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone, Copy)] 38 | #[serde(rename_all = "snake_case")] 39 | pub enum ColorScheme { 40 | Dark, 41 | Light, 42 | NoPreference 43 | } 44 | 45 | #[skip_serializing_none] 46 | #[derive(Debug, Deserialize, Serialize)] 47 | pub struct StorageState { 48 | pub cookies: Option>, 49 | pub origins: Option> 50 | } 51 | 52 | #[skip_serializing_none] 53 | #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] 54 | #[serde(rename_all = "camelCase")] 55 | pub struct Cookie { 56 | pub name: String, 57 | pub value: String, 58 | pub url: Option, 59 | pub domain: Option, 60 | pub path: Option, 61 | /// Optional Unix time in seconds. 62 | pub expires: Option, 63 | pub http_only: Option, 64 | pub secure: Option, 65 | pub same_site: Option 66 | } 67 | 68 | impl Cookie { 69 | pub fn with_url>(name: S, value: S, url: S) -> Self { 70 | Self { 71 | name: name.into(), 72 | value: value.into(), 73 | url: Some(url.into()), 74 | domain: None, 75 | path: None, 76 | expires: None, 77 | http_only: None, 78 | secure: None, 79 | same_site: None 80 | } 81 | } 82 | 83 | pub fn with_domain_path>(name: S, value: S, domain: S, path: S) -> Self { 84 | Self { 85 | name: name.into(), 86 | value: value.into(), 87 | url: None, 88 | domain: Some(domain.into()), 89 | path: Some(path.into()), 90 | expires: None, 91 | http_only: None, 92 | secure: None, 93 | same_site: None 94 | } 95 | } 96 | } 97 | 98 | #[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone, Copy)] 99 | pub enum SameSite { 100 | Lax, 101 | None, 102 | Strict 103 | } 104 | 105 | #[derive(Debug, Deserialize, Serialize, PartialEq, Clone)] 106 | #[serde(rename_all = "camelCase")] 107 | pub struct OriginState { 108 | pub origin: String, 109 | pub local_storage: Vec 110 | } 111 | 112 | #[derive(Debug, Deserialize, Serialize, PartialEq, Clone)] 113 | #[serde(rename_all = "camelCase")] 114 | pub struct LocalStorageEntry { 115 | pub name: String, 116 | pub value: String 117 | } 118 | 119 | #[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone, Copy, Hash)] 120 | #[serde(rename_all = "lowercase")] 121 | pub enum DocumentLoadState { 122 | DomContentLoaded, 123 | Load, 124 | NetworkIdle 125 | } 126 | 127 | #[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone, Copy)] 128 | pub enum KeyboardModifier { 129 | Alt, 130 | Control, 131 | Meta, 132 | Shift 133 | } 134 | 135 | #[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone, Copy)] 136 | #[serde(rename_all = "lowercase")] 137 | pub enum MouseButton { 138 | Left, 139 | Middle, 140 | Right 141 | } 142 | 143 | #[derive(Debug, Deserialize, Serialize, PartialEq, Clone, Copy)] 144 | pub struct Position { 145 | pub x: f64, 146 | pub y: f64 147 | } 148 | 149 | impl From<(f64, f64)> for Position { 150 | fn from((x, y): (f64, f64)) -> Self { Self { x, y } } 151 | } 152 | 153 | #[derive(Debug, Deserialize, Serialize, PartialEq, Clone, Copy)] 154 | pub struct FloatRect { 155 | /// the x coordinate of the element in pixels. 156 | pub x: f64, 157 | pub y: f64, 158 | /// the width of the element in pixels. 159 | pub width: f64, 160 | pub height: f64 161 | } 162 | 163 | #[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone, Copy)] 164 | #[serde(rename_all = "lowercase")] 165 | pub enum ScreenshotType { 166 | Jpeg, 167 | Png 168 | } 169 | 170 | #[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone, Copy)] 171 | #[serde(rename_all = "lowercase")] 172 | pub enum ElementState { 173 | Disabled, 174 | Editable, 175 | Enabled, 176 | Hidden, 177 | Stable, 178 | Visible 179 | } 180 | 181 | #[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone, Copy)] 182 | #[serde(rename_all = "lowercase")] 183 | pub enum WaitForSelectorState { 184 | Attached, 185 | Detached, 186 | Visible, 187 | Hidden 188 | } 189 | 190 | #[derive(Debug, Deserialize, Serialize, PartialEq, Clone)] 191 | pub struct Header { 192 | pub name: String, 193 | pub value: String 194 | } 195 | 196 | impl From
for (String, String) { 197 | fn from(Header { name, value }: Header) -> Self { (name, value) } 198 | } 199 | 200 | impl From<(String, String)> for Header { 201 | fn from((k, v): (String, String)) -> Self { Self { name: k, value: v } } 202 | } 203 | 204 | #[derive(Debug, Serialize, PartialEq, Clone)] 205 | #[serde(untagged)] 206 | pub enum Length<'a> { 207 | Value(f64), 208 | WithUnit(&'a str) 209 | } 210 | 211 | impl<'a> From for Length<'a> { 212 | fn from(x: f64) -> Self { Self::Value(x) } 213 | } 214 | 215 | impl<'a> From<&'a str> for Length<'a> { 216 | fn from(x: &'a str) -> Self { Self::WithUnit(x) } 217 | } 218 | 219 | #[skip_serializing_none] 220 | #[derive(Debug, Serialize, PartialEq, Clone)] 221 | pub struct PdfMargins<'a, 'b, 'c, 'd> { 222 | pub top: Option>, 223 | pub right: Option>, 224 | pub bottom: Option>, 225 | pub left: Option> 226 | } 227 | 228 | #[derive(Debug, Serialize, PartialEq)] 229 | pub struct File { 230 | pub name: String, 231 | pub mime: String, 232 | pub buffer: String 233 | } 234 | 235 | impl File { 236 | pub fn new(name: String, mime: String, body: &[u8]) -> Self { 237 | let buffer = base64::encode(body); 238 | Self { name, mime, buffer } 239 | } 240 | } 241 | /// Browser distribution channel. 242 | // TODO: kebab case 243 | #[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone, Copy)] 244 | #[serde(rename_all = "kebab-case")] 245 | pub enum BrowserChannel { 246 | Chrome, 247 | ChromeBeta, 248 | ChromeDev, 249 | ChromeCanary, 250 | Msedge, 251 | MsedgeBeta, 252 | MsedgeDev, 253 | MsedgeCanary, 254 | FirefoxStable 255 | } 256 | 257 | #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] 258 | #[serde(rename_all = "camelCase")] 259 | pub struct SourceLocation { 260 | pub url: String, 261 | /// 0-based line number in the resource. 262 | pub line_number: i32, 263 | /// 0-based column number in the resource. 264 | pub column_number: i32 265 | } 266 | 267 | #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] 268 | #[serde(rename_all = "camelCase")] 269 | pub struct ResponseTiming { 270 | /// Request start time in milliseconds elapsed since January 1, 1970 00:00:00 UTC 271 | pub start_time: f64, 272 | /// Time immediately before the browser starts the domain name lookup for the resource. The value is given in milliseconds\nrelative to `startTime`, -1 if not available. 273 | pub domain_lookup_start: f64, 274 | /// Time immediately after the browser starts the domain name lookup for the resource. The value is given in milliseconds\nrelative to `startTime`, -1 if not available. 275 | pub domain_lookup_end: f64, 276 | /// Time immediately before the user agent starts establishing the connection to the server to retrieve the resource. The\nvalue is given in milliseconds relative to `startTime`, -1 if not available. 277 | pub connect_start: f64, 278 | /// Time immediately before the browser starts the handshake process to secure the current connection. The value is given in\nmilliseconds relative to `startTime`, -1 if not available. 279 | pub secure_connection_start: f64, 280 | /// Time immediately before the user agent starts establishing the connection to the server to retrieve the resource. The\nvalue is given in milliseconds relative to `startTime`, -1 if not available. 281 | pub connect_end: f64, 282 | /// Time immediately before the browser starts requesting the resource from the server, cache, or local resource. The value\nis given in milliseconds relative to `startTime`, -1 if not available. 283 | pub request_start: f64, 284 | /// Time immediately after the browser starts requesting the resource from the server, cache, or local resource. The value\nis given in milliseconds relative to `startTime`, -1 if not available. 285 | pub response_start: f64 286 | } 287 | -------------------------------------------------------------------------------- /src/imp/video.rs: -------------------------------------------------------------------------------- 1 | use crate::imp::{artifact::Artifact, core::*, prelude::*}; 2 | 3 | #[derive(Debug, Clone)] 4 | pub(crate) struct Video { 5 | artifact: Weak 6 | } 7 | 8 | impl Video { 9 | pub(crate) fn new(artifact: Weak) -> Self { Self { artifact } } 10 | 11 | pub(crate) fn path(&self) -> Result { 12 | Ok(upgrade(&self.artifact)?.absolute_path.as_str().into()) 13 | } 14 | 15 | pub(crate) async fn save_as>(&self, path: P) -> ArcResult<()> { 16 | upgrade(&self.artifact)?.save_as(path).await 17 | } 18 | 19 | pub(crate) async fn delete(&self) -> ArcResult<()> { upgrade(&self.artifact)?.delete().await } 20 | } 21 | -------------------------------------------------------------------------------- /src/imp/websocket.rs: -------------------------------------------------------------------------------- 1 | use crate::imp::{core::*, prelude::*}; 2 | 3 | #[derive(Debug)] 4 | pub(crate) struct WebSocket { 5 | channel: ChannelOwner, 6 | url: String, 7 | var: Mutex, 8 | tx: Mutex>> 9 | } 10 | 11 | #[derive(Debug, Default)] 12 | struct Variable { 13 | is_closed: bool 14 | } 15 | 16 | impl WebSocket { 17 | pub(crate) fn try_new(channel: ChannelOwner) -> Result { 18 | let Initializer { url } = serde_json::from_value(channel.initializer.clone())?; 19 | Ok(Self { 20 | channel, 21 | url, 22 | var: Mutex::default(), 23 | tx: Mutex::default() 24 | }) 25 | } 26 | 27 | pub(crate) fn url(&self) -> &str { &self.url } 28 | } 29 | 30 | impl WebSocket { 31 | pub(crate) fn is_closed(&self) -> bool { self.var.lock().unwrap().is_closed } 32 | 33 | fn on_frame_sent(&self, params: Map) -> Result<(), Error> { 34 | let buffer = parse_frame(params)?; 35 | self.emit_event(Evt::FrameSent(buffer)); 36 | Ok(()) 37 | } 38 | 39 | fn on_frame_received(&self, params: Map) -> Result<(), Error> { 40 | let buffer = parse_frame(params)?; 41 | self.emit_event(Evt::FrameReceived(buffer)); 42 | Ok(()) 43 | } 44 | } 45 | 46 | fn parse_frame(params: Map) -> Result { 47 | #[derive(Deserialize)] 48 | struct De { 49 | opcode: i32, 50 | data: String 51 | } 52 | let De { opcode, data } = serde_json::from_value(params.into())?; 53 | let buffer = if opcode == 2 { 54 | let bytes = base64::decode(data).map_err(Error::InvalidBase64)?; 55 | Buffer::Bytes(bytes) 56 | } else { 57 | Buffer::String(data) 58 | }; 59 | Ok(buffer) 60 | } 61 | 62 | impl RemoteObject for WebSocket { 63 | fn channel(&self) -> &ChannelOwner { &self.channel } 64 | fn channel_mut(&mut self) -> &mut ChannelOwner { &mut self.channel } 65 | 66 | fn handle_event( 67 | &self, 68 | _ctx: &Context, 69 | method: Str, 70 | params: Map 71 | ) -> Result<(), Error> { 72 | match method.as_str() { 73 | "framesent" => self.on_frame_sent(params)?, 74 | "framereceived" => self.on_frame_received(params)?, 75 | "error" => { 76 | let error: Value = params.get("error").cloned().unwrap_or_default(); 77 | self.emit_event(Evt::Error(error)); 78 | } 79 | "close" => { 80 | self.var.lock().unwrap().is_closed = true; 81 | self.emit_event(Evt::Close); 82 | } 83 | _ => {} 84 | } 85 | Ok(()) 86 | } 87 | } 88 | 89 | #[derive(Debug, Clone)] 90 | pub(crate) enum Evt { 91 | FrameSent(Buffer), 92 | FrameReceived(Buffer), 93 | Error(Value), 94 | Close 95 | } 96 | 97 | #[derive(Debug, Clone)] 98 | pub enum Buffer { 99 | Bytes(Vec), 100 | String(String) 101 | } 102 | 103 | impl EventEmitter for WebSocket { 104 | type Event = Evt; 105 | 106 | fn tx(&self) -> Option> { self.tx.lock().unwrap().clone() } 107 | 108 | fn set_tx(&self, tx: broadcast::Sender) { *self.tx.lock().unwrap() = Some(tx); } 109 | } 110 | 111 | #[derive(Debug, Clone, Copy, PartialEq)] 112 | pub enum EventType { 113 | FrameSent, 114 | FrameReceived, 115 | Error, 116 | Close 117 | } 118 | 119 | impl IsEvent for Evt { 120 | type EventType = EventType; 121 | 122 | fn event_type(&self) -> Self::EventType { 123 | match self { 124 | Evt::FrameSent(_) => EventType::FrameSent, 125 | Evt::FrameReceived(_) => EventType::FrameReceived, 126 | Evt::Error(_) => EventType::Error, 127 | Evt::Close => EventType::Close 128 | } 129 | } 130 | } 131 | 132 | #[derive(Debug, Deserialize)] 133 | #[serde(rename_all = "camelCase")] 134 | struct Initializer { 135 | url: String 136 | } 137 | -------------------------------------------------------------------------------- /src/imp/worker.rs: -------------------------------------------------------------------------------- 1 | use crate::imp::{ 2 | browser_context::BrowserContext, core::*, js_handle::JsHandle, page::Page, prelude::* 3 | }; 4 | 5 | #[derive(Debug)] 6 | pub(crate) struct Worker { 7 | channel: ChannelOwner, 8 | url: String, 9 | var: Mutex, 10 | tx: Mutex>> 11 | } 12 | 13 | #[derive(Debug, Default)] 14 | pub(crate) struct Variable { 15 | page: Option>, 16 | browser_context: Option> 17 | } 18 | 19 | impl Worker { 20 | pub(crate) fn try_new(channel: ChannelOwner) -> Result { 21 | let Initializer { url } = serde_json::from_value(channel.initializer.clone())?; 22 | Ok(Self { 23 | channel, 24 | url, 25 | var: Mutex::default(), 26 | tx: Mutex::default() 27 | }) 28 | } 29 | 30 | pub(crate) fn url(&self) -> &str { &self.url } 31 | 32 | pub(crate) async fn eval(&self, expression: &str) -> ArcResult 33 | where 34 | U: DeserializeOwned 35 | { 36 | self.evaluate::<(), U>(expression, None).await 37 | } 38 | 39 | pub(crate) async fn evaluate(&self, expression: &str, arg: Option) -> ArcResult 40 | where 41 | T: Serialize, 42 | U: DeserializeOwned 43 | { 44 | #[derive(Serialize)] 45 | #[serde(rename_all = "camelCase")] 46 | struct Args<'a> { 47 | expression: &'a str, 48 | arg: Value 49 | } 50 | let arg = ser::to_value(&arg).map_err(Error::SerializationPwJson)?; 51 | let args = Args { expression, arg }; 52 | let v = send_message!(self, "evaluateExpression", args); 53 | let first = first(&v).ok_or(Error::ObjectNotFound)?; 54 | Ok(de::from_value(first).map_err(Error::DeserializationPwJson)?) 55 | } 56 | 57 | pub(crate) async fn eval_handle(&self, expression: &str) -> ArcResult> { 58 | self.evaluate_handle::<()>(expression, None).await 59 | } 60 | 61 | pub(crate) async fn evaluate_handle( 62 | &self, 63 | expression: &str, 64 | arg: Option 65 | ) -> ArcResult> 66 | where 67 | T: Serialize 68 | { 69 | #[derive(Serialize)] 70 | #[serde(rename_all = "camelCase")] 71 | struct Args<'a> { 72 | expression: &'a str, 73 | arg: Value 74 | } 75 | let arg = ser::to_value(&arg).map_err(Error::SerializationPwJson)?; 76 | let args = Args { expression, arg }; 77 | let v = send_message!(self, "evaluateExpressionHandle", args); 78 | let guid = only_guid(&v)?; 79 | let h = get_object!(self.context()?.lock().unwrap(), guid, JsHandle)?; 80 | Ok(h) 81 | } 82 | } 83 | 84 | impl Worker { 85 | pub(crate) fn set_page(&self, page: Weak) { self.var.lock().unwrap().page = Some(page); } 86 | 87 | // pub(crate) fn set_browser_context(&self, browser_context: Weak) { 88 | // self.var.lock().unwrap().browser_context = Some(browser_context); 89 | //} 90 | 91 | fn on_close(&self, ctx: &Context) -> Result<(), Error> { 92 | let this = get_object!(ctx, self.guid(), Worker)?; 93 | let var = self.var.lock().unwrap(); 94 | if let Some(page) = var.page.as_ref().and_then(Weak::upgrade) { 95 | page.remove_worker(&this); 96 | } 97 | // var.context.remove_service_worker(&this) 98 | self.emit_event(Evt::Close); 99 | Ok(()) 100 | } 101 | } 102 | 103 | impl RemoteObject for Worker { 104 | fn channel(&self) -> &ChannelOwner { &self.channel } 105 | fn channel_mut(&mut self) -> &mut ChannelOwner { &mut self.channel } 106 | 107 | fn handle_event( 108 | &self, 109 | ctx: &Context, 110 | method: Str, 111 | _params: Map 112 | ) -> Result<(), Error> { 113 | if method.as_str() == "close" { 114 | self.on_close(ctx)?; 115 | } 116 | Ok(()) 117 | } 118 | } 119 | 120 | #[derive(Debug, Deserialize)] 121 | #[serde(rename_all = "camelCase")] 122 | struct Initializer { 123 | url: String 124 | } 125 | 126 | #[derive(Debug, Clone)] 127 | pub(crate) enum Evt { 128 | Close 129 | } 130 | 131 | #[derive(Debug, Clone, Copy, PartialEq)] 132 | pub enum EventType { 133 | Close 134 | } 135 | 136 | impl IsEvent for Evt { 137 | type EventType = EventType; 138 | 139 | fn event_type(&self) -> Self::EventType { 140 | match self { 141 | Self::Close => EventType::Close 142 | } 143 | } 144 | } 145 | 146 | impl EventEmitter for Worker { 147 | type Event = Evt; 148 | fn tx(&self) -> Option> { self.tx.lock().unwrap().clone() } 149 | fn set_tx(&self, tx: broadcast::Sender) { *self.tx.lock().unwrap() = Some(tx); } 150 | } 151 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate serde; 3 | #[macro_use] 4 | extern crate serde_with; 5 | 6 | pub mod api; 7 | mod imp; 8 | 9 | pub use crate::imp::core::{Driver, Error}; 10 | pub use api::playwright::Playwright; 11 | 12 | #[doc(hidden)] 13 | #[macro_export] 14 | macro_rules! runtime_test { 15 | ($name:tt, $main:stmt) => { 16 | #[cfg(feature = "rt-tokio")] 17 | #[test] 18 | fn $name() { 19 | env_logger::builder().is_test(true).try_init().ok(); 20 | tokio::runtime::Builder::new_current_thread() 21 | .enable_all() 22 | .build() 23 | .unwrap() 24 | .block_on(async { $main }); 25 | } 26 | 27 | #[cfg(feature = "rt-actix")] 28 | #[test] 29 | fn $name() { 30 | env_logger::builder().is_test(true).try_init().ok(); 31 | actix_rt::System::new().block_on(async { $main }); 32 | } 33 | 34 | #[cfg(feature = "rt-async-std")] 35 | #[test] 36 | fn $name() { 37 | env_logger::builder().is_test(true).try_init().ok(); 38 | async_std::task::block_on(async { $main }); 39 | } 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use playwright::*; 2 | use std::{env, io, process}; 3 | 4 | fn main() { 5 | let envs = env::vars_os(); 6 | let args = { 7 | let mut a = env::args_os(); 8 | a.next(); 9 | a 10 | }; 11 | let status = run(args, envs).unwrap(); 12 | if let Some(status) = status.code() { 13 | std::process::exit(status) 14 | } 15 | } 16 | 17 | fn run(args: env::ArgsOs, envs: env::VarsOs) -> io::Result { 18 | let driver = Driver::install().unwrap(); 19 | process::Command::new(driver.executable()) 20 | .args(args) 21 | .envs(envs) 22 | .status() 23 | } 24 | -------------------------------------------------------------------------------- /tests/browser/mod.rs: -------------------------------------------------------------------------------- 1 | use super::Which; 2 | use playwright::api::{Browser, BrowserType}; 3 | 4 | pub async fn all(t: &BrowserType, which: Which) -> Browser { 5 | launch_close_browser(t).await; 6 | let b = launch(t).await; 7 | assert!(b.exists()); 8 | version_should_work(&b, which); 9 | contexts_should_work(&b).await; 10 | b 11 | } 12 | 13 | async fn launch(t: &BrowserType) -> Browser { 14 | t.launcher() 15 | .headless(false) 16 | .clear_headless() 17 | .launch() 18 | .await 19 | .unwrap() 20 | } 21 | 22 | async fn launch_close_browser(t: &BrowserType) { 23 | let (b1, b2) = tokio::join!(launch(t), launch(t)); 24 | assert_ne!(b1, b2); 25 | b1.close().await.unwrap(); 26 | b2.close().await.unwrap(); 27 | assert!(!b1.exists()); 28 | } 29 | 30 | // 'version should work' 31 | fn version_should_work(b: &Browser, which: Which) { 32 | let version = b.version().unwrap(); 33 | match which { 34 | Which::Chromium => { 35 | assert_eq!(version.split('.').count(), 4); 36 | for x in version.split('.') { 37 | x.parse::().unwrap(); 38 | } 39 | } 40 | _ => { 41 | dbg!(&version); 42 | let mut it = version.split('.'); 43 | it.next().unwrap().parse::().unwrap(); 44 | let s = it.next().unwrap(); 45 | let c: char = s.chars().next().unwrap(); 46 | match c { 47 | '0'..='9' => {} 48 | _ => unreachable!() 49 | } 50 | } 51 | } 52 | } 53 | 54 | async fn contexts_should_work(b: &Browser) { 55 | let len = b.contexts().unwrap().len(); 56 | let context = b.context_builder().build().await.unwrap(); 57 | assert_eq!(b.contexts().unwrap().len(), len + 1); 58 | context.close().await.unwrap(); 59 | context.close().await.unwrap(); 60 | assert_eq!(b.contexts().unwrap().len(), len); 61 | } 62 | -------------------------------------------------------------------------------- /tests/browser_context/mod.rs: -------------------------------------------------------------------------------- 1 | use super::Which; 2 | use playwright::api::{ 3 | browser::RecordVideo, Browser, BrowserContext, BrowserType, Cookie, LocalStorageEntry, 4 | OriginState, StorageState 5 | }; 6 | 7 | pub async fn all( 8 | browser: &Browser, 9 | persistent: &BrowserContext, 10 | port: u16, 11 | _which: Which 12 | ) -> BrowserContext { 13 | let c = launch(browser).await; 14 | assert_ne!(persistent, &c); 15 | assert!(c.browser().unwrap().is_some()); 16 | storage_state(&c, port).await; 17 | set_offline_should_work(browser, port).await; 18 | set_timeout(&c).await; 19 | cookies_should_work(&c).await; 20 | add_init_script_should_work(&c).await; 21 | pages_should_work(&c).await; 22 | c 23 | } 24 | 25 | pub async fn persistent(t: &BrowserType, _port: u16, which: Which) -> BrowserContext { 26 | let c = launch_persistent_context(t).await; 27 | if Which::Firefox != which { 28 | // XXX: launch with permissions not work on firefox 29 | check_launched_permissions(&c).await; 30 | } 31 | c 32 | } 33 | 34 | async fn launch(b: &Browser) -> BrowserContext { 35 | let c = b 36 | .context_builder() 37 | .user_agent("asdf") 38 | .permissions(&["geolocation".into()]) 39 | .accept_downloads(true) 40 | .has_touch(true) 41 | .record_video(RecordVideo { 42 | dir: &super::temp_dir().join("video"), 43 | size: None 44 | }) 45 | .storage_state(StorageState { 46 | cookies: Some(vec![Cookie::with_url( 47 | "name1", 48 | "value1", 49 | "https://example.com" 50 | )]), 51 | origins: Some(vec![OriginState { 52 | origin: "https://example.com".into(), 53 | local_storage: vec![LocalStorageEntry { 54 | name: "name1".into(), 55 | value: "value1".into() 56 | }] 57 | }]) 58 | }) 59 | .build() 60 | .await 61 | .unwrap(); 62 | c.set_extra_http_headers(vec![("foo".into(), "bar".into())]) 63 | .await 64 | .unwrap(); 65 | c 66 | } 67 | 68 | async fn launch_persistent_context(t: &BrowserType) -> BrowserContext { 69 | t.persistent_context_launcher("./target".as_ref()) 70 | .user_agent("asdf") 71 | .permissions(&["geolocation".into()]) 72 | .launch() 73 | .await 74 | .unwrap() 75 | } 76 | 77 | async fn pages_should_work(c: &BrowserContext) { 78 | let len = c.pages().unwrap().len(); 79 | let page = c.new_page().await.unwrap(); 80 | assert_eq!(c.pages().unwrap().len(), len + 1); 81 | page.close(None).await.unwrap(); 82 | page.close(None).await.unwrap(); 83 | assert_eq!(c.pages().unwrap().len(), len); 84 | } 85 | 86 | async fn set_timeout(c: &BrowserContext) { 87 | c.set_default_navigation_timeout(10000).await.unwrap(); 88 | c.set_default_timeout(10000).await.unwrap(); 89 | } 90 | 91 | async fn cookies_should_work(c: &BrowserContext) { 92 | ensure_cookies_are_cleared(c).await; 93 | let cookie = Cookie { 94 | name: "foo".into(), 95 | value: "bar".into(), 96 | url: Some("https://example.com/".into()), 97 | domain: None, 98 | path: None, 99 | expires: None, 100 | http_only: None, 101 | secure: None, 102 | same_site: None 103 | }; 104 | c.add_cookies(&[cookie.clone()]).await.unwrap(); 105 | let cookies = c.cookies(&[]).await.unwrap(); 106 | let first = cookies.into_iter().next().unwrap(); 107 | assert_eq!(&first.name, "foo"); 108 | assert_eq!(&first.value, "bar"); 109 | ensure_cookies_are_cleared(c).await; 110 | } 111 | 112 | async fn ensure_cookies_are_cleared(c: &BrowserContext) { 113 | c.clear_cookies().await.unwrap(); 114 | let cs = c.cookies(&[]).await.unwrap(); 115 | assert_eq!(0, cs.len()); 116 | } 117 | 118 | async fn check_launched_permissions(c: &BrowserContext) { 119 | assert_eq!(get_permission(c, "geolocation").await, "granted"); 120 | c.clear_permissions().await.unwrap(); 121 | assert_eq!(get_permission(c, "geolocation").await, "prompt"); 122 | } 123 | 124 | async fn get_permission(c: &BrowserContext, name: &str) -> String { 125 | let p = c.new_page().await.unwrap(); 126 | let res = p 127 | .evaluate( 128 | "name => navigator.permissions.query({name}).then(result => result.state)", 129 | name 130 | ) 131 | .await 132 | .unwrap(); 133 | p.close(None).await.unwrap(); 134 | res 135 | } 136 | 137 | async fn add_init_script_should_work(c: &BrowserContext) { 138 | c.add_init_script("HOGE = 2").await.unwrap(); 139 | let p = c.new_page().await.unwrap(); 140 | let x: i32 = p.eval("() => HOGE").await.unwrap(); 141 | assert_eq!(x, 2); 142 | p.close(None).await.unwrap(); 143 | } 144 | 145 | async fn set_offline_should_work(browser: &Browser, port: u16) { 146 | let c = browser 147 | .context_builder() 148 | .offline(true) 149 | .build() 150 | .await 151 | .unwrap(); 152 | let page = c.new_page().await.unwrap(); 153 | let url = super::url_static(port, "/empty.html"); 154 | let err = page.goto_builder(&url).goto().await; 155 | assert!(err.is_err()); 156 | c.set_offline(false).await.unwrap(); 157 | let response = page.goto_builder(&url).goto().await.unwrap(); 158 | assert_eq!(response.unwrap().status().unwrap(), 200); 159 | c.close().await.unwrap(); 160 | } 161 | 162 | async fn storage_state(c: &BrowserContext, port: u16) { 163 | let page = c.new_page().await.unwrap(); 164 | let url = super::url_static(port, "/empty.html"); 165 | page.goto_builder(&url).goto().await.unwrap(); 166 | page.eval::<()>("() => { localStorage['name2'] = 'value2'; }") 167 | .await 168 | .unwrap(); 169 | let storage = c.storage_state().await.unwrap(); 170 | assert!(storage 171 | .cookies 172 | .unwrap() 173 | .into_iter() 174 | .any(|c| c.name == "name1" && c.value == "value1")); 175 | assert_eq!( 176 | storage.origins.unwrap(), 177 | &[ 178 | OriginState { 179 | origin: "https://example.com".into(), 180 | local_storage: vec![LocalStorageEntry { 181 | name: "name1".into(), 182 | value: "value1".into() 183 | }] 184 | }, 185 | OriginState { 186 | origin: super::origin(port), 187 | local_storage: vec![LocalStorageEntry { 188 | name: "name2".into(), 189 | value: "value2".into() 190 | }] 191 | } 192 | ] 193 | ); 194 | } 195 | -------------------------------------------------------------------------------- /tests/browser_type/mod.rs: -------------------------------------------------------------------------------- 1 | use super::Which; 2 | use playwright::api::{BrowserType, Playwright}; 3 | 4 | pub async fn all(playwright: &Playwright, which: Which) -> BrowserType { 5 | let t = match which { 6 | Which::Webkit => playwright.webkit(), 7 | Which::Firefox => playwright.firefox(), 8 | Which::Chromium => playwright.chromium() 9 | }; 10 | name_should_work(&t, which); 11 | executable_should_exist(&t); 12 | should_handle_timeout(&t).await; 13 | should_fire_close(&t).await; 14 | t 15 | } 16 | 17 | fn name_should_work(t: &BrowserType, which: Which) { 18 | let name = t.name().unwrap(); 19 | match which { 20 | Which::Webkit => assert_eq!(name, "webkit"), 21 | Which::Firefox => assert_eq!(name, "firefox"), 22 | Which::Chromium => assert_eq!(name, "chromium") 23 | } 24 | } 25 | 26 | fn executable_should_exist(t: &BrowserType) { 27 | let executable = t.executable().unwrap(); 28 | assert!(executable.is_file()); 29 | } 30 | 31 | // 'should handle timeout' 32 | async fn should_handle_timeout(t: &BrowserType) { 33 | let result = t.launcher().timeout(0.1).launch().await; 34 | assert!(result.is_err()); 35 | let err = result.err().unwrap(); 36 | match &*err { 37 | playwright::Error::ErrorResponded(_) => {} 38 | e => { 39 | dbg!(e); 40 | unreachable!(); 41 | } 42 | } 43 | } 44 | 45 | // 'should fire close event for all contexts' 46 | async fn should_fire_close(t: &BrowserType) { 47 | use playwright::api::browser_context::{Event, EventType}; 48 | let browser = t.launcher().launch().await.unwrap(); 49 | let context = browser.context_builder().build().await.unwrap(); 50 | let (wait, close) = tokio::join!(context.expect_event(EventType::Close), browser.close()); 51 | close.unwrap(); 52 | assert_eq!(wait.unwrap(), Event::Close); 53 | } 54 | -------------------------------------------------------------------------------- /tests/connect/mod.rs: -------------------------------------------------------------------------------- 1 | use super::{free_local_port, install_browser, playwright_with_driver, Which}; 2 | use playwright::api::{page, Browser, BrowserType}; 3 | use serde::Deserialize; 4 | 5 | pub(super) async fn connect_over_cdp(which: Which) { 6 | let playwright = playwright_with_driver().await; 7 | install_browser(&playwright, which); 8 | let browser_type = match which { 9 | Which::Chromium => playwright.chromium(), 10 | _ => return 11 | }; 12 | 13 | http(&browser_type).await; 14 | ws(&browser_type).await; 15 | } 16 | 17 | async fn http(browser_type: &BrowserType) { 18 | let port = free_local_port().unwrap(); 19 | let browser = browser_type 20 | .launcher() 21 | .args(&[format!("--remote-debugging-port={}", port)]) 22 | .launch() 23 | .await 24 | .unwrap(); 25 | let endpoint_url = format!("http://localhost:{}", port); 26 | // wait needed? 27 | let cdp1: Browser = browser_type 28 | .connect_over_cdp_builder(&endpoint_url) 29 | .connect_over_cdp() 30 | .await 31 | .unwrap(); 32 | let cdp2: Browser = browser_type 33 | .connect_over_cdp_builder(&endpoint_url) 34 | .connect_over_cdp() 35 | .await 36 | .unwrap(); 37 | 38 | { 39 | assert_eq!(cdp1.contexts().unwrap().len(), 1); 40 | let page1 = cdp1.contexts().unwrap()[0].new_page().await.unwrap(); 41 | let (a, b) = tokio::join!( 42 | page1.expect_event(page::EventType::DomContentLoaded), 43 | page1.goto_builder("https://example.com/").goto() 44 | ); 45 | a.unwrap(); 46 | b.unwrap(); 47 | assert_eq!(cdp2.contexts().unwrap().len(), 1); 48 | let cdp2_pages = cdp2.contexts().unwrap()[0].pages().unwrap(); 49 | let page2 = cdp2_pages.into_iter().next().unwrap(); 50 | assert_eq!(page2.url().unwrap(), "https://example.com/"); 51 | } 52 | 53 | cdp1.close().await.unwrap(); 54 | cdp2.close().await.unwrap(); 55 | browser.close().await.unwrap(); 56 | } 57 | 58 | async fn ws(browser_type: &BrowserType) { 59 | let port = free_local_port().unwrap(); 60 | let browser = browser_type 61 | .launcher() 62 | .args(&[format!("--remote-debugging-port={}", port)]) 63 | .launch() 64 | .await 65 | .unwrap(); 66 | let ws_endpoint = fetch_ws_endpoint(browser_type, port).await; 67 | { 68 | let cdp1 = browser_type 69 | .connect_over_cdp_builder(&ws_endpoint) 70 | .connect_over_cdp() 71 | .await 72 | .unwrap(); 73 | assert_eq!(cdp1.contexts().unwrap().len(), 1); 74 | cdp1.close().await.unwrap(); 75 | } 76 | { 77 | let cdp2 = browser_type 78 | .connect_over_cdp_builder(&ws_endpoint) 79 | .connect_over_cdp() 80 | .await 81 | .unwrap(); 82 | assert_eq!(cdp2.contexts().unwrap().len(), 1); 83 | cdp2.close().await.unwrap(); 84 | } 85 | browser.close().await.unwrap(); 86 | } 87 | 88 | async fn fetch_ws_endpoint(browser_type: &BrowserType, port: u16) -> String { 89 | let browser = browser_type.launcher().launch().await.unwrap(); 90 | let browser_context = browser.context_builder().build().await.unwrap(); 91 | let page = browser_context.new_page().await.unwrap(); 92 | let url = format!("http://localhost:{}/json/version/", port); 93 | let (event, goto) = tokio::join!( 94 | page.expect_event(page::EventType::Response), 95 | page.goto_builder(&url).goto() 96 | ); 97 | goto.unwrap(); 98 | let response = match event { 99 | Ok(page::Event::Response(res)) => res, 100 | _ => unreachable!() 101 | }; 102 | let text = response.text().await.unwrap(); 103 | #[derive(Debug, Deserialize)] 104 | #[serde(rename_all = "camelCase")] 105 | struct De<'a> { 106 | web_socket_debugger_url: &'a str 107 | } 108 | let De { 109 | web_socket_debugger_url 110 | } = serde_json::from_str(&text).unwrap(); 111 | web_socket_debugger_url.into() 112 | } 113 | -------------------------------------------------------------------------------- /tests/devices/mod.rs: -------------------------------------------------------------------------------- 1 | use super::Which; 2 | use playwright::{ 3 | api::{page, Page, Viewport}, 4 | Playwright 5 | }; 6 | 7 | pub async fn all(playwright: &Playwright, port: u16, _which: Which) { 8 | let _devices = playwright.devices(); 9 | let device = playwright.device("iPhone 11 Pro").unwrap(); 10 | dbg!(&device); 11 | let chromium = playwright.chromium(); 12 | let browser = chromium.launcher().launch().await.unwrap(); 13 | let ctx = browser 14 | .context_builder() 15 | .set_device(&device) 16 | .build() 17 | .await 18 | .unwrap(); 19 | let page = ctx.new_page().await.unwrap(); 20 | check_user_agent(&page, port).await; 21 | check_size(&page).await; 22 | assert!((device_pixel_ratio(&page).await - 3.0).abs() < f64::EPSILON); 23 | assert!(has_touch(&page).await); 24 | // TODO: is_mobile 25 | let tmp_dir = tempdir::TempDir::new("playwright-rust").unwrap(); 26 | dbg!(&tmp_dir); 27 | chromium 28 | .persistent_context_launcher(tmp_dir.path()) 29 | .set_device(&device) 30 | .launch() 31 | .await 32 | .unwrap(); 33 | } 34 | 35 | async fn check_size(page: &Page) { 36 | page.set_content_builder( 37 | r#""# 38 | ) 39 | .set_content() 40 | .await 41 | .unwrap(); 42 | let screen: Viewport = page 43 | .eval("() => ({width: window.screen.width, height: window.screen.height})") 44 | .await 45 | .unwrap(); 46 | let viewport: Viewport = page 47 | .eval("() => ({width: window.innerWidth, height: window.innerHeight})") 48 | .await 49 | .unwrap(); 50 | assert_eq!( 51 | screen, 52 | Viewport { 53 | width: 375, 54 | height: 812 55 | } 56 | ); 57 | assert_eq!( 58 | viewport, 59 | Viewport { 60 | width: 375, 61 | height: 635 62 | } 63 | ); 64 | } 65 | 66 | async fn device_pixel_ratio(page: &Page) -> f64 { 67 | page.eval("window.devicePixelRatio").await.unwrap() 68 | } 69 | 70 | async fn has_touch(page: &Page) -> bool { 71 | page.eval("() => 'ontouchstart' in window").await.unwrap() 72 | } 73 | 74 | async fn check_user_agent(page: &Page, port: u16) { 75 | let user_agent = "Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0 Mobile/15E148 Safari/604.1"; 76 | assert_eq!( 77 | page.eval::("() => navigator.userAgent") 78 | .await 79 | .unwrap(), 80 | user_agent 81 | ); 82 | let url = super::url_static(port, "/empty.html"); 83 | let (result, _) = tokio::join!( 84 | page.expect_event(page::EventType::Request), 85 | page.goto_builder(&url).goto() 86 | ); 87 | let request = match result.unwrap() { 88 | page::Event::Request(request) => request, 89 | _ => unreachable!() 90 | }; 91 | dbg!(&request.headers().unwrap()); 92 | assert_eq!(request.headers().unwrap()["user-agent"], user_agent); 93 | } 94 | -------------------------------------------------------------------------------- /tests/hello.rs: -------------------------------------------------------------------------------- 1 | use playwright::Playwright; 2 | 3 | playwright::runtime_test!(hello, { 4 | main().await.unwrap(); 5 | }); 6 | 7 | async fn main() -> Result<(), playwright::Error> { 8 | let playwright = Playwright::initialize().await?; // if drop all resources are disposed 9 | playwright.prepare()?; // install browsers 10 | let chromium = playwright.chromium(); 11 | let browser = chromium.launcher().headless(true).launch().await?; 12 | let context = browser.context_builder().build().await?; 13 | let page = context.new_page().await?; 14 | 15 | page.goto_builder("https://docs.rs/playwright/0.0.5/playwright/") 16 | .goto() 17 | .await?; 18 | // Exec js on browser and Deserialize with serde 19 | let url: String = page.eval("() => location.href").await?; 20 | assert_eq!(url, "https://docs.rs/playwright/0.0.5/playwright/"); 21 | 22 | // Wait until navigated 23 | page.click_builder(r#"a[title="playwright::api mod"]"#) 24 | .click() 25 | .await?; 26 | assert_eq!( 27 | page.url().unwrap(), 28 | "https://docs.rs/playwright/0.0.5/playwright/api/index.html" 29 | ); 30 | 31 | // Waiting load explicitly is unnecessary. 32 | // [many functions wait contents automaticaly](https://playwright.dev/docs/actionability/). 33 | page.expect_event(playwright::api::page::EventType::Load) 34 | .await?; 35 | 36 | Ok(()) 37 | } 38 | -------------------------------------------------------------------------------- /tests/selectors/mod.rs: -------------------------------------------------------------------------------- 1 | use super::Which; 2 | use playwright::api::{Playwright, Selectors}; 3 | 4 | pub async fn all(playwright: &Playwright, which: Which) { 5 | let selectors = playwright.selectors(); 6 | 7 | register_should_work(playwright, &selectors, which).await; 8 | } 9 | 10 | async fn register_should_work(playwright: &Playwright, selectors: &Selectors, which: Which) { 11 | let snip = "({ 12 | // Returns the first element matching given selector in the root's subtree. 13 | query(root, selector) { 14 | return root.querySelector(selector); 15 | }, 16 | 17 | // Returns all elements matching given selector in the root's subtree. 18 | queryAll(root, selector) { 19 | return Array.from(root.querySelectorAll(selector)); 20 | } 21 | })"; 22 | selectors.register("tag", snip, false).await.unwrap(); 23 | let t = match which { 24 | Which::Webkit => playwright.webkit(), 25 | Which::Firefox => playwright.firefox(), 26 | Which::Chromium => playwright.chromium() 27 | }; 28 | let browser = t.launcher().launch().await.unwrap(); 29 | let bc = browser.context_builder().build().await.unwrap(); 30 | let page = bc.new_page().await.unwrap(); 31 | page.set_content_builder("
") 32 | .set_content() 33 | .await 34 | .unwrap(); 35 | let _button = page.query_selector("tag=button").await.unwrap().unwrap(); 36 | page.click_builder(r#"tag=div >> text="Click me""#) 37 | .click() 38 | .await 39 | .unwrap(); 40 | } 41 | -------------------------------------------------------------------------------- /tests/server/empty.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/octaltree/playwright-rust/a672ce7311eb596459acf3bdeb1d09e177a488d1/tests/server/empty.html -------------------------------------------------------------------------------- /tests/server/empty2.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/octaltree/playwright-rust/a672ce7311eb596459acf3bdeb1d09e177a488d1/tests/server/empty2.html -------------------------------------------------------------------------------- /tests/server/form.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/server/worker.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/server/worker.js: -------------------------------------------------------------------------------- 1 | console.log('hello from the worker'); 2 | 3 | function workerFunction() { 4 | return 'worker function result'; 5 | } 6 | 7 | self.addEventListener('message', event => { 8 | console.log('got this data: ' + event.data); 9 | }); 10 | 11 | (async function() { 12 | while (true) { 13 | self.postMessage(workerFunction.toString()); 14 | await new Promise(x => setTimeout(x, 100)); 15 | } 16 | })(); 17 | -------------------------------------------------------------------------------- /tests/test.rs: -------------------------------------------------------------------------------- 1 | mod browser; 2 | mod browser_context; 3 | mod browser_type; 4 | mod devices; 5 | mod page; 6 | mod selectors; 7 | 8 | mod connect; 9 | 10 | #[cfg(feature = "rt-async-std")] 11 | use async_std::task::spawn; 12 | #[cfg(feature = "rt-actix")] 13 | use tokio::task::spawn; 14 | #[cfg(feature = "rt-tokio")] 15 | use tokio::task::spawn; 16 | 17 | use playwright::Playwright; 18 | use std::path::PathBuf; 19 | 20 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 21 | pub enum Which { 22 | Webkit, 23 | Firefox, 24 | Chromium 25 | } 26 | 27 | playwright::runtime_test!(chromium_page, page(Which::Chromium).await); 28 | playwright::runtime_test!(firefox_page, page(Which::Firefox).await); 29 | // playwright::runtime_test!(webkit_page, page(Which::Webkit).await); 30 | 31 | playwright::runtime_test!(chromium_selectors, selectors(Which::Chromium).await); 32 | playwright::runtime_test!(firefox_selectors, selectors(Which::Firefox).await); 33 | // playwright::runtime_test!(webkit_selectors, selectors(Which::Webkit).await); 34 | 35 | playwright::runtime_test!(chromium_devices, devices(Which::Chromium).await); 36 | playwright::runtime_test!(firefox_devices, devices(Which::Chromium).await); 37 | // playwright::runtime_test!(webkit_devices, devices(Which::Webkit).await); 38 | 39 | playwright::runtime_test!( 40 | connect_over_cdp, 41 | connect::connect_over_cdp(Which::Chromium).await 42 | ); 43 | 44 | async fn page(which: Which) { 45 | std::fs::create_dir_all(temp_dir()).unwrap(); 46 | let port = free_local_port().unwrap(); 47 | start_test_server(port).await; 48 | let playwright = playwright_with_driver().await; 49 | install_browser(&playwright, which); 50 | let browser_type = browser_type::all(&playwright, which).await; 51 | let browser = browser::all(&browser_type, which).await; 52 | let persistent = browser_context::persistent(&browser_type, port, which).await; 53 | let browser_context = browser_context::all(&browser, &persistent, port, which).await; 54 | page::all(&browser_context, port, which).await; 55 | } 56 | 57 | async fn selectors(which: Which) { 58 | let playwright = playwright_with_driver().await; 59 | install_browser(&playwright, which); 60 | selectors::all(&playwright, which).await; 61 | } 62 | 63 | async fn devices(which: Which) { 64 | let port = free_local_port().unwrap(); 65 | start_test_server(port).await; 66 | let playwright = playwright_with_driver().await; 67 | install_browser(&playwright, which); 68 | devices::all(&playwright, port, which).await; 69 | } 70 | 71 | fn install_browser(p: &Playwright, which: Which) { 72 | match which { 73 | Which::Webkit => p.install_webkit(), 74 | Which::Firefox => p.install_firefox(), 75 | Which::Chromium => p.install_chromium() 76 | } 77 | .unwrap(); 78 | } 79 | 80 | async fn playwright_with_driver() -> Playwright { 81 | use playwright::Driver; 82 | let driver = Driver::new(Driver::default_dest()); 83 | let mut playwright = Playwright::with_driver(driver).await.unwrap(); 84 | let _ = playwright.driver(); 85 | playwright 86 | } 87 | 88 | #[cfg(any(feature = "rt-tokio", feature = "rt-actix"))] 89 | async fn start_test_server(port: u16) { 90 | use warp::{ 91 | http::header::{HeaderMap, HeaderValue}, 92 | Filter 93 | }; 94 | let headers = { 95 | let mut headers = HeaderMap::new(); 96 | headers.insert( 97 | "Content-Type", 98 | HeaderValue::from_static("application/octet-stream") 99 | ); 100 | headers.insert( 101 | "Content-Disposition", 102 | HeaderValue::from_static("attachment") 103 | ); 104 | headers 105 | }; 106 | let r#static = warp::path("static").and(warp::fs::dir("tests/server")); 107 | let download = warp::path("download") 108 | .and(warp::fs::dir("tests/server")) 109 | .with(warp::reply::with::headers(headers)); 110 | let route = r#static.or(download); 111 | spawn(async move { 112 | warp::serve(route).run(([127, 0, 0, 1], port)).await; 113 | }); 114 | } 115 | 116 | #[cfg(feature = "rt-async-std")] 117 | async fn start_test_server(port: u16) { 118 | use tide::Server; 119 | let mut app = Server::new(); 120 | app.at("/static").serve_dir("tests/server/").unwrap(); 121 | app.at("/download") 122 | .with(tide::utils::After(|mut res: tide::Response| async move { 123 | res.insert_header("Content-Type", "application/octet-stream"); 124 | res.insert_header("Content-Disposition", "attachment"); 125 | Ok(res) 126 | })) 127 | .serve_dir("tests/server/") 128 | .unwrap(); 129 | spawn(async move { 130 | app.listen(format!("127.0.0.1:{}", port)).await.unwrap(); 131 | }); 132 | } 133 | 134 | // XXX: non thread safe 135 | fn free_local_port() -> Option { 136 | let socket = std::net::SocketAddrV4::new(std::net::Ipv4Addr::LOCALHOST, 0); 137 | std::net::TcpListener::bind(socket) 138 | .and_then(|listener| listener.local_addr()) 139 | .map(|addr| addr.port()) 140 | .ok() 141 | } 142 | 143 | fn url_static(port: u16, path: &str) -> String { 144 | format!("http://localhost:{}/static{}", port, path) 145 | } 146 | 147 | fn url_download(port: u16, path: &str) -> String { 148 | format!("http://localhost:{}/download{}", port, path) 149 | } 150 | 151 | fn origin(port: u16) -> String { format!("http://localhost:{}", port) } 152 | 153 | fn temp_dir() -> PathBuf { std::env::temp_dir().join("test-playwright-rust") } 154 | 155 | // let h = page.eval_handle("() => location.href").await.unwrap(); 156 | // let s: String = page 157 | // .evaluate("([s]) => s + location.href", Some(vec![h])) 158 | // .await 159 | // .unwrap(); 160 | // assert_eq!(s, "https://example.com/https://example.com/"); 161 | // let s: DateTime = page 162 | // .evaluate("d => d", Some(DateTime::from(chrono::Utc::now()))) 163 | // .await 164 | // .unwrap(); 165 | // println!("{:?}", s); 166 | // let (next_page, _) = tokio::join!( 167 | // context.expect_event(browser_context::EventType::Page), 168 | // page.click_builder("a") 169 | // .modifiers(vec![KeyboardModifier::Control]) 170 | // .click() 171 | // ); 172 | // let _next_page = match next_page.unwrap() { 173 | // browser_context::Event::Page(p) => p, 174 | // _ => unreachable!() 175 | // }; 176 | // //// let _ = p.main_frame().query_selector_all("a").await.unwrap(); 177 | // //// let _ = p.main_frame().title().await.unwrap(); 178 | // // let mut a = p.query_selector("a").await.unwrap().unwrap(); 179 | // // let _href = a.get_attribute("href").await.unwrap(); 180 | --------------------------------------------------------------------------------