Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat]: Percy Playwright Python Support #1

Merged
merged 11 commits into from
Jun 17, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,6 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

node_modules
yarn.lock
14 changes: 14 additions & 0 deletions .pylintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[MASTER]
fail-under=10.0
disable=
broad-except,
broad-exception-raised,
global-statement,
invalid-name,
missing-class-docstring,
missing-function-docstring,
missing-module-docstring,
multiple-statements,
no-member,
no-value-for-parameter,
redefined-builtin,
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.10.3
34 changes: 34 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
VENV=.venv/bin
REQUIREMENTS=$(wildcard requirements.txt development.txt)
MARKER=.initialized-with-makefile
VENVDEPS=$(REQUIREMENTS setup.py)

$(VENV):
python3 -m venv .venv
$(VENV)/python -m pip install --upgrade pip setuptools wheel
yarn

$(VENV)/$(MARKER): $(VENVDEPS) | $(VENV)
$(VENV)/pip install $(foreach path,$(REQUIREMENTS),-r $(path))
touch $(VENV)/$(MARKER)

# .PHONY: venv lint test clean build release

venv: $(VENV)/$(MARKER)

lint: venv
$(VENV)/pylint percy/* tests/*

test: venv
$(VENV)/python -m unittest tests.test_screenshot
$(VENV)/python -m unittest tests.test_cache
$(VENV)/python -m unittest tests.test_page_metadata

clean:
rm -rf $$(cat .gitignore)

build: venv
$(VENV)/python setup.py sdist bdist_wheel

# release: build
# $(VENV)/twine upload dist/* --username __token__ --password ${PYPI_TOKEN}
89 changes: 89 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,90 @@
# Percy playwright python
![Test](https://github.com/percy/percy-playwright-python/workflows/Test/badge.svg)
Amit3200 marked this conversation as resolved.
Show resolved Hide resolved

[Percy](https://percy.io) visual testing for Python Playwright.

## Installation

npm install `@percy/cli`:

```sh-session
$ npm install --save-dev @percy/cli
```

pip install Percy playwright package:

```ssh-session
$ pip install percy-playwright
```

## Usage

This is an example test using the `percy_snapshot` function.

``` python
from percy import percy_snapshot

browser = webdriver.Firefox()
browser.get('http://example.com')
Amit3200 marked this conversation as resolved.
Show resolved Hide resolved
# take a snapshot
percy_snapshot(browser, 'Python example')
```

Running the test above normally will result in the following log:

```sh-session
[percy] Percy is not running, disabling snapshots
```

When running with [`percy
exec`](https://github.com/percy/cli/tree/master/packages/cli-exec#percy-exec), and your project's
`PERCY_TOKEN`, a new Percy build will be created and snapshots will be uploaded to your project.

```sh-session
$ export PERCY_TOKEN=[your-project-token]
$ percy exec -- [python test command]
[percy] Percy has started!
[percy] Created build #1: https://percy.io/[your-project]
[percy] Snapshot taken "Python example"
[percy] Stopping percy...
[percy] Finalized build #1: https://percy.io/[your-project]
[percy] Done!
```

## Configuration

`percy_snapshot(driver, name[, **kwargs])`

- `page` (**required**) - A playwright page instance
- `name` (**required**) - The snapshot name; must be unique to each snapshot
- `**kwargs` - [See per-snapshot configuration options](https://docs.percy.io/docs/cli-configuration#per-snapshot-configuration)


## Percy on Automate

## Usage

``` python
from playwright.sync_api import sync_playwright
from percy import percy_screenshot, percy_snapshot

desired_cap = {
'browser': 'chrome',
'browser_version': 'latest',
'os': 'osx',
'os_version': 'ventura',
'name': 'Percy Playwright PoA Demo',
'build': 'percy-playwright-python-tutorial',
'browserstack.username': 'username',
'browserstack.accessKey': 'accesskey'
}

with sync_playwright() as playwright:
desired_caps = {}
cdpUrl = 'wss://cdp.browserstack.com/playwright?caps=' + urllib.parse.quote(json.dumps(desired_cap))
browser = playwright.chromium.connect(cdpUrl)
page = browser.new_page()
page.goto("https://percy.io/")
percy_screenshot(page, name = "Screenshot 1")
```
Amit3200 marked this conversation as resolved.
Show resolved Hide resolved
3 changes: 3 additions & 0 deletions development.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
httpretty==1.0.*
pylint==2.*
twine
22 changes: 22 additions & 0 deletions percy/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from percy.version import __version__
from percy.screenshot import percy_automate_screenshot

# import snapshot command
try:
from percy.screenshot import percy_snapshot
except ImportError:

def percy_snapshot(driver, *a, **kw):
raise ModuleNotFoundError(
"[percy] `percy-playwright-python` package is not installed, "
"please install it to use percy_snapshot command"
)


# for better backwards compatibility
def percySnapshot(browser, *a, **kw):
return percy_snapshot(driver=browser, *a, **kw)


def percy_screenshot(page, *a, **kw):
return percy_automate_screenshot(page, *a, **kw)
42 changes: 42 additions & 0 deletions percy/cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import time


class Cache:
CACHE = {}
CACHE_TIMEOUT = 5 * 60 # 300 seconds
TIMEOUT_KEY = "last_access_time"

# Caching Keys
session_details = "session_details"

@classmethod
def check_types(cls, session_id, property):
if not isinstance(session_id, str):
raise TypeError("Argument session_id should be string")
if not isinstance(property, str):
raise TypeError("Argument property should be string")

@classmethod
def set_cache(cls, session_id, property, value):
cls.check_types(session_id, property)
session = cls.CACHE.get(session_id, {})
session[cls.TIMEOUT_KEY] = time.time()
session[property] = value
cls.CACHE[session_id] = session

@classmethod
def get_cache(cls, session_id, property):
cls.cleanup_cache()
cls.check_types(session_id, property)
session = cls.CACHE.get(session_id, {})
return session.get(property, None)

@classmethod
def cleanup_cache(cls):
now = time.time()
for session_id, session in cls.CACHE.items():
timestamp = session[cls.TIMEOUT_KEY]
if now - timestamp >= cls.CACHE_TIMEOUT:
cls.CACHE[session_id] = {
cls.session_details: session[cls.session_details]
}
44 changes: 44 additions & 0 deletions percy/page_metadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# pylint: disable=protected-access
import json
from percy.cache import Cache


class PageMetaData:
def __init__(self, page):
self.page = page

def __fetch_guid(self, obj):
return obj._impl_obj._guid

@property
def framework(self):
return "playwright"

@property
def page_guid(self):
return self.__fetch_guid(self.page)

@property
def frame_guid(self):
return self.__fetch_guid(self.page.main_frame)

@property
def browser_guid(self):
return self.__fetch_guid(self.page.context.browser)

@property
def session_details(self):
session_details = Cache.get_cache(self.browser_guid, Cache.session_details)
if session_details is None:
session_details = json.loads(
self.page.evaluate(
"_ => {}", 'browserstack_executor: {"action": "getSessionDetails"}'
)
)
Cache.set_cache(self.browser_guid, Cache.session_details, session_details)
return session_details
return session_details

@property
def automate_session_id(self):
return self.session_details.get("hashed_id")
Loading