# Copyright (c) Microsoft Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import asyncio
import os
import re
from pathlib import Path
from typing import Dict, List, Optional

import pytest

from playwright.async_api import (
    BrowserContext,
    Error,
    JSHandle,
    Page,
    Route,
    TimeoutError,
)
from tests.server import Server, TestServerRequest
from tests.utils import TARGET_CLOSED_ERROR_MESSAGE, must


async def test_close_should_reject_all_promises(context: BrowserContext) -> None:
    new_page = await context.new_page()
    with pytest.raises(Error) as exc_info:
        await asyncio.gather(
            new_page.evaluate("() => new Promise(r => {})"), new_page.close()
        )
    assert " closed" in exc_info.value.message


async def test_closed_should_not_visible_in_context_pages(
    context: BrowserContext,
) -> None:
    page = await context.new_page()
    assert page in context.pages
    await page.close()
    assert page not in context.pages


async def test_close_should_run_beforeunload_if_asked_for(
    context: BrowserContext, server: Server, is_chromium: bool, is_webkit: bool
) -> None:
    page = await context.new_page()
    await page.goto(server.PREFIX + "/beforeunload.html")
    # We have to interact with a page so that 'beforeunload' handlers
    # fire.
    await page.click("body")

    async with page.expect_event("dialog") as dialog_info:
        await page.close(run_before_unload=True)
    dialog = await dialog_info.value

    assert dialog.type == "beforeunload"
    assert dialog.default_value == ""
    if is_chromium:
        assert dialog.message == ""
    elif is_webkit:
        assert dialog.message == "Leave?"
    else:
        assert (
            "This page is asking you to confirm that you want to leave"
            in dialog.message
        )
    async with page.expect_event("close"):
        await dialog.accept()


async def test_close_should_not_run_beforeunload_by_default(
    context: BrowserContext, server: Server
) -> None:
    page = await context.new_page()
    await page.goto(server.PREFIX + "/beforeunload.html")
    # We have to interact with a page so that 'beforeunload' handlers
    # fire.
    await page.click("body")
    await page.close()


async def test_should_be_able_to_navigate_away_from_page_with_before_unload(
    server: Server, page: Page
) -> None:
    await page.goto(server.PREFIX + "/beforeunload.html")
    # We have to interact with a page so that 'beforeunload' handlers
    # fire.
    await page.click("body")
    await page.goto(server.EMPTY_PAGE)


async def test_close_should_set_the_page_close_state(context: BrowserContext) -> None:
    page = await context.new_page()
    assert page.is_closed() is False
    await page.close()
    assert page.is_closed()


async def test_close_should_terminate_network_waiters(
    context: BrowserContext, server: Server
) -> None:
    page = await context.new_page()

    async def wait_for_request() -> Error:
        with pytest.raises(Error) as exc_info:
            async with page.expect_request(server.EMPTY_PAGE):
                pass
        return exc_info.value

    async def wait_for_response() -> Error:
        with pytest.raises(Error) as exc_info:
            async with page.expect_response(server.EMPTY_PAGE):
                pass
        return exc_info.value

    results = await asyncio.gather(
        wait_for_request(), wait_for_response(), page.close()
    )
    for i in range(2):
        error = results[i]
        assert error
        assert TARGET_CLOSED_ERROR_MESSAGE in error.message
        assert "Timeout" not in error.message


async def test_close_should_be_callable_twice(context: BrowserContext) -> None:
    page = await context.new_page()
    await asyncio.gather(
        page.close(),
        page.close(),
    )
    await page.close()


async def test_load_should_fire_when_expected(page: Page) -> None:
    async with page.expect_event("load"):
        await page.goto("about:blank")


@pytest.mark.skip("FIXME")
async def test_should_work_with_wait_for_loadstate(page: Page, server: Server) -> None:
    messages = []

    def _handler(request: TestServerRequest) -> None:
        messages.append("route")
        request.setHeader("Content-Type", "text/html")
        request.write(b"<link rel='stylesheet' href='./one-style.css'>")
        request.finish()

    server.set_route(
        "/empty.html",
        _handler,
    )

    await page.set_content(f'<a id="anchor" href="{server.EMPTY_PAGE}">empty.html</a>')

    async def wait_for_clickload() -> None:
        await page.click("a")
        await page.wait_for_load_state("load")
        messages.append("clickload")

    async def wait_for_page_load() -> None:
        await page.wait_for_event("load")
        messages.append("load")

    await asyncio.gather(
        wait_for_clickload(),
        wait_for_page_load(),
    )

    assert messages == ["route", "load", "clickload"]


async def test_async_stacks_should_work(page: Page, server: Server) -> None:
    await page.route(
        "**/empty.html", lambda route, response: asyncio.create_task(route.abort())
    )
    with pytest.raises(Error) as exc_info:
        await page.goto(server.EMPTY_PAGE)
    assert exc_info.value.stack
    assert __file__ in exc_info.value.stack


async def test_opener_should_provide_access_to_the_opener_page(page: Page) -> None:
    async with page.expect_popup() as popup_info:
        await page.evaluate("window.open('about:blank')")
    popup = await popup_info.value
    opener = await popup.opener()
    assert opener == page


async def test_opener_should_return_null_if_parent_page_has_been_closed(
    page: Page,
) -> None:
    async with page.expect_popup() as popup_info:
        await page.evaluate("window.open('about:blank')")
    popup = await popup_info.value
    await page.close()
    opener = await popup.opener()
    assert opener is None


async def test_domcontentloaded_should_fire_when_expected(
    page: Page, server: Server
) -> None:
    future = asyncio.create_task(page.goto("about:blank"))
    async with page.expect_event("domcontentloaded"):
        pass
    await future


