Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
29 changes: 22 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ python -m playwright install
| `ciBuildId` | `VRT_CIBUILDID` | none | CI build ID, such as the commit SHA; runs with the same ID share a build |
| `enableSoftAssert` | `VRT_ENABLESOFTASSERT` | `false` | Log a failed check instead of raising `TestRunError` |
| `httpTimeoutInSeconds` | `VRT_HTTPTIMEOUTINSECONDS` | `360` | How long to wait for the server's answer, `0` for no limit. Connecting is limited to 10 s. |
| `sourceBranch` | `VRT_SOURCEBRANCH` | the project's main branch | Branch whose baselines `branchName` is compared with, such as the target branch of a pull request |

The server compares images while it answers `track()`. With VLM checks or a busy comparison queue that can
take minutes, so keep `httpTimeoutInSeconds` above 300.
Expand Down Expand Up @@ -55,7 +56,8 @@ override it. `Config.default(path)` reads another file.
"branchName": "develop",
"ciBuildId": "commit_sha",
"enableSoftAssert": false,
"httpTimeoutInSeconds": 360
"httpTimeoutInSeconds": 360,
"sourceBranch": "main"
}
```

Expand All @@ -67,6 +69,7 @@ VRT_BRANCHNAME="develop" \
VRT_CIBUILDID="commit_sha" \
VRT_ENABLESOFTASSERT=true \
VRT_HTTPTIMEOUTINSECONDS=360 \
VRT_SOURCEBRANCH="main" \
python -m pytest
```

Expand All @@ -91,10 +94,17 @@ Without the context manager, call `build = vrt.start()` (returns `Build(id, proj
| Field | Meaning |
|---|---|
| `name` | Required. Name of the image in VRT |
| `imageBase64` | Required. The PNG image, base64 encoded |
| `os`, `browser`, `viewport`, `device` | Optional. Together with `name` they pick the baseline |
| `imageBase64` | The PNG image, base64 encoded, sent as JSON |
| `imagePath` | Or: path of a PNG file, sent as multipart |
| `imageBuffer` | Or: the PNG image as `bytes`, sent as multipart |
| `os`, `browser`, `viewport`, `device`, `customTags` | Optional. Together with `name` they pick the baseline |
| `diffTollerancePercent` | Optional. Allowed difference in %, default 0. The spelling is the API's. |
| `ignoreAreas` | Optional. List of `IgnoreArea(x, y, width, height)` in pixels |
| `comment` | Optional. Shown with the test run in the VRT app |
| `merge` | Optional. `True`: approving this run on a feature branch updates the baseline of the branch it was compared with |

Set exactly one of the image fields. Multipart avoids the server's JSON size limit (`BODY_PARSER_JSON_LIMIT`)
and the base64 overhead, so prefer it for large screenshots.

### Playwright

Expand All @@ -112,15 +122,15 @@ with sync_playwright() as playwright:
vrt.trackElementHandle(page.query_selector('#touchnav-wrapper'), 'Navigation')
```

`browser` is the Playwright browser name and `viewport` the page's viewport size. The async API has
The screenshot is sent as multipart. `browser` is the Playwright browser name and `viewport` the page's viewport size. The async API has
`trackPageAsync` and `trackElementHandleAsync`.

The last argument is optional: `PageTrackOptions` for `trackPage`, `ElementHandleTrackOptions` for
`trackElementHandle`. Both take:

- `diffTollerancePercent` and `ignoreAreas`, as in `TestRun`;
- `agent: Agent(os, device, viewport)`: baseline fields Playwright doesn't know. `viewport` is used only for
element handles;
- `diffTollerancePercent`, `ignoreAreas`, `customTags`, `comment` and `merge`, as in `TestRun`;
- `agent: Agent(os, device, viewport)`: baseline fields Playwright doesn't know. `viewport` is used for
element handles and for pages without a viewport size;
- `screenshotOptions`: arguments for Playwright's `screenshot()`. `PageScreenshotOptions` takes
`full_page`, `clip`, `omit_background`, `timeout`, `type`, `quality` and `path`.
`ElementHandleScreenshotOptions` takes the same, without `full_page` and `clip`.
Expand Down Expand Up @@ -158,6 +168,11 @@ All errors derive from `VisualRegressionTrackerError`.
| `MissingConfigurationError` | A required option is not set. Has the `field`. |
| `InvalidConfigurationError` | An option has an invalid value. Has the `field`. |
| `VisualRegressionTrackerError` | `track` or `stop` before `start`. |
| `ValueError` | A `TestRun` with more than one image field. |

When the server answers 503 with a `Retry-After` of at most 60 seconds (the comparison queue is full, or the
server is restarting), the SDK waits that long and sends the request again, up to 3 times, logging a warning
each time. After that, or without a usable `Retry-After`, it raises `ServerError`.

## Development

Expand Down
8 changes: 8 additions & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def config_file(tmpdir):
'apiKey': 'file api key',
'enableSoftAssert': True,
'httpTimeoutInSeconds': 5,
'sourceBranch': 'file source branch',
}))
yield p

