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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.21.0] - 2026-02-13

### Changed
- Refactored browser configuration: `browsers` is now a map where keys are user-defined aliases and values specify the browser `name` and `args`. Scenarios reference browsers by alias, allowing multiple configurations of the same browser engine
- Added `defaultBrowsers` field to specify which browser aliases to use when a scenario doesn't define its own

## [0.20.2] - 2026-02-12

### Fixed
Expand Down Expand Up @@ -146,6 +152,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Initial release

[0.21.0]: https://github.com/gooddata/gooddata-neobackstop/compare/v0.20.2...v0.21.0
[0.20.2]: https://github.com/gooddata/gooddata-neobackstop/compare/v0.20.1...v0.20.2
[0.20.1]: https://github.com/gooddata/gooddata-neobackstop/compare/v0.20.0...v0.20.1
[0.20.0]: https://github.com/gooddata/gooddata-neobackstop/compare/v0.19.1...v0.20.0
Expand Down
212 changes: 126 additions & 86 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,15 +126,19 @@ func main() {

// Convert to internal format
internalScenarios := converters.ScenariosToInternal(
cfg.Browsers, cfg.Viewports, cfg.RetryCount, scenarios,
cfg.DefaultBrowsers, cfg.Viewports, cfg.RetryCount, scenarios,
)

// Install and run Playwright
browsers := make([]string, len(cfg.Browsers))
for i, b := range cfg.Browsers {
browsers[i] = string(b)
// Grab unique browsers to install, from the browser alias map
browsers := map[string]interface{}{}
for _, b := range cfg.Browsers {
browsers[string(b.Name)] = nil
}
playwright.Install(&playwright.RunOptions{Browsers: browsers})

// Install and run Playwright
playwright.Install(&playwright.RunOptions{
Browsers: slices.Collect(maps.Keys(browsers)),
})
pw, _ := playwright.Run()

// Set up worker pool for screenshots
Expand Down Expand Up @@ -192,7 +196,7 @@ func main() {
json.Unmarshal(scenariosBytes, &scenarios)

internalScenarios := converters.ScenariosToInternal(
cfg.Browsers, cfg.Viewports, cfg.RetryCount, scenarios,
cfg.DefaultBrowsers, cfg.Viewports, cfg.RetryCount, scenarios,
)

// Find a specific scenario to debug
Expand All @@ -208,7 +212,7 @@ func main() {
pw, _ := playwright.Run()
browser, _ := pw.Chromium.Launch(playwright.BrowserTypeLaunchOptions{
Headless: playwright.Bool(false), // Show the browser!
Args: cfg.Args["chromium"],
Args: cfg.Browsers["chromium"].Args,
})

context, _ := browser.NewContext(playwright.BrowserNewContextOptions{
Expand All @@ -226,8 +230,7 @@ func main() {
}()

// Run the screenshot job with debug mode enabled
screenshotter.Job("debug |", "./debug-output", debugScenario.Viewport.Label,
page, *debugScenario, results, true, "test", cfg) // true = debug mode
screenshotter.Job("debug |", "./debug-output", debugScenario.Viewport.Label, page, *debugScenario, results, true, "test", cfg) // true = debug mode

browser.Close()
pw.Stop()
Expand Down Expand Up @@ -261,7 +264,54 @@ The main configuration file controls browser settings, viewports, output paths,
```json
{
"id": "my-visual-tests",
"browsers": ["chromium", "firefox"],
"browsers": {
"chromium": {
"name": "chromium",
"args": [
"--disable-infobars",
"--disable-background-networking",
"--disable-background-timer-throttling",
"--disable-backgrounding-occluded-windows",
"--disable-breakpad",
"--disable-client-side-phishing-detection",
"--disable-default-apps",
"--disable-dev-shm-usage",
"--disable-extensions",
"--disable-features=site-per-process",
"--disable-hang-monitor",
"--disable-ipc-flooding-protection",
"--disable-popup-blocking",
"--disable-prompt-on-repost",
"--disable-renderer-backgrounding",
"--disable-sync",
"--disable-translate",
"--metrics-recording-only",
"--no-first-run",
"--safebrowsing-disable-auto-update",
"--enable-automation",
"--disable-component-update",
"--disable-web-resource",
"--mute-audio",
"--no-sandbox",
"--disable-software-rasterizer",
"--disable-gpu",
"--disable-setuid-sandbox",
"--force-device-scale-factor=1"
]
},
"firefox": {
"name": "firefox",
"args": [
"--disable-dev-shm-usage",
"--disable-extensions",
"--enable-automation",
"--mute-audio",
"--no-sandbox",
"--disable-gpu"
]
}
},
"defaultBrowsers": ["chromium", "firefox"],
"viewports": [
{
"label": "desktop",
Expand All @@ -281,47 +331,6 @@ The main configuration file controls browser settings, viewports, output paths,
"showSuccessfulTests": false
},
"ciReportPath": "./output/ci-report",
"args": {
"chromium": [
"--disable-infobars",
"--disable-background-networking",
"--disable-background-timer-throttling",
"--disable-backgrounding-occluded-windows",
"--disable-breakpad",
"--disable-client-side-phishing-detection",
"--disable-default-apps",
"--disable-dev-shm-usage",
"--disable-extensions",
"--disable-features=site-per-process",
"--disable-hang-monitor",
"--disable-ipc-flooding-protection",
"--disable-popup-blocking",
"--disable-prompt-on-repost",
"--disable-renderer-backgrounding",
"--disable-sync",
"--disable-translate",
"--metrics-recording-only",
"--no-first-run",
"--safebrowsing-disable-auto-update",
"--enable-automation",
"--disable-component-update",
"--disable-web-resource",
"--mute-audio",
"--no-sandbox",
"--disable-software-rasterizer",
"--disable-gpu",
"--disable-setuid-sandbox",
"--force-device-scale-factor=1"
],
"firefox": [
"--disable-dev-shm-usage",
"--disable-extensions",
"--enable-automation",
"--mute-audio",
"--no-sandbox",
"--disable-gpu"
]
},
"asyncCaptureLimit": 2,
"asyncCompareLimit": 6,
"retryCount": 0
Expand All @@ -330,20 +339,51 @@ The main configuration file controls browser settings, viewports, output paths,

#### Configuration Options

| Option | Type | Description |
|----------------------------------|------------|--------------------------------------------|
| `id` | string | Identifier for the test suite |
| `browsers` | string[] | Browsers to use: `"chromium"`, `"firefox"` |
| `viewports` | Viewport[] | List of viewport configurations |
| `bitmapsReferencePath` | string | Path to store reference screenshots |
| `bitmapsTestPath` | string | Path to store test screenshots |
| `htmlReport.path` | string | Path for HTML report output |
| `htmlReport.showSuccessfulTests` | boolean | Include passing tests in HTML report |
| `ciReportPath` | string | Path for CI JSON report |
| `args` | object | Browser-specific launch arguments |
| `asyncCaptureLimit` | number | Max concurrent screenshot captures |
| `asyncCompareLimit` | number | Max concurrent image comparisons |
| `retryCount` | number | Extra retries on mismatch in test mode |
| Option | Type | Description |
|----------------------------------|-----------------------------|----------------------------------------------------------------|
| `id` | string | Identifier for the test suite |
| `browsers` | map\<string, BrowserConfig> | Browser alias map (see [Browser Aliases](#browser-aliases)) |
| `defaultBrowsers` | string[] | Browser aliases to use when a scenario doesn't specify its own |
| `viewports` | Viewport[] | List of viewport configurations |
| `bitmapsReferencePath` | string | Path to store reference screenshots |
| `bitmapsTestPath` | string | Path to store test screenshots |
| `htmlReport.path` | string | Path for HTML report output |
| `htmlReport.showSuccessfulTests` | boolean | Include passing tests in HTML report |
| `ciReportPath` | string | Path for CI JSON report |
| `asyncCaptureLimit` | number | Max concurrent screenshot captures |
| `asyncCompareLimit` | number | Max concurrent image comparisons |
| `retryCount` | number | Extra retries on mismatch in test mode |

#### Browser Aliases

A browser alias is a named configuration that pairs a browser type (`"chromium"` or `"firefox"`) with a set of launch arguments. The key in the `browsers` map is the alias, and the value specifies the browser `name` and `args`.

Aliases are used as prefixes in screenshot file names, so they **must be snake_case**.

If you only need one configuration per browser type, the recommended convention is to use the browser name itself as the alias (e.g. `"chromium"` for a Chromium config, `"firefox"` for a Firefox config).

When you need multiple configurations of the same browser engine (e.g. Chromium with different flags), use descriptive aliases:

```json
{
"browsers": {
"chromium_default": {
"name": "chromium",
"args": ["--no-sandbox", "--disable-gpu"]
},
"chromium_hidpi": {
"name": "chromium",
"args": ["--no-sandbox", "--force-device-scale-factor=2"]
}
},
"defaultBrowsers": ["chromium_default"]
}
```

| BrowserConfig Property | Type | Description |
|------------------------|----------|------------------------------------------------|
| `name` | string | Browser engine: `"chromium"` or `"firefox"` |
| `args` | string[] | Launch arguments passed to the browser |

#### Viewport Configuration

Expand Down Expand Up @@ -378,25 +418,25 @@ Defines the test scenarios - which pages to capture and how to interact with the

#### Scenario Options

| Option | Type | Description |
|-----------------------|------------------|---------------------------------------------------|
| `id` | string | Unique identifier for the scenario |
| `label` | string | Human-readable label (used in reports) |
| `url` | string | URL to navigate to |
| `browsers` | string[] | Override global browsers for this scenario |
| `viewports` | Viewport[] | Override global viewports for this scenario |
| `readySelector` | string | CSS selector to wait for before capture |
| `reloadAfterReady` | boolean | Reload page after ready selector appears |
| `delay` | number \| object | Wait time after ready (see below) |
| `keyPressSelector` | object | Element to focus and key to press |
| `hoverSelector` | string | Single element to hover over |
| `hoverSelectors` | array | Multiple elements to hover in sequence |
| `clickSelector` | string | Single element to click |
| `clickSelectors` | array | Multiple elements to click in sequence |
| `postInteractionWait` | string \| number | Wait after interactions (selector or ms) |
| `scrollToSelector` | string | Element to scroll into view |
| `misMatchThreshold` | number | Allowed mismatch percentage (0-100) |
| `retryCount` | number | Extra retries for the scenario (overrides global) |
| Option | Type | Description |
|-----------------------|------------------|----------------------------------------------------------------|
| `id` | string | Unique identifier for the scenario |
| `label` | string | Human-readable label (used in reports) |
| `url` | string | URL to navigate to |
| `browsers` | string[] | Override `defaultBrowsers` for this scenario (browser aliases) |
| `viewports` | Viewport[] | Override global viewports for this scenario |
| `readySelector` | string | CSS selector to wait for before capture |
| `reloadAfterReady` | boolean | Reload page after ready selector appears |
| `delay` | number \| object | Wait time after ready (see below) |
| `keyPressSelector` | object | Element to focus and key to press |
| `hoverSelector` | string | Single element to hover over |
| `hoverSelectors` | array | Multiple elements to hover in sequence |
| `clickSelector` | string | Single element to click |
| `clickSelectors` | array | Multiple elements to click in sequence |
| `postInteractionWait` | string \| number | Wait after interactions (selector or ms) |
| `scrollToSelector` | string | Element to scroll into view |
| `misMatchThreshold` | number | Allowed mismatch percentage (0-100) |
| `retryCount` | number | Extra retries for the scenario (overrides global) |

## Scenario Examples

Expand Down Expand Up @@ -435,7 +475,7 @@ Override global viewports for specific scenarios:

### With Browser Override

Run a scenario only on specific browsers:
Run a scenario only on specific browser aliases (must be defined in the config `browsers` map):

```json
{
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.20.2
0.21.0
27 changes: 16 additions & 11 deletions config/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,21 @@ type HtmlReportConfig struct {
ShowSuccessfulTests bool `json:"showSuccessfulTests"`
}

type BrowserSettings struct {
Name browser.Browser `json:"name"`
Args []string `json:"args"`
}

type Config struct {
Browsers []browser.Browser `json:"browsers"`
Viewports []viewport.Viewport `json:"viewports"`
BitmapsReferencePath string `json:"bitmapsReferencePath"`
BitmapsTestPath string `json:"bitmapsTestPath"`
HtmlReport HtmlReportConfig `json:"htmlReport"`
CiReportPath string `json:"ciReportPath"`
Args map[browser.Browser][]string `json:"args"`
AsyncCaptureLimit int `json:"asyncCaptureLimit"`
AsyncCompareLimit int `json:"asyncCompareLimit"`
RequireSameDimensions bool `json:"requireSameDimensions"`
RetryCount int `json:"retryCount"`
Browsers map[string]BrowserSettings `json:"browsers"`
DefaultBrowsers []string `json:"defaultBrowsers"`
Viewports []viewport.Viewport `json:"viewports"`
BitmapsReferencePath string `json:"bitmapsReferencePath"`
BitmapsTestPath string `json:"bitmapsTestPath"`
HtmlReport HtmlReportConfig `json:"htmlReport"`
CiReportPath string `json:"ciReportPath"`
AsyncCaptureLimit int `json:"asyncCaptureLimit"`
AsyncCompareLimit int `json:"asyncCompareLimit"`
RequireSameDimensions bool `json:"requireSameDimensions"`
RetryCount int `json:"retryCount"`
}
10 changes: 5 additions & 5 deletions converters/converters.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@ package converters
import (
"sort"

"github.com/gooddata/gooddata-neobackstop/browser"
"github.com/gooddata/gooddata-neobackstop/internals"
"github.com/gooddata/gooddata-neobackstop/scenario"
"github.com/gooddata/gooddata-neobackstop/viewport"
)

func scenarioToInternal(b browser.Browser, v viewport.Viewport, rc int, s scenario.Scenario) internals.Scenario {
func scenarioToInternal(b string, v viewport.Viewport, rc int, s scenario.Scenario) internals.Scenario {
retryCount := rc
if s.RetryCount != nil {
retryCount = *s.RetryCount
Expand Down Expand Up @@ -37,12 +36,13 @@ func scenarioToInternal(b browser.Browser, v viewport.Viewport, rc int, s scenar
}
}

func ScenariosToInternal(browsers []browser.Browser, viewports []viewport.Viewport, retryCount int, scenarios []scenario.Scenario) []internals.Scenario {
output := make([]internals.Scenario, 0) // we could pre-calculate this, but until we do multi-browser testing, it's not worth it
func ScenariosToInternal(defaultBrowsers []string, viewports []viewport.Viewport, retryCount int, scenarios []scenario.Scenario) []internals.Scenario {
output := make([]internals.Scenario, 0) // we could pre-calculate size of this, but until we do multi-browser testing, it's not worth it

for _, s := range scenarios {
// if the scenario has a browsers config, we use that, otherwise, we use the defaultBrowsers value in the config file
if s.Browsers == nil {
for _, b := range browsers {
for _, b := range defaultBrowsers {
if s.Viewports == nil {
for _, v := range viewports {
output = append(output, scenarioToInternal(b, v, retryCount, s))
Expand Down
Loading