async def test_wait_for_request(page: Page, server: Server) -> None:
    await page.goto(server.EMPTY_PAGE)
    async with page.expect_request(server.PREFIX + "/digits/2.png") as request_info:
        await page.evaluate(
            """() => {
                fetch('/digits/1.png')
                fetch('/digits/2.png')
                fetch('/digits/3.png')
            }"""
        )
    request = await request_info.value
    assert request.url == server.PREFIX + "/digits/2.png"


async def test_wait_for_request_should_work_with_predicate(
    page: Page, server: Server
) -> None:
    await page.goto(server.EMPTY_PAGE)
    async with page.expect_request(
        lambda request: request.url == server.PREFIX + "/digits/2.png"
    ) as request_info:
        await page.evaluate(
            """() => {
                fetch('/digits/1.png')
                fetch('/digits/2.png')
                fetch('/digits/3.png')
            }"""
        )
    request = await request_info.value
    assert request.url == server.PREFIX + "/digits/2.png"


async def test_wait_for_request_should_timeout(page: Page, server: Server) -> None:
    with pytest.raises(Error) as exc_info:
        async with page.expect_event("request", timeout=1):
            pass
    assert exc_info.type is TimeoutError


async def test_wait_for_request_should_respect_default_timeout(
    page: Page, server: Server
) -> None:
    page.set_default_timeout(1)
    with pytest.raises(Error) as exc_info:
        async with page.expect_event("request", lambda _: False):
            pass
    assert exc_info.type is TimeoutError


async def test_wait_for_request_should_work_with_no_timeout(
    page: Page, server: Server
) -> None:
    await page.goto(server.EMPTY_PAGE)
    async with page.expect_request(
        server.PREFIX + "/digits/2.png", timeout=0
    ) as request_info:
        await page.evaluate(
            """() => setTimeout(() => {
                fetch('/digits/1.png')
                fetch('/digits/2.png')
                fetch('/digits/3.png')
            }, 50)"""
        )
    request = await request_info.value
    assert request.url == server.PREFIX + "/digits/2.png"


async def test_wait_for_request_should_work_with_url_match(
    page: Page, server: Server
) -> None:
    await page.goto(server.EMPTY_PAGE)
    async with page.expect_request(re.compile(r"digits\/\d\.png")) as request_info:
        await page.evaluate("fetch('/digits/1.png')")
    request = await request_info.value
    assert request.url == server.PREFIX + "/digits/1.png"


async def test_wait_for_event_should_fail_with_error_upon_disconnect(
    page: Page,
) -> None:
    with pytest.raises(Error) as exc_info:
        async with page.expect_download():
            await page.close()
    assert TARGET_CLOSED_ERROR_MESSAGE in exc_info.value.message


async def test_wait_for_response_should_work(page: Page, server: Server) -> None:
    await page.goto(server.EMPTY_PAGE)
    async with page.expect_response(server.PREFIX + "/digits/2.png") as response_info:
        await page.evaluate(
            """() => {
                fetch('/digits/1.png')
                fetch('/digits/2.png')
                fetch('/digits/3.png')
            }"""
        )
    response = await response_info.value
    assert response.url == server.PREFIX + "/digits/2.png"


async def test_wait_for_response_should_respect_timeout(page: Page) -> None:
    with pytest.raises(Error) as exc_info:
        async with page.expect_response("**/*", timeout=1):
            pass
    assert exc_info.type is TimeoutError


async def test_wait_for_response_should_respect_default_timeout(page: Page) -> None:
    page.set_default_timeout(1)
    with pytest.raises(Error) as exc_info:
        async with page.expect_response(lambda _: False):
            pass
    assert exc_info.type is TimeoutError


async def test_wait_for_response_should_work_with_predicate(
    page: Page, server: Server
) -> None:
    await page.goto(server.EMPTY_PAGE)
    async with page.expect_response(
        lambda response: response.url == server.PREFIX + "/digits/2.png"
    ) as response_info:
        await page.evaluate(
            """() => {
                fetch('/digits/1.png')
                fetch('/digits/2.png')
                fetch('/digits/3.png')
            }"""
        )
    response = await response_info.value
    assert response.url == server.PREFIX + "/digits/2.png"


async def test_wait_for_response_should_work_with_no_timeout(
    page: Page, server: Server
) -> None:
    await page.goto(server.EMPTY_PAGE)
    async with page.expect_response(server.PREFIX + "/digits/2.png") as response_info:
        await page.evaluate(
            """() => {
                fetch('/digits/1.png')
                fetch('/digits/2.png')
                fetch('/digits/3.png')
            }"""
        )
    response = await response_info.value
    assert response.url == server.PREFIX + "/digits/2.png"


async def test_wait_for_response_should_use_context_timeout(
    page: Page, context: BrowserContext, server: Server
) -> None:
    await page.goto(server.EMPTY_PAGE)

    context.set_default_timeout(1_000)
    with pytest.raises(Error) as exc_info:
        async with page.expect_response("https://playwright.dev"):
            pass
    assert exc_info.type is TimeoutError
    assert "Timeout 1000ms exceeded" in exc_info.value.message


async def test_expect_response_should_not_hang_when_predicate_throws(
    page: Page,
) -> None:
    with pytest.raises(Exception, match="Oops!"):
        async with page.expect_response("**/*"):
            raise Exception("Oops!")


async def test_expose_binding(page: Page) -> None:
    binding_source = []

    def binding(source: Dict, a: int, b: int) -> int:
        binding_source.append(source)
        return a + b

    await page.expose_binding("add", lambda source, a, b: binding(source, a, b))

    result = await page.evaluate("add(5, 6)")

    assert binding_source[0]["context"] == page.context
    assert binding_source[0]["page"] == page
    assert binding_source[0]["frame"] == page.main_frame
    assert result == 11


async def test_expose_function(page: Page, server: Server) -> None:
    await page.expose_function("compute", lambda a, b: a * b)
    result = await page.evaluate("compute(9, 4)")
    assert result == 36