Expand All @@ -33,6 +34,7 @@ def config_env():
'VRT_APIKEY': 'env api key',
'VRT_ENABLESOFTASSERT': 'False',
'VRT_HTTPTIMEOUTINSECONDS': '7.5',
'VRT_SOURCEBRANCH': 'env source branch',
}
yield env

Expand All @@ -46,6 +48,7 @@ def test_config_from_file(config_file):
assert cfg.apiKey == 'file api key'
assert cfg.enableSoftAssert == True
assert cfg.httpTimeoutInSeconds == 5
assert cfg.sourceBranch == 'file source branch'


def test_config_from_environment(config_env):
Expand All @@ -57,6 +60,7 @@ def test_config_from_environment(config_env):
assert cfg.apiKey == 'env api key'
assert cfg.enableSoftAssert == False
assert cfg.httpTimeoutInSeconds == 7.5
assert cfg.sourceBranch == 'env source branch'


def config_from_file(tmp_path, data):
Expand All @@ -69,6 +73,10 @@ def test_config_timeout_defaults_to_360_seconds():
assert Config().httpTimeoutInSeconds == 360


def test_config_has_no_source_branch_by_default():
assert Config().sourceBranch is None


def test_config_treats_empty_timeout_variable_as_unset():
assert Config.from_environment({'VRT_HTTPTIMEOUTINSECONDS': ''}).httpTimeoutInSeconds == 360

Expand Down
29 changes: 16 additions & 13 deletions tests/test_playwright.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import pytest
import base64
import binascii
import subprocess
import sys
import visual_regression_tracker
Expand Down Expand Up @@ -94,13 +92,14 @@ def test_pvrt_trackPage__default_options(pvrt, sync_page, sync_browserType):

testRun = pvrt.track.call_args[0][0]
assert testRun.name == 'image name'
assert is_base64(testRun.imageBase64) and len(testRun.imageBase64) > 1024
assert testRun.imageBase64 is None and is_image(testRun.imageBuffer)
assert testRun.os is None
assert testRun.browser == sync_browserType.name
assert testRun.viewport == '1024x768'
assert testRun.device is None
assert testRun.diffTollerancePercent is None
assert testRun.ignoreAreas is None
assert (testRun.customTags, testRun.comment, testRun.merge) == (None, None, None)


