should not result in an
83 | // *additional* newline (a newline is already added when the div
84 | // started).
85 | //
86 | // Example markup:
87 | //
88 | // hello
89 | //
90 | //
world
91 | //
92 | // Another example:
93 | //
94 | // hello
95 | //
96 | //
97 | // See https://github.com/threema-ch/compose-area/issues/72
98 | // for details.
99 | } else {
100 | text.push('\n');
101 | }
102 | }
103 | _other => {}
104 | }
105 | }
106 | other => warn!("visit_child_nodes: Unhandled node type: {}", other),
107 | }
108 | }
109 | }
110 |
111 | #[cfg(test)]
112 | mod tests {
113 | use super::*;
114 |
115 | use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure};
116 |
117 | wasm_bindgen_test_configure!(run_in_browser);
118 |
119 | mod extract_text {
120 | use super::*;
121 |
122 | use percy_dom::{
123 | event::EventsByNodeIdx,
124 | prelude::{html, IterableNodes, VirtualNode},
125 | };
126 |
127 | struct ExtractTextTest {
128 | html: VirtualNode,
129 | expected: &'static str,
130 | }
131 |
132 | impl ExtractTextTest {
133 | fn test(&self) {
134 | // Get references to DOM objects
135 | let window = web_sys::window().expect("No global `window` exists");
136 | let document = window.document().expect("Should have a document on window");
137 |
138 | // Create wrapper element
139 | let test_wrapper = document
140 | .create_element("div")
141 | .expect("Could not create test wrapper");
142 |
143 | // Write HTML to DOM
144 | let node: Node = self.html.create_dom_node(0, &mut EventsByNodeIdx::new());
145 | test_wrapper
146 | .append_child(&node)
147 | .expect("Could not append node to test wrapper");
148 |
149 | // Extract and validate text
150 | let text: String = extract_text(&test_wrapper, false);
151 | assert_eq!(&text, self.expected);
152 | }
153 | }
154 |
155 | #[wasm_bindgen_test]
156 | fn simple() {
157 | ExtractTextTest {
158 | html: html! { { "Hello World" } },
159 | expected: "Hello World",
160 | }
161 | .test();
162 | }
163 |
164 | #[wasm_bindgen_test]
165 | fn single_div() {
166 | ExtractTextTest {
167 | html: html! {
{ "Hello World" }
},
168 | expected: "Hello World",
169 | }
170 | .test();
171 | }
172 |
173 | #[wasm_bindgen_test]
174 | fn single_span() {
175 | ExtractTextTest {
176 | html: html! {
{ "Hello World" } },
177 | expected: "Hello World",
178 | }
179 | .test();
180 | }
181 |
182 | #[wasm_bindgen_test]
183 | fn image() {
184 | ExtractTextTest {
185 | html: html! {
{ "Hello " }

World
},
186 | expected: "Hello BigWorld",
187 | }
188 | .test();
189 | }
190 |
191 | #[wasm_bindgen_test]
192 | fn newline_br() {
193 | ExtractTextTest {
194 | html: html! {
Hello
World
},
195 | expected: "Hello\nWorld",
196 | }
197 | .test();
198 | }
199 |
200 | #[wasm_bindgen_test]
201 | fn newline_single_div_first() {
202 | ExtractTextTest {
203 | html: html! {
},
204 | expected: "Hello\nWorld",
205 | }
206 | .test();
207 | }
208 |
209 | #[wasm_bindgen_test]
210 | fn newline_single_div_second() {
211 | ExtractTextTest {
212 | html: html! {
},
213 | expected: "Hello\nWorld",
214 | }
215 | .test();
216 | }
217 |
218 | #[wasm_bindgen_test]
219 | fn newline_double_div() {
220 | ExtractTextTest {
221 | html: html! {
},
222 | expected: "Hello\nWorld",
223 | }
224 | .test();
225 | }
226 |
227 | /// Regression test for https://github.com/threema-ch/compose-area/issues/72.
228 | #[wasm_bindgen_test]
229 | fn br_in_div() {
230 | ExtractTextTest {
231 | html: html! {
},
232 | expected: "Hello\n\nWorld",
233 | }
234 | .test();
235 | }
236 |
237 | /// Regression test for https://github.com/threema-ch/compose-area/issues/72.
238 | #[wasm_bindgen_test]
239 | fn br_in_nested_div() {
240 | ExtractTextTest {
241 | html: html! {
},
242 | expected: "Hello\n\nWorld",
243 | }
244 | .test();
245 | }
246 |
247 | #[wasm_bindgen_test]
248 | fn two_nested_divs() {
249 | ExtractTextTest {
250 | html: html! {
},
251 | expected: "Hello\nWorld",
252 | }
253 | .test();
254 | }
255 |
256 | #[wasm_bindgen_test]
257 | fn double_text_node() {
258 | let mut node = VirtualNode::element("span");
259 | node.as_velement_mut()
260 | .unwrap()
261 | .children
262 | .push(VirtualNode::text("Hello\n"));
263 | node.as_velement_mut()
264 | .unwrap()
265 | .children
266 | .push(VirtualNode::text("World"));
267 | ExtractTextTest {
268 | html: node,
269 | expected: "Hello\nWorld",
270 | }
271 | .test();
272 | }
273 |
274 | /// Regression test for https://github.com/threema-ch/compose-area/issues/75
275 | #[wasm_bindgen_test]
276 | fn newline_regression_75() {
277 | ExtractTextTest {
278 | html: html! {
},
279 | expected: "a\nb\nc",
280 | }
281 | .test();
282 | }
283 | }
284 | }
285 |
--------------------------------------------------------------------------------
/LICENSE-APACHE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | references:
4 | steps-integrationtests: &steps-integrationtests
5 | - attach_workspace:
6 | at: .
7 |
8 | # Load npm cache if possible.
9 | # Multiple caches are used to increase the chance of a cache hit.
10 | - restore_cache:
11 | keys:
12 | - v1-npm-cache-integrationtest-{{ arch }}-{{ .Branch }}
13 | - v1-npm-cache-integrationtest-{{ arch }}
14 |
15 | - run:
16 | name: Prepare non-global npm directory
17 | command: mkdir ~/.npm-global && npm config set prefix '~/.npm-global'
18 | - run:
19 | name: Set up test server
20 | command: >
21 | PATH=~/.npm-global/bin:$PATH
22 | && cd pkg
23 | && npm link
24 | && cd ../www
25 | && npm ci
26 | && npm link compose-area
27 | - run:
28 | name: Start test server
29 | command: cd www && npm run start
30 | background: true
31 | - run:
32 | name: Set up selenium tests
33 | command: cd selenium && npm ci
34 | - run:
35 | name: Run selenium tests
36 | command: cd selenium && npm test $BROWSER
37 |
38 | # Save cache
39 | - save_cache:
40 | key: v1-npm-cache-integrationtest-{{ arch }}-{{ .Branch }}
41 | paths:
42 | - www/node_modules
43 | - selenium/node_modules
44 | - save_cache:
45 | key: v1-npm-cache-integrationtest-{{ arch }}
46 | paths:
47 | - www/node_modules
48 | - selenium/node_modules
49 |
50 | jobs:
51 |
52 | build:
53 | docker:
54 | - image: rust:latest
55 | steps:
56 | - checkout
57 |
58 | # Load cargo target from cache if possible.
59 | # Multiple caches are used to increase the chance of a cache hit.
60 | - restore_cache:
61 | keys:
62 | - v2-cargo-cache-build-{{ arch }}-{{ .Branch }}
63 | - v2-cargo-cache-build-{{ arch }}
64 |
65 | # Install wasm
66 | - run:
67 | name: Add wasm32 target
68 | command: rustup target add wasm32-unknown-unknown
69 |
70 | # Install wasm tools
71 | - run:
72 | name: Install wasm-pack
73 | command: >
74 | curl -L https://github.com/rustwasm/wasm-pack/releases/download/v0.8.1/wasm-pack-v0.8.1-x86_64-unknown-linux-musl.tar.gz
75 | | tar --strip-components=1 --wildcards -xzf - "*/wasm-pack"
76 | && chmod +x wasm-pack
77 | && mv wasm-pack $CARGO_HOME/bin/
78 |
79 | # Show versions
80 | - run:
81 | name: Show versions
82 | command: rustc --version && cargo --version && wasm-pack --version
83 |
84 | # Build
85 | - run:
86 | name: Build compose-area
87 | command: wasm-pack build --release -t browser
88 | - persist_to_workspace:
89 | root: .
90 | paths:
91 | - pkg
92 | - selenium
93 | - www
94 |
95 | # Save cache
96 | - save_cache:
97 | key: v2-cargo-cache-build-{{ arch }}-{{ .Branch }}
98 | paths:
99 | - target
100 | - /usr/local/cargo
101 | - save_cache:
102 | key: v2-cargo-cache-build-{{ arch }}
103 | paths:
104 | - /usr/local/cargo
105 |
106 | lint:
107 | docker:
108 | - image: rust:latest
109 | steps:
110 | - checkout
111 |
112 | # Load cargo target from cache if possible.
113 | # Multiple caches are used to increase the chance of a cache hit.
114 | - restore_cache:
115 | keys:
116 | - v2-cargo-cache-lint-{{ arch }}-{{ .Branch }}
117 | - v2-cargo-cache-lint-{{ arch }}
118 |
119 | # Install clippy
120 | - run:
121 | name: Install clippy
122 | command: rustup component add clippy
123 |
124 | # Show versions
125 | - run:
126 | name: Show versions
127 | command: rustc --version && cargo --version && cargo clippy --version
128 |
129 | # Run linting checks
130 | - run:
131 | name: Run clippy
132 | command: cargo clean && cargo clippy --all-targets --all-features
133 |
134 | # Save cache
135 | - save_cache:
136 | key: v2-cargo-cache-lint-{{ arch }}-{{ .Branch }}
137 | paths:
138 | - /usr/local/cargo
139 | - save_cache:
140 | key: v2-cargo-cache-lint-{{ arch }}
141 | paths:
142 | - /usr/local/cargo
143 |
144 | fmt:
145 | docker:
146 | - image: rust:latest
147 | steps:
148 | - checkout
149 |
150 | # Load cargo target from cache if possible.
151 | # Multiple caches are used to increase the chance of a cache hit.
152 | - restore_cache:
153 | keys:
154 | - v2-cargo-cache-fmt-{{ arch }}-{{ .Branch }}
155 | - v2-cargo-cache-fmt-{{ arch }}
156 |
157 | # Install rustfmt
158 | - run:
159 | name: Install rustfmt
160 | command: rustup component add rustfmt
161 |
162 | # Show versions
163 | - run:
164 | name: Show versions
165 | command: rustc --version && cargo --version && cargo fmt --version
166 |
167 | # Run format checks
168 | - run:
169 | name: Run rustfmt
170 | command: cargo fmt -- --check
171 |
172 | # Save cache
173 | - save_cache:
174 | key: v2-cargo-cache-fmt-{{ arch }}-{{ .Branch }}
175 | paths:
176 | - /usr/local/cargo
177 | - save_cache:
178 | key: v2-cargo-cache-fmt-{{ arch }}
179 | paths:
180 | - /usr/local/cargo
181 |
182 | test-unit:
183 | docker:
184 | - image: rust:latest
185 | steps:
186 | - checkout
187 |
188 | # Load cargo target from cache if possible.
189 | # Multiple caches are used to increase the chance of a cache hit.
190 | - restore_cache:
191 | keys:
192 | - v2-cargo-cache-unittest-{{ arch }}-{{ .Branch }}
193 | - v2-cargo-cache-unittest-{{ arch }}
194 |
195 | # Show versions
196 | - run:
197 | name: Show versions
198 | command: rustc --version && cargo --version
199 |
200 | # Run tests
201 | - run:
202 | name: Run unit tests
203 | command: cargo test
204 |
205 | # Save cache
206 | - save_cache:
207 | key: v2-cargo-cache-unittest-{{ arch }}-{{ .Branch }}
208 | paths:
209 | - target
210 | - /usr/local/cargo
211 | - save_cache:
212 | key: v2-cargo-cache-unittest-{{ arch }}
213 | paths:
214 | - /usr/local/cargo
215 |
216 | test-browser:
217 | docker:
218 | - image: rust:latest
219 | steps:
220 | - checkout
221 |
222 | # Load cargo target from cache if possible.
223 | # Multiple caches are used to increase the chance of a cache hit.
224 | - restore_cache:
225 | keys:
226 | - v2-cargo-cache-browsertest-{{ arch }}-{{ .Branch }}
227 | - v2-cargo-cache-browsertest-{{ arch }}
228 |
229 | # Install wasm
230 | - run:
231 | name: Add wasm32 target
232 | command: rustup target add wasm32-unknown-unknown
233 |
234 | # Install wasm tools
235 | - run:
236 | name: Install wasm-pack
237 | command: >
238 | curl -L https://github.com/rustwasm/wasm-pack/releases/download/v0.8.1/wasm-pack-v0.8.1-x86_64-unknown-linux-musl.tar.gz
239 | | tar --strip-components=1 --wildcards -xzf - "*/wasm-pack"
240 | && chmod +x wasm-pack
241 | && mv wasm-pack $CARGO_HOME/bin/
242 |
243 | # Install browsers
244 | - run:
245 | name: Install latest firefox
246 | command: >
247 | apt-get update
248 | && apt-get install -y libgtk-3-0 libdbus-glib-1-2 libx11-xcb1 libasound2
249 | && wget -q -O - "https://download.mozilla.org/?product=firefox-latest-ssl&os=linux64&lang=en-US"
250 | | tar xj
251 |
252 | # Show versions
253 | - run:
254 | name: Show versions
255 | command: rustc --version && cargo --version && wasm-pack --version && firefox/firefox --version
256 |
257 | # Run tests
258 | - run:
259 | name: Run browser unit tests
260 | command: PATH=$(pwd)/firefox:$PATH wasm-pack test --headless --firefox
261 |
262 | # Save cache
263 | - save_cache:
264 | key: v2-cargo-cache-browsertest-{{ arch }}-{{ .Branch }}
265 | paths:
266 | - target
267 | - /usr/local/cargo
268 | - save_cache:
269 | key: v2-cargo-cache-browsertest-{{ arch }}
270 | paths:
271 | - /usr/local/cargo
272 |
273 | test-integration-firefox:
274 | docker:
275 | - image: circleci/node:16-browsers
276 | steps: *steps-integrationtests
277 | environment:
278 | BROWSER: firefox
279 |
280 | test-integration-chrome:
281 | docker:
282 | - image: circleci/node:16-browsers
283 | steps: *steps-integrationtests
284 | environment:
285 | BROWSER: chrome
286 |
287 | audit:
288 | docker:
289 | - image: dbrgn/cargo-audit:latest
290 | steps:
291 | - checkout
292 | - restore_cache:
293 | keys:
294 | - v2-cargo-audit-cache
295 | - run:
296 | name: Show versions
297 | command: rustc --version && cargo --version && cargo audit --version
298 | - run:
299 | name: Run cargo audit
300 | command: cargo audit
301 | - save_cache:
302 | key: v2-cargo-audit-cache
303 | paths:
304 | - /usr/local/cargo
305 |
306 | build-demo:
307 | docker:
308 | - image: circleci/node:16
309 | steps:
310 | - checkout
311 | - attach_workspace:
312 | at: .
313 |
314 | # Load npm cache if possible.
315 | # Multiple caches are used to increase the chance of a cache hit.
316 | - restore_cache:
317 | keys:
318 | - v1-npm-cache-demo-{{ arch }}-{{ .Branch }}
319 | - v1-npm-cache-demo-{{ arch }}
320 |
321 | # Build demo
322 | - run:
323 | name: Build demo page
324 | command: >
325 | cd www
326 | && echo "Installing dependencies..."
327 | && npm ci
328 | && echo "Copying npm package..."
329 | && rm -r node_modules/compose-area
330 | && cp -Rv ../pkg node_modules/compose-area
331 | && echo "Build..."
332 | && npm run build
333 | && cd ..
334 | - run:
335 | name: Prepare dist files
336 | command: >
337 | mkdir html
338 | && touch html/.nojekyll
339 | && cp -Rv www/dist/* html/
340 | && export VERSION=$(git show -s --format="Version: %h (%ci)")
341 | && sed -i "s/\[\[VERSION\]\]/${VERSION}/" html/index.html
342 | && sed -i "s/\[\[VERSION\]\]/${VERSION}/" html/benchmark.html
343 | - persist_to_workspace:
344 | root: .
345 | paths:
346 | - html
347 |
348 | # Save cache
349 | - save_cache:
350 | key: v1-npm-cache-demo-{{ arch }}-{{ .Branch }}
351 | paths:
352 | - www/node_modules
353 | - selenium/node_modules
354 | - save_cache:
355 | key: v1-npm-cache-demo-{{ arch }}
356 | paths:
357 | - www/node_modules
358 | - selenium/node_modules
359 |
360 | deploy-demo:
361 | docker:
362 | - image: circleci/node:16
363 | steps:
364 | - checkout
365 | - attach_workspace:
366 | at: .
367 |
368 | # Deploy
369 | - run:
370 | name: Install and configure deployment dependencies
371 | command: >
372 | npm install gh-pages@2
373 | && git config user.email "ci-build@circleci"
374 | && git config user.name "ci-build"
375 | - add_ssh_keys:
376 | fingerprints:
377 | - "32:c5:e4:2f:85:f2:6b:3e:ae:fa:60:9d:15:66:0e:55"
378 | - run:
379 | name: Deploy demo to gh-pages branch
380 | command: node_modules/.bin/gh-pages --dotfiles --message "[skip ci] Updates" --dist html
381 |
382 | workflows:
383 | version: 2
384 |
385 | build-and-test:
386 | jobs:
387 | - build
388 | - lint
389 | - fmt
390 | - test-unit
391 | - test-browser
392 | - test-integration-firefox:
393 | requires:
394 | - build
395 | - test-integration-chrome:
396 | requires:
397 | - build
398 | - build-demo:
399 | requires:
400 | - build
401 | - test-integration-firefox
402 | - test-integration-chrome
403 | - deploy-demo:
404 | requires:
405 | - build-demo
406 | filters:
407 | branches:
408 | only: master
409 |
410 | # Build master every week on Monday at 03:00 am
411 | weekly:
412 | triggers:
413 | - schedule:
414 | cron: "0 3 * * 1"
415 | filters:
416 | branches:
417 | only:
418 | - master
419 | jobs:
420 | - build
421 | - lint
422 | - fmt
423 | - test-unit
424 | - test-browser
425 | - test-integration-firefox:
426 | requires:
427 | - build
428 | - test-integration-chrome:
429 | requires:
430 | - build
431 | - build-demo:
432 | requires:
433 | - build
434 | - test-integration-firefox
435 | - test-integration-chrome
436 | - audit
437 |
--------------------------------------------------------------------------------
/selenium/tests.ts:
--------------------------------------------------------------------------------
1 | // Selenium docs:
2 | // https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/
3 | import { expect } from 'chai';
4 | import { By, Key, WebDriver } from 'selenium-webdriver';
5 |
6 | interface CaretPosition {
7 | start: number;
8 | end: number;
9 | }
10 |
11 | // Type aliases
12 | type Testfunc = (driver: WebDriver) => void;
13 |
14 | // Shared selectors
15 | const wrapper = By.id('wrapper');
16 | const emojiTongue = By.id('tongue');
17 | const emojiBeers = By.id('beers');
18 | const emojiFacepalm = By.id('facepalm');
19 | const headline = By.css('h2');
20 |
21 | // Emoji unicode
22 | const emojiStrTongue = '\ud83d\ude1c';
23 | const emojiStrBeers = '\ud83c\udf7b';
24 | const emojiStrFacepalm = '\ud83e\udd26\u200d\u2640\ufe0f';
25 |
26 |
27 | async function extractText(driver: WebDriver): Promise
{
28 | const text: string = await driver.executeScript(`
29 | return window.composeArea.get_text();
30 | `);
31 | return text;
32 | }
33 |
34 | async function isEmpty(driver: WebDriver): Promise {
35 | const isEmpty: boolean = await driver.executeScript(`
36 | return window.composeArea.is_empty();
37 | `);
38 | return isEmpty;
39 | }
40 |
41 | async function clearSelectionRange(driver: WebDriver): Promise {
42 | const clearBtn = await driver.findElement(By.id('clearselection'));
43 | await clearBtn.click();
44 | }
45 |
46 | async function skipInBrowser(driver: WebDriver, browser: string): Promise {
47 | const cap = await driver.getCapabilities()
48 | if (cap.get('browserName') === browser) {
49 | // Test skipped due to buggy webdriver behavior in Chrome.
50 | console.warn(`Warning: Skipping test in ${browser}`);
51 | return true;
52 | }
53 | return false;
54 | }
55 |
56 | /**
57 | * The wrapper element should be found on the test page.
58 | */
59 | async function wrapperFound(driver: WebDriver) {
60 | const wrapperElement = await driver.findElement(wrapper);
61 | expect(wrapperElement).to.exist;
62 | }
63 |
64 | /**
65 | * Text insertion and the `is_empty` method should work as intended.
66 | */
67 | async function insertText(driver: WebDriver) {
68 | await driver.sleep(100); // Wait for compose area init
69 | const wrapperElement = await driver.findElement(wrapper);
70 |
71 | expect(await extractText(driver)).to.equal('');
72 | expect(await isEmpty(driver)).to.be.true;
73 |
74 | await wrapperElement.click();
75 | await wrapperElement.sendKeys('abcde');
76 |
77 | expect(await extractText(driver)).to.equal('abcde');
78 | expect(await isEmpty(driver)).to.be.false;
79 | }
80 |
81 | /**
82 | * The emoji should be inserted in the proper order.
83 | */
84 | async function insertThreeEmoji(driver: WebDriver) {
85 | await driver.sleep(100); // Wait for compose area init
86 | const wrapperElement = await driver.findElement(wrapper);
87 | const e1 = await driver.findElement(emojiTongue);
88 | const e2 = await driver.findElement(emojiBeers);
89 | const e3 = await driver.findElement(emojiFacepalm);
90 |
91 | await wrapperElement.click();
92 |
93 | await e1.click();
94 | await e2.click();
95 | await e3.click();
96 |
97 | const text = await extractText(driver);
98 | expect(text).to.equal(emojiStrTongue + emojiStrBeers + emojiStrFacepalm);
99 | }
100 |
101 | /**
102 | * Insert text between two emoji.
103 | */
104 | async function insertTextBetweenEmoji(driver: WebDriver) {
105 | await driver.sleep(100); // Wait for compose area init
106 | const wrapperElement = await driver.findElement(wrapper);
107 | const e1 = await driver.findElement(emojiTongue);
108 | const e2 = await driver.findElement(emojiBeers);
109 |
110 | await wrapperElement.click();
111 |
112 | await e1.click();
113 | await e2.click();
114 |
115 | await wrapperElement.sendKeys(Key.ARROW_LEFT, 'X');
116 |
117 | const text = await extractText(driver);
118 | expect(text).to.equal(emojiStrTongue + 'X' + emojiStrBeers);
119 | }
120 |
121 | /**
122 | * Replace selected text with text.
123 | */
124 | async function replaceSelectedTextWithText(driver: WebDriver) {
125 | if (await skipInBrowser(driver, 'chrome')) { return; }
126 |
127 | await driver.sleep(100); // Wait for compose area init
128 | const wrapperElement = await driver.findElement(wrapper);
129 |
130 | await wrapperElement.click();
131 |
132 | await wrapperElement.sendKeys('abcde');
133 | await wrapperElement.sendKeys(Key.ARROW_LEFT);
134 | await wrapperElement.sendKeys(Key.SHIFT + Key.ARROW_LEFT);
135 | await wrapperElement.sendKeys(Key.SHIFT + Key.ARROW_LEFT);
136 | await wrapperElement.sendKeys(Key.SHIFT + Key.ARROW_LEFT);
137 | await wrapperElement.sendKeys('X');
138 |
139 | const text = await extractText(driver);
140 | expect(text).to.equal('aXe');
141 | }
142 |
143 | /**
144 | * Replace selected text with emoji.
145 | */
146 | async function replaceSelectedTextWithEmoji(driver: WebDriver) {
147 | if (await skipInBrowser(driver, 'chrome')) { return; }
148 |
149 | await driver.sleep(100); // Wait for compose area init
150 | const wrapperElement = await driver.findElement(wrapper);
151 | const emoji = await driver.findElement(emojiTongue);
152 |
153 | await wrapperElement.click();
154 |
155 | await wrapperElement.sendKeys('abcde');
156 | await wrapperElement.sendKeys(Key.ARROW_LEFT);
157 | await wrapperElement.sendKeys(Key.SHIFT + Key.ARROW_LEFT);
158 | await wrapperElement.sendKeys(Key.SHIFT + Key.ARROW_LEFT);
159 | await wrapperElement.sendKeys(Key.SHIFT + Key.ARROW_LEFT);
160 | await emoji.click();
161 |
162 | const text = await extractText(driver);
163 | expect(text).to.equal('a' + emojiStrTongue + 'e');
164 | }
165 |
166 | /**
167 | * Replace selected text and emoji.
168 | */
169 | async function replaceSelectedTextAndEmoji(driver: WebDriver) {
170 | if (await skipInBrowser(driver, 'chrome')) { return; }
171 |
172 | await driver.sleep(100); // Wait for compose area init
173 |
174 | const wrapperElement = await driver.findElement(wrapper);
175 | const emoji = await driver.findElement(emojiTongue);
176 |
177 | await wrapperElement.click();
178 |
179 | await wrapperElement.sendKeys('abc');
180 | emoji.click();
181 | await wrapperElement.sendKeys('de');
182 | await wrapperElement.sendKeys(Key.ARROW_LEFT);
183 | await wrapperElement.sendKeys(Key.SHIFT + Key.ARROW_LEFT);
184 | await wrapperElement.sendKeys(Key.SHIFT + Key.ARROW_LEFT);
185 | await wrapperElement.sendKeys(Key.SHIFT + Key.ARROW_LEFT);
186 | await wrapperElement.sendKeys('X');
187 |
188 | const text = await extractText(driver);
189 | expect(text).to.equal('abXe');
190 | }
191 |
192 | /**
193 | * Cursor position after replacing emoji.
194 | */
195 | async function replaceEmojiWithText(driver: WebDriver) {
196 | await driver.sleep(100); // Wait for compose area init
197 |
198 | const wrapperElement = await driver.findElement(wrapper);
199 | const emoji = await driver.findElement(emojiTongue);
200 |
201 | await wrapperElement.click();
202 |
203 | await wrapperElement.sendKeys('a');
204 | emoji.click();
205 | await wrapperElement.sendKeys(
206 | 'b',
207 | Key.ARROW_LEFT,
208 | Key.SHIFT + Key.ARROW_LEFT,
209 | 'A',
210 | 'B',
211 | );
212 |
213 | const text = await extractText(driver);
214 | expect(text).to.equal('aABb');
215 | }
216 |
217 | /**
218 | * Replace all text.
219 | */
220 | async function replaceAllText(driver: WebDriver) {
221 | // Doesn't work in Firefox. Disabled until
222 | // https://bugzilla.mozilla.org/show_bug.cgi?id=1529540 is resolved.
223 | if (await skipInBrowser(driver, 'firefox')) { return; }
224 |
225 | // Doesn't work in Chrome because every `sendKeys` call resets the focus.
226 | if (await skipInBrowser(driver, 'chrome')) { return; }
227 |
228 | await driver.sleep(100); // Wait for compose area init
229 | const wrapperElement = await driver.findElement(wrapper);
230 |
231 | await wrapperElement.click();
232 |
233 | await wrapperElement.sendKeys('abcde');
234 | await wrapperElement.sendKeys(Key.CONTROL + 'a');
235 | await wrapperElement.sendKeys('X');
236 |
237 | const text = await extractText(driver);
238 | expect(text).to.equal('X');
239 | }
240 |
241 | /**
242 | * Using the delete key.
243 | */
244 | async function deleteKey(driver: WebDriver) {
245 | await driver.sleep(100); // Wait for compose area init
246 | const wrapperElement = await driver.findElement(wrapper);
247 | const emoji = await driver.findElement(emojiTongue);
248 |
249 | await wrapperElement.click();
250 |
251 | await wrapperElement.sendKeys('abcd', Key.ENTER);
252 | await emoji.click();
253 |
254 | expect(await extractText(driver)).to.equal('abcd\n' + emojiStrTongue);
255 |
256 | await wrapperElement.sendKeys(
257 | Key.ARROW_LEFT,
258 | Key.ARROW_LEFT,
259 | Key.ARROW_LEFT, // Between c and d
260 | Key.DELETE,
261 | Key.DELETE,
262 | 'x',
263 | );
264 |
265 | expect(await extractText(driver)).to.equal('abcx' + emojiStrTongue);
266 | }
267 |
268 | /**
269 | * Cutting and pasting
270 | */
271 | async function cutAndPaste(driver: WebDriver) {
272 | if (await skipInBrowser(driver, 'chrome')) { return; }
273 |
274 | await driver.sleep(100); // Wait for compose area init
275 | const wrapperElement = await driver.findElement(wrapper);
276 | const emoji = await driver.findElement(emojiTongue);
277 |
278 | await wrapperElement.click();
279 |
280 | // Add text
281 | await wrapperElement.sendKeys('1234');
282 |
283 | // Highlight "23"
284 | await wrapperElement.sendKeys(Key.ARROW_LEFT);
285 | await wrapperElement.sendKeys(Key.SHIFT, Key.ARROW_LEFT);
286 | await wrapperElement.sendKeys(Key.SHIFT, Key.ARROW_LEFT);
287 |
288 | // Cut
289 | await wrapperElement.sendKeys(Key.CONTROL, 'x');
290 |
291 | // Paste at end
292 | await wrapperElement.sendKeys(Key.ARROW_RIGHT);
293 | await wrapperElement.sendKeys(Key.CONTROL, 'v');
294 |
295 | expect(await extractText(driver)).to.equal('1423');
296 | }
297 |
298 | /**
299 | * No contents should be inserted outside the wrapper (e.g. if the selection is
300 | * outside).
301 | */
302 | async function noInsertOutsideWrapper(driver: WebDriver) {
303 | await driver.sleep(100); // Wait for compose area init
304 | const wrapperElement = await driver.findElement(wrapper);
305 | const headlineElement = await driver.findElement(headline);
306 | const e = await driver.findElement(emojiBeers);
307 |
308 | await headlineElement.click();
309 | await e.click();
310 | await wrapperElement.sendKeys(' yeah');
311 |
312 | const text = await extractText(driver);
313 | expect(text).to.equal(`${emojiStrBeers} yeah`);
314 | }
315 |
316 | /**
317 | * When no selection range is present, insert at end. If a selection range is
318 | * outside the compose area, use the last known range.
319 | */
320 | async function handleSelectionChanges(driver: WebDriver) {
321 | await driver.sleep(100); // Wait for compose area init
322 | const wrapperElement = await driver.findElement(wrapper);
323 | const headlineElement = await driver.findElement(headline);
324 | const e1 = await driver.findElement(emojiBeers);
325 | const e2 = await driver.findElement(emojiTongue);
326 | const e3 = await driver.findElement(emojiFacepalm);
327 |
328 | // Add initial text
329 | await wrapperElement.click();
330 | await wrapperElement.sendKeys('1234');
331 | expect(await extractText(driver)).to.equal(`1234`);
332 |
333 | // Insert emoji
334 | await wrapperElement.sendKeys(Key.ARROW_LEFT, Key.ARROW_LEFT);
335 | await e1.click();
336 | expect(await extractText(driver)).to.equal(
337 | `12${emojiStrBeers}34`
338 | );
339 |
340 | // Clear selection range and insert emoji
341 | await clearSelectionRange(driver);
342 | await e2.click();
343 | expect(await extractText(driver)).to.equal(
344 | `12${emojiStrBeers}34${emojiStrTongue}`
345 | );
346 |
347 | // Change selection range
348 | await wrapperElement.click();
349 | await wrapperElement.sendKeys(Key.ARROW_LEFT, Key.ARROW_LEFT);
350 |
351 | // Click outside wrapper, then insert another emoji
352 | await headlineElement.click();
353 | await e3.click();
354 | expect(await extractText(driver)).to.equal(
355 | `12${emojiStrBeers}3${emojiStrFacepalm}4${emojiStrTongue}`
356 | );
357 | }
358 |
359 | /**
360 | * When inserting an empty line, the newlines should not be duplicated.
361 | * Regression test for https://github.com/threema-ch/compose-area/issues/72.
362 | */
363 | async function noDuplicatedNewlines1(driver: WebDriver) {
364 | await driver.sleep(100); // Wait for compose area init
365 | const wrapperElement = await driver.findElement(wrapper);
366 |
367 | await wrapperElement.click();
368 |
369 | await wrapperElement.sendKeys('Hello');
370 | await wrapperElement.sendKeys(Key.ENTER);
371 | await wrapperElement.sendKeys(Key.ENTER);
372 | await wrapperElement.sendKeys('World');
373 |
374 | const text = await extractText(driver);
375 | expect(text).to.equal('Hello\n\nWorld');
376 | }
377 |
378 | /**
379 | * When inserting an empty line, the newlines should not be duplicated.
380 | * Regression test for https://github.com/threema-ch/compose-area/issues/72.
381 | * This one only seems to apply to Chrome, not to Firefox.
382 | */
383 | async function noDuplicatedNewlines2(driver: WebDriver) {
384 | await driver.sleep(100); // Wait for compose area init
385 | const wrapperElement = await driver.findElement(wrapper);
386 |
387 | await wrapperElement.click();
388 |
389 | await wrapperElement.sendKeys('Hello');
390 | await wrapperElement.sendKeys(Key.ENTER);
391 | await wrapperElement.sendKeys('World');
392 |
393 | const text1 = await extractText(driver);
394 | expect(text1).to.equal('Hello\nWorld');
395 |
396 | await wrapperElement.sendKeys(Key.ARROW_UP);
397 | await wrapperElement.sendKeys(Key.ENTER);
398 |
399 | const text2 = await extractText(driver);
400 | expect(text2).to.equal('Hello\n\nWorld');
401 | }
402 |
403 | /**
404 | * When inserting an empty line, the newlines should not be duplicated.
405 | * Regression test for https://github.com/threema-ch/compose-area/issues/75.
406 | * This one only seems to apply to Firefox, not to Chrome.
407 | */
408 | async function noDuplicatedNewlines3(driver: WebDriver) {
409 | await driver.sleep(100); // Wait for compose area init
410 | const wrapperElement = await driver.findElement(wrapper);
411 |
412 | await wrapperElement.click();
413 |
414 | await wrapperElement.sendKeys('Hello');
415 | await wrapperElement.sendKeys(Key.SHIFT + Key.ENTER);
416 | await wrapperElement.sendKeys('Cruel');
417 | await wrapperElement.sendKeys(Key.ENTER);
418 | await wrapperElement.sendKeys('World');
419 |
420 | const text = await extractText(driver);
421 | expect(text).to.equal('Hello\nCruel\nWorld');
422 | }
423 |
424 | export const TESTS: Array<[string, Testfunc]> = [
425 | ['Make sure that the wrapper element can be found', wrapperFound],
426 | ['Insert text', insertText],
427 | ['Insert three emoji', insertThreeEmoji],
428 | ['Insert text between emoji', insertTextBetweenEmoji],
429 | ['Replace selected text with text', replaceSelectedTextWithText],
430 | ['Replace selected text with emoji', replaceSelectedTextWithEmoji],
431 | ['Replace selected text and emoji', replaceSelectedTextAndEmoji],
432 | ['Replace emoji with text', replaceEmojiWithText],
433 | ['Replace all text', replaceAllText],
434 | ['Use the delete key', deleteKey],
435 | ['Cut and paste', cutAndPaste],
436 | ['Don\'t insert outside wrapper', noInsertOutsideWrapper],
437 | ['Handle selection changes', handleSelectionChanges],
438 | ['Ensure that empty lines are not duplicated (variant 1)', noDuplicatedNewlines1],
439 | ['Ensure that empty lines are not duplicated (variant 2)', noDuplicatedNewlines2],
440 | ['Ensure that empty lines are not duplicated (variant 3)', noDuplicatedNewlines3],
441 | ];
442 |
--------------------------------------------------------------------------------
/selenium/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "compose-area-selenium-tests",
3 | "version": "0.1.0",
4 | "lockfileVersion": 2,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "compose-area-selenium-tests",
9 | "version": "0.1.0",
10 | "license": "MIT",
11 | "dependencies": {
12 | "@types/node": "^12.12.5",
13 | "@types/selenium-webdriver": "^4.0.5",
14 | "chai": "^4.2.0",
15 | "selenium-webdriver": "^4.0.0-alpha.5",
16 | "term-color": "^1.0.1",
17 | "ts-node": "^8.4.1",
18 | "typescript": "^3.6.4"
19 | }
20 | },
21 | "node_modules/@types/node": {
22 | "version": "12.20.55",
23 | "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz",
24 | "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="
25 | },
26 | "node_modules/@types/selenium-webdriver": {
27 | "version": "4.1.13",
28 | "resolved": "https://registry.npmjs.org/@types/selenium-webdriver/-/selenium-webdriver-4.1.13.tgz",
29 | "integrity": "sha512-kGpIh7bvu4HGCJXl4PEJ53kzpG4iXlRDd66SNNCfJ58QhFuk9skOm57lVffZap5ChEOJwbge/LJ9IVGVC8EEOg==",
30 | "dependencies": {
31 | "@types/ws": "*"
32 | }
33 | },
34 | "node_modules/@types/ws": {
35 | "version": "8.5.4",
36 | "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz",
37 | "integrity": "sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==",
38 | "dependencies": {
39 | "@types/node": "*"
40 | }
41 | },
42 | "node_modules/ansi-styles": {
43 | "version": "2.0.1",
44 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.0.1.tgz",
45 | "integrity": "sha512-0zjsXMhnTibRx8YrLgLKb5NvWEcHN/OZEe1NzR8VVrEM6xr7/NyLsoMVelAhaoJhOtpuexaeRGD8MF8Z64+9LQ==",
46 | "engines": {
47 | "node": ">=0.10.0"
48 | }
49 | },
50 | "node_modules/arg": {
51 | "version": "4.1.3",
52 | "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
53 | "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="
54 | },
55 | "node_modules/assertion-error": {
56 | "version": "1.1.0",
57 | "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
58 | "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
59 | "engines": {
60 | "node": "*"
61 | }
62 | },
63 | "node_modules/balanced-match": {
64 | "version": "1.0.2",
65 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
66 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
67 | },
68 | "node_modules/brace-expansion": {
69 | "version": "1.1.11",
70 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
71 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
72 | "dependencies": {
73 | "balanced-match": "^1.0.0",
74 | "concat-map": "0.0.1"
75 | }
76 | },
77 | "node_modules/buffer-from": {
78 | "version": "1.1.2",
79 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
80 | "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
81 | },
82 | "node_modules/chai": {
83 | "version": "4.3.7",
84 | "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz",
85 | "integrity": "sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==",
86 | "dependencies": {
87 | "assertion-error": "^1.1.0",
88 | "check-error": "^1.0.2",
89 | "deep-eql": "^4.1.2",
90 | "get-func-name": "^2.0.0",
91 | "loupe": "^2.3.1",
92 | "pathval": "^1.1.1",
93 | "type-detect": "^4.0.5"
94 | },
95 | "engines": {
96 | "node": ">=4"
97 | }
98 | },
99 | "node_modules/check-error": {
100 | "version": "1.0.2",
101 | "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
102 | "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==",
103 | "engines": {
104 | "node": "*"
105 | }
106 | },
107 | "node_modules/concat-map": {
108 | "version": "0.0.1",
109 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
110 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
111 | },
112 | "node_modules/core-util-is": {
113 | "version": "1.0.3",
114 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
115 | "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
116 | },
117 | "node_modules/deep-eql": {
118 | "version": "4.1.3",
119 | "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz",
120 | "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==",
121 | "dependencies": {
122 | "type-detect": "^4.0.0"
123 | },
124 | "engines": {
125 | "node": ">=6"
126 | }
127 | },
128 | "node_modules/diff": {
129 | "version": "4.0.2",
130 | "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
131 | "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
132 | "engines": {
133 | "node": ">=0.3.1"
134 | }
135 | },
136 | "node_modules/fs.realpath": {
137 | "version": "1.0.0",
138 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
139 | "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
140 | },
141 | "node_modules/get-func-name": {
142 | "version": "2.0.0",
143 | "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz",
144 | "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==",
145 | "engines": {
146 | "node": "*"
147 | }
148 | },
149 | "node_modules/glob": {
150 | "version": "7.2.3",
151 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
152 | "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
153 | "dependencies": {
154 | "fs.realpath": "^1.0.0",
155 | "inflight": "^1.0.4",
156 | "inherits": "2",
157 | "minimatch": "^3.1.1",
158 | "once": "^1.3.0",
159 | "path-is-absolute": "^1.0.0"
160 | },
161 | "engines": {
162 | "node": "*"
163 | },
164 | "funding": {
165 | "url": "https://github.com/sponsors/isaacs"
166 | }
167 | },
168 | "node_modules/immediate": {
169 | "version": "3.0.6",
170 | "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
171 | "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
172 | },
173 | "node_modules/inflight": {
174 | "version": "1.0.6",
175 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
176 | "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
177 | "dependencies": {
178 | "once": "^1.3.0",
179 | "wrappy": "1"
180 | }
181 | },
182 | "node_modules/inherits": {
183 | "version": "2.0.4",
184 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
185 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
186 | },
187 | "node_modules/isarray": {
188 | "version": "1.0.0",
189 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
190 | "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
191 | },
192 | "node_modules/jszip": {
193 | "version": "3.10.1",
194 | "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
195 | "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
196 | "dependencies": {
197 | "lie": "~3.3.0",
198 | "pako": "~1.0.2",
199 | "readable-stream": "~2.3.6",
200 | "setimmediate": "^1.0.5"
201 | }
202 | },
203 | "node_modules/lie": {
204 | "version": "3.3.0",
205 | "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
206 | "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
207 | "dependencies": {
208 | "immediate": "~3.0.5"
209 | }
210 | },
211 | "node_modules/loupe": {
212 | "version": "2.3.6",
213 | "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz",
214 | "integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==",
215 | "dependencies": {
216 | "get-func-name": "^2.0.0"
217 | }
218 | },
219 | "node_modules/make-error": {
220 | "version": "1.3.6",
221 | "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
222 | "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="
223 | },
224 | "node_modules/minimatch": {
225 | "version": "3.1.2",
226 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
227 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
228 | "dependencies": {
229 | "brace-expansion": "^1.1.7"
230 | },
231 | "engines": {
232 | "node": "*"
233 | }
234 | },
235 | "node_modules/once": {
236 | "version": "1.4.0",
237 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
238 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
239 | "dependencies": {
240 | "wrappy": "1"
241 | }
242 | },
243 | "node_modules/pako": {
244 | "version": "1.0.11",
245 | "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
246 | "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
247 | },
248 | "node_modules/path-is-absolute": {
249 | "version": "1.0.1",
250 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
251 | "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
252 | "engines": {
253 | "node": ">=0.10.0"
254 | }
255 | },
256 | "node_modules/pathval": {
257 | "version": "1.1.1",
258 | "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz",
259 | "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==",
260 | "engines": {
261 | "node": "*"
262 | }
263 | },
264 | "node_modules/process-nextick-args": {
265 | "version": "2.0.1",
266 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
267 | "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
268 | },
269 | "node_modules/readable-stream": {
270 | "version": "2.3.8",
271 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
272 | "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
273 | "dependencies": {
274 | "core-util-is": "~1.0.0",
275 | "inherits": "~2.0.3",
276 | "isarray": "~1.0.0",
277 | "process-nextick-args": "~2.0.0",
278 | "safe-buffer": "~5.1.1",
279 | "string_decoder": "~1.1.1",
280 | "util-deprecate": "~1.0.1"
281 | }
282 | },
283 | "node_modules/rimraf": {
284 | "version": "3.0.2",
285 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
286 | "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
287 | "dependencies": {
288 | "glob": "^7.1.3"
289 | },
290 | "bin": {
291 | "rimraf": "bin.js"
292 | },
293 | "funding": {
294 | "url": "https://github.com/sponsors/isaacs"
295 | }
296 | },
297 | "node_modules/safe-buffer": {
298 | "version": "5.1.2",
299 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
300 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
301 | },
302 | "node_modules/selenium-webdriver": {
303 | "version": "4.8.1",
304 | "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.8.1.tgz",
305 | "integrity": "sha512-p4MtfhCQdcV6xxkS7eI0tQN6+WNReRULLCAuT4RDGkrjfObBNXMJ3WT8XdK+aXTr5nnBKuh+PxIevM0EjJgkxA==",
306 | "dependencies": {
307 | "jszip": "^3.10.0",
308 | "tmp": "^0.2.1",
309 | "ws": ">=8.11.0"
310 | },
311 | "engines": {
312 | "node": ">= 14.20.0"
313 | }
314 | },
315 | "node_modules/setimmediate": {
316 | "version": "1.0.5",
317 | "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
318 | "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
319 | },
320 | "node_modules/source-map": {
321 | "version": "0.6.1",
322 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
323 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
324 | "engines": {
325 | "node": ">=0.10.0"
326 | }
327 | },
328 | "node_modules/source-map-support": {
329 | "version": "0.5.21",
330 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
331 | "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
332 | "dependencies": {
333 | "buffer-from": "^1.0.0",
334 | "source-map": "^0.6.0"
335 | }
336 | },
337 | "node_modules/string_decoder": {
338 | "version": "1.1.1",
339 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
340 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
341 | "dependencies": {
342 | "safe-buffer": "~5.1.0"
343 | }
344 | },
345 | "node_modules/supports-color": {
346 | "version": "1.3.1",
347 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-1.3.1.tgz",
348 | "integrity": "sha512-OHbMkscHFRcNWEcW80fYhCrzAjheSIBwJChpFaBqA6zEz53nxumqi6ukciRb/UA0/v2nDNMk28ce/uBbYRDsng==",
349 | "bin": {
350 | "supports-color": "cli.js"
351 | },
352 | "engines": {
353 | "node": ">=0.8.0"
354 | }
355 | },
356 | "node_modules/term-color": {
357 | "version": "1.0.1",
358 | "resolved": "https://registry.npmjs.org/term-color/-/term-color-1.0.1.tgz",
359 | "integrity": "sha512-4Ld+sFlAdziaaMabvBU215dxyMotGoz7yN+9GtPE7RhKvzXAmg8tD/nKohJp4v2bMdSsNO3FEIBxFDsXu0Pf8w==",
360 | "dependencies": {
361 | "ansi-styles": "2.0.1",
362 | "supports-color": "1.3.1"
363 | }
364 | },
365 | "node_modules/tmp": {
366 | "version": "0.2.1",
367 | "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
368 | "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==",
369 | "dependencies": {
370 | "rimraf": "^3.0.0"
371 | },
372 | "engines": {
373 | "node": ">=8.17.0"
374 | }
375 | },
376 | "node_modules/ts-node": {
377 | "version": "8.10.2",
378 | "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.10.2.tgz",
379 | "integrity": "sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA==",
380 | "dependencies": {
381 | "arg": "^4.1.0",
382 | "diff": "^4.0.1",
383 | "make-error": "^1.1.1",
384 | "source-map-support": "^0.5.17",
385 | "yn": "3.1.1"
386 | },
387 | "bin": {
388 | "ts-node": "dist/bin.js",
389 | "ts-node-script": "dist/bin-script.js",
390 | "ts-node-transpile-only": "dist/bin-transpile.js",
391 | "ts-script": "dist/bin-script-deprecated.js"
392 | },
393 | "engines": {
394 | "node": ">=6.0.0"
395 | },
396 | "peerDependencies": {
397 | "typescript": ">=2.7"
398 | }
399 | },
400 | "node_modules/type-detect": {
401 | "version": "4.0.8",
402 | "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
403 | "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
404 | "engines": {
405 | "node": ">=4"
406 | }
407 | },
408 | "node_modules/typescript": {
409 | "version": "3.9.10",
410 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz",
411 | "integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==",
412 | "bin": {
413 | "tsc": "bin/tsc",
414 | "tsserver": "bin/tsserver"
415 | },
416 | "engines": {
417 | "node": ">=4.2.0"
418 | }
419 | },
420 | "node_modules/util-deprecate": {
421 | "version": "1.0.2",
422 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
423 | "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
424 | },
425 | "node_modules/wrappy": {
426 | "version": "1.0.2",
427 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
428 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
429 | },
430 | "node_modules/ws": {
431 | "version": "8.13.0",
432 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
433 | "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==",
434 | "engines": {
435 | "node": ">=10.0.0"
436 | },
437 | "peerDependencies": {
438 | "bufferutil": "^4.0.1",
439 | "utf-8-validate": ">=5.0.2"
440 | },
441 | "peerDependenciesMeta": {
442 | "bufferutil": {
443 | "optional": true
444 | },
445 | "utf-8-validate": {
446 | "optional": true
447 | }
448 | }
449 | },
450 | "node_modules/yn": {
451 | "version": "3.1.1",
452 | "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
453 | "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
454 | "engines": {
455 | "node": ">=6"
456 | }
457 | }
458 | },
459 | "dependencies": {
460 | "@types/node": {
461 | "version": "12.20.55",
462 | "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz",
463 | "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="
464 | },
465 | "@types/selenium-webdriver": {
466 | "version": "4.1.13",
467 | "resolved": "https://registry.npmjs.org/@types/selenium-webdriver/-/selenium-webdriver-4.1.13.tgz",
468 | "integrity": "sha512-kGpIh7bvu4HGCJXl4PEJ53kzpG4iXlRDd66SNNCfJ58QhFuk9skOm57lVffZap5ChEOJwbge/LJ9IVGVC8EEOg==",
469 | "requires": {
470 | "@types/ws": "*"
471 | }
472 | },
473 | "@types/ws": {
474 | "version": "8.5.4",
475 | "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz",
476 | "integrity": "sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==",
477 | "requires": {
478 | "@types/node": "*"
479 | }
480 | },
481 | "ansi-styles": {
482 | "version": "2.0.1",
483 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.0.1.tgz",
484 | "integrity": "sha512-0zjsXMhnTibRx8YrLgLKb5NvWEcHN/OZEe1NzR8VVrEM6xr7/NyLsoMVelAhaoJhOtpuexaeRGD8MF8Z64+9LQ=="
485 | },
486 | "arg": {
487 | "version": "4.1.3",
488 | "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
489 | "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="
490 | },
491 | "assertion-error": {
492 | "version": "1.1.0",
493 | "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
494 | "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw=="
495 | },
496 | "balanced-match": {
497 | "version": "1.0.2",
498 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
499 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
500 | },
501 | "brace-expansion": {
502 | "version": "1.1.11",
503 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
504 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
505 | "requires": {
506 | "balanced-match": "^1.0.0",
507 | "concat-map": "0.0.1"
508 | }
509 | },
510 | "buffer-from": {
511 | "version": "1.1.2",
512 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
513 | "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
514 | },
515 | "chai": {
516 | "version": "4.3.7",
517 | "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz",
518 | "integrity": "sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==",
519 | "requires": {
520 | "assertion-error": "^1.1.0",
521 | "check-error": "^1.0.2",
522 | "deep-eql": "^4.1.2",
523 | "get-func-name": "^2.0.0",
524 | "loupe": "^2.3.1",
525 | "pathval": "^1.1.1",
526 | "type-detect": "^4.0.5"
527 | }
528 | },
529 | "check-error": {
530 | "version": "1.0.2",
531 | "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
532 | "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA=="
533 | },
534 | "concat-map": {
535 | "version": "0.0.1",
536 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
537 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
538 | },
539 | "core-util-is": {
540 | "version": "1.0.3",
541 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
542 | "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
543 | },
544 | "deep-eql": {
545 | "version": "4.1.3",
546 | "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz",
547 | "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==",
548 | "requires": {
549 | "type-detect": "^4.0.0"
550 | }
551 | },
552 | "diff": {
553 | "version": "4.0.2",
554 | "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
555 | "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A=="
556 | },
557 | "fs.realpath": {
558 | "version": "1.0.0",
559 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
560 | "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
561 | },
562 | "get-func-name": {
563 | "version": "2.0.0",
564 | "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz",
565 | "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig=="
566 | },
567 | "glob": {
568 | "version": "7.2.3",
569 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
570 | "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
571 | "requires": {
572 | "fs.realpath": "^1.0.0",
573 | "inflight": "^1.0.4",
574 | "inherits": "2",
575 | "minimatch": "^3.1.1",
576 | "once": "^1.3.0",
577 | "path-is-absolute": "^1.0.0"
578 | }
579 | },
580 | "immediate": {
581 | "version": "3.0.6",
582 | "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
583 | "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
584 | },
585 | "inflight": {
586 | "version": "1.0.6",
587 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
588 | "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
589 | "requires": {
590 | "once": "^1.3.0",
591 | "wrappy": "1"
592 | }
593 | },
594 | "inherits": {
595 | "version": "2.0.4",
596 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
597 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
598 | },
599 | "isarray": {
600 | "version": "1.0.0",
601 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
602 | "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
603 | },
604 | "jszip": {
605 | "version": "3.10.1",
606 | "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
607 | "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
608 | "requires": {
609 | "lie": "~3.3.0",
610 | "pako": "~1.0.2",
611 | "readable-stream": "~2.3.6",
612 | "setimmediate": "^1.0.5"
613 | }
614 | },
615 | "lie": {
616 | "version": "3.3.0",
617 | "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
618 | "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
619 | "requires": {
620 | "immediate": "~3.0.5"
621 | }
622 | },
623 | "loupe": {
624 | "version": "2.3.6",
625 | "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz",
626 | "integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==",
627 | "requires": {
628 | "get-func-name": "^2.0.0"
629 | }
630 | },
631 | "make-error": {
632 | "version": "1.3.6",
633 | "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
634 | "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="
635 | },
636 | "minimatch": {
637 | "version": "3.1.2",
638 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
639 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
640 | "requires": {
641 | "brace-expansion": "^1.1.7"
642 | }
643 | },
644 | "once": {
645 | "version": "1.4.0",
646 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
647 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
648 | "requires": {
649 | "wrappy": "1"
650 | }
651 | },
652 | "pako": {
653 | "version": "1.0.11",
654 | "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
655 | "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
656 | },
657 | "path-is-absolute": {
658 | "version": "1.0.1",
659 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
660 | "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="
661 | },
662 | "pathval": {
663 | "version": "1.1.1",
664 | "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz",
665 | "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ=="
666 | },
667 | "process-nextick-args": {
668 | "version": "2.0.1",
669 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
670 | "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
671 | },
672 | "readable-stream": {
673 | "version": "2.3.8",
674 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
675 | "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
676 | "requires": {
677 | "core-util-is": "~1.0.0",
678 | "inherits": "~2.0.3",
679 | "isarray": "~1.0.0",
680 | "process-nextick-args": "~2.0.0",
681 | "safe-buffer": "~5.1.1",
682 | "string_decoder": "~1.1.1",
683 | "util-deprecate": "~1.0.1"
684 | }
685 | },
686 | "rimraf": {
687 | "version": "3.0.2",
688 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
689 | "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
690 | "requires": {
691 | "glob": "^7.1.3"
692 | }
693 | },
694 | "safe-buffer": {
695 | "version": "5.1.2",
696 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
697 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
698 | },
699 | "selenium-webdriver": {
700 | "version": "4.8.1",
701 | "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.8.1.tgz",
702 | "integrity": "sha512-p4MtfhCQdcV6xxkS7eI0tQN6+WNReRULLCAuT4RDGkrjfObBNXMJ3WT8XdK+aXTr5nnBKuh+PxIevM0EjJgkxA==",
703 | "requires": {
704 | "jszip": "^3.10.0",
705 | "tmp": "^0.2.1",
706 | "ws": ">=8.11.0"
707 | }
708 | },
709 | "setimmediate": {
710 | "version": "1.0.5",
711 | "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
712 | "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
713 | },
714 | "source-map": {
715 | "version": "0.6.1",
716 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
717 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
718 | },
719 | "source-map-support": {
720 | "version": "0.5.21",
721 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
722 | "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
723 | "requires": {
724 | "buffer-from": "^1.0.0",
725 | "source-map": "^0.6.0"
726 | }
727 | },
728 | "string_decoder": {
729 | "version": "1.1.1",
730 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
731 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
732 | "requires": {
733 | "safe-buffer": "~5.1.0"
734 | }
735 | },
736 | "supports-color": {
737 | "version": "1.3.1",
738 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-1.3.1.tgz",
739 | "integrity": "sha512-OHbMkscHFRcNWEcW80fYhCrzAjheSIBwJChpFaBqA6zEz53nxumqi6ukciRb/UA0/v2nDNMk28ce/uBbYRDsng=="
740 | },
741 | "term-color": {
742 | "version": "1.0.1",
743 | "resolved": "https://registry.npmjs.org/term-color/-/term-color-1.0.1.tgz",
744 | "integrity": "sha512-4Ld+sFlAdziaaMabvBU215dxyMotGoz7yN+9GtPE7RhKvzXAmg8tD/nKohJp4v2bMdSsNO3FEIBxFDsXu0Pf8w==",
745 | "requires": {
746 | "ansi-styles": "2.0.1",
747 | "supports-color": "1.3.1"
748 | }
749 | },
750 | "tmp": {
751 | "version": "0.2.1",
752 | "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
753 | "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==",
754 | "requires": {
755 | "rimraf": "^3.0.0"
756 | }
757 | },
758 | "ts-node": {
759 | "version": "8.10.2",
760 | "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.10.2.tgz",
761 | "integrity": "sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA==",
762 | "requires": {
763 | "arg": "^4.1.0",
764 | "diff": "^4.0.1",
765 | "make-error": "^1.1.1",
766 | "source-map-support": "^0.5.17",
767 | "yn": "3.1.1"
768 | }
769 | },
770 | "type-detect": {
771 | "version": "4.0.8",
772 | "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
773 | "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g=="
774 | },
775 | "typescript": {
776 | "version": "3.9.10",
777 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz",
778 | "integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q=="
779 | },
780 | "util-deprecate": {
781 | "version": "1.0.2",
782 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
783 | "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
784 | },
785 | "wrappy": {
786 | "version": "1.0.2",
787 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
788 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
789 | },
790 | "ws": {
791 | "version": "8.13.0",
792 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
793 | "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==",
794 | "requires": {}
795 | },
796 | "yn": {
797 | "version": "3.1.1",
798 | "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
799 | "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q=="
800 | }
801 | }
802 | }
803 |
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
1 | #![deny(clippy::all)]
2 | #![warn(clippy::pedantic)]
3 | #![allow(
4 | clippy::non_ascii_literal,
5 | clippy::single_match_else,
6 | clippy::if_not_else,
7 | clippy::similar_names,
8 | clippy::module_name_repetitions,
9 | clippy::must_use_candidate,
10 | clippy::unused_unit, // TODO: Remove once https://github.com/rustwasm/wasm-bindgen/issues/2774 is released
11 | clippy::manual_let_else,
12 | )]
13 |
14 | #[macro_use]
15 | extern crate log;
16 |
17 | mod extract;
18 | mod selection;
19 | mod utils;
20 |
21 | use cfg_if::cfg_if;
22 | use log::Level;
23 | use wasm_bindgen::{prelude::*, JsCast};
24 | use web_sys::{self, Element, HtmlDocument, HtmlElement, Node, Range, Selection, Text};
25 |
26 | use crate::{
27 | extract::extract_text,
28 | selection::{activate_selection_range, glue_range_to_text, set_selection_range, Position},
29 | };
30 |
31 | cfg_if! {
32 | // When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
33 | // allocator.
34 | if #[cfg(feature = "wee_alloc")] {
35 | extern crate wee_alloc;
36 | #[global_allocator]
37 | static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
38 | }
39 | }
40 |
41 | /// The context object containing the state.
42 | #[wasm_bindgen]
43 | pub struct ComposeArea {
44 | window: web_sys::Window,
45 | document: web_sys::Document,
46 | wrapper: Element,
47 | /// The selection range. This will always be a selection within the compose
48 | /// area wrapper, if set.
49 | ///
50 | /// NOTE: When setting this value to a range, make sure that the range was
51 | /// cloned, so that updates to the range in the browser aren't reflected in
52 | /// this instance.
53 | selection_range: Option,
54 | /// Counter used for creating unique element IDs.
55 | counter: u32,
56 | }
57 |
58 | /// This enum is relevant when determining the current node while the caret is
59 | /// exactly between two nodes.
60 | ///
61 | /// Depending on this enum value, the node before or after the cursor is returned.
62 | #[derive(Debug, PartialEq, Eq, Copy, Clone)]
63 | pub enum Direction {
64 | Before,
65 | After,
66 | }
67 |
68 | #[wasm_bindgen]
69 | #[derive(Debug, Clone)]
70 | pub struct RangeResult {
71 | /// The selection range, if any.
72 | range: Option,
73 | /// Whether the selection range is not fully contained in the wrapper.
74 | /// This is set to `false` if no range could be found.
75 | outside: bool,
76 | }
77 |
78 | impl RangeResult {
79 | fn contained(range: Range) -> Self {
80 | Self {
81 | range: Some(range),
82 | outside: false,
83 | }
84 | }
85 |
86 | fn outside(range: Range) -> Self {
87 | Self {
88 | range: Some(range),
89 | outside: true,
90 | }
91 | }
92 |
93 | fn none() -> Self {
94 | Self {
95 | range: None,
96 | outside: false,
97 | }
98 | }
99 | }
100 |
101 | #[wasm_bindgen]
102 | impl RangeResult {
103 | fn format_node(node: &Node) -> String {
104 | let name = node.node_name();
105 | match node.dyn_ref::().map(Element::id) {
106 | Some(id) => format!("{}#{}", name.trim_matches('#'), id),
107 | None => name.trim_matches('#').to_string(),
108 | }
109 | }
110 |
111 | /// Return a compact or non-compact string representation of the range.
112 | fn to_string_impl(&self, compact: bool) -> String {
113 | match (&self.range, self.outside) {
114 | (_, true) => "Outside".to_string(),
115 | (None, _) => "None".to_string(),
116 | (Some(range), false) => {
117 | let sc = range.start_container().ok();
118 | let so = range.start_offset().ok();
119 | let ec = range.end_container().ok();
120 | let eo = range.end_offset().ok();
121 | match (sc, so, ec, eo, compact) {
122 | (Some(sc), Some(so), Some(ec), Some(eo), true) => {
123 | format!(
124 | "Range({}~{}, {}~{})",
125 | Self::format_node(&sc),
126 | &so,
127 | Self::format_node(&ec),
128 | &eo,
129 | )
130 | }
131 | (Some(sc), Some(so), Some(ec), Some(eo), false) => {
132 | format!(
133 | "Range {{\n \
134 | start: {} ~ {}\n \
135 | end: {} ~ {}\n\
136 | }}",
137 | Self::format_node(&sc),
138 | &so,
139 | Self::format_node(&ec),
140 | &eo,
141 | )
142 | }
143 | _ => "Incomplete Range".to_string(),
144 | }
145 | }
146 | }
147 | }
148 |
149 | /// Used by JS code to show a string representation of the range.
150 | #[allow(clippy::inherent_to_string)]
151 | pub fn to_string(&self) -> String {
152 | self.to_string_impl(false)
153 | }
154 |
155 | /// Used by JS code to show a string representation of the range.
156 | pub fn to_string_compact(&self) -> String {
157 | self.to_string_impl(true)
158 | }
159 | }
160 |
161 | #[wasm_bindgen]
162 | #[derive(Debug, Clone)]
163 | pub struct WordAtCaret {
164 | node: Node,
165 | before: String,
166 | after: String,
167 | offsets: (u32, u32),
168 | }
169 |
170 | #[wasm_bindgen]
171 | impl WordAtCaret {
172 | pub fn node(&self) -> Node {
173 | self.node.clone()
174 | }
175 |
176 | pub fn before(&self) -> String {
177 | self.before.clone()
178 | }
179 |
180 | pub fn after(&self) -> String {
181 | self.after.clone()
182 | }
183 |
184 | /// Return the UTF16 offset from the start node where the current word starts (inclusive).
185 | pub fn start_offset(&self) -> u32 {
186 | self.offsets.0
187 | }
188 |
189 | /// Return the UTF16 offset from the start node where the current word ends (exclusive).
190 | pub fn end_offset(&self) -> u32 {
191 | self.offsets.1
192 | }
193 | }
194 |
195 | #[wasm_bindgen]
196 | impl ComposeArea {
197 | /// Initialize a new compose area wrapper.
198 | ///
199 | /// If the `log_level` argument is supplied, the console logger is
200 | /// initialized. Valid log levels: `trace`, `debug`, `info`, `warn` or
201 | /// `error`.
202 | pub fn bind_to(wrapper: Element, log_level: Option) -> Self {
203 | utils::set_panic_hook();
204 |
205 | // Set log level
206 | if let Some(level) = log_level {
207 | match &*level {
208 | "trace" => utils::init_log(Level::Trace),
209 | "debug" => utils::init_log(Level::Debug),
210 | "info" => utils::init_log(Level::Info),
211 | "warn" => utils::init_log(Level::Warn),
212 | "error" => utils::init_log(Level::Error),
213 | other => {
214 | web_sys::console::warn_1(
215 | &format!("bind_to: Invalid log level: {other}").into(),
216 | );
217 | }
218 | }
219 | }
220 | trace!("[compose_area] bind_to");
221 |
222 | let window = web_sys::window().expect("No global `window` exists");
223 | let document = window.document().expect("Should have a document on window");
224 |
225 | // Initialize the wrapper element
226 | wrapper
227 | .class_list()
228 | .add_2("cawrapper", "initialized")
229 | .expect("Could not add wrapper classes");
230 | wrapper
231 | .set_attribute("contenteditable", "true")
232 | .expect("Could not set contenteditable attr");
233 |
234 | info!("[compose_area] Initialized");
235 |
236 | Self {
237 | window,
238 | document,
239 | wrapper,
240 | selection_range: None,
241 | counter: 0,
242 | }
243 | }
244 |
245 | /// Store the current selection range.
246 | /// Return the stored range.
247 | pub fn store_selection_range(&mut self) -> RangeResult {
248 | trace!("[compose_area] store_selection_range");
249 | let range_result = self.fetch_range();
250 | trace!(
251 | "[compose_area] Range: {}",
252 | range_result.to_string().replace('\n', "")
253 | );
254 |
255 | // Ignore selections outside the wrapper
256 | if !range_result.outside {
257 | // Note: We need to clone the range object. Otherwise, changes to the
258 | // range in the DOM will be reflected in our stored reference.
259 | self.selection_range = range_result.clone().range.map(|range| range.clone_range());
260 | }
261 |
262 | range_result
263 | }
264 |
265 | /// Restore the stored selection range.
266 | ///
267 | /// Return a boolean indicating whether a selection range was stored (and
268 | /// thus restored).
269 | pub fn restore_selection_range(&self) -> bool {
270 | trace!("[compose_area] restore_selection_range");
271 | if let Some(ref range) = self.selection_range {
272 | // Get the current selection
273 | let selection = match self.fetch_selection() {
274 | Some(selection) => selection,
275 | None => {
276 | error!("[compose_area] No selection found");
277 | return false;
278 | }
279 | };
280 |
281 | // Restore the range
282 | if selection.remove_all_ranges().is_err() {
283 | error!("[compose_area] Removing all ranges failed");
284 | }
285 | match selection.add_range(range) {
286 | Ok(_) => true,
287 | Err(_) => {
288 | error!("[compose_area] Adding range failed");
289 | false
290 | }
291 | }
292 | } else {
293 | trace!("[compose_area] No stored range");
294 | false
295 | }
296 | }
297 |
298 | /// Ensure that there's an active selection inside the compose are. Then
299 | /// exec the specified command, normalize the compose area and store the
300 | /// new selection range.
301 | fn exec_command(&mut self, command_id: &str, value: &str) {
302 | // Ensure that there's an active selection inside the compose area.
303 | let active_range = self.fetch_range();
304 | if active_range.range.is_none() || active_range.outside {
305 | // No active selection range inside the compose area.
306 | match self.selection_range {
307 | Some(ref range) => {
308 | activate_selection_range(
309 | &self
310 | .fetch_selection()
311 | .expect("Could not get window selection"),
312 | range,
313 | );
314 | }
315 | None => {
316 | // No stored selection range. Create a new selection at the end end.
317 | let last_child_node = utils::get_last_child(&self.wrapper);
318 | self.selection_range = match last_child_node {
319 | Some(ref node) => {
320 | // Insert at the very end, unless the last element in the
321 | // area is a `
` node. This is needed because Firefox
322 | // always adds a trailing newline that isn't rendered
323 | let mut insert_before = false;
324 | if let Some(element) = node.dyn_ref::() {
325 | if element.tag_name() == "BR" {
326 | insert_before = true;
327 | }
328 | }
329 | if insert_before {
330 | set_selection_range(&Position::Before(node), None)
331 | } else {
332 | set_selection_range(&Position::After(node), None)
333 | }
334 | }
335 | None => set_selection_range(&Position::Offset(&self.wrapper, 0), None),
336 | }
337 | .map(|range| range.clone_range());
338 | }
339 | }
340 | }
341 |
342 | // Execute command
343 | self.document
344 | .dyn_ref::()
345 | .expect("Document is not a HtmlDocument")
346 | .exec_command_with_show_ui_and_value(command_id, false, value)
347 | .expect("Could not exec command");
348 | self.normalize();
349 | self.store_selection_range();
350 | }
351 |
352 | /// Return and increment the counter variable.
353 | fn get_counter(&mut self) -> u32 {
354 | let val = self.counter;
355 | self.counter += 1;
356 | val
357 | }
358 |
359 | /// Insert an image at the current caret position.
360 | ///
361 | /// Return a reference to the inserted image element.
362 | pub fn insert_image(&mut self, src: &str, alt: &str, cls: &str) -> HtmlElement {
363 | debug!("[compose_area] insert_image ({})", &alt);
364 |
365 | // NOTE: Ideally we'd create an image node here and would then use
366 | // `insert_node`. But unfortunately that will not modify the undo
367 | // stack of the browser (see https://stackoverflow.com/a/15895618).
368 | // Thus, we need to resort to an ugly `execCommand` with a HTML
369 | // string. Furthermore, we need to create a random ID in order
370 | // to be able to find the image again in the DOM.
371 |
372 | let img_id = format!("__$$compose_area_img_{}", self.get_counter());
373 | let html = format!(
374 | "
",
375 | img_id,
376 | src.replace('"', ""),
377 | alt.replace('"', ""),
378 | cls.replace('"', ""),
379 | );
380 | self.insert_html(&html);
381 |
382 | self.document
383 | .get_element_by_id(&img_id)
384 | .expect("Could not find inserted image node")
385 | .dyn_into::()
386 | .expect("Could not cast image element into HtmlElement")
387 | }
388 |
389 | /// Insert plain text at the current caret position.
390 | pub fn insert_text(&mut self, text: &str) {
391 | debug!("[compose_area] insert_text ({})", text);
392 | self.exec_command("insertText", text);
393 | }
394 |
395 | /// Insert HTML at the current caret position.
396 | ///
397 | /// Note: This is potentially dangerous, make sure that you only insert
398 | /// HTML from trusted sources!
399 | pub fn insert_html(&mut self, html: &str) {
400 | debug!("[compose_area] insert_html ({})", html);
401 | self.exec_command("insertHTML", html);
402 | }
403 |
404 | /// Insert the specified node at the previously stored selection range.
405 | /// Set the caret position to right after the newly inserted node.
406 | ///
407 | /// **NOTE:** Due to browser limitations, this will not result in a new
408 | /// entry in the browser's internal undo stack. This means that the node
409 | /// insertion cannot be undone using Ctrl+Z.
410 | pub fn insert_node(&mut self, node_ref: &Node) {
411 | debug!("[compose_area] insert_node");
412 |
413 | // Insert the node
414 | if let Some(ref range) = self.selection_range {
415 | range
416 | .delete_contents()
417 | .expect("Could not remove selection contents");
418 | range.insert_node(node_ref).expect("Could not insert node");
419 | } else {
420 | // No current selection. Append at end, unless the last element in
421 | // the area is a `
` node. This is needed because Firefox always
422 | // adds a trailing newline that isn't rendered.
423 | let last_child_node = utils::get_last_child(&self.wrapper);
424 | match last_child_node.and_then(|n| n.dyn_into::().ok()) {
425 | Some(ref element) if element.tag_name() == "BR" => {
426 | self.wrapper
427 | .insert_before(node_ref, Some(element))
428 | .expect("Could not insert child");
429 | }
430 | Some(_) | None => {
431 | self.wrapper
432 | .append_child(node_ref)
433 | .expect("Could not append child");
434 | }
435 | };
436 | }
437 |
438 | // Update selection
439 | self.selection_range =
440 | set_selection_range(&Position::After(node_ref), None).map(|range| range.clone_range());
441 |
442 | // Normalize elements
443 | self.normalize();
444 | }
445 |
446 | /// Normalize the contents of the wrapper element.
447 | ///
448 | /// See
449 | fn normalize(&self) {
450 | trace!("[compose_area] normalize");
451 | self.wrapper.normalize();
452 | }
453 |
454 | /// Return the DOM selection.
455 | fn fetch_selection(&self) -> Option {
456 | trace!("[compose_area] fetch_selection");
457 | self.window
458 | .get_selection()
459 | .expect("Could not get selection from window")
460 | }
461 |
462 | /// Return the last range of the selection that is within the wrapper
463 | /// element.
464 | pub fn fetch_range(&self) -> RangeResult {
465 | trace!("[compose_area] fetch_range");
466 | let selection = match self.fetch_selection() {
467 | Some(sel) => sel,
468 | None => {
469 | error!("[compose_area] Could not find selection");
470 | return RangeResult::none();
471 | }
472 | };
473 | let mut candidate: Option = None;
474 | for i in 0..selection.range_count() {
475 | let range = selection
476 | .get_range_at(i)
477 | .expect("Could not get range from selection");
478 | candidate = Some(range.clone());
479 | let container = range
480 | .common_ancestor_container()
481 | .expect("Could not get common ancestor container for range");
482 | if self.wrapper.contains(Some(&container)) {
483 | return RangeResult::contained(range);
484 | }
485 | }
486 | match candidate {
487 | Some(range) => RangeResult::outside(range),
488 | None => RangeResult::none(),
489 | }
490 | }
491 |
492 | /// Extract the text in the compose area.
493 | ///
494 | /// Convert elements like images to alt text.
495 | ///
496 | /// Args:
497 | /// - `no_trim`: If set to `true`, don't trim leading / trailing whitespace
498 | /// from returned text. Default: `false`.
499 | pub fn get_text(&self, no_trim: Option) -> String {
500 | debug!("[compose_area] get_text");
501 | extract_text(&self.wrapper, no_trim.unwrap_or(false))
502 | }
503 |
504 | /// Return whether the compose area is empty.
505 | ///
506 | /// Note: Right now this is a convenience wrapper around
507 | /// `get_text(no_trim).length === 0`, but it might get optimized in the
508 | /// future.
509 | ///
510 | /// Args:
511 | /// - `no_trim`: If set to `true`, don't trim leading / trailing whitespace
512 | /// from returned text. Default: `false`.
513 | pub fn is_empty(&self, no_trim: Option) -> bool {
514 | debug!("[compose_area] is_empty");
515 | extract_text(&self.wrapper, no_trim.unwrap_or(false)).is_empty()
516 | }
517 |
518 | /// Focus the compose area.
519 | pub fn focus(&self) {
520 | debug!("[compose_area] focus");
521 | self.restore_selection_range();
522 | if let Some(e) = self.wrapper.dyn_ref::() {
523 | e.focus()
524 | .unwrap_or_else(|_| error!("[compose_area] Could not focus compose area"));
525 | }
526 | }
527 |
528 | /// Clear the contents of the compose area.
529 | pub fn clear(&mut self) {
530 | debug!("[compose_area] clear");
531 | while self.wrapper.has_child_nodes() {
532 | let last_child = self
533 | .wrapper
534 | .last_child()
535 | .expect("Could not find last child");
536 | self.wrapper
537 | .remove_child(&last_child)
538 | .expect("Could not remove last child");
539 | }
540 | self.selection_range = None;
541 | }
542 |
543 | /// Return the word (whitespace delimited) at the current caret position.
544 | ///
545 | /// Note: This methods uses the range that was last set with
546 | /// `store_selection_range`.
547 | pub fn get_word_at_caret(&mut self) -> Option {
548 | debug!("[compose_area] get_word_at_caret");
549 |
550 | if let Some(ref range) = self.selection_range {
551 | // Clone the current range so we don't modify any existing selection
552 | let mut range = range.clone_range();
553 |
554 | // Ensure that range is relative to a text node
555 | if !glue_range_to_text(&mut range) {
556 | return None;
557 | }
558 |
559 | // Get the container element (which is the same for start and end
560 | // since the range is collapsed) and offset. After having called
561 | // the `glue_range_to_text` function, this will be a text node.
562 | let node: Text = range
563 | .start_container()
564 | .expect("Could not get start container")
565 | .dyn_into::()
566 | .expect("Node is not a text node");
567 | let offset: u32 = range.start_offset().expect("Could not get start offset");
568 |
569 | // Note that the offset refers to JS characters, not bytes.
570 | let text: String = node.data();
571 | let mut before: Vec = vec![];
572 | let mut after: Vec = vec![];
573 | let mut start = 0;
574 | let mut end = 0;
575 | let is_word_boundary = |c: u16| c == 0x20 /* space */ || c == 0x09 /* tab */;
576 | for (i, c) in text.encode_utf16().enumerate() {
577 | if i < offset as usize {
578 | if is_word_boundary(c) {
579 | before.clear();
580 | start = i + 1;
581 | } else {
582 | before.push(c);
583 | }
584 | } else {
585 | if is_word_boundary(c) {
586 | end = i;
587 | break;
588 | }
589 | after.push(c);
590 | }
591 | }
592 | if end <= start {
593 | end = text.encode_utf16().count();
594 | }
595 |
596 | // Note: Decoding should not be able to fail since it was
597 | // previously encoded from a string.
598 | #[allow(clippy::cast_possible_truncation)]
599 | Some(WordAtCaret {
600 | node: node
601 | .dyn_into::()
602 | .expect("Could not turn Text into Node"),
603 | before: String::from_utf16(&before).expect("Could not decode UTF16 value"),
604 | after: String::from_utf16(&after).expect("Could not decode UTF16 value"),
605 | offsets: (start as u32, end as u32),
606 | })
607 | } else {
608 | None
609 | }
610 | }
611 |
612 | /// Select the word (whitespace delimited) at the current caret position.
613 | ///
614 | /// Note: This methods uses the range that was last set with
615 | /// `store_selection_range`.
616 | pub fn select_word_at_caret(&mut self) -> bool {
617 | debug!("[compose_area] select_word_at_caret");
618 |
619 | if let Some(wac) = self.get_word_at_caret() {
620 | let node = wac.node();
621 | set_selection_range(
622 | &Position::Offset(&node, wac.start_offset()),
623 | Some(&Position::Offset(&node, wac.end_offset())),
624 | )
625 | .is_some()
626 | } else {
627 | false
628 | }
629 | }
630 | }
631 |
632 | #[cfg(test)]
633 | mod tests {
634 | use super::*;
635 |
636 | use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure};
637 |
638 | wasm_bindgen_test_configure!(run_in_browser);
639 |
640 | fn init() -> ComposeArea {
641 | // Get references
642 | let window = web_sys::window().expect("No global `window` exists");
643 | let document = window.document().expect("Should have a document on window");
644 |
645 | // Create wrapper element
646 | let wrapper = document
647 | .create_element("div")
648 | .expect("Could not create wrapper div");
649 | wrapper
650 | .set_attribute("style", "white-space: pre-wrap;")
651 | .expect("Could not set style on wrapper div");
652 | document.body().unwrap().append_child(&wrapper).unwrap();
653 |
654 | // Bind to wrapper
655 | ComposeArea::bind_to(wrapper, Some("trace".into()))
656 | }
657 |
658 | /// Create and return a text node.
659 | fn text_node(ca: &ComposeArea, text: &str) -> Node {
660 | ca.document.create_text_node(text).unchecked_into()
661 | }
662 |
663 | /// Create and return a newline node.
664 | fn element_node(ca: &ComposeArea, name: &str) -> Node {
665 | ca.document.create_element(name).unwrap().unchecked_into()
666 | }
667 |
668 | #[derive(Copy, Clone, Debug)]
669 | struct Img {
670 | src: &'static str,
671 | alt: &'static str,
672 | cls: &'static str,
673 | }
674 |
675 | impl Img {
676 | fn html(&self, counter: u32) -> String {
677 | format!(
678 | r#"
"#,
679 | counter, self.src, self.alt, self.cls,
680 | )
681 | }
682 |
683 | fn as_node(&self, ca: &ComposeArea) -> Node {
684 | let img = ca.document.create_element("img").unwrap();
685 | img.set_attribute("src", self.src).unwrap();
686 | img.set_attribute("alt", self.alt).unwrap();
687 | img.set_attribute("class", self.cls).unwrap();
688 | img.unchecked_into()
689 | }
690 | }
691 |
692 | mod insert_node {
693 | use super::*;
694 |
695 | use std::convert::TryFrom;
696 |
697 | struct PositionByIndex {
698 | /// The index of the child nodes.
699 | ///
700 | /// For example, `[1]` means "the second child node". `[1, 0]`
701 | /// means the first child node of the first child node.
702 | node_index: Vec,
703 | offset: Option,
704 | }
705 |
706 | impl PositionByIndex {
707 | fn offset(node_index: usize, offset: u32) -> Self {
708 | Self {
709 | node_index: vec![node_index],
710 | offset: Some(offset),
711 | }
712 | }
713 |
714 | fn after(node_index: usize) -> Self {
715 | Self {
716 | node_index: vec![node_index],
717 | offset: None,
718 | }
719 | }
720 |
721 | fn after_nested(node_index: Vec) -> Self {
722 | Self {
723 | node_index,
724 | offset: None,
725 | }
726 | }
727 | }
728 |
729 | struct InsertNodeTest {
730 | children: Vec,
731 | selection_start: PositionByIndex,
732 | selection_end: Option,
733 | node: N,
734 | final_html: String,
735 | }
736 |
737 | impl InsertNodeTest {
738 | fn get_node(&self, indices: &[usize]) -> Node {
739 | assert!(!indices.is_empty());
740 | let mut node: Node = self.children.as_slice().get(indices[0]).unwrap().clone();
741 | for i in indices.iter().skip(1) {
742 | node = node
743 | .unchecked_ref::()
744 | .child_nodes()
745 | .item(u32::try_from(*i).unwrap())
746 | .expect("Child node not found");
747 | }
748 | node
749 | }
750 |
751 | fn do_test(&self, ca: &mut ComposeArea, insert_func: F)
752 | where
753 | F: FnOnce(&mut ComposeArea, &N),
754 | {
755 | // Add child nodes
756 | for child in &self.children {
757 | ca.wrapper.append_child(child).unwrap();
758 | }
759 |
760 | // Add selection
761 | let node_start = self.get_node(&self.selection_start.node_index);
762 | let pos_start = {
763 | match self.selection_start.offset {
764 | Some(offset) => Position::Offset(&node_start, offset),
765 | None => Position::After(&node_start),
766 | }
767 | };
768 | match self.selection_end {
769 | Some(ref sel) => {
770 | let node_end = self.get_node(&sel.node_index);
771 | set_selection_range(
772 | &pos_start,
773 | Some(&match sel.offset {
774 | Some(offset) => Position::Offset(&node_end, offset),
775 | None => Position::After(&node_end),
776 | }),
777 | )
778 | }
779 | None => set_selection_range(&pos_start, None),
780 | };
781 |
782 | // Insert node and verify
783 | ca.store_selection_range();
784 | insert_func(ca, &self.node);
785 | assert_eq!(ca.wrapper.inner_html(), self.final_html);
786 | }
787 | }
788 |
789 | impl InsertNodeTest<&'static str> {
790 | fn test(&self, ca: &mut ComposeArea) {
791 | self.do_test(ca, |ca, node| {
792 | ca.insert_text(node);
793 | });
794 | }
795 | }
796 |
797 | impl InsertNodeTest
{
798 | fn test(&self, ca: &mut ComposeArea) {
799 | self.do_test(ca, |ca, node| {
800 | ca.insert_image(node.src, node.alt, node.cls);
801 | });
802 | }
803 | }
804 |
805 | mod text {
806 | use super::*;
807 |
808 | #[wasm_bindgen_test]
809 | fn at_end() {
810 | let mut ca = init();
811 | InsertNodeTest {
812 | children: vec![text_node(&ca, "hello ")],
813 | selection_start: PositionByIndex::after(0),
814 | selection_end: None,
815 | node: "world",
816 | final_html: "hello world".into(),
817 | }
818 | .test(&mut ca);
819 | }
820 |
821 | #[wasm_bindgen_test]
822 | fn in_the_middle() {
823 | let mut ca = init();
824 | InsertNodeTest {
825 | children: vec![text_node(&ca, "ab")],
826 | selection_start: PositionByIndex::offset(0, 1),
827 | selection_end: None,
828 | node: "XY",
829 | final_html: "aXYb".into(),
830 | }
831 | .test(&mut ca);
832 | }
833 |
834 | #[wasm_bindgen_test]
835 | fn replace_text() {
836 | let mut ca = init();
837 | InsertNodeTest {
838 | children: vec![text_node(&ca, "abcd")],
839 | selection_start: PositionByIndex::offset(0, 1),
840 | selection_end: Some(PositionByIndex::offset(0, 3)),
841 | node: "X",
842 | final_html: "aXd".into(),
843 | }
844 | .test(&mut ca);
845 | }
846 |
847 | #[wasm_bindgen_test]
848 | fn replace_nodes() {
849 | let mut ca = init();
850 | let img = Img {
851 | src: "img.jpg",
852 | alt: "😀",
853 | cls: "em",
854 | };
855 | InsertNodeTest {
856 | children: vec![text_node(&ca, "ab"), img.as_node(&ca)],
857 | selection_start: PositionByIndex::offset(0, 1),
858 | selection_end: Some(PositionByIndex::after(1)),
859 | node: "z",
860 | final_html: "az".into(),
861 | }
862 | .test(&mut ca);
863 | }
864 | }
865 |
866 | mod image {
867 | use super::*;
868 |
869 | #[wasm_bindgen_test]
870 | fn at_end() {
871 | let mut ca = init();
872 | let img = Img {
873 | src: "img.jpg",
874 | alt: "😀",
875 | cls: "em",
876 | };
877 | InsertNodeTest {
878 | children: vec![text_node(&ca, "hi ")],
879 | selection_start: PositionByIndex::after(0),
880 | selection_end: None,
881 | node: img,
882 | final_html: format!("hi {}", img.html(0)),
883 | }
884 | .test(&mut ca);
885 | }
886 |
887 | /// If there is no selection but a trailing newline, element
888 | /// will replace that trailing newline due to the way how the
889 | /// `insertHTML` command works.
890 | #[wasm_bindgen_test]
891 | fn at_end_with_br() {
892 | let mut ca = init();
893 | let img = Img {
894 | src: "img.jpg",
895 | alt: "😀",
896 | cls: "em",
897 | };
898 |
899 | // Prepare wrapper
900 | ca.wrapper.set_inner_html("
");
901 |
902 | // Ensure that there's no selection left in the DOM
903 | selection::unset_selection_range();
904 |
905 | // Insert node and verify
906 | ca.insert_image(img.src, img.alt, img.cls);
907 | assert_eq!(ca.wrapper.inner_html(), img.html(0));
908 | }
909 |
910 | #[wasm_bindgen_test]
911 | fn split_text() {
912 | let mut ca = init();
913 | let img = Img {
914 | src: "img.jpg",
915 | alt: "😀",
916 | cls: "em",
917 | };
918 | InsertNodeTest {
919 | children: vec![text_node(&ca, "bonjour")],
920 | selection_start: PositionByIndex::offset(0, 3),
921 | selection_end: None,
922 | node: img,
923 | final_html: format!("bon{}jour", img.html(0)),
924 | }
925 | .test(&mut ca);
926 | }
927 |
928 | #[wasm_bindgen_test]
929 | fn between_nodes_br() {
930 | let mut ca = init();
931 | let img = Img {
932 | src: "img.jpg",
933 | alt: "😀",
934 | cls: "em",
935 | };
936 | InsertNodeTest {
937 | children: vec![
938 | text_node(&ca, "a"),
939 | element_node(&ca, "br"),
940 | text_node(&ca, "b"),
941 | ],
942 | selection_start: PositionByIndex::after(0),
943 | selection_end: None,
944 | node: img,
945 | final_html: format!("a{}
b", img.html(0)),
946 | }
947 | .test(&mut ca);
948 | }
949 |
950 | #[wasm_bindgen_test]
951 | fn between_nodes_div() {
952 | let mut ca = init();
953 | let img = Img {
954 | src: "img.jpg",
955 | alt: "😀",
956 | cls: "em",
957 | };
958 | let div_a = {
959 | let div = element_node(&ca, "div");
960 | div.append_child(&text_node(&ca, "a")).unwrap();
961 | div
962 | };
963 | let div_b = {
964 | let div = element_node(&ca, "div");
965 | div.append_child(&text_node(&ca, "b")).unwrap();
966 | div.append_child(&element_node(&ca, "br")).unwrap();
967 | div
968 | };
969 | InsertNodeTest {
970 | children: vec![div_a, div_b],
971 | selection_start: PositionByIndex::after_nested(vec![0, 0]),
972 | selection_end: None,
973 | node: img,
974 | final_html: format!("a{}
b
", img.html(0)),
975 | }
976 | .test(&mut ca);
977 | }
978 | }
979 | }
980 |
981 | mod selection_range {
982 | use super::*;
983 |
984 | #[wasm_bindgen_test]
985 | fn restore_selection_range() {
986 | let mut ca = init();
987 | let node = text_node(&ca, "abc");
988 | ca.wrapper.append_child(&node).unwrap();
989 |
990 | // Highlight "b"
991 | set_selection_range(
992 | &Position::Offset(&node, 1),
993 | Some(&Position::Offset(&node, 2)),
994 | );
995 | let range_result = ca.fetch_range();
996 | assert!(!range_result.outside);
997 | let range = range_result.range.expect("Could not get range");
998 | assert_eq!(range.start_offset().unwrap(), 1);
999 | assert_eq!(range.end_offset().unwrap(), 2);
1000 |
1001 | // Store range
1002 | ca.store_selection_range();
1003 |
1004 | // Change range, highlight "a"
1005 | set_selection_range(
1006 | &Position::Offset(&node, 0),
1007 | Some(&Position::Offset(&node, 1)),
1008 | );
1009 | let range_result = ca.fetch_range();
1010 | assert!(!range_result.outside);
1011 | let range = range_result.range.expect("Could not get range");
1012 | assert_eq!(range.start_offset().unwrap(), 0);
1013 | assert_eq!(range.end_offset().unwrap(), 1);
1014 |
1015 | // Retore range
1016 | ca.restore_selection_range();
1017 | let range_result = ca.fetch_range();
1018 | assert!(!range_result.outside);
1019 | let range = range_result.range.expect("Could not get range");
1020 | assert_eq!(range.start_offset().unwrap(), 1);
1021 | assert_eq!(range.end_offset().unwrap(), 2);
1022 | }
1023 |
1024 | #[wasm_bindgen_test]
1025 | fn get_range_result() {
1026 | let ca = init();
1027 | let inner_text_node = text_node(&ca, "abc");
1028 | ca.wrapper.append_child(&inner_text_node).unwrap();
1029 |
1030 | // No range set
1031 | selection::unset_selection_range();
1032 | let range_result = ca.fetch_range();
1033 | assert!(range_result.range.is_none());
1034 | assert!(!range_result.outside);
1035 |
1036 | // Range is outside
1037 | let outer_text_node = ca.document.create_text_node("hello");
1038 | ca.document
1039 | .body()
1040 | .unwrap()
1041 | .append_child(&outer_text_node)
1042 | .unwrap();
1043 | set_selection_range(&Position::Offset(&outer_text_node, 0), None);
1044 | let range_result = ca.fetch_range();
1045 | assert!(range_result.range.is_some());
1046 | assert!(range_result.outside);
1047 |
1048 | // Inside wrapper
1049 | set_selection_range(&Position::Offset(&inner_text_node, 0), None);
1050 | let range_result = ca.fetch_range();
1051 | assert!(range_result.range.is_some());
1052 | assert!(!range_result.outside);
1053 | }
1054 | }
1055 |
1056 | mod clear {
1057 | use super::*;
1058 |
1059 | #[wasm_bindgen_test]
1060 | fn clear_contents() {
1061 | // Init, no child nodes
1062 | let mut ca = init();
1063 | assert_eq!(ca.wrapper.child_nodes().length(), 0);
1064 |
1065 | // Append some child nodes
1066 | ca.wrapper.append_child(&text_node(&ca, "abc")).unwrap();
1067 | ca.wrapper.append_child(&element_node(&ca, "br")).unwrap();
1068 | assert_eq!(ca.wrapper.child_nodes().length(), 2);
1069 |
1070 | // Clear
1071 | ca.clear();
1072 | assert_eq!(ca.wrapper.child_nodes().length(), 0);
1073 | }
1074 | }
1075 |
1076 | mod word_at_caret {
1077 | use super::*;
1078 |
1079 | #[wasm_bindgen_test]
1080 | fn empty() {
1081 | let mut ca = init();
1082 | let wac = ca.get_word_at_caret();
1083 | assert!(wac.is_none());
1084 | }
1085 |
1086 | #[wasm_bindgen_test]
1087 | fn in_text() {
1088 | let mut ca = init();
1089 |
1090 | let text = ca.document.create_text_node("hello world!\tgoodbye.");
1091 | ca.wrapper.append_child(&text).unwrap();
1092 | set_selection_range(&Position::Offset(&text, 9), None);
1093 | ca.store_selection_range();
1094 |
1095 | let wac = ca
1096 | .get_word_at_caret()
1097 | .expect("get_word_at_caret returned None");
1098 | assert_eq!(&wac.before(), "wor");
1099 | assert_eq!(&wac.after(), "ld!");
1100 | assert_eq!(wac.start_offset(), 6);
1101 | assert_eq!(wac.end_offset(), 12);
1102 | }
1103 |
1104 | #[wasm_bindgen_test]
1105 | fn after_text() {
1106 | let mut ca = init();
1107 |
1108 | let text = ca.document.create_text_node("hello world");
1109 | ca.wrapper.append_child(&text).unwrap();
1110 | set_selection_range(&Position::After(&text), None);
1111 | ca.store_selection_range();
1112 |
1113 | let wac = ca
1114 | .get_word_at_caret()
1115 | .expect("get_word_at_caret returned None");
1116 | assert_eq!(&wac.before(), "world");
1117 | assert_eq!(&wac.after(), "");
1118 | assert_eq!(wac.start_offset(), 6);
1119 | assert_eq!(wac.end_offset(), 11);
1120 | }
1121 |
1122 | #[wasm_bindgen_test]
1123 | fn before_word() {
1124 | let mut ca = init();
1125 |
1126 | let text = ca.document.create_text_node("hello world");
1127 | ca.wrapper.append_child(&text).unwrap();
1128 | set_selection_range(&Position::Offset(&text, 0), None);
1129 | ca.store_selection_range();
1130 |
1131 | let wac = ca
1132 | .get_word_at_caret()
1133 | .expect("get_word_at_caret returned None");
1134 | assert_eq!(&wac.before(), "");
1135 | assert_eq!(&wac.after(), "hello");
1136 | assert_eq!(wac.start_offset(), 0);
1137 | assert_eq!(wac.end_offset(), 5);
1138 | }
1139 |
1140 | #[wasm_bindgen_test]
1141 | fn single_word() {
1142 | let mut ca = init();
1143 |
1144 | let text = ca.document.create_text_node(":ok:");
1145 | ca.wrapper.append_child(&text).unwrap();
1146 | set_selection_range(&Position::Offset(&text, 4), None);
1147 | ca.store_selection_range();
1148 |
1149 | let wac = ca
1150 | .get_word_at_caret()
1151 | .expect("get_word_at_caret returned None");
1152 | assert_eq!(&wac.before(), ":ok:");
1153 | assert_eq!(&wac.after(), "");
1154 | assert_eq!(wac.start_offset(), 0);
1155 | assert_eq!(wac.end_offset(), 4);
1156 | }
1157 | }
1158 | }
1159 |
--------------------------------------------------------------------------------