async def test_expose_function_should_throw_exception_in_page_context(
    page: Page, server: Server
) -> None:
    def throw() -> None:
        raise Exception("WOOF WOOF")

    await page.expose_function("woof", lambda: throw())
    result = await page.evaluate(
        """async() => {
            try {
                await woof()
            } catch (e) {
                return {message: e.message, stack: e.stack}
            }
        }"""
    )
    assert result["message"] == "WOOF WOOF"
    assert __file__ in result["stack"]


async def test_expose_function_should_be_callable_from_inside_add_init_script(
    page: Page,
) -> None:
    called = []
    await page.expose_function("woof", lambda: called.append(True))
    await page.add_init_script("woof()")
    await page.reload()
    assert called == [True]


async def test_expose_function_should_survive_navigation(
    page: Page, server: Server
) -> None:
    await page.expose_function("compute", lambda a, b: a * b)
    await page.goto(server.EMPTY_PAGE)
    result = await page.evaluate("compute(9, 4)")
    assert result == 36


async def test_expose_function_should_await_returned_promise(page: Page) -> None:
    async def mul(a: int, b: int) -> int:
        return a * b

    await page.expose_function("compute", mul)
    assert await page.evaluate("compute(3, 5)") == 15


async def test_expose_function_should_work_on_frames(
    page: Page, server: Server
) -> None:
    await page.expose_function("compute", lambda a, b: a * b)
    await page.goto(server.PREFIX + "/frames/nested-frames.html")
    frame = page.frames[1]
    assert await frame.evaluate("compute(3, 5)") == 15


async def test_expose_function_should_work_on_frames_before_navigation(
    page: Page, server: Server
) -> None:
    await page.goto(server.PREFIX + "/frames/nested-frames.html")
    await page.expose_function("compute", lambda a, b: a * b)
    frame = page.frames[1]
    assert await frame.evaluate("compute(3, 5)") == 15


async def test_expose_function_should_work_after_cross_origin_navigation(
    page: Page, server: Server
) -> None:
    await page.goto(server.EMPTY_PAGE)
    await page.expose_function("compute", lambda a, b: a * b)
    await page.goto(server.CROSS_PROCESS_PREFIX + "/empty.html")
    assert await page.evaluate("compute(9, 4)") == 36


async def test_expose_function_should_work_with_complex_objects(
    page: Page, server: Server
) -> None:
    await page.expose_function("complexObject", lambda a, b: dict(x=a["x"] + b["x"]))
    result = await page.evaluate("complexObject({x: 5}, {x: 2})")
    assert result["x"] == 7


async def test_expose_bindinghandle_should_work(page: Page, server: Server) -> None:
    targets: List[JSHandle] = []

    def logme(t: JSHandle) -> int:
        targets.append(t)
        return 17

    await page.expose_binding("logme", lambda source, t: logme(t), handle=True)
    result = await page.evaluate("logme({ foo: 42 })")
    assert (await targets[0].evaluate("x => x.foo")) == 42
    assert result == 17


async def test_page_error_should_fire(
    page: Page, server: Server, browser_name: str
) -> None:
    url = server.PREFIX + "/error.html"
    async with page.expect_event("pageerror") as error_info:
        await page.goto(url)
    error = await error_info.value
    assert error.name == "Error"
    assert error.message == "Fancy error!"
    # Note that WebKit reports the stack of the 'throw' statement instead of the Error constructor call.
    if browser_name == "chromium":
        assert (
            error.stack
            == """Error: Fancy error!
    at c (myscript.js:14:11)
    at b (myscript.js:10:5)
    at a (myscript.js:6:5)
    at myscript.js:3:1"""
        )
    if browser_name == "firefox":
        assert (
            error.stack
            == """Error: Fancy error!
    at c (myscript.js:14:11)
    at b (myscript.js:10:5)
    at a (myscript.js:6:5)
    at  (myscript.js:3:1)"""
        )
    if browser_name == "webkit":
        assert (
            error.stack
            == f"""Error: Fancy error!
    at c ({url}:14:36)
    at b ({url}:10:6)
    at a ({url}:6:6)
    at global code ({url}:3:2)"""
        )


async def test_page_error_should_handle_odd_values(page: Page) -> None:
    cases = [["null", "null"], ["undefined", "undefined"], ["0", "0"], ['""', ""]]
    for [value, message] in cases:
        async with page.expect_event("pageerror") as error_info:
            await page.evaluate(f"() => setTimeout(() => {{ throw {value}; }}, 0)")
        error = await error_info.value
        assert error.message == message


async def test_page_error_should_handle_object(page: Page, is_chromium: bool) -> None:
    async with page.expect_event("pageerror") as error_info:
        await page.evaluate("() => setTimeout(() => { throw {}; }, 0)")
    error = await error_info.value
    assert error.message == "Object" if is_chromium else "[object Object]"


async def test_page_error_should_handle_window(page: Page, is_chromium: bool) -> None:
    async with page.expect_event("pageerror") as error_info:
        await page.evaluate("() => setTimeout(() => { throw window; }, 0)")
    error = await error_info.value
    assert error.message == "Window" if is_chromium else "[object Window]"


async def test_page_error_should_pass_error_name_property(page: Page) -> None:
    async with page.expect_event("pageerror") as error_info:
        await page.evaluate(
            """() => setTimeout(() => {
            const error = new Error("my-message");
            error.name = "my-name";
            throw error;
        }, 0)
        """
        )
    error = await error_info.value
    assert error.message == "my-message"
    assert error.name == "my-name"


expected_output = "<html><head></head><body><div>hello</div></body></html>"


async def test_set_content_should_work(page: Page, server: Server) -> None:
    await page.set_content("<div>hello</div>")
    result = await page.content()
    assert result == expected_output