def test_pvrt_trackPage(pvrt, sync_page, sync_browserType):
Expand All @@ -120,6 +119,9 @@ def test_pvrt_trackPage(pvrt, sync_page, sync_browserType):
omit_background=False,
full_page=True,
),
customTags='tag',
comment='why',
merge=True,
agent=visual_regression_tracker.playwright.Agent(
os='os',
device='device',
Expand All @@ -132,12 +134,13 @@ def test_pvrt_trackPage(pvrt, sync_page, sync_browserType):

testRun = pvrt.track.call_args[0][0]
assert testRun.name == 'image name'
assert is_base64(testRun.imageBase64) and len(testRun.imageBase64) > 1024
assert testRun.imageBase64 is None and is_image(testRun.imageBuffer)
assert testRun.os == 'os'
assert testRun.browser == sync_browserType.name
assert testRun.viewport == '1024x768'
assert testRun.device == 'device'
assert testRun.diffTollerancePercent == 10
assert (testRun.customTags, testRun.comment, testRun.merge) == ('tag', 'why', True)
assert testRun.ignoreAreas == [
visual_regression_tracker.types.IgnoreArea(1, 2, 3, 4),
]
Expand All @@ -156,13 +159,14 @@ def test_pvrt_trackElementHandle__default_options(pvrt, sync_elementHandle, sync

testRun = pvrt.track.call_args[0][0]
assert testRun.name == 'image name'
assert is_base64(testRun.imageBase64) and len(testRun.imageBase64) > 1024
assert testRun.imageBase64 is None and is_image(testRun.imageBuffer)
assert testRun.os is None
assert testRun.browser == sync_browserType.name
assert testRun.viewport is None
assert testRun.device is None
assert testRun.diffTollerancePercent is None
assert testRun.ignoreAreas is None
assert (testRun.customTags, testRun.comment, testRun.merge) == (None, None, None)


def test_pvrt_trackElementHandle(pvrt, sync_elementHandle, sync_browserType):
Expand All @@ -181,6 +185,9 @@ def test_pvrt_trackElementHandle(pvrt, sync_elementHandle, sync_browserType):
quality=100,
omit_background=False,
),
customTags='tag',
comment='why',
merge=True,
agent=visual_regression_tracker.playwright.Agent(
os='os',
device='device',
Expand All @@ -193,12 +200,13 @@ def test_pvrt_trackElementHandle(pvrt, sync_elementHandle, sync_browserType):

testRun = pvrt.track.call_args[0][0]
assert testRun.name == 'image name'
assert is_base64(testRun.imageBase64) and len(testRun.imageBase64) > 1024
assert testRun.imageBase64 is None and is_image(testRun.imageBuffer)
assert testRun.os == 'os'
assert testRun.browser == sync_browserType.name
assert testRun.viewport == '123x345'
assert testRun.device == 'device'
assert testRun.diffTollerancePercent == 10
assert (testRun.customTags, testRun.comment, testRun.merge) == ('tag', 'why', True)
assert testRun.ignoreAreas == [
visual_regression_tracker.types.IgnoreArea(1, 2, 3, 4),
]
Expand All @@ -209,10 +217,5 @@ def sync_elementHandle(sync_page):
return sync_page.query_selector('form')


def is_base64(val):
try:
assert isinstance(val, str)
base64.b64decode(val)
return True
except binascii.Error:
return False
def is_image(val):
return isinstance(val, bytes) and len(val) > 1024
29 changes: 16 additions & 13 deletions tests/test_playwright_async.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import base64
import binascii
import pytest
import pytest_asyncio
import visual_regression_tracker
Expand Down Expand Up @@ -68,13 +66,14 @@ async def test_pvrt_trackPageAsync__default_options(pvrt, async_page, async_brow

testRun = pvrt.track.call_args[0][0]
assert testRun.name == 'image name'
assert is_base64(testRun.imageBase64) and len(testRun.imageBase64) > 1024
assert testRun.imageBase64 is None and is_image(testRun.imageBuffer)
assert testRun.os is None
assert testRun.browser == async_browserType.name
assert testRun.viewport == '1024x768'
assert testRun.device is None
assert testRun.diffTollerancePercent is None
assert testRun.ignoreAreas is None
assert (testRun.customTags, testRun.comment, testRun.merge) == (None, None, None)


@pytest.mark.asyncio(loop_scope='session')
Expand All @@ -95,6 +94,9 @@ async def test_pvrt_trackPageAsync(pvrt, async_page, async_browserType):
omit_background=False,
full_page=True,
),
customTags='tag',
comment='why',
merge=True,
agent=visual_regression_tracker.playwright.Agent(
os='os',
device='device',
Expand All @@ -107,12 +109,13 @@ async def test_pvrt_trackPageAsync(pvrt, async_page, async_browserType):

testRun = pvrt.track.call_args[0][0]
assert testRun.name == 'image name'
assert is_base64(testRun.imageBase64) and len(testRun.imageBase64) > 1024
assert testRun.imageBase64 is None and is_image(testRun.imageBuffer)
assert testRun.os == 'os'
assert testRun.browser == async_browserType.name
assert testRun.viewport == '1024x768'
assert testRun.device == 'device'
assert testRun.diffTollerancePercent == 10
assert (testRun.customTags, testRun.comment, testRun.merge) == ('tag', 'why', True)
assert testRun.ignoreAreas == [
visual_regression_tracker.types.IgnoreArea(1, 2, 3, 4),
]
Expand All @@ -133,13 +136,14 @@ async def test_pvrt_trackElementHandleAsync__default_options(pvrt, async_element

testRun = pvrt.track.call_args[0][0]
assert testRun.name == 'image name'
assert is_base64(testRun.imageBase64) and len(testRun.imageBase64) > 1024
assert testRun.imageBase64 is None and is_image(testRun.imageBuffer)
assert testRun.os is None
assert testRun.browser == async_browserType.name
assert testRun.viewport is None
assert testRun.device is None
assert testRun.diffTollerancePercent is None
assert testRun.ignoreAreas is None
assert (testRun.customTags, testRun.comment, testRun.merge) == (None, None, None)


@pytest.mark.asyncio(loop_scope='session')
Expand All @@ -159,6 +163,9 @@ async def test_pvrt_trackElementHandleAsync(pvrt, async_elementHandle, async_bro
quality=100,
omit_background=False,
),
customTags='tag',
comment='why',
merge=True,
agent=visual_regression_tracker.playwright.Agent(
os='os',
device='device',
Expand All @@ -171,21 +178,17 @@ async def test_pvrt_trackElementHandleAsync(pvrt, async_elementHandle, async_bro

testRun = pvrt.track.call_args[0][0]
assert testRun.name == 'image name'
assert is_base64(testRun.imageBase64) and len(testRun.imageBase64) > 1024
assert testRun.imageBase64 is None and is_image(testRun.imageBuffer)
assert testRun.os == 'os'
assert testRun.browser == async_browserType.name
assert testRun.viewport == '123x345'
assert testRun.device == 'device'
assert testRun.diffTollerancePercent == 10
assert (testRun.customTags, testRun.comment, testRun.merge) == ('tag', 'why', True)
assert testRun.ignoreAreas == [
visual_regression_tracker.types.IgnoreArea(1, 2, 3, 4),
]


def is_base64(val):
try:
assert isinstance(val, str)
base64.b64decode(val)
return True
except binascii.Error:
return False
def is_image(val):
return isinstance(val, bytes) and len(val) > 1024
Loading
Loading