From d7b822d2f9052a31c030955f85dfe7726e6d719b Mon Sep 17 00:00:00 2001 From: "Ross A. Wollman" Date: Thu, 23 Jun 2022 15:53:46 -0700 Subject: [PATCH 1/6] chore: roll to 1.23 (1.24.0-alpha-1656026114000) - fromServiceWorkers - toHaveValues - browserType - ignoreCase --- README.md | 4 +- playwright/_impl/_api_structures.py | 1 + playwright/_impl/_assertions.py | 75 ++++- playwright/_impl/_browser.py | 7 + playwright/_impl/_browser_type.py | 2 + playwright/_impl/_helper.py | 1 + playwright/_impl/_network.py | 4 + playwright/async_api/_generated.py | 175 +++++++++- playwright/sync_api/_generated.py | 201 +++++++++--- scripts/expected_api_mismatch.txt | 8 + setup.py | 2 +- tests/async/test_assertions.py | 296 ++++++++++++++++- tests/async/test_browser.py | 4 + ...st_browsercontext_service_worker_policy.py | 24 ++ tests/async/test_network.py | 13 + tests/sync/test_assertions.py | 307 +++++++++++++++++- tests/sync/test_browser.py | 21 ++ ...st_browsercontext_service_worker_policy.py | 24 ++ tests/sync/test_network.py | 12 + 19 files changed, 1107 insertions(+), 74 deletions(-) create mode 100644 tests/async/test_browsercontext_service_worker_policy.py create mode 100644 tests/sync/test_browser.py create mode 100644 tests/sync/test_browsercontext_service_worker_policy.py diff --git a/README.md b/README.md index 242cad50e..6a1f7da36 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ Playwright is a Python library to automate [Chromium](https://www.chromium.org/H | | Linux | macOS | Windows | | :--- | :---: | :---: | :---: | -| Chromium 102.0.5005.61 | ✅ | ✅ | ✅ | +| Chromium 103.0.5060.53 | ✅ | ✅ | ✅ | | WebKit 15.4 | ✅ | ✅ | ✅ | -| Firefox 99.0.1 | ✅ | ✅ | ✅ | +| Firefox 100.0.2 | ✅ | ✅ | ✅ | ## Documentation diff --git a/playwright/_impl/_api_structures.py b/playwright/_impl/_api_structures.py index 4d9f3fa72..cc0eb39e9 100644 --- a/playwright/_impl/_api_structures.py +++ b/playwright/_impl/_api_structures.py @@ -179,6 +179,7 @@ class ExpectedTextValue(TypedDict, total=False): regexFlags: str matchSubstring: bool normalizeWhiteSpace: bool + ignoreCase: Optional[bool] class FrameExpectOptions(TypedDict, total=False): diff --git a/playwright/_impl/_assertions.py b/playwright/_impl/_assertions.py index 404980c26..a58ce67cb 100644 --- a/playwright/_impl/_assertions.py +++ b/playwright/_impl/_assertions.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Any, List, Pattern, Union +from typing import Any, List, Optional, Pattern, Union from urllib.parse import urljoin from playwright._impl._api_structures import ExpectedTextValue, FrameExpectOptions @@ -122,11 +122,15 @@ async def to_contain_text( expected: Union[List[Union[Pattern, str]], Pattern, str], use_inner_text: bool = None, timeout: float = None, + ignore_case: bool = None, ) -> None: __tracebackhide__ = True if isinstance(expected, list): expected_text = to_expected_text_values( - expected, match_substring=True, normalize_white_space=True + expected, + match_substring=True, + normalize_white_space=True, + ignore_case=ignore_case, ) await self._expect_impl( "to.contain.text.array", @@ -140,7 +144,10 @@ async def to_contain_text( ) else: expected_text = to_expected_text_values( - [expected], match_substring=True, normalize_white_space=True + [expected], + match_substring=True, + normalize_white_space=True, + ignore_case=ignore_case, ) await self._expect_impl( "to.have.text", @@ -158,9 +165,10 @@ async def not_to_contain_text( expected: Union[List[Union[Pattern, str]], Pattern, str], use_inner_text: bool = None, timeout: float = None, + ignore_case: bool = None, ) -> None: __tracebackhide__ = True - await self._not.to_contain_text(expected, use_inner_text, timeout) + await self._not.to_contain_text(expected, use_inner_text, timeout, ignore_case) async def to_have_attribute( self, @@ -335,16 +343,41 @@ async def not_to_have_value( __tracebackhide__ = True await self._not.to_have_value(value, timeout) + async def to_have_values( + self, + values: List[Union[Pattern, str]], + timeout: float = None, + ) -> None: + __tracebackhide__ = True + expected_text = to_expected_text_values(values) + await self._expect_impl( + "to.have.values", + FrameExpectOptions(expectedText=expected_text, timeout=timeout), + values, + "Locator expected to have Values", + ) + + async def not_to_have_values( + self, + values: List[Union[Pattern, str]], + timeout: float = None, + ) -> None: + __tracebackhide__ = True + await self._not.to_have_values(values, timeout) + async def to_have_text( self, expected: Union[List[Union[Pattern, str]], Pattern, str], use_inner_text: bool = None, timeout: float = None, + ignore_case: bool = None, ) -> None: __tracebackhide__ = True if isinstance(expected, list): expected_text = to_expected_text_values( - expected, normalize_white_space=True + expected, + normalize_white_space=True, + ignore_case=ignore_case, ) await self._expect_impl( "to.have.text.array", @@ -358,7 +391,7 @@ async def to_have_text( ) else: expected_text = to_expected_text_values( - [expected], normalize_white_space=True + [expected], normalize_white_space=True, ignore_case=ignore_case ) await self._expect_impl( "to.have.text", @@ -376,9 +409,10 @@ async def not_to_have_text( expected: Union[List[Union[Pattern, str]], Pattern, str], use_inner_text: bool = None, timeout: float = None, + ignore_case: bool = None, ) -> None: __tracebackhide__ = True - await self._not.to_have_text(expected, use_inner_text, timeout) + await self._not.to_have_text(expected, use_inner_text, timeout, ignore_case) async def to_be_checked( self, @@ -568,14 +602,20 @@ async def not_to_be_ok(self) -> None: def expected_regex( - pattern: Pattern, match_substring: bool, normalize_white_space: bool + pattern: Pattern, + match_substring: bool, + normalize_white_space: bool, + ignore_case: Optional[bool] = None, ) -> ExpectedTextValue: expected = ExpectedTextValue( regexSource=pattern.pattern, regexFlags=escape_regex_flags(pattern), matchSubstring=match_substring, normalizeWhiteSpace=normalize_white_space, + ignoreCase=ignore_case, ) + if expected["ignoreCase"] is None: + del expected["ignoreCase"] return expected @@ -583,18 +623,25 @@ def to_expected_text_values( items: Union[List[Pattern], List[str], List[Union[str, Pattern]]], match_substring: bool = False, normalize_white_space: bool = False, + ignore_case: Optional[bool] = None, ) -> List[ExpectedTextValue]: out: List[ExpectedTextValue] = [] assert isinstance(items, list) for item in items: if isinstance(item, str): + o = ExpectedTextValue( + string=item, + matchSubstring=match_substring, + normalizeWhiteSpace=normalize_white_space, + ignoreCase=ignore_case, + ) + if o["ignoreCase"] is None: + del o["ignoreCase"] + out.append(o) + elif isinstance(item, Pattern): out.append( - ExpectedTextValue( - string=item, - matchSubstring=match_substring, - normalizeWhiteSpace=normalize_white_space, + expected_regex( + item, match_substring, normalize_white_space, ignore_case ) ) - elif isinstance(item, Pattern): - out.append(expected_regex(item, match_substring, normalize_white_space)) return out diff --git a/playwright/_impl/_browser.py b/playwright/_impl/_browser.py index 9f0adcc92..62c9ba4ec 100644 --- a/playwright/_impl/_browser.py +++ b/playwright/_impl/_browser.py @@ -32,6 +32,7 @@ ColorScheme, ForcedColors, ReducedMotion, + ServiceWorkersPolicy, async_readfile, is_safe_close_error, locals_to_params, @@ -75,6 +76,10 @@ def _on_close(self) -> None: def contexts(self) -> List[BrowserContext]: return self._contexts.copy() + @property + def browser_type(self) -> "BrowserType": + return self._browser_type + def is_connected(self) -> bool: return self._is_connected @@ -110,6 +115,7 @@ async def new_context( storageState: Union[StorageState, str, Path] = None, baseURL: str = None, strictSelectors: bool = None, + serviceWorkers: ServiceWorkersPolicy = None, ) -> BrowserContext: params = locals_to_params(locals()) await normalize_context_params(self._connection._is_sync, params) @@ -154,6 +160,7 @@ async def new_page( storageState: Union[StorageState, str, Path] = None, baseURL: str = None, strictSelectors: bool = None, + serviceWorkers: ServiceWorkersPolicy = None, ) -> Page: params = locals_to_params(locals()) context = await self.new_context(**params) diff --git a/playwright/_impl/_browser_type.py b/playwright/_impl/_browser_type.py index 3bb631145..f38aad50d 100644 --- a/playwright/_impl/_browser_type.py +++ b/playwright/_impl/_browser_type.py @@ -37,6 +37,7 @@ Env, ForcedColors, ReducedMotion, + ServiceWorkersPolicy, locals_to_params, ) from playwright._impl._transport import WebSocketTransport @@ -138,6 +139,7 @@ async def launch_persistent_context( recordVideoSize: ViewportSize = None, baseURL: str = None, strictSelectors: bool = None, + serviceWorkers: ServiceWorkersPolicy = None, ) -> BrowserContext: userDataDir = str(Path(userDataDir)) params = locals_to_params(locals()) diff --git a/playwright/_impl/_helper.py b/playwright/_impl/_helper.py index d4709d473..9656ae8e3 100644 --- a/playwright/_impl/_helper.py +++ b/playwright/_impl/_helper.py @@ -62,6 +62,7 @@ DocumentLoadState = Literal["commit", "domcontentloaded", "load", "networkidle"] KeyboardModifier = Literal["Alt", "Control", "Meta", "Shift"] MouseButton = Literal["left", "middle", "right"] +ServiceWorkersPolicy = Literal["allow", "block"] class ErrorPayload(TypedDict, total=False): diff --git a/playwright/_impl/_network.py b/playwright/_impl/_network.py index 91511b1f8..0230c2dba 100644 --- a/playwright/_impl/_network.py +++ b/playwright/_impl/_network.py @@ -343,6 +343,10 @@ def status_text(self) -> str: def headers(self) -> Headers: return self._provisional_headers.headers() + @property + def from_service_worker(self) -> bool: + return self._initializer["fromServiceWorker"] + async def all_headers(self) -> Headers: return (await self._actual_headers()).headers() diff --git a/playwright/async_api/_generated.py b/playwright/async_api/_generated.py index 934438b24..a47af60a0 100644 --- a/playwright/async_api/_generated.py +++ b/playwright/async_api/_generated.py @@ -419,6 +419,19 @@ def headers(self) -> typing.Dict[str, str]: """ return mapping.from_maybe_impl(self._impl_obj.headers) + @property + def from_service_worker(self) -> bool: + """Response.from_service_worker + + Indicates whether this Response was fullfilled by a Service Worker's Fetch Handler (i.e. via + [FetchEvent.respondWith](https://developer.mozilla.org/en-US/docs/Web/API/FetchEvent/respondWith)). + + Returns + ------- + bool + """ + return mapping.from_maybe_impl(self._impl_obj.from_service_worker) + @property def request(self) -> "Request": """Response.request @@ -695,8 +708,8 @@ async def handle(route, request): # override headers headers = { **request.headers, - \"foo\": \"bar\" # set \"foo\" header - \"origin\": None # remove \"origin\" header + \"foo\": \"foo-value\" # set \"foo\" header + \"bar\": None # remove \"bar\" header } await route.continue_(headers=headers) } @@ -7555,7 +7568,7 @@ async def route( > NOTE: The handler will only be called for the first url if the response is a redirect. > NOTE: `page.route()` will not intercept requests intercepted by Service Worker. See [this](https://github.com/microsoft/playwright/issues/1090) issue. We recommend disabling Service Workers when using - request interception. Via `await context.addInitScript(() => delete window.navigator.serviceWorker);` + request interception by setting `Browser.newContext.serviceWorkers` to `'block'`. An example of a naive handler that aborts all image requests: @@ -10244,9 +10257,9 @@ async def route( Routing provides the capability to modify network requests that are made by any page in the browser context. Once route is enabled, every request matching the url pattern will stall unless it's continued, fulfilled or aborted. - > NOTE: `page.route()` will not intercept requests intercepted by Service Worker. See + > NOTE: `browser_context.route()` will not intercept requests intercepted by Service Worker. See [this](https://github.com/microsoft/playwright/issues/1090) issue. We recommend disabling Service Workers when using - request interception. Via `await context.addInitScript(() => delete window.navigator.serviceWorker);` + request interception by setting `Browser.newContext.serviceWorkers` to `'block'`. An example of a naive handler that aborts all image requests: @@ -10565,6 +10578,18 @@ def contexts(self) -> typing.List["BrowserContext"]: """ return mapping.from_impl_list(self._impl_obj.contexts) + @property + def browser_type(self) -> "BrowserType": + """Browser.browser_type + + Get the browser type (chromium, firefox or webkit) that the browser belongs to. + + Returns + ------- + BrowserType + """ + return mapping.from_impl(self._impl_obj.browser_type) + @property def version(self) -> str: """Browser.version @@ -10621,7 +10646,8 @@ async def new_context( record_video_size: ViewportSize = None, storage_state: typing.Union[StorageState, str, pathlib.Path] = None, base_url: str = None, - strict_selectors: bool = None + strict_selectors: bool = None, + service_workers: Literal["allow", "block"] = None ) -> "BrowserContext": """Browser.new_context @@ -10723,9 +10749,13 @@ async def new_context( - baseURL: `http://localhost:3000/foo` (without trailing slash) and navigating to `./bar.html` results in `http://localhost:3000/bar.html` strict_selectors : Union[bool, NoneType] - It specified, enables strict selectors mode for this context. In the strict selectors mode all operations on selectors + If specified, enables strict selectors mode for this context. In the strict selectors mode all operations on selectors that imply single target DOM element will throw when more than one element matches the selector. See `Locator` to learn more about the strict mode. + service_workers : Union["allow", "block", NoneType] + Whether to allow sites to register Service workers. Defaults to `'allow'`. + - `'allow'`: [Service Workers](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) can be registered. + - `'block'`: Playwright will block all registration of Service Workers. Returns ------- @@ -10764,6 +10794,7 @@ async def new_context( storageState=storage_state, baseURL=base_url, strictSelectors=strict_selectors, + serviceWorkers=service_workers, ) ) @@ -10799,7 +10830,8 @@ async def new_page( record_video_size: ViewportSize = None, storage_state: typing.Union[StorageState, str, pathlib.Path] = None, base_url: str = None, - strict_selectors: bool = None + strict_selectors: bool = None, + service_workers: Literal["allow", "block"] = None ) -> "Page": """Browser.new_page @@ -10896,9 +10928,13 @@ async def new_page( - baseURL: `http://localhost:3000/foo` (without trailing slash) and navigating to `./bar.html` results in `http://localhost:3000/bar.html` strict_selectors : Union[bool, NoneType] - It specified, enables strict selectors mode for this context. In the strict selectors mode all operations on selectors + If specified, enables strict selectors mode for this context. In the strict selectors mode all operations on selectors that imply single target DOM element will throw when more than one element matches the selector. See `Locator` to learn more about the strict mode. + service_workers : Union["allow", "block", NoneType] + Whether to allow sites to register Service workers. Defaults to `'allow'`. + - `'allow'`: [Service Workers](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) can be registered. + - `'block'`: Playwright will block all registration of Service Workers. Returns ------- @@ -10937,6 +10973,7 @@ async def new_page( storageState=storage_state, baseURL=base_url, strictSelectors=strict_selectors, + serviceWorkers=service_workers, ) ) @@ -11231,7 +11268,8 @@ async def launch_persistent_context( record_video_dir: typing.Union[str, pathlib.Path] = None, record_video_size: ViewportSize = None, base_url: str = None, - strict_selectors: bool = None + strict_selectors: bool = None, + service_workers: Literal["allow", "block"] = None ) -> "BrowserContext": """BrowserType.launch_persistent_context @@ -11368,9 +11406,13 @@ async def launch_persistent_context( - baseURL: `http://localhost:3000/foo` (without trailing slash) and navigating to `./bar.html` results in `http://localhost:3000/bar.html` strict_selectors : Union[bool, NoneType] - It specified, enables strict selectors mode for this context. In the strict selectors mode all operations on selectors + If specified, enables strict selectors mode for this context. In the strict selectors mode all operations on selectors that imply single target DOM element will throw when more than one element matches the selector. See `Locator` to learn more about the strict mode. + service_workers : Union["allow", "block", NoneType] + Whether to allow sites to register Service workers. Defaults to `'allow'`. + - `'allow'`: [Service Workers](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) can be registered. + - `'block'`: Playwright will block all registration of Service Workers. Returns ------- @@ -11423,6 +11465,7 @@ async def launch_persistent_context( recordVideoSize=record_video_size, baseURL=base_url, strictSelectors=strict_selectors, + serviceWorkers=service_workers, ) ) @@ -12336,7 +12379,17 @@ def filter( ) -> "Locator": """Locator.filter - This method narrows existing locator according to the options, for example filters by text. + This method narrows existing locator according to the options, for example filters by text. It can be chained to filter + multiple times. + + ```py + row_locator = page.lsocator(\"tr\") + # ... + await row_locator + .filter(has_text=\"text in column 1\") + .filter(has=page.locator(\"tr\", has_text=\"column 2 button\")) + .screenshot() + ``` Parameters ---------- @@ -14088,7 +14141,8 @@ async def to_contain_text( ], *, use_inner_text: bool = None, - timeout: float = None + timeout: float = None, + ignore_case: bool = None ) -> NoneType: """LocatorAssertions.to_contain_text @@ -14122,6 +14176,9 @@ async def to_contain_text( Whether to use `element.innerText` instead of `element.textContent` when retrieving DOM node text. timeout : Union[float, NoneType] Time to retry the assertion for. + ignore_case : Union[bool, NoneType] + Whether to perform case-insensitive match. `ignoreCase` option takes precedence over the corresponding regular + expression flag if specified. """ __tracebackhide__ = True @@ -14130,6 +14187,7 @@ async def to_contain_text( expected=mapping.to_impl(expected), use_inner_text=use_inner_text, timeout=timeout, + ignore_case=ignore_case, ) ) @@ -14140,7 +14198,8 @@ async def not_to_contain_text( ], *, use_inner_text: bool = None, - timeout: float = None + timeout: float = None, + ignore_case: bool = None ) -> NoneType: """LocatorAssertions.not_to_contain_text @@ -14154,6 +14213,9 @@ async def not_to_contain_text( Whether to use `element.innerText` instead of `element.textContent` when retrieving DOM node text. timeout : Union[float, NoneType] Time to retry the assertion for. + ignore_case : Union[bool, NoneType] + Whether to perform case-insensitive match. `ignoreCase` option takes precedence over the corresponding regular + expression flag if specified. """ __tracebackhide__ = True @@ -14162,6 +14224,7 @@ async def not_to_contain_text( expected=mapping.to_impl(expected), use_inner_text=use_inner_text, timeout=timeout, + ignore_case=ignore_case, ) ) @@ -14554,6 +14617,76 @@ async def not_to_have_value( await self._impl_obj.not_to_have_value(value=value, timeout=timeout) ) + async def to_have_values( + self, + values: typing.List[typing.Union[typing.Pattern, str]], + *, + timeout: float = None + ) -> NoneType: + """LocatorAssertions.to_have_values + + Ensures the `Locator` points to multi-select/combobox (i.e. a `select` with the `multiple` attribute) and the specified + values are selected. + + For example, given the following element: + + ```html + + ``` + + ```py + import re + from playwright.async_api import expect + + locator = page.locator(\"id=favorite-colors\") + await locator.select_option([\"R\", \"G\"]) + await expect(locator).to_have_values([re.compile(r\"R\"), re.compile(r\"G\")]) + ``` + + Parameters + ---------- + values : List[Union[Pattern, str]] + Expected options currently selected. + timeout : Union[float, NoneType] + Time to retry the assertion for. + """ + __tracebackhide__ = True + + return mapping.from_maybe_impl( + await self._impl_obj.to_have_values( + values=mapping.to_impl(values), timeout=timeout + ) + ) + + async def not_to_have_values( + self, + values: typing.List[typing.Union[typing.Pattern, str]], + *, + timeout: float = None + ) -> NoneType: + """LocatorAssertions.not_to_have_values + + The opposite of `locator_assertions.to_have_values()`. + + Parameters + ---------- + values : List[Union[Pattern, str]] + Expected options currently selected. + timeout : Union[float, NoneType] + Time to retry the assertion for. + """ + __tracebackhide__ = True + + return mapping.from_maybe_impl( + await self._impl_obj.not_to_have_values( + values=mapping.to_impl(values), timeout=timeout + ) + ) + async def to_have_text( self, expected: typing.Union[ @@ -14561,7 +14694,8 @@ async def to_have_text( ], *, use_inner_text: bool = None, - timeout: float = None + timeout: float = None, + ignore_case: bool = None ) -> NoneType: """LocatorAssertions.to_have_text @@ -14593,6 +14727,9 @@ async def to_have_text( Whether to use `element.innerText` instead of `element.textContent` when retrieving DOM node text. timeout : Union[float, NoneType] Time to retry the assertion for. + ignore_case : Union[bool, NoneType] + Whether to perform case-insensitive match. `ignoreCase` option takes precedence over the corresponding regular + expression flag if specified. """ __tracebackhide__ = True @@ -14601,6 +14738,7 @@ async def to_have_text( expected=mapping.to_impl(expected), use_inner_text=use_inner_text, timeout=timeout, + ignore_case=ignore_case, ) ) @@ -14611,7 +14749,8 @@ async def not_to_have_text( ], *, use_inner_text: bool = None, - timeout: float = None + timeout: float = None, + ignore_case: bool = None ) -> NoneType: """LocatorAssertions.not_to_have_text @@ -14625,6 +14764,9 @@ async def not_to_have_text( Whether to use `element.innerText` instead of `element.textContent` when retrieving DOM node text. timeout : Union[float, NoneType] Time to retry the assertion for. + ignore_case : Union[bool, NoneType] + Whether to perform case-insensitive match. `ignoreCase` option takes precedence over the corresponding regular + expression flag if specified. """ __tracebackhide__ = True @@ -14633,6 +14775,7 @@ async def not_to_have_text( expected=mapping.to_impl(expected), use_inner_text=use_inner_text, timeout=timeout, + ignore_case=ignore_case, ) ) diff --git a/playwright/sync_api/_generated.py b/playwright/sync_api/_generated.py index 9c0ea5094..7882d2282 100644 --- a/playwright/sync_api/_generated.py +++ b/playwright/sync_api/_generated.py @@ -421,6 +421,19 @@ def headers(self) -> typing.Dict[str, str]: """ return mapping.from_maybe_impl(self._impl_obj.headers) + @property + def from_service_worker(self) -> bool: + """Response.from_service_worker + + Indicates whether this Response was fullfilled by a Service Worker's Fetch Handler (i.e. via + [FetchEvent.respondWith](https://developer.mozilla.org/en-US/docs/Web/API/FetchEvent/respondWith)). + + Returns + ------- + bool + """ + return mapping.from_maybe_impl(self._impl_obj.from_service_worker) + @property def request(self) -> "Request": """Response.request @@ -705,8 +718,8 @@ def handle(route, request): # override headers headers = { **request.headers, - \"foo\": \"bar\" # set \"foo\" header - \"origin\": None # remove \"origin\" header + \"foo\": \"foo-value\" # set \"foo\" header + \"bar\": None # remove \"bar\" header } route.continue_(headers=headers) } @@ -1824,17 +1837,6 @@ def select_option( handle.select_option(value=[\"red\", \"green\", \"blue\"]) ``` - ```py - # single selection matching the value - handle.select_option(\"blue\") - # single selection matching both the value and the label - handle.select_option(label=\"blue\") - # multiple selection - handle.select_option(\"red\", \"green\", \"blue\") - # multiple selection for blue, red and second option - handle.select_option(value=\"blue\", { index: 2 }, \"red\") - ``` - Parameters ---------- value : Union[List[str], str, NoneType] @@ -7586,7 +7588,7 @@ def route( > NOTE: The handler will only be called for the first url if the response is a redirect. > NOTE: `page.route()` will not intercept requests intercepted by Service Worker. See [this](https://github.com/microsoft/playwright/issues/1090) issue. We recommend disabling Service Workers when using - request interception. Via `await context.addInitScript(() => delete window.navigator.serviceWorker);` + request interception by setting `Browser.newContext.serviceWorkers` to `'block'`. An example of a naive handler that aborts all image requests: @@ -10272,9 +10274,9 @@ def route( Routing provides the capability to modify network requests that are made by any page in the browser context. Once route is enabled, every request matching the url pattern will stall unless it's continued, fulfilled or aborted. - > NOTE: `page.route()` will not intercept requests intercepted by Service Worker. See + > NOTE: `browser_context.route()` will not intercept requests intercepted by Service Worker. See [this](https://github.com/microsoft/playwright/issues/1090) issue. We recommend disabling Service Workers when using - request interception. Via `await context.addInitScript(() => delete window.navigator.serviceWorker);` + request interception by setting `Browser.newContext.serviceWorkers` to `'block'`. An example of a naive handler that aborts all image requests: @@ -10598,6 +10600,18 @@ def contexts(self) -> typing.List["BrowserContext"]: """ return mapping.from_impl_list(self._impl_obj.contexts) + @property + def browser_type(self) -> "BrowserType": + """Browser.browser_type + + Get the browser type (chromium, firefox or webkit) that the browser belongs to. + + Returns + ------- + BrowserType + """ + return mapping.from_impl(self._impl_obj.browser_type) + @property def version(self) -> str: """Browser.version @@ -10654,7 +10668,8 @@ def new_context( record_video_size: ViewportSize = None, storage_state: typing.Union[StorageState, str, pathlib.Path] = None, base_url: str = None, - strict_selectors: bool = None + strict_selectors: bool = None, + service_workers: Literal["allow", "block"] = None ) -> "BrowserContext": """Browser.new_context @@ -10756,9 +10771,13 @@ def new_context( - baseURL: `http://localhost:3000/foo` (without trailing slash) and navigating to `./bar.html` results in `http://localhost:3000/bar.html` strict_selectors : Union[bool, NoneType] - It specified, enables strict selectors mode for this context. In the strict selectors mode all operations on selectors + If specified, enables strict selectors mode for this context. In the strict selectors mode all operations on selectors that imply single target DOM element will throw when more than one element matches the selector. See `Locator` to learn more about the strict mode. + service_workers : Union["allow", "block", NoneType] + Whether to allow sites to register Service workers. Defaults to `'allow'`. + - `'allow'`: [Service Workers](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) can be registered. + - `'block'`: Playwright will block all registration of Service Workers. Returns ------- @@ -10798,6 +10817,7 @@ def new_context( storageState=storage_state, baseURL=base_url, strictSelectors=strict_selectors, + serviceWorkers=service_workers, ) ) ) @@ -10834,7 +10854,8 @@ def new_page( record_video_size: ViewportSize = None, storage_state: typing.Union[StorageState, str, pathlib.Path] = None, base_url: str = None, - strict_selectors: bool = None + strict_selectors: bool = None, + service_workers: Literal["allow", "block"] = None ) -> "Page": """Browser.new_page @@ -10931,9 +10952,13 @@ def new_page( - baseURL: `http://localhost:3000/foo` (without trailing slash) and navigating to `./bar.html` results in `http://localhost:3000/bar.html` strict_selectors : Union[bool, NoneType] - It specified, enables strict selectors mode for this context. In the strict selectors mode all operations on selectors + If specified, enables strict selectors mode for this context. In the strict selectors mode all operations on selectors that imply single target DOM element will throw when more than one element matches the selector. See `Locator` to learn more about the strict mode. + service_workers : Union["allow", "block", NoneType] + Whether to allow sites to register Service workers. Defaults to `'allow'`. + - `'allow'`: [Service Workers](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) can be registered. + - `'block'`: Playwright will block all registration of Service Workers. Returns ------- @@ -10973,6 +10998,7 @@ def new_page( storageState=storage_state, baseURL=base_url, strictSelectors=strict_selectors, + serviceWorkers=service_workers, ) ) ) @@ -11272,7 +11298,8 @@ def launch_persistent_context( record_video_dir: typing.Union[str, pathlib.Path] = None, record_video_size: ViewportSize = None, base_url: str = None, - strict_selectors: bool = None + strict_selectors: bool = None, + service_workers: Literal["allow", "block"] = None ) -> "BrowserContext": """BrowserType.launch_persistent_context @@ -11409,9 +11436,13 @@ def launch_persistent_context( - baseURL: `http://localhost:3000/foo` (without trailing slash) and navigating to `./bar.html` results in `http://localhost:3000/bar.html` strict_selectors : Union[bool, NoneType] - It specified, enables strict selectors mode for this context. In the strict selectors mode all operations on selectors + If specified, enables strict selectors mode for this context. In the strict selectors mode all operations on selectors that imply single target DOM element will throw when more than one element matches the selector. See `Locator` to learn more about the strict mode. + service_workers : Union["allow", "block", NoneType] + Whether to allow sites to register Service workers. Defaults to `'allow'`. + - `'allow'`: [Service Workers](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) can be registered. + - `'block'`: Playwright will block all registration of Service Workers. Returns ------- @@ -11465,6 +11496,7 @@ def launch_persistent_context( recordVideoSize=record_video_size, baseURL=base_url, strictSelectors=strict_selectors, + serviceWorkers=service_workers, ) ) ) @@ -12398,7 +12430,17 @@ def filter( ) -> "Locator": """Locator.filter - This method narrows existing locator according to the options, for example filters by text. + This method narrows existing locator according to the options, for example filters by text. It can be chained to filter + multiple times. + + ```py + row_locator = page.lsocator(\"tr\") + # ... + row_locator + .filter(has_text=\"text in column 1\") + .filter(has=page.locator(\"tr\", has_text=\"column 2 button\")) + .screenshot() + ``` Parameters ---------- @@ -12945,17 +12987,6 @@ def select_option( element.select_option(value=[\"red\", \"green\", \"blue\"]) ``` - ```py - # single selection matching the value - element.select_option(\"blue\") - # single selection matching both the value and the label - element.select_option(label=\"blue\") - # multiple selection - element.select_option(\"red\", \"green\", \"blue\") - # multiple selection for blue, red and second option - element.select_option(value=\"blue\", { index: 2 }, \"red\") - ``` - Parameters ---------- value : Union[List[str], str, NoneType] @@ -14219,7 +14250,8 @@ def to_contain_text( ], *, use_inner_text: bool = None, - timeout: float = None + timeout: float = None, + ignore_case: bool = None ) -> NoneType: """LocatorAssertions.to_contain_text @@ -14253,6 +14285,9 @@ def to_contain_text( Whether to use `element.innerText` instead of `element.textContent` when retrieving DOM node text. timeout : Union[float, NoneType] Time to retry the assertion for. + ignore_case : Union[bool, NoneType] + Whether to perform case-insensitive match. `ignoreCase` option takes precedence over the corresponding regular + expression flag if specified. """ __tracebackhide__ = True @@ -14262,6 +14297,7 @@ def to_contain_text( expected=mapping.to_impl(expected), use_inner_text=use_inner_text, timeout=timeout, + ignore_case=ignore_case, ) ) ) @@ -14273,7 +14309,8 @@ def not_to_contain_text( ], *, use_inner_text: bool = None, - timeout: float = None + timeout: float = None, + ignore_case: bool = None ) -> NoneType: """LocatorAssertions.not_to_contain_text @@ -14287,6 +14324,9 @@ def not_to_contain_text( Whether to use `element.innerText` instead of `element.textContent` when retrieving DOM node text. timeout : Union[float, NoneType] Time to retry the assertion for. + ignore_case : Union[bool, NoneType] + Whether to perform case-insensitive match. `ignoreCase` option takes precedence over the corresponding regular + expression flag if specified. """ __tracebackhide__ = True @@ -14296,6 +14336,7 @@ def not_to_contain_text( expected=mapping.to_impl(expected), use_inner_text=use_inner_text, timeout=timeout, + ignore_case=ignore_case, ) ) ) @@ -14703,6 +14744,80 @@ def not_to_have_value( self._sync(self._impl_obj.not_to_have_value(value=value, timeout=timeout)) ) + def to_have_values( + self, + values: typing.List[typing.Union[typing.Pattern, str]], + *, + timeout: float = None + ) -> NoneType: + """LocatorAssertions.to_have_values + + Ensures the `Locator` points to multi-select/combobox (i.e. a `select` with the `multiple` attribute) and the specified + values are selected. + + For example, given the following element: + + ```html + + ``` + + ```py + import re + from playwright.sync_api import expect + + locator = page.locator(\"id=favorite-colors\") + locator.select_option([\"R\", \"G\"]) + expect(locator).to_have_values([re.compile(r\"R\"), re.compile(r\"G\")]) + ``` + + Parameters + ---------- + values : List[Union[Pattern, str]] + Expected options currently selected. + timeout : Union[float, NoneType] + Time to retry the assertion for. + """ + __tracebackhide__ = True + + return mapping.from_maybe_impl( + self._sync( + self._impl_obj.to_have_values( + values=mapping.to_impl(values), timeout=timeout + ) + ) + ) + + def not_to_have_values( + self, + values: typing.List[typing.Union[typing.Pattern, str]], + *, + timeout: float = None + ) -> NoneType: + """LocatorAssertions.not_to_have_values + + The opposite of `locator_assertions.to_have_values()`. + + Parameters + ---------- + values : List[Union[Pattern, str]] + Expected options currently selected. + timeout : Union[float, NoneType] + Time to retry the assertion for. + """ + __tracebackhide__ = True + + return mapping.from_maybe_impl( + self._sync( + self._impl_obj.not_to_have_values( + values=mapping.to_impl(values), timeout=timeout + ) + ) + ) + def to_have_text( self, expected: typing.Union[ @@ -14710,7 +14825,8 @@ def to_have_text( ], *, use_inner_text: bool = None, - timeout: float = None + timeout: float = None, + ignore_case: bool = None ) -> NoneType: """LocatorAssertions.to_have_text @@ -14742,6 +14858,9 @@ def to_have_text( Whether to use `element.innerText` instead of `element.textContent` when retrieving DOM node text. timeout : Union[float, NoneType] Time to retry the assertion for. + ignore_case : Union[bool, NoneType] + Whether to perform case-insensitive match. `ignoreCase` option takes precedence over the corresponding regular + expression flag if specified. """ __tracebackhide__ = True @@ -14751,6 +14870,7 @@ def to_have_text( expected=mapping.to_impl(expected), use_inner_text=use_inner_text, timeout=timeout, + ignore_case=ignore_case, ) ) ) @@ -14762,7 +14882,8 @@ def not_to_have_text( ], *, use_inner_text: bool = None, - timeout: float = None + timeout: float = None, + ignore_case: bool = None ) -> NoneType: """LocatorAssertions.not_to_have_text @@ -14776,6 +14897,9 @@ def not_to_have_text( Whether to use `element.innerText` instead of `element.textContent` when retrieving DOM node text. timeout : Union[float, NoneType] Time to retry the assertion for. + ignore_case : Union[bool, NoneType] + Whether to perform case-insensitive match. `ignoreCase` option takes precedence over the corresponding regular + expression flag if specified. """ __tracebackhide__ = True @@ -14785,6 +14909,7 @@ def not_to_have_text( expected=mapping.to_impl(expected), use_inner_text=use_inner_text, timeout=timeout, + ignore_case=ignore_case, ) ) ) diff --git a/scripts/expected_api_mismatch.txt b/scripts/expected_api_mismatch.txt index 7e6b3cda6..e362d28d4 100644 --- a/scripts/expected_api_mismatch.txt +++ b/scripts/expected_api_mismatch.txt @@ -18,3 +18,11 @@ Method not implemented: Error.name Method not implemented: Error.stack Method not implemented: Error.message Method not implemented: PlaywrightAssertions.expect + +# Pending 1.23 ports +Parameter not implemented: BrowserType.launch_persistent_context(record_har_url_filter=) +Method not implemented: BrowserContext.route_from_har +Method not implemented: Route.fallback +Parameter not implemented: Browser.new_page(record_har_url_filter=) +Method not implemented: Page.route_from_har +Parameter not implemented: Browser.new_context(record_har_url_filter=) diff --git a/setup.py b/setup.py index 7a4b63d36..32feeadb6 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ InWheel = None from wheel.bdist_wheel import bdist_wheel as BDistWheelCommand -driver_version = "1.23.0-alpha-may-27-2022" +driver_version = "1.24.0-alpha-1656026114000" def extractall(zip: zipfile.ZipFile, path: str) -> None: diff --git a/tests/async/test_assertions.py b/tests/async/test_assertions.py index 224568f3a..1475413b4 100644 --- a/tests/async/test_assertions.py +++ b/tests/async/test_assertions.py @@ -17,7 +17,7 @@ import pytest -from playwright.async_api import Browser, Page, expect +from playwright.async_api import Browser, Error, Page, expect from tests.server import Server @@ -170,6 +170,81 @@ async def test_assertions_locator_to_have_js_property( ) +async def test_to_have_js_property_pass_string(page: Page) -> None: + await page.set_content("
") + await page.eval_on_selector("div", "e => e.foo = 'string'") + locator = page.locator("div") + await expect(locator).to_have_js_property("foo", "string") + + +async def test_to_have_js_property_fail_string(page: Page) -> None: + await page.set_content("
") + await page.eval_on_selector("div", "e => e.foo = 'string'") + locator = page.locator("div") + with pytest.raises(AssertionError): + await expect(locator).to_have_js_property("foo", "error", timeout=500) + + +async def test_to_have_js_property_pass_number(page: Page) -> None: + await page.set_content("
") + await page.eval_on_selector("div", "e => e.foo = 2021") + locator = page.locator("div") + await expect(locator).to_have_js_property("foo", 2021) + + +async def test_to_have_js_property_fail_number(page: Page) -> None: + await page.set_content("
") + await page.eval_on_selector("div", "e => e.foo = 2021") + locator = page.locator("div") + with pytest.raises(AssertionError): + await expect(locator).to_have_js_property("foo", 1, timeout=500) + + +async def test_to_have_js_property_pass_boolean(page: Page) -> None: + await page.set_content("
") + await page.eval_on_selector("div", "e => e.foo = true") + locator = page.locator("div") + await expect(locator).to_have_js_property("foo", True) + + +async def test_to_have_js_property_fail_boolean(page: Page) -> None: + await page.set_content("
") + await page.eval_on_selector("div", "e => e.foo = false") + locator = page.locator("div") + with pytest.raises(AssertionError): + await expect(locator).to_have_js_property("foo", True, timeout=500) + + +async def test_to_have_js_property_pass_boolean_2(page: Page) -> None: + await page.set_content("
") + await page.eval_on_selector("div", "e => e.foo = false") + locator = page.locator("div") + await expect(locator).to_have_js_property("foo", False) + + +async def test_to_have_js_property_fail_boolean_2(page: Page) -> None: + await page.set_content("
") + await page.eval_on_selector("div", "e => e.foo = false") + locator = page.locator("div") + with pytest.raises(AssertionError): + await expect(locator).to_have_js_property("foo", True, timeout=500) + + +async def test_to_have_js_property_pass_undefined(page: Page) -> None: + await page.set_content("
") + locator = page.locator("div") + await expect(locator).to_have_js_property( + "foo", None + ) # Python does not have an undefined + + +async def test_to_have_js_property_pass_null(page: Page) -> None: + await page.set_content("
") + await page.eval_on_selector("div", "e => e.foo = null") + locator = page.locator("div") + await expect(locator).to_have_js_property("foo", None) + + async def test_assertions_locator_to_have_text(page: Page, server: Server) -> None: await page.goto(server.EMPTY_PAGE) await page.set_content("
kek
") @@ -183,6 +258,109 @@ async def test_assertions_locator_to_have_text(page: Page, server: Server) -> No ) +@pytest.mark.parametrize( + "method", + ["to_have_text", "to_contain_text"], +) +async def test_ignore_case(page: Page, server: Server, method: str) -> None: + await page.goto(server.EMPTY_PAGE) + await page.set_content("
apple BANANA
orange
") + await getattr(expect(page.locator("div#target")), method)("apple BANANA") + await getattr(expect(page.locator("div#target")), method)( + "apple banana", ignore_case=True + ) + # defaults false + with pytest.raises(AssertionError) as excinfo: + await getattr(expect(page.locator("div#target")), method)( + "apple banana", timeout=300 + ) + expected_error_msg = method.replace("_", " ") + assert expected_error_msg in str(excinfo.value) + + # Array Variants + await getattr(expect(page.locator("div")), method)(["apple BANANA", "orange"]) + await getattr(expect(page.locator("div")), method)( + ["apple banana", "ORANGE"], ignore_case=True + ) + # defaults false + with pytest.raises(AssertionError) as excinfo: + await getattr(expect(page.locator("div")), method)( + ["apple banana", "ORANGE"], timeout=300 + ) + assert expected_error_msg in str(excinfo.value) + + # not variant + await getattr(expect(page.locator("div#target")), f"not_{method}")("apple banana") + with pytest.raises(AssertionError) as excinfo: + await getattr(expect(page.locator("div#target")), f"not_{method}")( + "apple banana", ignore_case=True, timeout=300 + ) + assert f"not {expected_error_msg}" in str(excinfo) + + +@pytest.mark.parametrize( + "method", + ["to_have_text", "to_contain_text"], +) +async def test_ignore_case_regex(page: Page, server: Server, method: str) -> None: + await page.goto(server.EMPTY_PAGE) + await page.set_content("
apple BANANA
orange
") + await getattr(expect(page.locator("div#target")), method)( + re.compile("apple BANANA") + ) + await getattr(expect(page.locator("div#target")), method)( + re.compile("apple banana"), ignore_case=True + ) + # defaults to regex flag + with pytest.raises(AssertionError) as excinfo: + await getattr(expect(page.locator("div#target")), method)( + re.compile("apple banana"), timeout=300 + ) + expected_error_msg = method.replace("_", " ") + assert expected_error_msg in str(excinfo.value) + # overrides regex flag + with pytest.raises(AssertionError) as excinfo: + await getattr(expect(page.locator("div#target")), method)( + re.compile("apple banana", re.IGNORECASE), ignore_case=False, timeout=300 + ) + assert expected_error_msg in str(excinfo.value) + + # Array Variants + await getattr(expect(page.locator("div")), method)( + [re.compile("apple BANANA"), re.compile("orange")] + ) + await getattr(expect(page.locator("div")), method)( + [re.compile("apple banana"), re.compile("ORANGE")], ignore_case=True + ) + # defaults regex flag + with pytest.raises(AssertionError) as excinfo: + await getattr(expect(page.locator("div")), method)( + [re.compile("apple banana"), re.compile("ORANGE")], timeout=300 + ) + assert expected_error_msg in str(excinfo.value) + # overrides regex flag + with pytest.raises(AssertionError) as excinfo: + await getattr(expect(page.locator("div")), method)( + [ + re.compile("apple banana", re.IGNORECASE), + re.compile("ORANGE", re.IGNORECASE), + ], + ignore_case=False, + timeout=300, + ) + assert expected_error_msg in str(excinfo.value) + + # not variant + await getattr(expect(page.locator("div#target")), f"not_{method}")( + re.compile("apple banana") + ) + with pytest.raises(AssertionError) as excinfo: + await getattr(expect(page.locator("div#target")), f"not_{method}")( + re.compile("apple banana"), ignore_case=True, timeout=300 + ) + assert f"not {expected_error_msg}" in str(excinfo) + + async def test_assertions_locator_to_have_value(page: Page, server: Server) -> None: await page.goto(server.EMPTY_PAGE) await page.set_content("") @@ -193,6 +371,122 @@ async def test_assertions_locator_to_have_value(page: Page, server: Server) -> N await expect(my_input).to_have_value("kektus") +async def test_to_have_values_works_with_text(page: Page, server: Server) -> None: + await page.set_content( + """ + + """ + ) + locator = page.locator("select") + await locator.select_option(["R", "G"]) + await expect(locator).to_have_values(["R", "G"]) + + +async def test_to_have_values_follows_labels(page: Page, server: Server) -> None: + await page.set_content( + """ + + + """ + ) + locator = page.locator("text=Pick a Color") + await locator.select_option(["R", "G"]) + await expect(locator).to_have_values(["R", "G"]) + + +async def test_to_have_values_exact_match_with_text(page: Page, server: Server) -> None: + await page.set_content( + """ + + """ + ) + locator = page.locator("select") + await locator.select_option(["RR", "GG"]) + with pytest.raises(AssertionError) as excinfo: + await expect(locator).to_have_values(["R", "G"], timeout=500) + assert "Locator expected to have Values '['R', 'G']'" in str(excinfo.value) + assert "Actual value: ['RR', 'GG']" in str(excinfo.value) + + +async def test_to_have_values_works_with_regex(page: Page, server: Server) -> None: + await page.set_content( + """ + + """ + ) + locator = page.locator("select") + await locator.select_option(["R", "G"]) + await expect(locator).to_have_values([re.compile("R"), re.compile("G")]) + + +async def test_to_have_values_fails_when_items_not_selected( + page: Page, server: Server +) -> None: + await page.set_content( + """ + + """ + ) + locator = page.locator("select") + await locator.select_option(["B"]) + with pytest.raises(AssertionError) as excinfo: + await expect(locator).to_have_values(["R", "G"], timeout=500) + assert "Locator expected to have Values '['R', 'G']'" in str(excinfo.value) + assert "Actual value: ['B']" in str(excinfo.value) + + +async def test_to_have_values_fails_when_multiple_not_specified( + page: Page, server: Server +) -> None: + await page.set_content( + """ + + """ + ) + locator = page.locator("select") + await locator.select_option(["B"]) + with pytest.raises(Error) as excinfo: + await expect(locator).to_have_values(["R", "G"], timeout=500) + assert "Error: Not a select element with a multiple attribute" in str(excinfo.value) + + +async def test_to_have_values_fails_when_not_a_select_element( + page: Page, server: Server +) -> None: + await page.set_content( + """ + + """ + ) + locator = page.locator("input") + with pytest.raises(Error) as excinfo: + await expect(locator).to_have_values(["R", "G"], timeout=500) + assert "Error: Not a select element with a multiple attribute" in str(excinfo.value) + + async def test_assertions_locator_to_be_checked(page: Page, server: Server) -> None: await page.goto(server.EMPTY_PAGE) await page.set_content("") diff --git a/tests/async/test_browser.py b/tests/async/test_browser.py index e077e2b92..b9124c102 100644 --- a/tests/async/test_browser.py +++ b/tests/async/test_browser.py @@ -47,3 +47,7 @@ async def test_version_should_work(browser: Browser, is_chromium): assert re.match(r"^\d+\.\d+\.\d+\.\d+$", version) else: assert re.match(r"^\d+\.\d+", version) + + +async def test_should_return_browser_type(browser, browser_type): + assert browser.browser_type is browser_type diff --git a/tests/async/test_browsercontext_service_worker_policy.py b/tests/async/test_browsercontext_service_worker_policy.py new file mode 100644 index 000000000..71a9636c7 --- /dev/null +++ b/tests/async/test_browsercontext_service_worker_policy.py @@ -0,0 +1,24 @@ +from playwright.async_api import Browser +from tests.server import Server + + +async def test_should_allow_service_workers_by_default( + browser: Browser, server: Server +) -> None: + context = await browser.new_context() + page = await context.new_page() + await page.goto(server.PREFIX + "/serviceworkers/fetchdummy/sw.html") + await page.evaluate("() => window.activationPromise") + await context.close() + + +async def test_block_blocks_service_worker_registration( + browser: Browser, server: Server +) -> None: + context = await browser.new_context(service_workers="block") + page = await context.new_page() + async with page.expect_console_message( + lambda m: "Service Worker registration blocked by Playwright" == m.text + ): + await page.goto(server.PREFIX + "/serviceworkers/fetchdummy/sw.html") + await context.close() diff --git a/tests/async/test_network.py b/tests/async/test_network.py index cb2a01b26..5f93f7f82 100644 --- a/tests/async/test_network.py +++ b/tests/async/test_network.py @@ -797,3 +797,16 @@ async def test_response_security_details_none_without_https(page: Page, server: response = await page.goto(server.EMPTY_PAGE) security_details = await response.security_details() assert security_details is None + + +async def test_should_report_if_request_was_from_service_worker( + page: Page, server: Server +) -> None: + response = await page.goto(server.PREFIX + "/serviceworkers/fetch/sw.html") + assert response + assert not response.from_service_worker + await page.evaluate("() => window.activationPromise") + async with page.expect_response("**/example.txt") as response_info: + await page.evaluate("() => fetch('/example.txt')") + response = await response_info.value + assert response.from_service_worker diff --git a/tests/sync/test_assertions.py b/tests/sync/test_assertions.py index b52700dc0..3e51e7ab2 100644 --- a/tests/sync/test_assertions.py +++ b/tests/sync/test_assertions.py @@ -17,7 +17,7 @@ import pytest -from playwright.sync_api import Browser, Page, expect +from playwright.sync_api import Browser, Error, Page, expect from tests.server import Server @@ -158,7 +158,84 @@ def test_assertions_locator_to_have_js_property(page: Page, server: Server) -> N ) -def test_assertions_locator_to_have_text(page: Page, server: Server) -> None: +def test_to_have_js_property_pass_string(page: Page) -> None: + page.set_content("
") + page.eval_on_selector("div", "e => e.foo = 'string'") + locator = page.locator("div") + expect(locator).to_have_js_property("foo", "string") + + +def test_to_have_js_property_fail_string(page: Page) -> None: + page.set_content("
") + page.eval_on_selector("div", "e => e.foo = 'string'") + locator = page.locator("div") + with pytest.raises(AssertionError): + expect(locator).to_have_js_property("foo", "error", timeout=500) + + +def test_to_have_js_property_pass_number(page: Page) -> None: + page.set_content("
") + page.eval_on_selector("div", "e => e.foo = 2021") + locator = page.locator("div") + expect(locator).to_have_js_property("foo", 2021) + + +def test_to_have_js_property_fail_number(page: Page) -> None: + page.set_content("
") + page.eval_on_selector("div", "e => e.foo = 2021") + locator = page.locator("div") + with pytest.raises(AssertionError): + expect(locator).to_have_js_property("foo", 1, timeout=500) + + +def test_to_have_js_property_pass_boolean(page: Page) -> None: + page.set_content("
") + page.eval_on_selector("div", "e => e.foo = true") + locator = page.locator("div") + expect(locator).to_have_js_property("foo", True) + + +def test_to_have_js_property_fail_boolean(page: Page) -> None: + page.set_content("
") + page.eval_on_selector("div", "e => e.foo = false") + locator = page.locator("div") + with pytest.raises(AssertionError): + expect(locator).to_have_js_property("foo", True, timeout=500) + + +def test_to_have_js_property_pass_boolean_2(page: Page) -> None: + page.set_content("
") + page.eval_on_selector("div", "e => e.foo = false") + locator = page.locator("div") + expect(locator).to_have_js_property("foo", False) + + +def test_to_have_js_property_fail_boolean_2(page: Page) -> None: + page.set_content("
") + page.eval_on_selector("div", "e => e.foo = false") + locator = page.locator("div") + with pytest.raises(AssertionError): + expect(locator).to_have_js_property("foo", True, timeout=500) + + +def test_to_have_js_property_pass_undefined(page: Page) -> None: + page.set_content("
") + locator = page.locator("div") + expect(locator).to_have_js_property( + "foo", None + ) # Python does not have an undefined + + +def test_to_have_js_property_pass_null(page: Page) -> None: + page.set_content("
") + page.eval_on_selector("div", "e => e.foo = null") + locator = page.locator("div") + expect(locator).to_have_js_property("foo", None) + + +def test_to_have_js_property_assertions_locator_to_have_text( + page: Page, server: Server +) -> None: page.goto(server.EMPTY_PAGE) page.set_content("
kek
") expect(page.locator("div#foobar")).to_have_text("kek") @@ -169,6 +246,116 @@ def test_assertions_locator_to_have_text(page: Page, server: Server) -> None: expect(page.locator("div")).to_have_text(["Text 1", re.compile(r"Text \d+a")]) +@pytest.mark.parametrize( + "method", + ["to_have_text", "to_contain_text"], +) +def test_ignore_case(page: Page, server: Server, method: str) -> None: + page.goto(server.EMPTY_PAGE) + page.set_content("
apple BANANA
orange
") + getattr(expect(page.locator("div#target")), method)("apple BANANA") + getattr(expect(page.locator("div#target")), method)( + "apple banana", ignore_case=True + ) + # defaults false + with pytest.raises(AssertionError) as excinfo: + getattr(expect(page.locator("div#target")), method)( + "apple banana", + timeout=300, + ) + expected_error_msg = method.replace("_", " ") + assert expected_error_msg in str(excinfo.value) + + # Array Variants + getattr(expect(page.locator("div")), method)(["apple BANANA", "orange"]) + getattr(expect(page.locator("div")), method)( + ["apple banana", "ORANGE"], ignore_case=True + ) + # defaults false + with pytest.raises(AssertionError) as excinfo: + getattr(expect(page.locator("div")), method)( + ["apple banana", "ORANGE"], + timeout=300, + ) + assert expected_error_msg in str(excinfo.value) + + # not variant + getattr(expect(page.locator("div#target")), f"not_{method}")("apple banana") + with pytest.raises(AssertionError) as excinfo: + getattr(expect(page.locator("div#target")), f"not_{method}")( + "apple banana", + ignore_case=True, + timeout=300, + ) + assert f"not {expected_error_msg}" in str(excinfo) + + +@pytest.mark.parametrize( + "method", + ["to_have_text", "to_contain_text"], +) +def test_ignore_case_regex(page: Page, server: Server, method: str) -> None: + page.goto(server.EMPTY_PAGE) + page.set_content("
apple BANANA
orange
") + getattr(expect(page.locator("div#target")), method)(re.compile("apple BANANA")) + getattr(expect(page.locator("div#target")), method)( + re.compile("apple banana"), ignore_case=True + ) + # defaults to regex flag + with pytest.raises(AssertionError) as excinfo: + getattr(expect(page.locator("div#target")), method)( + re.compile("apple banana"), timeout=300 + ) + expected_error_msg = method.replace("_", " ") + assert expected_error_msg in str(excinfo.value) + # overrides regex flag + with pytest.raises(AssertionError) as excinfo: + getattr(expect(page.locator("div#target")), method)( + re.compile("apple banana", re.IGNORECASE), + ignore_case=False, + timeout=300, + ) + assert expected_error_msg in str(excinfo.value) + + # Array Variants + getattr(expect(page.locator("div")), method)( + [re.compile("apple BANANA"), re.compile("orange")] + ) + getattr(expect(page.locator("div")), method)( + [re.compile("apple banana"), re.compile("ORANGE")], ignore_case=True + ) + # defaults regex flag + with pytest.raises(AssertionError) as excinfo: + getattr(expect(page.locator("div")), method)( + [re.compile("apple banana"), re.compile("ORANGE")], + timeout=300, + ) + assert expected_error_msg in str(excinfo.value) + # overrides regex flag + with pytest.raises(AssertionError) as excinfo: + getattr(expect(page.locator("div")), method)( + [ + re.compile("apple banana", re.IGNORECASE), + re.compile("ORANGE", re.IGNORECASE), + ], + ignore_case=False, + timeout=300, + ) + assert expected_error_msg in str(excinfo.value) + + # not variant + getattr(expect(page.locator("div#target")), f"not_{method}")( + re.compile("apple banana") + ) + with pytest.raises(AssertionError) as excinfo: + getattr(expect(page.locator("div#target")), f"not_{method}")( + re.compile("apple banana"), + ignore_case=True, + timeout=300, + ) + assert f"not {expected_error_msg}" in str(excinfo) + + def test_assertions_locator_to_have_value(page: Page, server: Server) -> None: page.goto(server.EMPTY_PAGE) page.set_content("") @@ -179,6 +366,122 @@ def test_assertions_locator_to_have_value(page: Page, server: Server) -> None: expect(my_input).to_have_value("kektus") +def test_to_have_values_works_with_text(page: Page, server: Server) -> None: + page.set_content( + """ + + """ + ) + locator = page.locator("select") + locator.select_option(["R", "G"]) + expect(locator).to_have_values(["R", "G"]) + + +def test_to_have_values_follows_labels(page: Page, server: Server) -> None: + page.set_content( + """ + + + """ + ) + locator = page.locator("text=Pick a Color") + locator.select_option(["R", "G"]) + expect(locator).to_have_values(["R", "G"]) + + +def test_to_have_values_exact_match_with_text(page: Page, server: Server) -> None: + page.set_content( + """ + + """ + ) + locator = page.locator("select") + locator.select_option(["RR", "GG"]) + with pytest.raises(AssertionError) as excinfo: + expect(locator).to_have_values(["R", "G"], timeout=500) + assert "Locator expected to have Values '['R', 'G']'" in str(excinfo.value) + assert "Actual value: ['RR', 'GG']" in str(excinfo.value) + + +def test_to_have_values_works_with_regex(page: Page, server: Server) -> None: + page.set_content( + """ + + """ + ) + locator = page.locator("select") + locator.select_option(["R", "G"]) + expect(locator).to_have_values([re.compile("R"), re.compile("G")]) + + +def test_to_have_values_fails_when_items_not_selected( + page: Page, server: Server +) -> None: + page.set_content( + """ + + """ + ) + locator = page.locator("select") + locator.select_option(["B"]) + with pytest.raises(AssertionError) as excinfo: + expect(locator).to_have_values(["R", "G"], timeout=500) + assert "Locator expected to have Values '['R', 'G']'" in str(excinfo.value) + assert "Actual value: ['B']" in str(excinfo.value) + + +def test_to_have_values_fails_when_multiple_not_specified( + page: Page, server: Server +) -> None: + page.set_content( + """ + + """ + ) + locator = page.locator("select") + locator.select_option(["B"]) + with pytest.raises(Error) as excinfo: + expect(locator).to_have_values(["R", "G"], timeout=500) + assert "Error: Not a select element with a multiple attribute" in str(excinfo.value) + + +def test_to_have_values_fails_when_not_a_select_element( + page: Page, server: Server +) -> None: + page.set_content( + """ + + """ + ) + locator = page.locator("input") + with pytest.raises(Error) as excinfo: + expect(locator).to_have_values(["R", "G"], timeout=500) + assert "Error: Not a select element with a multiple attribute" in str(excinfo.value) + + def test_assertions_locator_to_be_checked(page: Page, server: Server) -> None: page.goto(server.EMPTY_PAGE) page.set_content("") diff --git a/tests/sync/test_browser.py b/tests/sync/test_browser.py new file mode 100644 index 000000000..31d43e68f --- /dev/null +++ b/tests/sync/test_browser.py @@ -0,0 +1,21 @@ +# 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. + +from playwright.sync_api import Browser, BrowserType + + +def test_should_return_browser_type( + browser: Browser, browser_type: BrowserType +) -> None: + assert browser.browser_type is browser_type diff --git a/tests/sync/test_browsercontext_service_worker_policy.py b/tests/sync/test_browsercontext_service_worker_policy.py new file mode 100644 index 000000000..7f4350d7f --- /dev/null +++ b/tests/sync/test_browsercontext_service_worker_policy.py @@ -0,0 +1,24 @@ +from playwright.sync_api import Browser +from tests.server import Server + + +def test_should_allow_service_workers_by_default( + browser: Browser, server: Server +) -> None: + context = browser.new_context() + page = context.new_page() + page.goto(server.PREFIX + "/serviceworkers/fetchdummy/sw.html") + page.evaluate("() => window.activationPromise") + context.close() + + +def test_block_blocks_service_worker_registration( + browser: Browser, server: Server +) -> None: + context = browser.new_context(service_workers="block") + page = context.new_page() + with page.expect_console_message( + lambda m: "Service Worker registration blocked by Playwright" == m.text + ): + page.goto(server.PREFIX + "/serviceworkers/fetchdummy/sw.html") + context.close() diff --git a/tests/sync/test_network.py b/tests/sync/test_network.py index 4ea2ba7de..da4b52424 100644 --- a/tests/sync/test_network.py +++ b/tests/sync/test_network.py @@ -90,3 +90,15 @@ def handle_request(route: Route) -> None: assert response assert response.status == 200 assert response.json() == {"foo": "bar"} + + +def test_should_report_if_request_was_from_service_worker( + page: Page, server: Server +) -> None: + response = page.goto(server.PREFIX + "/serviceworkers/fetch/sw.html") + assert response + assert not response.from_service_worker + page.evaluate("() => window.activationPromise") + with page.expect_response("**/example.txt") as response_info: + page.evaluate("() => fetch('/example.txt')") + assert response_info.value.from_service_worker From 20f80d1f6440c0ba7c2574f09ea8107eddf63119 Mon Sep 17 00:00:00 2001 From: "Ross A. Wollman" Date: Thu, 23 Jun 2022 16:33:28 -0700 Subject: [PATCH 2/6] re-baseline tests with HAR encoding changes --- tests/async/test_assertions.py | 1 + tests/async/test_har.py | 8 +++----- tests/sync/test_assertions.py | 1 + tests/sync/test_har.py | 8 +++----- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/tests/async/test_assertions.py b/tests/async/test_assertions.py index 1475413b4..367354aa9 100644 --- a/tests/async/test_assertions.py +++ b/tests/async/test_assertions.py @@ -230,6 +230,7 @@ async def test_to_have_js_property_fail_boolean_2(page: Page) -> None: await expect(locator).to_have_js_property("foo", True, timeout=500) +@pytest.mark.skip("https://github.com/microsoft/playwright/issues/14909") async def test_to_have_js_property_pass_undefined(page: Page) -> None: await page.set_content("
") locator = page.locator("div") diff --git a/tests/async/test_har.py b/tests/async/test_har.py index 6e6015f7b..6cb6f2472 100644 --- a/tests/async/test_har.py +++ b/tests/async/test_har.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import base64 import json import os @@ -41,7 +40,8 @@ async def test_should_omit_content(browser, server, tmpdir): assert "log" in data log = data["log"] content1 = log["entries"][0]["response"]["content"] - assert "text" not in content1 + assert "text" in content1 + assert "encoding" not in content1 async def test_should_not_omit_content(browser, server, tmpdir): @@ -70,7 +70,5 @@ async def test_should_include_content(browser, server, tmpdir): log = data["log"] content1 = log["entries"][0]["response"]["content"] - assert content1["encoding"] == "base64" assert content1["mimeType"] == "text/html" - s = base64.b64decode(content1["text"]).decode() - assert "HAR Page" in s + assert "HAR Page" in content1["text"] diff --git a/tests/sync/test_assertions.py b/tests/sync/test_assertions.py index 3e51e7ab2..e509f0e77 100644 --- a/tests/sync/test_assertions.py +++ b/tests/sync/test_assertions.py @@ -218,6 +218,7 @@ def test_to_have_js_property_fail_boolean_2(page: Page) -> None: expect(locator).to_have_js_property("foo", True, timeout=500) +@pytest.mark.skip("https://github.com/microsoft/playwright/issues/14909") def test_to_have_js_property_pass_undefined(page: Page) -> None: page.set_content("
") locator = page.locator("div") diff --git a/tests/sync/test_har.py b/tests/sync/test_har.py index 52e3166cf..2561c7fd6 100644 --- a/tests/sync/test_har.py +++ b/tests/sync/test_har.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import base64 import json import os from pathlib import Path @@ -44,7 +43,8 @@ def test_should_omit_content(browser: Browser, server: Server, tmpdir: Path) -> log = data["log"] content1 = log["entries"][0]["response"]["content"] - assert "text" not in content1 + assert "text" in content1 + assert "encoding" not in content1 def test_should_include_content(browser: Browser, server: Server, tmpdir: Path) -> None: @@ -59,7 +59,5 @@ def test_should_include_content(browser: Browser, server: Server, tmpdir: Path) log = data["log"] content1 = log["entries"][0]["response"]["content"] - assert content1["encoding"] == "base64" assert content1["mimeType"] == "text/html" - s = base64.b64decode(content1["text"]).decode() - assert "HAR Page" in s + assert "HAR Page" in content1["text"] From ab51236b50ab526cf5764ed71f7ef96517c8d49b Mon Sep 17 00:00:00 2001 From: "Ross A. Wollman" Date: Thu, 23 Jun 2022 17:29:30 -0700 Subject: [PATCH 3/6] port https://github.com/dgozman/playwright/blob/1c7a014393ada783a3567f3277bf11f081bc4710/tests/page/page-autowaiting-basic.spec.ts#L21 --- playwright/_impl/_frame.py | 4 ++++ playwright/_impl/_page.py | 4 ---- tests/async/test_page.py | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/playwright/_impl/_frame.py b/playwright/_impl/_frame.py index 471ea7dcb..bf2ce590c 100644 --- a/playwright/_impl/_frame.py +++ b/playwright/_impl/_frame.py @@ -94,6 +94,10 @@ def _on_load_state( self._event_emitter.emit("loadstate", add) elif remove and remove in self._load_states: self._load_states.remove(remove) + if not self._parent_frame and add == "load" and self._page: + self._page.emit("load", self) + if not self._parent_frame and add == "domcontentloaded" and self._page: + self._page.emit("domcontentloaded", self) def _on_frame_navigated(self, event: FrameNavigatedEvent) -> None: self._url = event["url"] diff --git a/playwright/_impl/_page.py b/playwright/_impl/_page.py index 98916923a..043a1fea3 100644 --- a/playwright/_impl/_page.py +++ b/playwright/_impl/_page.py @@ -164,9 +164,6 @@ def __init__( ) self._channel.on("crash", lambda _: self._on_crash()) self._channel.on("dialog", lambda params: self._on_dialog(params)) - self._channel.on( - "domcontentloaded", lambda _: self.emit(Page.Events.DOMContentLoaded, self) - ) self._channel.on("download", lambda params: self._on_download(params)) self._channel.on( "fileChooser", @@ -185,7 +182,6 @@ def __init__( "frameDetached", lambda params: self._on_frame_detached(from_channel(params["frame"])), ) - self._channel.on("load", lambda _: self.emit(Page.Events.Load, self)) self._channel.on( "pageError", lambda params: self.emit( diff --git a/tests/async/test_page.py b/tests/async/test_page.py index 8cc426661..3254188c6 100644 --- a/tests/async/test_page.py +++ b/tests/async/test_page.py @@ -130,6 +130,39 @@ async def test_load_should_fire_when_expected(page): await page.goto("about:blank") +async def test_should_work_with_wait_for_loadstate(page: Page, server: Server) -> None: + messages = [] + server.set_route( + "/empty.html", + lambda route, response: ( + messages.append("route"), + response.set_header("Content-Type", "text/html"), + response.set_content( + "", response.finish() + ), + ), + ) + + return messages + await page.set_content(f'empty.html') + + async def wait_for_clickload(): + await page.click("a") + await page.wait_for_load_state("load") + messages.append("clickload") + + async def wait_for_page_load(): + 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, server): await page.route( "**/empty.html", lambda route, response: asyncio.create_task(route.abort()) From 725d7ec69318e8bbbaa71f68fa4f3584e409b73c Mon Sep 17 00:00:00 2001 From: "Ross A. Wollman" Date: Thu, 23 Jun 2022 17:41:02 -0700 Subject: [PATCH 4/6] use driver from 1.23 release branch --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 32feeadb6..2023d03be 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ InWheel = None from wheel.bdist_wheel import bdist_wheel as BDistWheelCommand -driver_version = "1.24.0-alpha-1656026114000" +driver_version = "1.23.0-beta-1656026605000" def extractall(zip: zipfile.ZipFile, path: str) -> None: From 69d5673935ffc1b20b9ade4a51226a5b963edbf0 Mon Sep 17 00:00:00 2001 From: "Ross A. Wollman" Date: Thu, 23 Jun 2022 21:00:14 -0700 Subject: [PATCH 5/6] guard against uninitialized _page field --- playwright/_impl/_frame.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/playwright/_impl/_frame.py b/playwright/_impl/_frame.py index bf2ce590c..6d20c34f0 100644 --- a/playwright/_impl/_frame.py +++ b/playwright/_impl/_frame.py @@ -94,9 +94,19 @@ def _on_load_state( self._event_emitter.emit("loadstate", add) elif remove and remove in self._load_states: self._load_states.remove(remove) - if not self._parent_frame and add == "load" and self._page: + if ( + not self._parent_frame + and add == "load" + and hasattr(self, "_page") + and self._page + ): self._page.emit("load", self) - if not self._parent_frame and add == "domcontentloaded" and self._page: + if ( + not self._parent_frame + and add == "domcontentloaded" + and hasattr(self, "_page") + and self._page + ): self._page.emit("domcontentloaded", self) def _on_frame_navigated(self, event: FrameNavigatedEvent) -> None: From b339646da88d4ecb7f9638041213b240b4dd12c2 Mon Sep 17 00:00:00 2001 From: "Ross A. Wollman" Date: Thu, 23 Jun 2022 21:26:02 -0700 Subject: [PATCH 6/6] port updated locale test --- tests/async/test_defaultbrowsercontext.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/async/test_defaultbrowsercontext.py b/tests/async/test_defaultbrowsercontext.py index bd5a061dc..ebe3a86c7 100644 --- a/tests/async/test_defaultbrowsercontext.py +++ b/tests/async/test_defaultbrowsercontext.py @@ -235,8 +235,8 @@ async def test_should_support_timezone_id_option(launch_persistent): async def test_should_support_locale_option(launch_persistent): - (page, context) = await launch_persistent(locale="fr-CH") - assert await page.evaluate("() => navigator.language") == "fr-CH" + (page, context) = await launch_persistent(locale="fr-FR") + assert await page.evaluate("() => navigator.language") == "fr-FR" async def test_should_support_geolocation_and_permission_option(