async def test_set_content_should_work_with_domcontentloaded(
    page: Page, server: Server
) -> None:
    await page.set_content("<div>hello</div>", wait_until="domcontentloaded")
    result = await page.content()
    assert result == expected_output


async def test_set_content_should_work_with_doctype(page: Page, server: Server) -> None:
    doctype = "<!DOCTYPE html>"
    await page.set_content(f"{doctype}<div>hello</div>")
    result = await page.content()
    assert result == f"{doctype}{expected_output}"


async def test_set_content_should_work_with_HTML_4_doctype(
    page: Page, server: Server
) -> None:
    doctype = '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">'
    await page.set_content(f"{doctype}<div>hello</div>")
    result = await page.content()
    assert result == f"{doctype}{expected_output}"


async def test_set_content_should_respect_timeout(page: Page, server: Server) -> None:
    img_path = "/img.png"
    # stall for image
    server.set_route(img_path, lambda request: None)
    with pytest.raises(Error) as exc_info:
        await page.set_content(
            f'<img src="{server.PREFIX + img_path}"></img>', timeout=1
        )
    assert exc_info.type is TimeoutError


async def test_set_content_should_respect_default_navigation_timeout(
    page: Page, server: Server
) -> None:
    page.set_default_navigation_timeout(1)
    img_path = "/img.png"
    # stall for image
    await page.route(img_path, lambda route, request: None)

    with pytest.raises(Error) as exc_info:
        await page.set_content(f'<img src="{server.PREFIX + img_path}"></img>')
    assert "Timeout 1ms exceeded" in exc_info.value.message
    assert exc_info.type is TimeoutError


async def test_set_content_should_await_resources_to_load(
    page: Page, server: Server
) -> None:
    img_route: "asyncio.Future[Route]" = asyncio.Future()
    await page.route("**/img.png", lambda route, request: img_route.set_result(route))
    loaded = []

    async def load() -> None:
        await page.set_content(f'<img src="{server.PREFIX}/img.png"></img>')
        loaded.append(True)

    content_promise = asyncio.create_task(load())
    await asyncio.sleep(0)  # execute scheduled tasks, but don't await them
    route = await img_route
    assert loaded == []
    asyncio.create_task(route.continue_())
    await content_promise


async def test_set_content_should_work_with_tricky_content(page: Page) -> None:
    await page.set_content("<div>hello world</div>" + "\x7F")
    assert await page.eval_on_selector("div", "div => div.textContent") == "hello world"


async def test_set_content_should_work_with_accents(page: Page) -> None:
    await page.set_content("<div>aberración</div>")
    assert await page.eval_on_selector("div", "div => div.textContent") == "aberración"


async def test_set_content_should_work_with_emojis(page: Page) -> None:
    await page.set_content("<div>🐥</div>")
    assert await page.eval_on_selector("div", "div => div.textContent") == "🐥"


async def test_set_content_should_work_with_newline(page: Page) -> None:
    await page.set_content("<div>\n</div>")
    assert await page.eval_on_selector("div", "div => div.textContent") == "\n"


async def test_add_script_tag_should_work_with_a_url(
    page: Page, server: Server
) -> None:
    await page.goto(server.EMPTY_PAGE)
    script_handle = await page.add_script_tag(url="/injectedfile.js")
    assert script_handle.as_element()
    assert await page.evaluate("__injected") == 42


async def test_add_script_tag_should_work_with_a_url_and_type_module(
    page: Page, server: Server
) -> None:
    await page.goto(server.EMPTY_PAGE)
    await page.add_script_tag(url="/es6/es6import.js", type="module")
    assert await page.evaluate("__es6injected") == 42


async def test_add_script_tag_should_work_with_a_path_and_type_module(
    page: Page, server: Server, assetdir: Path
) -> None:
    await page.goto(server.EMPTY_PAGE)
    await page.add_script_tag(path=assetdir / "es6" / "es6pathimport.js", type="module")
    await page.wait_for_function("window.__es6injected")
    assert await page.evaluate("__es6injected") == 42


async def test_add_script_tag_should_work_with_a_content_and_type_module(
    page: Page, server: Server
) -> None:
    await page.goto(server.EMPTY_PAGE)
    await page.add_script_tag(
        content="import num from '/es6/es6module.js';window.__es6injected = num;",
        type="module",
    )
    await page.wait_for_function("window.__es6injected")
    assert await page.evaluate("__es6injected") == 42


async def test_add_script_tag_should_throw_an_error_if_loading_from_url_fail(
    page: Page, server: Server
) -> None:
    await page.goto(server.EMPTY_PAGE)
    with pytest.raises(Error) as exc_info:
        await page.add_script_tag(url="/nonexistfile.js")
    assert exc_info.value


async def test_add_script_tag_should_work_with_a_path(
    page: Page, server: Server, assetdir: Path
) -> None:
    await page.goto(server.EMPTY_PAGE)
    script_handle = await page.add_script_tag(path=assetdir / "injectedfile.js")
    assert script_handle.as_element()
    assert await page.evaluate("__injected") == 42


@pytest.mark.skip_browser("webkit")
async def test_add_script_tag_should_include_source_url_when_path_is_provided(
    page: Page, server: Server, assetdir: Path
) -> None:
    # Lacking sourceURL support in WebKit
    await page.goto(server.EMPTY_PAGE)
    await page.add_script_tag(path=assetdir / "injectedfile.js")
    result = await page.evaluate("__injectedError.stack")
    assert os.path.join("assets", "injectedfile.js") in result


async def test_add_script_tag_should_work_with_content(
    page: Page, server: Server
) -> None:
    await page.goto(server.EMPTY_PAGE)
    script_handle = await page.add_script_tag(content="window.__injected = 35;")
    assert script_handle.as_element()
    assert await page.evaluate("__injected") == 35


