percy-playwright 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Percy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,164 @@
1
+ Metadata-Version: 2.1
2
+ Name: percy-playwright
3
+ Version: 1.0.0
4
+ Summary: Python client for visual testing with Percy
5
+ Home-page: https://github.com/percy/percy-playwright-python
6
+ Author: Perceptual Inc.
7
+ Author-email: team@percy.io
8
+ License: MIT
9
+ Keywords: percy visual testing
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Natural Language :: English
14
+ Classifier: Programming Language :: Python :: 3.6
15
+ Classifier: Programming Language :: Python :: 3.7
16
+ Classifier: Programming Language :: Python :: 3.8
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Requires-Python: >=3.6
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: playwright>=1.28.0
22
+ Requires-Dist: requests==2.*
23
+
24
+ # Percy playwright python
25
+ ![Test](https://github.com/percy/percy-playwright-python/workflows/Test/badge.svg)
26
+
27
+ [Percy](https://percy.io) visual testing for Python Playwright.
28
+
29
+ ## Installation
30
+
31
+ npm install `@percy/cli`:
32
+
33
+ ```sh-session
34
+ $ npm install --save-dev @percy/cli
35
+ ```
36
+
37
+ pip install Percy playwright package:
38
+
39
+ ```ssh-session
40
+ $ pip install percy-playwright
41
+ ```
42
+
43
+ ## Usage
44
+
45
+ This is an example test using the `percy_snapshot` function.
46
+
47
+ ``` python
48
+ from percy import percy_snapshot
49
+
50
+ with sync_playwright() as playwright:
51
+ browser = playwright.chromium.connect()
52
+ page = browser.new_page()
53
+ page.goto('http://example.com')
54
+
55
+ # take a snapshot
56
+ percy_snapshot(browser, 'Python example')
57
+ ```
58
+
59
+ Running the test above normally will result in the following log:
60
+
61
+ ```sh-session
62
+ [percy] Percy is not running, disabling snapshots
63
+ ```
64
+
65
+ When running with [`percy
66
+ exec`](https://github.com/percy/cli/tree/master/packages/cli-exec#percy-exec), and your project's
67
+ `PERCY_TOKEN`, a new Percy build will be created and snapshots will be uploaded to your project.
68
+
69
+ ```sh-session
70
+ $ export PERCY_TOKEN=[your-project-token]
71
+ $ percy exec -- [python test command]
72
+ [percy] Percy has started!
73
+ [percy] Created build #1: https://percy.io/[your-project]
74
+ [percy] Snapshot taken "Python example"
75
+ [percy] Stopping percy...
76
+ [percy] Finalized build #1: https://percy.io/[your-project]
77
+ [percy] Done!
78
+ ```
79
+
80
+ ## Configuration
81
+
82
+ `percy_snapshot(page, name[, **kwargs])`
83
+
84
+ - `page` (**required**) - A playwright page instance
85
+ - `name` (**required**) - The snapshot name; must be unique to each snapshot
86
+ - `**kwargs` - [See per-snapshot configuration options](https://www.browserstack.com/docs/percy/take-percy-snapshots/overview#per-snapshot-configuration)
87
+
88
+
89
+ ## Percy on Automate
90
+
91
+ ## Usage
92
+
93
+ ``` python
94
+ from playwright.sync_api import sync_playwright
95
+ from percy import percy_screenshot, percy_snapshot
96
+
97
+ desired_cap = {
98
+ 'browser': 'chrome',
99
+ 'browser_version': 'latest',
100
+ 'os': 'osx',
101
+ 'os_version': 'ventura',
102
+ 'name': 'Percy Playwright PoA Demo',
103
+ 'build': 'percy-playwright-python-tutorial',
104
+ 'browserstack.username': 'username',
105
+ 'browserstack.accessKey': 'accesskey'
106
+ }
107
+
108
+ with sync_playwright() as playwright:
109
+ cdpUrl = 'wss://cdp.browserstack.com/playwright?caps=' + urllib.parse.quote(json.dumps(desired_cap))
110
+ browser = playwright.chromium.connect(cdpUrl)
111
+ page = browser.new_page()
112
+ page.goto("https://percy.io/")
113
+ percy_screenshot(page, name = "Screenshot 1")
114
+ ```
115
+ # take a snapshot
116
+ ```python
117
+ percy_screenshot(page, name = 'Screenshot 1')
118
+ ```
119
+
120
+ - `page` (**required**) - A Playwright page instance
121
+ - `name` (**required**) - The screenshot name; must be unique to each screenshot
122
+ - `options` (**optional**) - There are various options supported by percy_screenshot to server further functionality.
123
+ - `sync` - Boolean value by default it falls back to `false`, Gives the processed result around screenshot [From CLI v1.28.8]
124
+ - `full_page` - Boolean value by default it falls back to `false`, Takes full page screenshot [From CLI v1.28.8]
125
+ - `freeze_animated_image` - Boolean value by default it falls back to `false`, you can pass `true` and percy will freeze image based animations.
126
+ - `freeze_image_by_selectors` -List of selectors. Images will be freezed which are passed using selectors. For this to work `freeze_animated_image` must be set to true.
127
+ - `freeze_image_by_xpaths` - List of xpaths. Images will be freezed which are passed using xpaths. For this to work `freeze_animated_image` must be set to true.
128
+ - `percy_css` - Custom CSS to be added to DOM before the screenshot being taken. Note: This gets removed once the screenshot is taken.
129
+ - `ignore_region_xpaths` - List of xpaths. elements in the DOM can be ignored using xpath
130
+ - `ignore_region_selectors` - List of selectors. elements in the DOM can be ignored using selectors.
131
+ - `custom_ignore_regions` - List of custom objects. elements can be ignored using custom boundaries. Just passing a simple object for it like below.
132
+ - example: ```{"top": 10, "right": 10, "bottom": 120, "left": 10}```
133
+ - In above example it will draw rectangle of ignore region as per given coordinates.
134
+ - `top` (int): Top coordinate of the ignore region.
135
+ - `bottom` (int): Bottom coordinate of the ignore region.
136
+ - `left` (int): Left coordinate of the ignore region.
137
+ - `right` (int): Right coordinate of the ignore region.
138
+ - `consider_region_xpaths` - List of xpaths. elements in the DOM can be considered for diffing and will be ignored by Intelli Ignore using xpaths.
139
+ - `consider_region_selectors` - List of selectors. elements in the DOM can be considered for diffing and will be ignored by Intelli Ignore using selectors.
140
+ - `custom_consider_regions` - List of custom objects. elements can be considered for diffing and will be ignored by Intelli Ignore using custom boundaries
141
+ - example:```{"top": 10, "right": 10, "bottom": 120, "left": 10}```
142
+ - In above example it will draw rectangle of consider region will be drawn.
143
+ - Parameters:
144
+ - `top` (int): Top coordinate of the consider region.
145
+ - `bottom` (int): Bottom coordinate of the consider region.
146
+ - `left` (int): Left coordinate of the consider region.
147
+ - `right` (int): Right coordinate of the consider region.
148
+
149
+
150
+ ### Creating Percy on automate build
151
+ Note: Automate Percy Token starts with `auto` keyword. The command can be triggered using `exec` keyword.
152
+
153
+ ```sh-session
154
+ $ export PERCY_TOKEN=[your-project-token]
155
+ $ percy exec -- [python test command]
156
+ [percy] Percy has started!
157
+ [percy] [Python example] : Starting automate screenshot ...
158
+ [percy] Screenshot taken "Python example"
159
+ [percy] Stopping percy...
160
+ [percy] Finalized build #1: https://percy.io/[your-project]
161
+ [percy] Done!
162
+ ```
163
+
164
+ Refer to docs here: [Percy on Automate](https://www.browserstack.com/docs/percy/integrate/functional-and-visual)
@@ -0,0 +1,141 @@
1
+ # Percy playwright python
2
+ ![Test](https://github.com/percy/percy-playwright-python/workflows/Test/badge.svg)
3
+
4
+ [Percy](https://percy.io) visual testing for Python Playwright.
5
+
6
+ ## Installation
7
+
8
+ npm install `@percy/cli`:
9
+
10
+ ```sh-session
11
+ $ npm install --save-dev @percy/cli
12
+ ```
13
+
14
+ pip install Percy playwright package:
15
+
16
+ ```ssh-session
17
+ $ pip install percy-playwright
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ This is an example test using the `percy_snapshot` function.
23
+
24
+ ``` python
25
+ from percy import percy_snapshot
26
+
27
+ with sync_playwright() as playwright:
28
+ browser = playwright.chromium.connect()
29
+ page = browser.new_page()
30
+ page.goto('http://example.com')
31
+
32
+ # take a snapshot
33
+ percy_snapshot(browser, 'Python example')
34
+ ```
35
+
36
+ Running the test above normally will result in the following log:
37
+
38
+ ```sh-session
39
+ [percy] Percy is not running, disabling snapshots
40
+ ```
41
+
42
+ When running with [`percy
43
+ exec`](https://github.com/percy/cli/tree/master/packages/cli-exec#percy-exec), and your project's
44
+ `PERCY_TOKEN`, a new Percy build will be created and snapshots will be uploaded to your project.
45
+
46
+ ```sh-session
47
+ $ export PERCY_TOKEN=[your-project-token]
48
+ $ percy exec -- [python test command]
49
+ [percy] Percy has started!
50
+ [percy] Created build #1: https://percy.io/[your-project]
51
+ [percy] Snapshot taken "Python example"
52
+ [percy] Stopping percy...
53
+ [percy] Finalized build #1: https://percy.io/[your-project]
54
+ [percy] Done!
55
+ ```
56
+
57
+ ## Configuration
58
+
59
+ `percy_snapshot(page, name[, **kwargs])`
60
+
61
+ - `page` (**required**) - A playwright page instance
62
+ - `name` (**required**) - The snapshot name; must be unique to each snapshot
63
+ - `**kwargs` - [See per-snapshot configuration options](https://www.browserstack.com/docs/percy/take-percy-snapshots/overview#per-snapshot-configuration)
64
+
65
+
66
+ ## Percy on Automate
67
+
68
+ ## Usage
69
+
70
+ ``` python
71
+ from playwright.sync_api import sync_playwright
72
+ from percy import percy_screenshot, percy_snapshot
73
+
74
+ desired_cap = {
75
+ 'browser': 'chrome',
76
+ 'browser_version': 'latest',
77
+ 'os': 'osx',
78
+ 'os_version': 'ventura',
79
+ 'name': 'Percy Playwright PoA Demo',
80
+ 'build': 'percy-playwright-python-tutorial',
81
+ 'browserstack.username': 'username',
82
+ 'browserstack.accessKey': 'accesskey'
83
+ }
84
+
85
+ with sync_playwright() as playwright:
86
+ cdpUrl = 'wss://cdp.browserstack.com/playwright?caps=' + urllib.parse.quote(json.dumps(desired_cap))
87
+ browser = playwright.chromium.connect(cdpUrl)
88
+ page = browser.new_page()
89
+ page.goto("https://percy.io/")
90
+ percy_screenshot(page, name = "Screenshot 1")
91
+ ```
92
+ # take a snapshot
93
+ ```python
94
+ percy_screenshot(page, name = 'Screenshot 1')
95
+ ```
96
+
97
+ - `page` (**required**) - A Playwright page instance
98
+ - `name` (**required**) - The screenshot name; must be unique to each screenshot
99
+ - `options` (**optional**) - There are various options supported by percy_screenshot to server further functionality.
100
+ - `sync` - Boolean value by default it falls back to `false`, Gives the processed result around screenshot [From CLI v1.28.8]
101
+ - `full_page` - Boolean value by default it falls back to `false`, Takes full page screenshot [From CLI v1.28.8]
102
+ - `freeze_animated_image` - Boolean value by default it falls back to `false`, you can pass `true` and percy will freeze image based animations.
103
+ - `freeze_image_by_selectors` -List of selectors. Images will be freezed which are passed using selectors. For this to work `freeze_animated_image` must be set to true.
104
+ - `freeze_image_by_xpaths` - List of xpaths. Images will be freezed which are passed using xpaths. For this to work `freeze_animated_image` must be set to true.
105
+ - `percy_css` - Custom CSS to be added to DOM before the screenshot being taken. Note: This gets removed once the screenshot is taken.
106
+ - `ignore_region_xpaths` - List of xpaths. elements in the DOM can be ignored using xpath
107
+ - `ignore_region_selectors` - List of selectors. elements in the DOM can be ignored using selectors.
108
+ - `custom_ignore_regions` - List of custom objects. elements can be ignored using custom boundaries. Just passing a simple object for it like below.
109
+ - example: ```{"top": 10, "right": 10, "bottom": 120, "left": 10}```
110
+ - In above example it will draw rectangle of ignore region as per given coordinates.
111
+ - `top` (int): Top coordinate of the ignore region.
112
+ - `bottom` (int): Bottom coordinate of the ignore region.
113
+ - `left` (int): Left coordinate of the ignore region.
114
+ - `right` (int): Right coordinate of the ignore region.
115
+ - `consider_region_xpaths` - List of xpaths. elements in the DOM can be considered for diffing and will be ignored by Intelli Ignore using xpaths.
116
+ - `consider_region_selectors` - List of selectors. elements in the DOM can be considered for diffing and will be ignored by Intelli Ignore using selectors.
117
+ - `custom_consider_regions` - List of custom objects. elements can be considered for diffing and will be ignored by Intelli Ignore using custom boundaries
118
+ - example:```{"top": 10, "right": 10, "bottom": 120, "left": 10}```
119
+ - In above example it will draw rectangle of consider region will be drawn.
120
+ - Parameters:
121
+ - `top` (int): Top coordinate of the consider region.
122
+ - `bottom` (int): Bottom coordinate of the consider region.
123
+ - `left` (int): Left coordinate of the consider region.
124
+ - `right` (int): Right coordinate of the consider region.
125
+
126
+
127
+ ### Creating Percy on automate build
128
+ Note: Automate Percy Token starts with `auto` keyword. The command can be triggered using `exec` keyword.
129
+
130
+ ```sh-session
131
+ $ export PERCY_TOKEN=[your-project-token]
132
+ $ percy exec -- [python test command]
133
+ [percy] Percy has started!
134
+ [percy] [Python example] : Starting automate screenshot ...
135
+ [percy] Screenshot taken "Python example"
136
+ [percy] Stopping percy...
137
+ [percy] Finalized build #1: https://percy.io/[your-project]
138
+ [percy] Done!
139
+ ```
140
+
141
+ Refer to docs here: [Percy on Automate](https://www.browserstack.com/docs/percy/integrate/functional-and-visual)
@@ -0,0 +1,22 @@
1
+ from percy.version import __version__
2
+ from percy.screenshot import percy_automate_screenshot
3
+
4
+ # import snapshot command
5
+ try:
6
+ from percy.screenshot import percy_snapshot
7
+ except ImportError:
8
+
9
+ def percy_snapshot(page, *a, **kw):
10
+ raise ModuleNotFoundError(
11
+ "[percy] `percy-playwright-python` package is not installed, "
12
+ "please install it to use percy_snapshot command"
13
+ )
14
+
15
+
16
+ # for better backwards compatibility
17
+ def percySnapshot(browser, *a, **kw):
18
+ return percy_snapshot(page=browser, *a, **kw)
19
+
20
+
21
+ def percy_screenshot(page, *a, **kw):
22
+ return percy_automate_screenshot(page, *a, **kw)
@@ -0,0 +1,42 @@
1
+ import time
2
+
3
+
4
+ class Cache:
5
+ CACHE = {}
6
+ CACHE_TIMEOUT = 5 * 60 # 300 seconds
7
+ TIMEOUT_KEY = "last_access_time"
8
+
9
+ # Caching Keys
10
+ session_details = "session_details"
11
+
12
+ @classmethod
13
+ def check_types(cls, session_id, property):
14
+ if not isinstance(session_id, str):
15
+ raise TypeError("Argument session_id should be string")
16
+ if not isinstance(property, str):
17
+ raise TypeError("Argument property should be string")
18
+
19
+ @classmethod
20
+ def set_cache(cls, session_id, property, value):
21
+ cls.check_types(session_id, property)
22
+ session = cls.CACHE.get(session_id, {})
23
+ session[cls.TIMEOUT_KEY] = time.time()
24
+ session[property] = value
25
+ cls.CACHE[session_id] = session
26
+
27
+ @classmethod
28
+ def get_cache(cls, session_id, property):
29
+ cls.cleanup_cache()
30
+ cls.check_types(session_id, property)
31
+ session = cls.CACHE.get(session_id, {})
32
+ return session.get(property, None)
33
+
34
+ @classmethod
35
+ def cleanup_cache(cls):
36
+ now = time.time()
37
+ for session_id, session in cls.CACHE.items():
38
+ timestamp = session[cls.TIMEOUT_KEY]
39
+ if now - timestamp >= cls.CACHE_TIMEOUT:
40
+ cls.CACHE[session_id] = {
41
+ cls.session_details: session[cls.session_details]
42
+ }
@@ -0,0 +1,44 @@
1
+ # pylint: disable=protected-access
2
+ import json
3
+ from percy.cache import Cache
4
+
5
+
6
+ class PageMetaData:
7
+ def __init__(self, page):
8
+ self.page = page
9
+
10
+ def __fetch_guid(self, obj):
11
+ return obj._impl_obj._guid
12
+
13
+ @property
14
+ def framework(self):
15
+ return "playwright"
16
+
17
+ @property
18
+ def page_guid(self):
19
+ return self.__fetch_guid(self.page)
20
+
21
+ @property
22
+ def frame_guid(self):
23
+ return self.__fetch_guid(self.page.main_frame)
24
+
25
+ @property
26
+ def browser_guid(self):
27
+ return self.__fetch_guid(self.page.context.browser)
28
+
29
+ @property
30
+ def session_details(self):
31
+ session_details = Cache.get_cache(self.browser_guid, Cache.session_details)
32
+ if session_details is None:
33
+ session_details = json.loads(
34
+ self.page.evaluate(
35
+ "_ => {}", 'browserstack_executor: {"action": "getSessionDetails"}'
36
+ )
37
+ )
38
+ Cache.set_cache(self.browser_guid, Cache.session_details, session_details)
39
+ return session_details
40
+ return session_details
41
+
42
+ @property
43
+ def automate_session_id(self):
44
+ return self.session_details.get("hashed_id")
@@ -0,0 +1,168 @@
1
+ import os
2
+ import json
3
+ import platform
4
+ from functools import lru_cache
5
+ import requests
6
+
7
+ from playwright._repo_version import version as PLAYWRIGHT_VERSION
8
+ from percy.version import __version__ as SDK_VERSION
9
+ from percy.page_metadata import PageMetaData
10
+
11
+ # Collect client environment information
12
+ CLIENT_INFO = "percy-playwright-python/" + SDK_VERSION
13
+ ENV_INFO = ["playwright/" + PLAYWRIGHT_VERSION, "python/" + platform.python_version()]
14
+
15
+ # Maybe get the CLI API address from the environment
16
+ PERCY_CLI_API = os.environ.get("PERCY_CLI_API") or "http://localhost:5338"
17
+ PERCY_DEBUG = os.environ.get("PERCY_LOGLEVEL") == "debug"
18
+
19
+ # for logging
20
+ LABEL = "[\u001b[35m" + ("percy:python" if PERCY_DEBUG else "percy") + "\u001b[39m]"
21
+
22
+
23
+ # Check if Percy is enabled, caching the result so it is only checked once
24
+ @lru_cache(maxsize=None)
25
+ def is_percy_enabled():
26
+ try:
27
+ response = requests.get(f"{PERCY_CLI_API}/percy/healthcheck", timeout=30)
28
+ response.raise_for_status()
29
+ data = response.json()
30
+ session_type = data.get("type", None)
31
+
32
+ if not data["success"]:
33
+ raise Exception(data["error"])
34
+ version = response.headers.get("x-percy-core-version")
35
+
36
+ if not version:
37
+ print(
38
+ f"{LABEL} You may be using @percy/agent "
39
+ "which is no longer supported by this SDK. "
40
+ "Please uninstall @percy/agent and install @percy/cli instead. "
41
+ "https://www.browserstack.com/docs/percy/migration/migrate-to-cli"
42
+ )
43
+ return False
44
+
45
+ if version.split(".")[0] != "1":
46
+ print(f"{LABEL} Unsupported Percy CLI version, {version}")
47
+ return False
48
+
49
+ return session_type
50
+ except Exception as e:
51
+ print(f"{LABEL} Percy is not running, disabling snapshots")
52
+ if PERCY_DEBUG:
53
+ print(f"{LABEL} {e}")
54
+ return False
55
+
56
+
57
+ # Fetch the @percy/dom script, caching the result so it is only fetched once
58
+ @lru_cache(maxsize=None)
59
+ def fetch_percy_dom():
60
+ response = requests.get(f"{PERCY_CLI_API}/percy/dom.js", timeout=30)
61
+ response.raise_for_status()
62
+ return response.text
63
+
64
+
65
+ # Take a DOM snapshot and post it to the snapshot endpoint
66
+ def percy_snapshot(page, name, **kwargs):
67
+ session_type = is_percy_enabled()
68
+ if session_type is False:
69
+ return None # Since session_type can be None for old CLI version
70
+ if session_type == "automate":
71
+ raise Exception(
72
+ "Invalid function call - "
73
+ "percy_snapshot(). "
74
+ "Please use percy_screenshot() function while using Percy with Automate. "
75
+ "For more information on usage of PercyScreenshot, "
76
+ "refer https://www.browserstack.com/docs/percy/integrate/functional-and-visual"
77
+ )
78
+
79
+ try:
80
+ # Inject the DOM serialization script
81
+ # print(fetch_percy_dom())
82
+ page.evaluate(fetch_percy_dom())
83
+
84
+ # Serialize and capture the DOM
85
+ dom_snapshot_script = f"PercyDOM.serialize({json.dumps(kwargs)})"
86
+
87
+ # Return the serialized DOM Snapshot
88
+ dom_snapshot = page.evaluate(dom_snapshot_script)
89
+
90
+ # Post the DOM to the snapshot endpoint with snapshot options and other info
91
+ response = requests.post(
92
+ f"{PERCY_CLI_API}/percy/snapshot",
93
+ json={
94
+ **kwargs,
95
+ **{
96
+ "client_info": CLIENT_INFO,
97
+ "environment_info": ENV_INFO,
98
+ "dom_snapshot": dom_snapshot,
99
+ "url": page.url,
100
+ "name": name,
101
+ },
102
+ },
103
+ timeout=600,
104
+ )
105
+
106
+ # Handle errors
107
+ response.raise_for_status()
108
+ data = response.json()
109
+
110
+ if not data["success"]:
111
+ raise Exception(data["error"])
112
+ return data.get("data", None)
113
+ except Exception as e:
114
+ print(f'{LABEL} Could not take DOM snapshot "{name}"')
115
+ print(f"{LABEL} {e}")
116
+ return None
117
+
118
+
119
+ def percy_automate_screenshot(page, name, options=None, **kwargs):
120
+ session_type = is_percy_enabled()
121
+ if session_type is False:
122
+ return None # Since session_type can be None for old CLI version
123
+ if session_type == "web":
124
+ raise Exception(
125
+ "Invalid function call - "
126
+ "percy_screenshot(). Please use percy_snapshot() function for taking screenshot. "
127
+ "percy_screenshot() should be used only while using Percy with Automate. "
128
+ "For more information on usage of percy_snapshot(), "
129
+ "refer doc for your language https://www.browserstack.com/docs/percy/integrate/overview"
130
+ )
131
+
132
+ if options is None:
133
+ options = {}
134
+
135
+ try:
136
+ metadata = PageMetaData(page)
137
+
138
+ # Post to automateScreenshot endpoint with page options and other info
139
+ response = requests.post(
140
+ f"{PERCY_CLI_API}/percy/automateScreenshot",
141
+ json={
142
+ **kwargs,
143
+ **{
144
+ "client_info": CLIENT_INFO,
145
+ "environment_info": ENV_INFO,
146
+ "sessionId": metadata.automate_session_id,
147
+ "pageGuid": metadata.page_guid,
148
+ "frameGuid": metadata.frame_guid,
149
+ "framework": metadata.framework,
150
+ "snapshotName": name,
151
+ "options": options,
152
+ },
153
+ },
154
+ timeout=600,
155
+ )
156
+
157
+ # Handle errors
158
+ response.raise_for_status()
159
+ data = response.json()
160
+
161
+ if not data["success"]:
162
+ raise Exception(data["error"])
163
+
164
+ return data.get("data", None)
165
+ except Exception as e:
166
+ print(f'{LABEL} Could not take Screenshot "{name}"')
167
+ print(f"{LABEL} {e}")
168
+ return None
@@ -0,0 +1 @@
1
+ __version__ = "1.0.0"
@@ -0,0 +1,164 @@
1
+ Metadata-Version: 2.1
2
+ Name: percy-playwright
3
+ Version: 1.0.0
4
+ Summary: Python client for visual testing with Percy
5
+ Home-page: https://github.com/percy/percy-playwright-python
6
+ Author: Perceptual Inc.
7
+ Author-email: team@percy.io
8
+ License: MIT
9
+ Keywords: percy visual testing
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Natural Language :: English
14
+ Classifier: Programming Language :: Python :: 3.6
15
+ Classifier: Programming Language :: Python :: 3.7
16
+ Classifier: Programming Language :: Python :: 3.8
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Requires-Python: >=3.6
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: playwright>=1.28.0
22
+ Requires-Dist: requests==2.*
23
+
24
+ # Percy playwright python
25
+ ![Test](https://github.com/percy/percy-playwright-python/workflows/Test/badge.svg)
26
+
27
+ [Percy](https://percy.io) visual testing for Python Playwright.
28
+
29
+ ## Installation
30
+
31
+ npm install `@percy/cli`:
32
+
33
+ ```sh-session
34
+ $ npm install --save-dev @percy/cli
35
+ ```
36
+
37
+ pip install Percy playwright package:
38
+
39
+ ```ssh-session
40
+ $ pip install percy-playwright
41
+ ```
42
+
43
+ ## Usage
44
+
45
+ This is an example test using the `percy_snapshot` function.
46
+
47
+ ``` python
48
+ from percy import percy_snapshot
49
+
50
+ with sync_playwright() as playwright:
51
+ browser = playwright.chromium.connect()
52
+ page = browser.new_page()
53
+ page.goto('http://example.com')
54
+
55
+ # take a snapshot
56
+ percy_snapshot(browser, 'Python example')
57
+ ```
58
+
59
+ Running the test above normally will result in the following log:
60
+
61
+ ```sh-session
62
+ [percy] Percy is not running, disabling snapshots
63
+ ```
64
+
65
+ When running with [`percy
66
+ exec`](https://github.com/percy/cli/tree/master/packages/cli-exec#percy-exec), and your project's
67
+ `PERCY_TOKEN`, a new Percy build will be created and snapshots will be uploaded to your project.
68
+
69
+ ```sh-session
70
+ $ export PERCY_TOKEN=[your-project-token]
71
+ $ percy exec -- [python test command]
72
+ [percy] Percy has started!
73
+ [percy] Created build #1: https://percy.io/[your-project]
74
+ [percy] Snapshot taken "Python example"
75
+ [percy] Stopping percy...
76
+ [percy] Finalized build #1: https://percy.io/[your-project]
77
+ [percy] Done!
78
+ ```
79
+
80
+ ## Configuration
81
+
82
+ `percy_snapshot(page, name[, **kwargs])`
83
+
84
+ - `page` (**required**) - A playwright page instance
85
+ - `name` (**required**) - The snapshot name; must be unique to each snapshot
86
+ - `**kwargs` - [See per-snapshot configuration options](https://www.browserstack.com/docs/percy/take-percy-snapshots/overview#per-snapshot-configuration)
87
+
88
+
89
+ ## Percy on Automate
90
+
91
+ ## Usage
92
+
93
+ ``` python
94
+ from playwright.sync_api import sync_playwright
95
+ from percy import percy_screenshot, percy_snapshot
96
+
97
+ desired_cap = {
98
+ 'browser': 'chrome',
99
+ 'browser_version': 'latest',
100
+ 'os': 'osx',
101
+ 'os_version': 'ventura',
102
+ 'name': 'Percy Playwright PoA Demo',
103
+ 'build': 'percy-playwright-python-tutorial',
104
+ 'browserstack.username': 'username',
105
+ 'browserstack.accessKey': 'accesskey'
106
+ }
107
+
108
+ with sync_playwright() as playwright:
109
+ cdpUrl = 'wss://cdp.browserstack.com/playwright?caps=' + urllib.parse.quote(json.dumps(desired_cap))
110
+ browser = playwright.chromium.connect(cdpUrl)
111
+ page = browser.new_page()
112
+ page.goto("https://percy.io/")
113
+ percy_screenshot(page, name = "Screenshot 1")
114
+ ```
115
+ # take a snapshot
116
+ ```python
117
+ percy_screenshot(page, name = 'Screenshot 1')
118
+ ```
119
+
120
+ - `page` (**required**) - A Playwright page instance
121
+ - `name` (**required**) - The screenshot name; must be unique to each screenshot
122
+ - `options` (**optional**) - There are various options supported by percy_screenshot to server further functionality.
123
+ - `sync` - Boolean value by default it falls back to `false`, Gives the processed result around screenshot [From CLI v1.28.8]
124
+ - `full_page` - Boolean value by default it falls back to `false`, Takes full page screenshot [From CLI v1.28.8]
125
+ - `freeze_animated_image` - Boolean value by default it falls back to `false`, you can pass `true` and percy will freeze image based animations.
126
+ - `freeze_image_by_selectors` -List of selectors. Images will be freezed which are passed using selectors. For this to work `freeze_animated_image` must be set to true.
127
+ - `freeze_image_by_xpaths` - List of xpaths. Images will be freezed which are passed using xpaths. For this to work `freeze_animated_image` must be set to true.
128
+ - `percy_css` - Custom CSS to be added to DOM before the screenshot being taken. Note: This gets removed once the screenshot is taken.
129
+ - `ignore_region_xpaths` - List of xpaths. elements in the DOM can be ignored using xpath
130
+ - `ignore_region_selectors` - List of selectors. elements in the DOM can be ignored using selectors.
131
+ - `custom_ignore_regions` - List of custom objects. elements can be ignored using custom boundaries. Just passing a simple object for it like below.
132
+ - example: ```{"top": 10, "right": 10, "bottom": 120, "left": 10}```
133
+ - In above example it will draw rectangle of ignore region as per given coordinates.
134
+ - `top` (int): Top coordinate of the ignore region.
135
+ - `bottom` (int): Bottom coordinate of the ignore region.
136
+ - `left` (int): Left coordinate of the ignore region.
137
+ - `right` (int): Right coordinate of the ignore region.
138
+ - `consider_region_xpaths` - List of xpaths. elements in the DOM can be considered for diffing and will be ignored by Intelli Ignore using xpaths.
139
+ - `consider_region_selectors` - List of selectors. elements in the DOM can be considered for diffing and will be ignored by Intelli Ignore using selectors.
140
+ - `custom_consider_regions` - List of custom objects. elements can be considered for diffing and will be ignored by Intelli Ignore using custom boundaries
141
+ - example:```{"top": 10, "right": 10, "bottom": 120, "left": 10}```
142
+ - In above example it will draw rectangle of consider region will be drawn.
143
+ - Parameters:
144
+ - `top` (int): Top coordinate of the consider region.
145
+ - `bottom` (int): Bottom coordinate of the consider region.
146
+ - `left` (int): Left coordinate of the consider region.
147
+ - `right` (int): Right coordinate of the consider region.
148
+
149
+
150
+ ### Creating Percy on automate build
151
+ Note: Automate Percy Token starts with `auto` keyword. The command can be triggered using `exec` keyword.
152
+
153
+ ```sh-session
154
+ $ export PERCY_TOKEN=[your-project-token]
155
+ $ percy exec -- [python test command]
156
+ [percy] Percy has started!
157
+ [percy] [Python example] : Starting automate screenshot ...
158
+ [percy] Screenshot taken "Python example"
159
+ [percy] Stopping percy...
160
+ [percy] Finalized build #1: https://percy.io/[your-project]
161
+ [percy] Done!
162
+ ```
163
+
164
+ Refer to docs here: [Percy on Automate](https://www.browserstack.com/docs/percy/integrate/functional-and-visual)
@@ -0,0 +1,17 @@
1
+ LICENSE
2
+ README.md
3
+ setup.py
4
+ percy/__init__.py
5
+ percy/cache.py
6
+ percy/page_metadata.py
7
+ percy/screenshot.py
8
+ percy/version.py
9
+ percy_playwright.egg-info/PKG-INFO
10
+ percy_playwright.egg-info/SOURCES.txt
11
+ percy_playwright.egg-info/dependency_links.txt
12
+ percy_playwright.egg-info/not-zip-safe
13
+ percy_playwright.egg-info/requires.txt
14
+ percy_playwright.egg-info/top_level.txt
15
+ tests/test_cache.py
16
+ tests/test_page_metadata.py
17
+ tests/test_screenshot.py
@@ -0,0 +1,2 @@
1
+ playwright>=1.28.0
2
+ requests==2.*
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,41 @@
1
+ from setuptools import setup
2
+ from os import path
3
+ import percy
4
+
5
+ # read the README for long_description
6
+ cwd = path.abspath(path.dirname(__file__))
7
+ with open(path.join(cwd, 'README.md'), encoding='utf-8') as f:
8
+ long_description = f.read()
9
+
10
+ setup(
11
+ name='percy-playwright',
12
+ description='Python client for visual testing with Percy',
13
+ long_description=long_description,
14
+ long_description_content_type='text/markdown',
15
+ version=percy.__version__,
16
+ license='MIT',
17
+ author='Perceptual Inc.',
18
+ author_email='team@percy.io',
19
+ url='https://github.com/percy/percy-playwright-python',
20
+ keywords='percy visual testing',
21
+ packages=['percy'],
22
+ include_package_data=True,
23
+ install_requires=[
24
+ 'playwright>=1.28.0',
25
+ 'requests==2.*'
26
+ ],
27
+ python_requires='>=3.6',
28
+ classifiers=[
29
+ 'Development Status :: 5 - Production/Stable',
30
+ 'Intended Audience :: Developers',
31
+ 'License :: OSI Approved :: MIT License',
32
+ 'Natural Language :: English',
33
+ 'Programming Language :: Python :: 3.6',
34
+ 'Programming Language :: Python :: 3.7',
35
+ 'Programming Language :: Python :: 3.8',
36
+ 'Programming Language :: Python :: 3.9',
37
+ ],
38
+ test_suite='tests',
39
+ tests_require=['playwright', 'httpretty'],
40
+ zip_safe=False
41
+ )
@@ -0,0 +1,63 @@
1
+ # . # pylint: disable=[arguments-differ, protected-access]
2
+ import time
3
+ import unittest
4
+ from unittest.mock import patch
5
+ from percy.cache import Cache
6
+
7
+
8
+ class TestCache(unittest.TestCase):
9
+ def setUp(self) -> None:
10
+ self.cache = Cache
11
+ self.session_id = "session_id_123"
12
+ self.session_details = {
13
+ "browser": "chrome",
14
+ "platform": "windows",
15
+ "browserVersion": "115.0.1",
16
+ "hashed_id": "abcdef",
17
+ }
18
+
19
+ self.cache.set_cache(
20
+ self.session_id, Cache.session_details, self.session_details
21
+ )
22
+ self.cache.set_cache(self.session_id, "key-1", "some-value")
23
+
24
+ def test_set_cache(self):
25
+ with self.assertRaises(Exception) as e:
26
+ self.cache.set_cache(123, 123, 123)
27
+ self.assertEqual(str(e.exception), "Argument session_id should be string")
28
+
29
+ with self.assertRaises(Exception) as e:
30
+ self.cache.set_cache(self.session_id, 123, 123)
31
+ self.assertEqual(str(e.exception), "Argument property should be string")
32
+
33
+ self.assertIn(self.session_id, self.cache.CACHE)
34
+ self.assertDictEqual(
35
+ self.cache.CACHE[self.session_id][Cache.session_details],
36
+ self.session_details,
37
+ )
38
+
39
+ def test_get_cache_invalid_args(self):
40
+ with self.assertRaises(Exception) as e:
41
+ self.cache.get_cache(123, 123)
42
+ self.assertEqual(str(e.exception), "Argument session_id should be string")
43
+
44
+ with self.assertRaises(Exception) as e:
45
+ self.cache.get_cache(self.session_id, 123)
46
+ self.assertEqual(str(e.exception), "Argument property should be string")
47
+
48
+ @patch.object(Cache, "cleanup_cache")
49
+ def test_get_cache_success(self, mock_cleanup_cache):
50
+ session_details = self.cache.get_cache(self.session_id, Cache.session_details)
51
+ self.assertDictEqual(session_details, self.session_details)
52
+ mock_cleanup_cache.assert_called()
53
+
54
+ @patch("percy.cache.Cache.CACHE_TIMEOUT", 1)
55
+ def test_cleanup_cache(self):
56
+ cache_timeout = self.cache.CACHE_TIMEOUT
57
+ time.sleep(cache_timeout + 1)
58
+ self.assertIn(self.session_id, self.cache.CACHE)
59
+ self.assertIn("key-1", self.cache.CACHE[self.session_id])
60
+ self.cache.cleanup_cache()
61
+ self.assertIn(self.session_id, self.cache.CACHE)
62
+ self.assertIn("session_details", self.cache.CACHE[self.session_id])
63
+ self.assertNotIn("key-1", self.cache.CACHE[self.session_id])
@@ -0,0 +1,51 @@
1
+ # pylint: disable=[abstract-class-instantiated, arguments-differ, protected-access]
2
+ import json
3
+ import unittest
4
+ from unittest.mock import MagicMock, patch
5
+ from percy.cache import Cache
6
+ from percy.page_metadata import PageMetaData
7
+
8
+
9
+ class TestPageMetaData(unittest.TestCase):
10
+ @patch("percy.cache.Cache.get_cache")
11
+ @patch("percy.cache.Cache.set_cache")
12
+ def test_page_metadata(self, mock_set_cache, mock_get_cache):
13
+ # Mock the page and its properties
14
+ page = MagicMock()
15
+ page._impl_obj._guid = "page-guid"
16
+ page.main_frame._impl_obj._guid = "frame-guid"
17
+ page.context.browser._impl_obj._guid = "browser-guid"
18
+ page.evaluate.return_value = json.dumps({"hashed_id": "session-id"})
19
+
20
+ # Set up the mocks
21
+ mock_get_cache.return_value = None
22
+
23
+ # Create an instance of PageMetaData
24
+ page_metadata = PageMetaData(page)
25
+
26
+ # Test framework property
27
+ self.assertEqual(page_metadata.framework, "playwright")
28
+
29
+ # Test page_guid property
30
+ self.assertEqual(page_metadata.page_guid, "page-guid")
31
+
32
+ # Test frame_guid property
33
+ self.assertEqual(page_metadata.frame_guid, "frame-guid")
34
+
35
+ # Test browser_guid property
36
+ self.assertEqual(page_metadata.browser_guid, "browser-guid")
37
+
38
+ # Test session_details property when cache is empty
39
+ self.assertEqual(page_metadata.session_details, {"hashed_id": "session-id"})
40
+ mock_set_cache.assert_called_once_with(
41
+ "browser-guid", Cache.session_details, {"hashed_id": "session-id"}
42
+ )
43
+
44
+ # Test session_details property when cache is not empty
45
+ mock_get_cache.return_value = {"hashed_id": "cached-session-id"}
46
+ self.assertEqual(
47
+ page_metadata.session_details, {"hashed_id": "cached-session-id"}
48
+ )
49
+
50
+ # Test automate_session_id property
51
+ self.assertEqual(page_metadata.automate_session_id, "cached-session-id")
@@ -0,0 +1,372 @@
1
+ # pylint: disable=[abstract-class-instantiated, arguments-differ, protected-access]
2
+ import json
3
+ import unittest
4
+ import platform
5
+ from threading import Thread
6
+ from http.server import BaseHTTPRequestHandler, HTTPServer
7
+ from unittest.mock import patch, MagicMock
8
+ import httpretty
9
+
10
+ from playwright.sync_api import sync_playwright
11
+ from playwright._repo_version import version as PLAYWRIGHT_VERSION
12
+ from percy.version import __version__ as SDK_VERSION
13
+ from percy.screenshot import (
14
+ is_percy_enabled,
15
+ fetch_percy_dom,
16
+ percy_snapshot,
17
+ percy_automate_screenshot,
18
+ )
19
+ import percy.screenshot as local
20
+
21
+ LABEL = local.LABEL
22
+
23
+
24
+ # mock a simple webpage to snapshot
25
+ class MockServerRequestHandler(BaseHTTPRequestHandler):
26
+ def do_GET(self):
27
+ self.send_response(200)
28
+ self.send_header("Content-Type", "text/html")
29
+ self.end_headers()
30
+ self.wfile.write(("Snapshot Me").encode("utf-8"))
31
+
32
+ def log_message(self, format, *args):
33
+ return
34
+
35
+
36
+ # daemon threads automatically shut down when the main process exits
37
+ mock_server = HTTPServer(("localhost", 8000), MockServerRequestHandler)
38
+ mock_server_thread = Thread(target=mock_server.serve_forever)
39
+ mock_server_thread.daemon = True
40
+ mock_server_thread.start()
41
+
42
+ # initializing mock data
43
+ data_object = {"sync": "true", "diff": 0}
44
+
45
+
46
+ # mock helpers
47
+ def mock_healthcheck(fail=False, fail_how="error", session_type=None):
48
+ health_body = {"success": True}
49
+ health_headers = {"X-Percy-Core-Version": "1.0.0"}
50
+ health_status = 200
51
+
52
+ if fail and fail_how == "error":
53
+ health_body = {"success": False, "error": "test"}
54
+ health_status = 500
55
+ elif fail and fail_how == "wrong-version":
56
+ health_headers = {"X-Percy-Core-Version": "2.0.0"}
57
+ elif fail and fail_how == "no-version":
58
+ health_headers = {}
59
+
60
+ if session_type:
61
+ health_body["type"] = session_type
62
+
63
+ health_body = json.dumps(health_body)
64
+ httpretty.register_uri(
65
+ httpretty.GET,
66
+ "http://localhost:5338/percy/healthcheck",
67
+ body=health_body,
68
+ adding_headers=health_headers,
69
+ status=health_status,
70
+ )
71
+ httpretty.register_uri(
72
+ httpretty.GET,
73
+ "http://localhost:5338/percy/dom.js",
74
+ body="window.PercyDOM = { serialize: () => document.documentElement.outerHTML };",
75
+ status=200,
76
+ )
77
+
78
+
79
+ def mock_snapshot(fail=False, data=False):
80
+ httpretty.register_uri(
81
+ httpretty.POST,
82
+ "http://localhost:5338/percy/snapshot",
83
+ body=json.dumps(
84
+ {
85
+ "success": "false" if fail else "true",
86
+ "error": "test" if fail else None,
87
+ "data": data_object if data else None,
88
+ }
89
+ ),
90
+ status=(500 if fail else 200),
91
+ )
92
+
93
+
94
+ class TestPercySnapshot(unittest.TestCase):
95
+ @classmethod
96
+ def setUpClass(cls):
97
+ cls.p = sync_playwright().start()
98
+ # Launch the browser
99
+ cls.browser = cls.p.chromium.launch(
100
+ headless=True
101
+ ) # Set headless=True if you don't want to see the browser
102
+ context = cls.browser.new_context()
103
+ cls.page = context.new_page()
104
+
105
+ @classmethod
106
+ def tearDownClass(cls):
107
+ cls.browser.close()
108
+ cls.p.stop()
109
+
110
+ def setUp(self):
111
+ # clear the cached value for testing
112
+ local.is_percy_enabled.cache_clear()
113
+ local.fetch_percy_dom.cache_clear()
114
+ self.page.goto("http://localhost:8000")
115
+ httpretty.enable()
116
+
117
+ def tearDown(self):
118
+ httpretty.disable()
119
+ httpretty.reset()
120
+
121
+ def test_throws_error_when_a_page_is_not_provided(self):
122
+ with self.assertRaises(Exception):
123
+ percy_snapshot()
124
+
125
+ def test_throws_error_when_a_name_is_not_provided(self):
126
+ with self.assertRaises(Exception):
127
+ percy_snapshot(self.page)
128
+
129
+ def test_disables_snapshots_when_the_healthcheck_fails(self):
130
+ mock_healthcheck(fail=True)
131
+
132
+ with patch("builtins.print") as mock_print:
133
+ percy_snapshot(self.page, "Snapshot 1")
134
+ percy_snapshot(self.page, "Snapshot 2")
135
+
136
+ mock_print.assert_called_with(
137
+ f"{LABEL} Percy is not running, disabling snapshots"
138
+ )
139
+
140
+ self.assertEqual(httpretty.last_request().path, "/percy/healthcheck")
141
+
142
+ def test_disables_snapshots_when_the_healthcheck_version_is_wrong(self):
143
+ mock_healthcheck(fail=True, fail_how="wrong-version")
144
+
145
+ with patch("builtins.print") as mock_print:
146
+ percy_snapshot(self.page, "Snapshot 1")
147
+ percy_snapshot(self.page, "Snapshot 2")
148
+
149
+ mock_print.assert_called_with(
150
+ f"{LABEL} Unsupported Percy CLI version, 2.0.0"
151
+ )
152
+
153
+ self.assertEqual(httpretty.last_request().path, "/percy/healthcheck")
154
+
155
+ def test_disables_snapshots_when_the_healthcheck_version_is_missing(self):
156
+ mock_healthcheck(fail=True, fail_how="no-version")
157
+
158
+ with patch("builtins.print") as mock_print:
159
+ percy_snapshot(self.page, "Snapshot 1")
160
+ percy_snapshot(self.page, "Snapshot 2")
161
+
162
+ mock_print.assert_called_with(
163
+ f"{LABEL} You may be using @percy/agent which is no longer supported by this SDK. "
164
+ "Please uninstall @percy/agent and install @percy/cli instead. "
165
+ "https://www.browserstack.com/docs/percy/migration/migrate-to-cli"
166
+ )
167
+
168
+ self.assertEqual(httpretty.last_request().path, "/percy/healthcheck")
169
+
170
+ def test_posts_snapshots_to_the_local_percy_server(self):
171
+ mock_healthcheck()
172
+ mock_snapshot()
173
+
174
+ percy_snapshot(self.page, "Snapshot 1")
175
+ response = percy_snapshot(self.page, "Snapshot 2", enable_javascript=True)
176
+
177
+ self.assertEqual(httpretty.last_request().path, "/percy/snapshot")
178
+
179
+ s1 = httpretty.latest_requests()[2].parsed_body
180
+ self.assertEqual(s1["name"], "Snapshot 1")
181
+ self.assertEqual(s1["url"], "http://localhost:8000/")
182
+ self.assertEqual(
183
+ s1["dom_snapshot"], "<html><head></head><body>Snapshot Me</body></html>"
184
+ )
185
+ self.assertRegex(s1["client_info"], r"percy-playwright-python/\d+")
186
+ self.assertRegex(s1["environment_info"][0], r"playwright/\d+")
187
+ self.assertRegex(s1["environment_info"][1], r"python/\d+")
188
+
189
+ s2 = httpretty.latest_requests()[3].parsed_body
190
+ self.assertEqual(s2["name"], "Snapshot 2")
191
+ self.assertEqual(s2["enable_javascript"], True)
192
+ self.assertEqual(response, None)
193
+
194
+ def test_posts_snapshots_to_the_local_percy_server_for_sync(self):
195
+ mock_healthcheck()
196
+ mock_snapshot(False, True)
197
+
198
+ percy_snapshot(self.page, "Snapshot 1")
199
+ response = percy_snapshot(
200
+ self.page, "Snapshot 2", enable_javascript=True, sync=True
201
+ )
202
+
203
+ self.assertEqual(httpretty.last_request().path, "/percy/snapshot")
204
+
205
+ s1 = httpretty.latest_requests()[2].parsed_body
206
+ self.assertEqual(s1["name"], "Snapshot 1")
207
+ self.assertEqual(s1["url"], "http://localhost:8000/")
208
+ self.assertEqual(
209
+ s1["dom_snapshot"], "<html><head></head><body>Snapshot Me</body></html>"
210
+ )
211
+ self.assertRegex(s1["client_info"], r"percy-playwright-python/\d+")
212
+ self.assertRegex(s1["environment_info"][0], r"playwright/\d+")
213
+ self.assertRegex(s1["environment_info"][1], r"python/\d+")
214
+
215
+ s2 = httpretty.latest_requests()[3].parsed_body
216
+ self.assertEqual(s2["name"], "Snapshot 2")
217
+ self.assertEqual(s2["enable_javascript"], True)
218
+ self.assertEqual(s2["sync"], True)
219
+ self.assertEqual(response, data_object)
220
+
221
+ mock_healthcheck()
222
+ mock_snapshot()
223
+
224
+ percy_snapshot(self.page, "Snapshot")
225
+
226
+ self.assertEqual(httpretty.last_request().path, "/percy/snapshot")
227
+
228
+ s1 = httpretty.latest_requests()[-1].parsed_body
229
+ self.assertEqual(s1["name"], "Snapshot")
230
+ self.assertEqual(s1["url"], "http://localhost:8000/")
231
+ self.assertEqual(
232
+ s1["dom_snapshot"], "<html><head></head><body>Snapshot Me</body></html>"
233
+ )
234
+
235
+ def test_handles_snapshot_errors(self):
236
+ mock_healthcheck(session_type="web")
237
+ mock_snapshot(fail=True)
238
+
239
+ with patch("builtins.print") as mock_print:
240
+ percy_snapshot(self.page, "Snapshot 1")
241
+
242
+ mock_print.assert_any_call(
243
+ f'{LABEL} Could not take DOM snapshot "Snapshot 1"'
244
+ )
245
+
246
+ def test_raise_error_poa_token_with_snapshot(self):
247
+ mock_healthcheck(session_type="automate")
248
+
249
+ with self.assertRaises(Exception) as context:
250
+ percy_snapshot(self.page, "Snapshot 1")
251
+
252
+ self.assertEqual(
253
+ "Invalid function call - "
254
+ "percy_snapshot(). Please use percy_screenshot() "
255
+ "function while using Percy with Automate."
256
+ " For more information on usage of PercyScreenshot, refer https://www.browserstack.com/"
257
+ "docs/percy/integrate/functional-and-visual",
258
+ str(context.exception),
259
+ )
260
+
261
+
262
+ class TestPercyFunctions(unittest.TestCase):
263
+ @patch("requests.get")
264
+ def test_is_percy_enabled(self, mock_get):
265
+ # Mock successful health check
266
+ mock_get.return_value.status_code = 200
267
+ mock_get.return_value.json.return_value = {"success": True, "type": "web"}
268
+ mock_get.return_value.headers = {"x-percy-core-version": "1.0.0"}
269
+
270
+ self.assertEqual(is_percy_enabled(), "web")
271
+
272
+ # Clear the cache to test the unsuccessful scenario
273
+ is_percy_enabled.cache_clear()
274
+
275
+ # Mock unsuccessful health check
276
+ mock_get.return_value.json.return_value = {"success": False, "error": "error"}
277
+ self.assertFalse(is_percy_enabled())
278
+
279
+ @patch("requests.get")
280
+ def test_fetch_percy_dom(self, mock_get):
281
+ # Mock successful fetch of dom.js
282
+ mock_get.return_value.status_code = 200
283
+ mock_get.return_value.text = "some_js_code"
284
+
285
+ self.assertEqual(fetch_percy_dom(), "some_js_code")
286
+
287
+ @patch("requests.post")
288
+ @patch("percy.screenshot.fetch_percy_dom")
289
+ @patch("percy.screenshot.is_percy_enabled")
290
+ def test_percy_snapshot(
291
+ self, mock_is_percy_enabled, mock_fetch_percy_dom, mock_post
292
+ ):
293
+ # Mock Percy enabled
294
+ mock_is_percy_enabled.return_value = "web"
295
+ mock_fetch_percy_dom.return_value = "some_js_code"
296
+ page = MagicMock()
297
+ page.evaluate.side_effect = [
298
+ "dom_snapshot",
299
+ json.dumps({"hashed_id": "session-id"}),
300
+ ]
301
+ page.url = "http://example.com"
302
+ mock_post.return_value.status_code = 200
303
+ mock_post.return_value.json.return_value = {
304
+ "success": True,
305
+ "data": "snapshot_data",
306
+ }
307
+
308
+ # Call the function
309
+ result = percy_snapshot(page, "snapshot_name")
310
+
311
+ # Check the results
312
+ self.assertEqual(result, "snapshot_data")
313
+ mock_post.assert_called_once()
314
+
315
+ @patch("requests.post")
316
+ @patch("percy.screenshot.is_percy_enabled")
317
+ def test_percy_automate_screenshot(self, mock_is_percy_enabled, mock_post):
318
+ # Mock Percy enabled for automate
319
+ is_percy_enabled.cache_clear()
320
+ mock_is_percy_enabled.return_value = "automate"
321
+ page = MagicMock()
322
+
323
+ page._impl_obj._guid = "page@abc"
324
+ page.main_frame._impl_obj._guid = "frame@abc"
325
+ page.context.browser._impl_obj._guid = "browser@abc"
326
+ page.evaluate.return_value = '{"hashed_id": "session_id"}'
327
+
328
+ # Mock the response for the POST request
329
+ mock_post.return_value.status_code = 200
330
+ mock_post.return_value.json.return_value = {
331
+ "success": True,
332
+ "data": "screenshot_data",
333
+ }
334
+
335
+ # Call the function
336
+ result = percy_automate_screenshot(page, "screenshot_name")
337
+
338
+ # Assertions
339
+ self.assertEqual(result, "screenshot_data")
340
+ mock_post.assert_called_once_with(
341
+ "http://localhost:5338/percy/automateScreenshot",
342
+ json={
343
+ "client_info": f"percy-playwright-python/{SDK_VERSION}",
344
+ "environment_info": [
345
+ f"playwright/{PLAYWRIGHT_VERSION}",
346
+ f"python/{platform.python_version()}",
347
+ ],
348
+ "sessionId": "session_id",
349
+ "pageGuid": "page@abc",
350
+ "frameGuid": "frame@abc",
351
+ "framework": "playwright",
352
+ "snapshotName": "screenshot_name",
353
+ "options": {},
354
+ },
355
+ timeout=600,
356
+ )
357
+
358
+ @patch("percy.screenshot.is_percy_enabled")
359
+ def test_percy_automate_screenshot_invalid_call(self, mock_is_percy_enabled):
360
+ # Mock Percy enabled for web
361
+ mock_is_percy_enabled.return_value = "web"
362
+ page = MagicMock()
363
+
364
+ # Call the function and expect an exception
365
+ with self.assertRaises(Exception) as context:
366
+ percy_automate_screenshot(page, "screenshot_name")
367
+
368
+ self.assertTrue("Invalid function call" in str(context.exception))
369
+
370
+
371
+ if __name__ == "__main__":
372
+ unittest.main()