|
| 1 | +from pathlib import Path |
| 2 | + |
| 3 | +import pytest |
| 4 | + |
| 5 | +from playwright.sync_api import Browser, Error, Page, Selectors |
| 6 | + |
| 7 | +from .utils import Utils |
| 8 | + |
| 9 | + |
| 10 | +def test_selectors_register_should_work( |
| 11 | + selectors: Selectors, browser: Browser, browser_name: str |
| 12 | +) -> None: |
| 13 | + tag_selector = """ |
| 14 | + { |
| 15 | + create(root, target) { |
| 16 | + return target.nodeName; |
| 17 | + }, |
| 18 | + query(root, selector) { |
| 19 | + return root.querySelector(selector); |
| 20 | + }, |
| 21 | + queryAll(root, selector) { |
| 22 | + return Array.from(root.querySelectorAll(selector)); |
| 23 | + } |
| 24 | + }""" |
| 25 | + |
| 26 | + selector_name = f"tag_{browser_name}" |
| 27 | + selector2_name = f"tag2_{browser_name}" |
| 28 | + |
| 29 | + # Register one engine before creating context. |
| 30 | + selectors.register(selector_name, tag_selector) |
| 31 | + |
| 32 | + context = browser.new_context() |
| 33 | + # Register another engine after creating context. |
| 34 | + selectors.register(selector2_name, tag_selector) |
| 35 | + |
| 36 | + page = context.new_page() |
| 37 | + page.set_content("<div><span></span></div><div></div>") |
| 38 | + |
| 39 | + assert page.eval_on_selector(f"{selector_name}=DIV", "e => e.nodeName") == "DIV" |
| 40 | + assert page.eval_on_selector(f"{selector_name}=SPAN", "e => e.nodeName") == "SPAN" |
| 41 | + assert page.eval_on_selector_all(f"{selector_name}=DIV", "es => es.length") == 2 |
| 42 | + |
| 43 | + assert page.eval_on_selector(f"{selector2_name}=DIV", "e => e.nodeName") == "DIV" |
| 44 | + assert page.eval_on_selector(f"{selector2_name}=SPAN", "e => e.nodeName") == "SPAN" |
| 45 | + assert page.eval_on_selector_all(f"{selector2_name}=DIV", "es => es.length") == 2 |
| 46 | + |
| 47 | + # Selector names are case-sensitive. |
| 48 | + with pytest.raises(Error) as exc: |
| 49 | + page.query_selector("tAG=DIV") |
| 50 | + assert 'Unknown engine "tAG" while parsing selector tAG=DIV' in exc.value.message |
| 51 | + |
| 52 | + context.close() |
| 53 | + |
| 54 | + |
| 55 | +def test_selectors_register_should_work_with_path( |
| 56 | + selectors: Selectors, page: Page, utils: Utils, assetdir: Path |
| 57 | +) -> None: |
| 58 | + utils.register_selector_engine( |
| 59 | + selectors, "foo", path=assetdir / "sectionselectorengine.js" |
| 60 | + ) |
| 61 | + page.set_content("<section></section>") |
| 62 | + assert page.eval_on_selector("foo=whatever", "e => e.nodeName") == "SECTION" |
| 63 | + |
| 64 | + |
| 65 | +def test_selectors_register_should_work_in_main_and_isolated_world( |
| 66 | + selectors: Selectors, page: Page, utils: Utils |
| 67 | +) -> None: |
| 68 | + dummy_selector_script = """{ |
| 69 | + create(root, target) { }, |
| 70 | + query(root, selector) { |
| 71 | + return window.__answer; |
| 72 | + }, |
| 73 | + queryAll(root, selector) { |
| 74 | + return window['__answer'] ? [window['__answer'], document.body, document.documentElement] : []; |
| 75 | + } |
| 76 | + }""" |
| 77 | + |
| 78 | + utils.register_selector_engine(selectors, "main", dummy_selector_script) |
| 79 | + utils.register_selector_engine( |
| 80 | + selectors, "isolated", dummy_selector_script, content_script=True |
| 81 | + ) |
| 82 | + page.set_content("<div><span><section></section></span></div>") |
| 83 | + page.evaluate('() => window.__answer = document.querySelector("span")') |
| 84 | + # Works in main if asked. |
| 85 | + assert page.eval_on_selector("main=ignored", "e => e.nodeName") == "SPAN" |
| 86 | + assert page.eval_on_selector("css=div >> main=ignored", "e => e.nodeName") == "SPAN" |
| 87 | + assert page.eval_on_selector_all( |
| 88 | + "main=ignored", "es => window.__answer !== undefined" |
| 89 | + ) |
| 90 | + assert ( |
| 91 | + page.eval_on_selector_all("main=ignored", "es => es.filter(e => e).length") == 3 |
| 92 | + ) |
| 93 | + # Works in isolated by default. |
| 94 | + assert page.query_selector("isolated=ignored") is None |
| 95 | + assert page.query_selector("css=div >> isolated=ignored") is None |
| 96 | + # $$eval always works in main, to avoid adopting nodes one by one. |
| 97 | + assert page.eval_on_selector_all( |
| 98 | + "isolated=ignored", "es => window.__answer !== undefined" |
| 99 | + ) |
| 100 | + assert ( |
| 101 | + page.eval_on_selector_all("isolated=ignored", "es => es.filter(e => e).length") |
| 102 | + == 3 |
| 103 | + ) |
| 104 | + # At least one engine in main forces all to be in main. |
| 105 | + assert ( |
| 106 | + page.eval_on_selector("main=ignored >> isolated=ignored", "e => e.nodeName") |
| 107 | + == "SPAN" |
| 108 | + ) |
| 109 | + assert ( |
| 110 | + page.eval_on_selector("isolated=ignored >> main=ignored", "e => e.nodeName") |
| 111 | + == "SPAN" |
| 112 | + ) |
| 113 | + # Can be chained to css. |
| 114 | + assert ( |
| 115 | + page.eval_on_selector("main=ignored >> css=section", "e => e.nodeName") |
| 116 | + == "SECTION" |
| 117 | + ) |
| 118 | + |
| 119 | + |
| 120 | +def test_selectors_register_should_handle_errors( |
| 121 | + selectors: Selectors, page: Page, utils: Utils |
| 122 | +) -> None: |
| 123 | + with pytest.raises(Error) as exc: |
| 124 | + page.query_selector("neverregister=ignored") |
| 125 | + assert ( |
| 126 | + 'Unknown engine "neverregister" while parsing selector neverregister=ignored' |
| 127 | + in exc.value.message |
| 128 | + ) |
| 129 | + |
| 130 | + dummy_selector_engine_script = """{ |
| 131 | + create(root, target) { |
| 132 | + return target.nodeName; |
| 133 | + }, |
| 134 | + query(root, selector) { |
| 135 | + return root.querySelector('dummy'); |
| 136 | + }, |
| 137 | + queryAll(root, selector) { |
| 138 | + return Array.from(root.query_selector_all('dummy')); |
| 139 | + } |
| 140 | + }""" |
| 141 | + |
| 142 | + with pytest.raises(Error) as exc: |
| 143 | + selectors.register("$", dummy_selector_engine_script) |
| 144 | + assert ( |
| 145 | + exc.value.message |
| 146 | + == "Selector engine name may only contain [a-zA-Z0-9_] characters" |
| 147 | + ) |
| 148 | + |
| 149 | + # Selector names are case-sensitive. |
| 150 | + utils.register_selector_engine(selectors, "dummy", dummy_selector_engine_script) |
| 151 | + utils.register_selector_engine(selectors, "duMMy", dummy_selector_engine_script) |
| 152 | + |
| 153 | + with pytest.raises(Error) as exc: |
| 154 | + selectors.register("dummy", dummy_selector_engine_script) |
| 155 | + assert exc.value.message == '"dummy" selector engine has been already registered' |
| 156 | + |
| 157 | + with pytest.raises(Error) as exc: |
| 158 | + selectors.register("css", dummy_selector_engine_script) |
| 159 | + assert exc.value.message == '"css" is a predefined selector engine' |
| 160 | + |
| 161 | + |
| 162 | +def test_should_work_with_layout_selectors(page: Page) -> None: |
| 163 | + # +--+ +--+ |
| 164 | + # | 1| | 2| |
| 165 | + # +--+ ++-++ |
| 166 | + # | 3| | 4| |
| 167 | + # +-------+ ++-++ |
| 168 | + # | 0 | | 5| |
| 169 | + # | +--+ +--+--+ |
| 170 | + # | | 6| | 7| |
| 171 | + # | +--+ +--+ |
| 172 | + # | | |
| 173 | + # O-------+ |
| 174 | + # +--+ |
| 175 | + # | 8| |
| 176 | + # +--++--+ |
| 177 | + # | 9| |
| 178 | + # +--+ |
| 179 | + |
| 180 | + boxes = [ |
| 181 | + # x, y, width, height |
| 182 | + [0, 0, 150, 150], |
| 183 | + [100, 200, 50, 50], |
| 184 | + [200, 200, 50, 50], |
| 185 | + [100, 150, 50, 50], |
| 186 | + [201, 150, 50, 50], |
| 187 | + [200, 100, 50, 50], |
| 188 | + [50, 50, 50, 50], |
| 189 | + [150, 50, 50, 50], |
| 190 | + [150, -51, 50, 50], |
| 191 | + [201, -101, 50, 50], |
| 192 | + ] |
| 193 | + page.set_content( |
| 194 | + '<container style="width: 500px; height: 500px; position: relative;"></container>' |
| 195 | + ) |
| 196 | + page.eval_on_selector( |
| 197 | + "container", |
| 198 | + """(container, boxes) => { |
| 199 | + for (let i = 0; i < boxes.length; i++) { |
| 200 | + const div = document.createElement('div'); |
| 201 | + div.style.position = 'absolute'; |
| 202 | + div.style.overflow = 'hidden'; |
| 203 | + div.style.boxSizing = 'border-box'; |
| 204 | + div.style.border = '1px solid black'; |
| 205 | + div.id = 'id' + i; |
| 206 | + div.textContent = 'id' + i; |
| 207 | + const box = boxes[i]; |
| 208 | + div.style.left = box[0] + 'px'; |
| 209 | + // Note that top is a flipped y coordinate. |
| 210 | + div.style.top = (250 - box[1] - box[3]) + 'px'; |
| 211 | + div.style.width = box[2] + 'px'; |
| 212 | + div.style.height = box[3] + 'px'; |
| 213 | + container.appendChild(div); |
| 214 | + const span = document.createElement('span'); |
| 215 | + span.textContent = '' + i; |
| 216 | + div.appendChild(span); |
| 217 | + } |
| 218 | + }""", |
| 219 | + boxes, |
| 220 | + ) |
| 221 | + |
| 222 | + assert page.eval_on_selector("div:right-of(#id6)", "e => e.id") == "id7" |
| 223 | + assert page.eval_on_selector("div:right-of(#id1)", "e => e.id") == "id2" |
| 224 | + assert page.eval_on_selector("div:right-of(#id3)", "e => e.id") == "id4" |
| 225 | + assert page.query_selector("div:right-of(#id4)") is None |
| 226 | + assert page.eval_on_selector("div:right-of(#id0)", "e => e.id") == "id7" |
| 227 | + assert page.eval_on_selector("div:right-of(#id8)", "e => e.id") == "id9" |
| 228 | + assert ( |
| 229 | + page.eval_on_selector_all( |
| 230 | + "div:right-of(#id3)", "els => els.map(e => e.id).join(',')" |
| 231 | + ) |
| 232 | + == "id4,id2,id5,id7,id8,id9" |
| 233 | + ) |
| 234 | + assert ( |
| 235 | + page.eval_on_selector_all( |
| 236 | + "div:right-of(#id3, 50)", "els => els.map(e => e.id).join(',')" |
| 237 | + ) |
| 238 | + == "id2,id5,id7,id8" |
| 239 | + ) |
| 240 | + assert ( |
| 241 | + page.eval_on_selector_all( |
| 242 | + "div:right-of(#id3, 49)", "els => els.map(e => e.id).join(',')" |
| 243 | + ) |
| 244 | + == "id7,id8" |
| 245 | + ) |
| 246 | + |
| 247 | + assert page.eval_on_selector("div:left-of(#id2)", "e => e.id") == "id1" |
| 248 | + assert page.query_selector("div:left-of(#id0)") is None |
| 249 | + assert page.eval_on_selector("div:left-of(#id5)", "e => e.id") == "id0" |
| 250 | + assert page.eval_on_selector("div:left-of(#id9)", "e => e.id") == "id8" |
| 251 | + assert page.eval_on_selector("div:left-of(#id4)", "e => e.id") == "id3" |
| 252 | + assert ( |
| 253 | + page.eval_on_selector_all( |
| 254 | + "div:left-of(#id5)", "els => els.map(e => e.id).join(',')" |
| 255 | + ) |
| 256 | + == "id0,id7,id3,id1,id6,id8" |
| 257 | + ) |
| 258 | + assert ( |
| 259 | + page.eval_on_selector_all( |
| 260 | + "div:left-of(#id5, 3)", "els => els.map(e => e.id).join(',')" |
| 261 | + ) |
| 262 | + == "id7,id8" |
| 263 | + ) |
| 264 | + |
| 265 | + assert page.eval_on_selector("div:above(#id0)", "e => e.id") == "id3" |
| 266 | + assert page.eval_on_selector("div:above(#id5)", "e => e.id") == "id4" |
| 267 | + assert page.eval_on_selector("div:above(#id7)", "e => e.id") == "id5" |
| 268 | + assert page.eval_on_selector("div:above(#id8)", "e => e.id") == "id0" |
| 269 | + assert page.eval_on_selector("div:above(#id9)", "e => e.id") == "id8" |
| 270 | + assert page.query_selector("div:above(#id2)") is None |
| 271 | + assert ( |
| 272 | + page.eval_on_selector_all( |
| 273 | + "div:above(#id5)", "els => els.map(e => e.id).join(',')" |
| 274 | + ) |
| 275 | + == "id4,id2,id3,id1" |
| 276 | + ) |
| 277 | + assert ( |
| 278 | + page.eval_on_selector_all( |
| 279 | + "div:above(#id5, 20)", "els => els.map(e => e.id).join(',')" |
| 280 | + ) |
| 281 | + == "id4,id3" |
| 282 | + ) |
| 283 | + |
| 284 | + assert page.eval_on_selector("div:below(#id4)", "e => e.id") == "id5" |
| 285 | + assert page.eval_on_selector("div:below(#id3)", "e => e.id") == "id0" |
| 286 | + assert page.eval_on_selector("div:below(#id2)", "e => e.id") == "id4" |
| 287 | + assert page.eval_on_selector("div:below(#id6)", "e => e.id") == "id8" |
| 288 | + assert page.eval_on_selector("div:below(#id7)", "e => e.id") == "id8" |
| 289 | + assert page.eval_on_selector("div:below(#id8)", "e => e.id") == "id9" |
| 290 | + assert page.query_selector("div:below(#id9)") is None |
| 291 | + assert ( |
| 292 | + page.eval_on_selector_all( |
| 293 | + "div:below(#id3)", "els => els.map(e => e.id).join(',')" |
| 294 | + ) |
| 295 | + == "id0,id5,id6,id7,id8,id9" |
| 296 | + ) |
| 297 | + assert ( |
| 298 | + page.eval_on_selector_all( |
| 299 | + "div:below(#id3, 105)", "els => els.map(e => e.id).join(',')" |
| 300 | + ) |
| 301 | + == "id0,id5,id6,id7" |
| 302 | + ) |
| 303 | + |
| 304 | + assert page.eval_on_selector("div:near(#id0)", "e => e.id") == "id3" |
| 305 | + assert ( |
| 306 | + page.eval_on_selector_all( |
| 307 | + "div:near(#id7)", "els => els.map(e => e.id).join(',')" |
| 308 | + ) |
| 309 | + == "id0,id5,id3,id6" |
| 310 | + ) |
| 311 | + assert ( |
| 312 | + page.eval_on_selector_all( |
| 313 | + "div:near(#id0)", "els => els.map(e => e.id).join(',')" |
| 314 | + ) |
| 315 | + == "id3,id6,id7,id8,id1,id5" |
| 316 | + ) |
| 317 | + assert ( |
| 318 | + page.eval_on_selector_all( |
| 319 | + "div:near(#id6)", "els => els.map(e => e.id).join(',')" |
| 320 | + ) |
| 321 | + == "id0,id3,id7" |
| 322 | + ) |
| 323 | + assert ( |
| 324 | + page.eval_on_selector_all( |
| 325 | + "div:near(#id6, 10)", "els => els.map(e => e.id).join(',')" |
| 326 | + ) |
| 327 | + == "id0" |
| 328 | + ) |
| 329 | + assert ( |
| 330 | + page.eval_on_selector_all( |
| 331 | + "div:near(#id0, 100)", "els => els.map(e => e.id).join(',')" |
| 332 | + ) |
| 333 | + == "id3,id6,id7,id8,id1,id5,id4,id2" |
| 334 | + ) |
| 335 | + |
| 336 | + assert ( |
| 337 | + page.eval_on_selector_all( |
| 338 | + "div:below(#id5):above(#id8)", "els => els.map(e => e.id).join(',')" |
| 339 | + ) |
| 340 | + == "id7,id6" |
| 341 | + ) |
| 342 | + assert page.eval_on_selector("div:below(#id5):above(#id8)", "e => e.id") == "id7" |
| 343 | + |
| 344 | + assert ( |
| 345 | + page.eval_on_selector_all( |
| 346 | + "div:right-of(#id0) + div:above(#id8)", |
| 347 | + "els => els.map(e => e.id).join(',')", |
| 348 | + ) |
| 349 | + == "id5,id6,id3" |
| 350 | + ) |
| 351 | + |
| 352 | + with pytest.raises(Error) as exc_info: |
| 353 | + page.query_selector(":near(50)") |
| 354 | + assert ( |
| 355 | + '"near" engine expects a selector list and optional maximum distance in pixels' |
| 356 | + in exc_info.value.message |
| 357 | + ) |
| 358 | + with pytest.raises(Error) as exc_info: |
| 359 | + page.query_selector('left-of="div"') |
| 360 | + assert '"left-of" selector cannot be first' in exc_info.value.message |
0 commit comments