@pytest.mark.skip_browser("firefox")
async def test_add_script_tag_should_throw_when_added_with_content_to_the_csp_page(
    page: Page, server: Server
) -> None:
    # Firefox fires onload for blocked script before it issues the CSP console error.
    await page.goto(server.PREFIX + "/csp.html")
    with pytest.raises(Error) as exc_info:
        await page.add_script_tag(content="window.__injected = 35;")
    assert exc_info.value


async def test_add_script_tag_should_throw_when_added_with_URL_to_the_csp_page(
    page: Page, server: Server
) -> None:
    await page.goto(server.PREFIX + "/csp.html")
    with pytest.raises(Error) as exc_info:
        await page.add_script_tag(url=server.CROSS_PROCESS_PREFIX + "/injectedfile.js")
    assert exc_info.value


async def test_add_script_tag_should_throw_a_nice_error_when_the_request_fails(
    page: Page, server: Server
) -> None:
    await page.goto(server.EMPTY_PAGE)
    url = server.PREFIX + "/this_does_not_exist.js"
    with pytest.raises(Error) as exc_info:
        await page.add_script_tag(url=url)
    assert url in exc_info.value.message


async def test_add_style_tag_should_work_with_a_url(page: Page, server: Server) -> None:
    await page.goto(server.EMPTY_PAGE)
    style_handle = await page.add_style_tag(url="/injectedstyle.css")
    assert style_handle.as_element()
    assert (
        await page.evaluate(
            "window.getComputedStyle(document.querySelector('body')).getPropertyValue('background-color')"
        )
        == "rgb(255, 0, 0)"
    )


async def test_add_style_tag_should_throw_an_error_if_loading_from_url_fail(
    page: Page, server: Server
) -> None:
    await page.goto(server.EMPTY_PAGE)
    with pytest.raises(Error) as exc_info:
        await page.add_style_tag(url="/nonexistfile.js")
    assert exc_info.value


async def test_add_style_tag_should_work_with_a_path(
    page: Page, server: Server, assetdir: Path
) -> None:
    await page.goto(server.EMPTY_PAGE)
    style_handle = await page.add_style_tag(path=assetdir / "injectedstyle.css")
    assert style_handle.as_element()
    assert (
        await page.evaluate(
            "window.getComputedStyle(document.querySelector('body')).getPropertyValue('background-color')"
        )
        == "rgb(255, 0, 0)"
    )


async def test_add_style_tag_should_include_source_url_when_path_is_provided(
    page: Page, server: Server, assetdir: Path
) -> None:
    await page.goto(server.EMPTY_PAGE)
    await page.add_style_tag(path=assetdir / "injectedstyle.css")
    style_handle = await page.query_selector("style")
    style_content = await page.evaluate("style => style.innerHTML", style_handle)
    assert os.path.join("assets", "injectedstyle.css") in style_content


async def test_add_style_tag_should_work_with_content(
    page: Page, server: Server
) -> None:
    await page.goto(server.EMPTY_PAGE)
    style_handle = await page.add_style_tag(content="body { background-color: green; }")
    assert style_handle.as_element()
    assert (
        await page.evaluate(
            "window.getComputedStyle(document.querySelector('body')).getPropertyValue('background-color')"
        )
        == "rgb(0, 128, 0)"
    )


async def test_add_style_tag_should_throw_when_added_with_content_to_the_CSP_page(
    page: Page, server: Server
) -> None:
    await page.goto(server.PREFIX + "/csp.html")
    with pytest.raises(Error) as exc_info:
        await page.add_style_tag(content="body { background-color: green; }")
    assert exc_info.value


async def test_add_style_tag_should_throw_when_added_with_URL_to_the_CSP_page(
    page: Page, server: Server
) -> None:
    await page.goto(server.PREFIX + "/csp.html")
    with pytest.raises(Error) as exc_info:
        await page.add_style_tag(url=server.CROSS_PROCESS_PREFIX + "/injectedstyle.css")
    assert exc_info.value


async def test_url_should_work(page: Page, server: Server) -> None:
    assert page.url == "about:blank"
    await page.goto(server.EMPTY_PAGE)
    assert page.url == server.EMPTY_PAGE


async def test_url_should_include_hashes(page: Page, server: Server) -> None:
    await page.goto(server.EMPTY_PAGE + "#hash")
    assert page.url == server.EMPTY_PAGE + "#hash"
    await page.evaluate("window.location.hash = 'dynamic'")
    assert page.url == server.EMPTY_PAGE + "#dynamic"


async def test_title_should_return_the_page_title(page: Page, server: Server) -> None:
    await page.goto(server.PREFIX + "/title.html")
    assert await page.title() == "Woof-Woof"


async def give_it_a_chance_to_fill(page: Page) -> None:
    for i in range(5):
        await page.evaluate(
            "() => new Promise(f => requestAnimationFrame(() => requestAnimationFrame(f)))"
        )


async def test_fill_should_fill_textarea(page: Page, server: Server) -> None:
    await page.goto(server.PREFIX + "/input/textarea.html")
    await page.fill("textarea", "some value")
    assert await page.evaluate("result") == "some value"


async def test_fill_should_fill_input(page: Page, server: Server) -> None:
    await page.goto(server.PREFIX + "/input/textarea.html")
    await page.fill("input", "some value")
    assert await page.evaluate("result") == "some value"


async def test_fill_should_throw_on_unsupported_inputs(
    page: Page, server: Server
) -> None:
    await page.goto(server.PREFIX + "/input/textarea.html")
    for type in [
        "button",
        "checkbox",
        "file",
        "image",
        "radio",
        "reset",
        "submit",
    ]:
        await page.eval_on_selector(
            "input", "(input, type) => input.setAttribute('type', type)", type
        )
        with pytest.raises(Error) as exc_info:
            await page.fill("input", "")
        assert f'Input of type "{type}" cannot be filled' in exc_info.value.message


