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.
- percy_playwright-1.0.0/LICENSE +21 -0
- percy_playwright-1.0.0/PKG-INFO +164 -0
- percy_playwright-1.0.0/README.md +141 -0
- percy_playwright-1.0.0/percy/__init__.py +22 -0
- percy_playwright-1.0.0/percy/cache.py +42 -0
- percy_playwright-1.0.0/percy/page_metadata.py +44 -0
- percy_playwright-1.0.0/percy/screenshot.py +168 -0
- percy_playwright-1.0.0/percy/version.py +1 -0
- percy_playwright-1.0.0/percy_playwright.egg-info/PKG-INFO +164 -0
- percy_playwright-1.0.0/percy_playwright.egg-info/SOURCES.txt +17 -0
- percy_playwright-1.0.0/percy_playwright.egg-info/dependency_links.txt +1 -0
- percy_playwright-1.0.0/percy_playwright.egg-info/not-zip-safe +1 -0
- percy_playwright-1.0.0/percy_playwright.egg-info/requires.txt +2 -0
- percy_playwright-1.0.0/percy_playwright.egg-info/top_level.txt +1 -0
- percy_playwright-1.0.0/setup.cfg +4 -0
- percy_playwright-1.0.0/setup.py +41 -0
- percy_playwright-1.0.0/tests/test_cache.py +63 -0
- percy_playwright-1.0.0/tests/test_page_metadata.py +51 -0
- percy_playwright-1.0.0/tests/test_screenshot.py +372 -0
|
@@ -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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
percy
|
|
@@ -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()
|