async def test_fill_should_fill_different_input_types(
    page: Page, server: Server
) -> None:
    await page.goto(server.PREFIX + "/input/textarea.html")
    for type in ["password", "search", "tel", "text", "url"]:
        await page.eval_on_selector(
            "input", "(input, type) => input.setAttribute('type', type)", type
        )
        await page.fill("input", "text " + type)
        assert await page.evaluate("result") == "text " + type


async def test_fill_should_fill_date_input_after_clicking(
    page: Page, server: Server
) -> None:
    await page.set_content("<input type=date>")
    await page.click("input")
    await page.fill("input", "2020-03-02")
    assert await page.eval_on_selector("input", "input => input.value") == "2020-03-02"


@pytest.mark.skip_browser("webkit")
async def test_fill_should_throw_on_incorrect_date(page: Page, server: Server) -> None:
    # Disabled as in upstream, we should validate time in the Playwright lib
    await page.set_content("<input type=date>")
    with pytest.raises(Error) as exc_info:
        await page.fill("input", "2020-13-05")
    assert "Malformed value" in exc_info.value.message


async def test_fill_should_fill_time_input(page: Page, server: Server) -> None:
    await page.set_content("<input type=time>")
    await page.fill("input", "13:15")
    assert await page.eval_on_selector("input", "input => input.value") == "13:15"


@pytest.mark.skip_browser("webkit")
async def test_fill_should_throw_on_incorrect_time(page: Page, server: Server) -> None:
    # Disabled as in upstream, we should validate time in the Playwright lib
    await page.set_content("<input type=time>")
    with pytest.raises(Error) as exc_info:
        await page.fill("input", "25:05")
    assert "Malformed value" in exc_info.value.message


async def test_fill_should_fill_datetime_local_input(
    page: Page, server: Server
) -> None:
    await page.set_content("<input type=datetime-local>")
    await page.fill("input", "2020-03-02T05:15")
    assert (
        await page.eval_on_selector("input", "input => input.value")
        == "2020-03-02T05:15"
    )


@pytest.mark.only_browser("chromium")
async def test_fill_should_throw_on_incorrect_datetime_local(page: Page) -> None:
    await page.set_content("<input type=datetime-local>")
    with pytest.raises(Error) as exc_info:
        await page.fill("input", "abc")
    assert "Malformed value" in exc_info.value.message


async def test_fill_should_fill_contenteditable(page: Page, server: Server) -> None:
    await page.goto(server.PREFIX + "/input/textarea.html")
    await page.fill("div[contenteditable]", "some value")
    assert (
        await page.eval_on_selector("div[contenteditable]", "div => div.textContent")
        == "some value"
    )


async def test_fill_should_fill_elements_with_existing_value_and_selection(
    page: Page, server: Server
) -> None:
    await page.goto(server.PREFIX + "/input/textarea.html")

    await page.eval_on_selector("input", "input => input.value = 'value one'")
    await page.fill("input", "another value")
    assert await page.evaluate("result") == "another value"

    await page.eval_on_selector(
        "input",
        """input => {
        input.selectionStart = 1
        input.selectionEnd = 2
    }""",
    )

    await page.fill("input", "maybe this one")
    assert await page.evaluate("result") == "maybe this one"

    await page.eval_on_selector(
        "div[contenteditable]",
        """div => {
        div.innerHTML = 'some text <span>some more text<span> and even more text'
        range = document.createRange()
        range.selectNodeContents(div.querySelector('span'))
        selection = window.getSelection()
        selection.removeAllRanges()
        selection.addRange(range)
    }""",
    )

    await page.fill("div[contenteditable]", "replace with this")
    assert (
        await page.eval_on_selector("div[contenteditable]", "div => div.textContent")
        == "replace with this"
    )


async def test_fill_should_throw_when_element_is_not_an_input_textarea_or_contenteditable(
    page: Page, server: Server
) -> None:
    await page.goto(server.PREFIX + "/input/textarea.html")
    with pytest.raises(Error) as exc_info:
        await page.fill("body", "")
    assert "Element is not an <input>" in exc_info.value.message


async def test_fill_should_throw_if_passed_a_non_string_value(
    page: Page, server: Server
) -> None:
    await page.goto(server.PREFIX + "/input/textarea.html")
    with pytest.raises(Error) as exc_info:
        await page.fill("textarea", 123)  # type: ignore
    assert "expected string, got number" in exc_info.value.message


async def test_fill_should_retry_on_disabled_element(
    page: Page, server: Server
) -> None:
    await page.goto(server.PREFIX + "/input/textarea.html")
    await page.eval_on_selector("input", "i => i.disabled = true")
    done = []

    async def fill() -> None:
        await page.fill("input", "some value")
        done.append(True)

    promise = asyncio.create_task(fill())
    await give_it_a_chance_to_fill(page)
    assert done == []
    assert await page.evaluate("result") == ""

    await page.eval_on_selector("input", "i => i.disabled = false")
    await promise
    assert await page.evaluate("result") == "some value"


async def test_fill_should_retry_on_readonly_element(
    page: Page, server: Server
) -> None:
    await page.goto(server.PREFIX + "/input/textarea.html")
    await page.eval_on_selector("textarea", "i => i.readOnly = true")
    done = []

    async def fill() -> None:
        await page.fill("textarea", "some value")
        done.append(True)

    promise = asyncio.create_task(fill())
    await give_it_a_chance_to_fill(page)
    assert done == []
    assert await page.evaluate("result") == ""

    await page.eval_on_selector("textarea", "i => i.readOnly = false")
    await promise
    assert await page.evaluate("result") == "some value"


async def test_fill_should_retry_on_invisible_element(
    page: Page, server: Server
) -> None:
    await page.goto(server.PREFIX + "/input/textarea.html")
    await page.eval_on_selector("input", "i => i.style.display = 'none'")
    done = []

    async def fill() -> None:
        await page.fill("input", "some value")
        done.append(True)

    promise = asyncio.create_task(fill())
    await give_it_a_chance_to_fill(page)
    assert done == []
    assert await page.evaluate("result") == ""

    await page.eval_on_selector("input", "i => i.style.display = 'inline'")
    await promise
    assert await page.evaluate("result") == "some value"


async def test_fill_should_be_able_to_fill_the_body(page: Page) -> None:
    await page.set_content('<body contentEditable="true"></body>')
    await page.fill("body", "some value")
    assert await page.evaluate("document.body.textContent") == "some value"


async def test_fill_should_fill_fixed_position_input(page: Page) -> None:
    await page.set_content('<input style="position: fixed;" />')
    await page.fill("input", "some value")
    assert await page.evaluate("document.querySelector('input').value") == "some value"


async def test_fill_should_be_able_to_fill_when_focus_is_in_the_wrong_frame(
    page: Page,
) -> None:
    await page.set_content(
        """
      <div contentEditable="true"></div>
      <iframe></iframe>
    """
    )
    await page.focus("iframe")
    await page.fill("div", "some value")
    assert await page.eval_on_selector("div", "d => d.textContent") == "some value"


async def test_fill_should_be_able_to_fill_the_input_type_number_(page: Page) -> None:
    await page.set_content('<input id="input" type="number"></input>')
    await page.fill("input", "42")
    assert await page.evaluate("input.value") == "42"


async def test_fill_should_be_able_to_fill_exponent_into_the_input_type_number_(
    page: Page,
) -> None:
    await page.set_content('<input id="input" type="number"></input>')
    await page.fill("input", "-10e5")
    assert await page.evaluate("input.value") == "-10e5"


async def test_fill_should_be_able_to_fill_input_type_number__with_empty_string(
    page: Page,
) -> None:
    await page.set_content('<input id="input" type="number" value="123"></input>')
    await page.fill("input", "")
    assert await page.evaluate("input.value") == ""


async def test_fill_should_not_be_able_to_fill_text_into_the_input_type_number_(
    page: Page,
) -> None:
    await page.set_content('<input id="input" type="number"></input>')
    with pytest.raises(Error) as exc_info:
        await page.fill("input", "abc")
    assert "Cannot type text into input[type=number]" in exc_info.value.message


async def test_fill_should_be_able_to_clear_using_fill(
    page: Page, server: Server
) -> None:
    await page.goto(server.PREFIX + "/input/textarea.html")
    await page.fill("input", "some value")
    assert await page.evaluate("result") == "some value"
    await page.fill("input", "")
    assert await page.evaluate("result") == ""


async def test_close_event_should_work_with_window_close(
    page: Page, server: Server
) -> None:
    async with page.expect_popup() as popup_info:
        await page.evaluate("window['newPage'] = window.open('about:blank')")
    popup = await popup_info.value

    async with popup.expect_event("close"):
        await page.evaluate("window['newPage'].close()")


async def test_close_event_should_work_with_page_close(
    context: BrowserContext, server: Server
) -> None:
    page = await context.new_page()
    async with page.expect_event("close"):
        await page.close()


async def test_page_context_should_return_the_correct_browser_instance(
    page: Page, context: BrowserContext
) -> None:
    assert page.context == context


async def test_frame_should_respect_name(page: Page, server: Server) -> None:
    await page.set_content("<iframe name=target></iframe>")
    assert page.frame(name="bogus") is None
    frame = page.frame(name="target")
    assert frame
    assert frame == page.main_frame.child_frames[0]


async def test_frame_should_respect_url(page: Page, server: Server) -> None:
    await page.set_content(f'<iframe src="{server.EMPTY_PAGE}"></iframe>')
    assert page.frame(url=re.compile(r"bogus")) is None
    assert must(page.frame(url=re.compile(r"empty"))).url == server.EMPTY_PAGE


async def test_press_should_work(page: Page, server: Server) -> None:
    await page.goto(server.PREFIX + "/input/textarea.html")
    await page.press("textarea", "a")
    assert await page.evaluate("document.querySelector('textarea').value") == "a"


async def test_frame_press_should_work(page: Page, server: Server) -> None:
    await page.set_content(
        f'<iframe name=inner src="{server.PREFIX}/input/textarea.html"></iframe>'
    )
    frame = page.frame("inner")
    assert frame
    await frame.press("textarea", "a")
    assert await frame.evaluate("document.querySelector('textarea').value") == "a"


async def test_should_emulate_reduced_motion(page: Page, server: Server) -> None:
    assert await page.evaluate(
        "matchMedia('(prefers-reduced-motion: no-preference)').matches"
    )
    await page.emulate_media(reduced_motion="reduce")
    assert await page.evaluate("matchMedia('(prefers-reduced-motion: reduce)').matches")
    assert not await page.evaluate(
        "matchMedia('(prefers-reduced-motion: no-preference)').matches"
    )
    await page.emulate_media(reduced_motion="no-preference")
    assert not await page.evaluate(
        "matchMedia('(prefers-reduced-motion: reduce)').matches"
    )
    assert await page.evaluate(
        "matchMedia('(prefers-reduced-motion: no-preference)').matches"
    )


async def test_input_value(page: Page, server: Server) -> None:
    await page.goto(server.PREFIX + "/input/textarea.html")

    await page.fill("input", "my-text-content")
    assert await page.input_value("input") == "my-text-content"

    await page.fill("input", "")
    assert await page.input_value("input") == ""


async def test_drag_and_drop_helper_method(page: Page, server: Server) -> None:
    await page.goto(server.PREFIX + "/drag-n-drop.html")
    await page.drag_and_drop("#source", "#target")
    assert (
        await page.eval_on_selector(
            "#target", "target => target.contains(document.querySelector('#source'))"
        )
        is True
    )


async def test_drag_and_drop_with_position(page: Page, server: Server) -> None:
    await page.goto(server.EMPTY_PAGE)
    await page.set_content(
        """
      <div style="width:100px;height:100px;background:red;" id="red">
      </div>
      <div style="width:100px;height:100px;background:blue;" id="blue">
      </div>
    """
    )
    events_handle = await page.evaluate_handle(
        """
        () => {
        const events = [];
        document.getElementById('red').addEventListener('mousedown', event => {
            events.push({
            type: 'mousedown',
            x: event.offsetX,
            y: event.offsetY,
            });
        });
        document.getElementById('blue').addEventListener('mouseup', event => {
            events.push({
            type: 'mouseup',
            x: event.offsetX,
            y: event.offsetY,
            });
        });
        return events;
        }
    """
    )
    await page.drag_and_drop(
        "#red",
        "#blue",
        source_position={"x": 34, "y": 7},
        target_position={"x": 10, "y": 20},
    )
    assert await events_handle.json_value() == [
        {"type": "mousedown", "x": 34, "y": 7},
        {"type": "mouseup", "x": 10, "y": 20},
    ]


async def test_should_check_box_using_set_checked(page: Page) -> None:
    await page.set_content("`<input id='checkbox' type='checkbox'></input>`")
    await page.set_checked("input", True)
    assert await page.evaluate("checkbox.checked") is True
    await page.set_checked("input", False)
    assert await page.evaluate("checkbox.checked") is False


async def test_should_set_bodysize_and_headersize(page: Page, server: Server) -> None:
    await page.goto(server.EMPTY_PAGE)
    async with page.expect_request("*/**") as request_info:
        await page.evaluate(
            "() => fetch('./get', { method: 'POST', body: '12345'}).then(r => r.text())"
        )
    request = await request_info.value
    sizes = await request.sizes()
    assert sizes["requestBodySize"] == 5
    assert sizes["requestHeadersSize"] >= 300


async def test_should_set_bodysize_to_0(page: Page, server: Server) -> None:
    await page.goto(server.EMPTY_PAGE)
    async with page.expect_request("*/**") as request_info:
        await page.evaluate("() => fetch('./get').then(r => r.text())")
    request = await request_info.value
    sizes = await request.sizes()
    assert sizes["requestBodySize"] == 0
    assert sizes["requestHeadersSize"] >= 200


async def test_should_emulate_forced_colors(page: Page) -> None:
    assert await page.evaluate("matchMedia('(forced-colors: none)').matches")
    await page.emulate_media(forced_colors="none")
    assert await page.evaluate("matchMedia('(forced-colors: none)').matches")
    assert not await page.evaluate("matchMedia('(forced-colors: active)').matches")
    await page.emulate_media(forced_colors="active")
    assert await page.evaluate("matchMedia('(forced-colors: active)').matches")
    assert not await page.evaluate("matchMedia('(forced-colors: none)').matches")


async def test_should_emulate_contrast(page: Page) -> None:
    assert await page.evaluate(
        "matchMedia('(prefers-contrast: no-preference)').matches"
    )
    await page.emulate_media(contrast="no-preference")
    assert await page.evaluate(
        "matchMedia('(prefers-contrast: no-preference)').matches"
    )
    assert not await page.evaluate("matchMedia('(prefers-contrast: more)').matches")
    await page.emulate_media(contrast="more")
    assert not await page.evaluate(
        "matchMedia('(prefers-contrast: no-preference)').matches"
    )
    assert await page.evaluate("matchMedia('(prefers-contrast: more)').matches")
    await page.emulate_media(contrast="null")
    assert await page.evaluate(
        "matchMedia('(prefers-contrast: no-preference)').matches"
    )


async def test_should_not_throw_when_continuing_while_page_is_closing(
    page: Page, server: Server
) -> None:
    done: Optional[asyncio.Future] = None

    def handle_route(route: Route) -> None:
        nonlocal done
        done = asyncio.gather(route.continue_(), page.close())

    await page.route("**/*", handle_route)
    with pytest.raises(Error):
        await page.goto(server.EMPTY_PAGE)
    await must(done)


async def test_should_not_throw_when_continuing_after_page_is_closed(
    page: Page, server: Server
) -> None:
    done: "asyncio.Future[bool]" = asyncio.Future()

    async def handle_route(route: Route) -> None:
        await page.close()
        await route.continue_()
        nonlocal done
        done.set_result(True)

    await page.route("**/*", handle_route)
    with pytest.raises(Error):
        await page.goto(server.EMPTY_PAGE)
    await done


async def test_expose_binding_should_serialize_cycles(page: Page) -> None:
    binding_values = []

    def binding(source: Dict, o: Dict) -> None:
        binding_values.append(o)

    await page.expose_binding("log", lambda source, o: binding(source, o))
    await page.evaluate("const a = {}; a.b = a; window.log(a)")
    assert binding_values[0]["b"] == binding_values[0]


async def test_page_pause_should_reset_default_timeouts(
    page: Page, headless: bool, server: Server
) -> None:
    if not headless:
        pytest.skip()

    await page.goto(server.EMPTY_PAGE)
    await page.pause()
    with pytest.raises(Error, match="Timeout 30000ms exceeded."):
        await page.get_by_text("foo").click()


async def test_page_pause_should_reset_custom_timeouts(
    page: Page, headless: bool, server: Server
) -> None:
    if not headless:
        pytest.skip()

    page.set_default_timeout(123)
    page.set_default_navigation_timeout(456)
    await page.goto(server.EMPTY_PAGE)
    await page.pause()
    with pytest.raises(Error, match="Timeout 123ms exceeded."):
        await page.get_by_text("foo").click()

    server.set_route("/empty.html", lambda route: None)
    with pytest.raises(Error, match="Timeout 456ms exceeded."):
        await page.goto(server.EMPTY_PAGE)
