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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"scripts": {
"build": "pnpm -r build",
"lint": "eslint '**/*.ts' --ignore-pattern '**/*.d.ts'",
"lint:fix": "pnpm lint --fix",
"fix": "pnpm lint --fix",
"lint:ci": "pnpm lint --max-warnings 0",
"format": "pnpm -r format",
"format:ci": "pnpm -r format:ci",
Expand Down
98 changes: 98 additions & 0 deletions packages/cypress-plugin-visual-regression-diff/MIGRATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Migration Guide

## 4.0.x -> 4.1.x

### Migrating to Cypress 16 (`Cypress.expose` API)

Cypress 16 removes `Cypress.env()` in favor of the new `Cypress.expose()` API (introduced in Cypress 15.10.0).
This plugin supports both APIs automatically based on the detected Cypress version:

- **Cypress ≥ 15.10** – reads plugin options via `Cypress.expose()` / `config.expose`
- **Cypress < 15.10** – reads plugin options via `Cypress.env()` / `config.env` (legacy)

No code change is required inside your tests. You only need to update **how you supply plugin options**.

### What changed

| Location | Before (all Cypress versions) | After (Cypress ≥ 15.10) |
| ------------------- | ----------------------------- | ------------------------------------------- |
| CLI flag | `--env "key=value"` | `--expose "key=value"` |
| `cypress.config.ts` | `env: { key: value }` | `expose: { key: value }` |
| `cypress.env.json` | `{ "key": "value" }` | use `expose` in `cypress.config.ts` instead |

### Plugin option names

All option names stay the same — they are prefixed with `pluginVisualRegression`:

| Option | Config key |
| ------------------------ | ---------------------------------------------- |
| `updateImages` | `pluginVisualRegressionUpdateImages` |
| `cleanupUnusedImages` | `pluginVisualRegressionCleanupUnusedImages` |
| `diffConfig` | `pluginVisualRegressionDiffConfig` |
| `forceDeviceScaleFactor` | `pluginVisualRegressionForceDeviceScaleFactor` |
| `maxDiffThreshold` | `pluginVisualRegressionMaxDiffThreshold` |

### CLI

```bash
# Before (deprecated in 15.10, removed in 16)
npx cypress run --env "pluginVisualRegressionUpdateImages=true"

# After (Cypress ≥ 15.10)
npx cypress run --expose "pluginVisualRegressionUpdateImages=true"
```

### cypress.config.ts

```ts
// Before
export default defineConfig({
env: {
pluginVisualRegressionUpdateImages: true,
pluginVisualRegressionDiffConfig: { threshold: 0.01 },
},
});

// After (Cypress ≥ 15.10)
export default defineConfig({
expose: {
pluginVisualRegressionUpdateImages: true,
pluginVisualRegressionDiffConfig: { threshold: 0.01 },
},
});
```

### cypress.env.json

`cypress.env.json` only works with `Cypress.env()` and is not supported by the `expose` API.
Migrate any plugin options from `cypress.env.json` into the `expose` block of `cypress.config.ts`.

```jsonc
// cypress.env.json — REMOVE these plugin keys:
{
// "pluginVisualRegressionUpdateImages": true, ← move to cypress.config.ts expose block
}
```

### Supporting both Cypress 15.x and 16.x simultaneously

If you need your config to work on both Cypress 15.9 and below **and** 15.10+, you can supply the
option in both places — the plugin will prefer `expose` on newer Cypress and fall back to `env` on
older ones:

```ts
export default defineConfig({
expose: {
pluginVisualRegressionUpdateImages: true,
},
env: {
pluginVisualRegressionUpdateImages: true, // fallback for Cypress < 15.10
},
});
```

### References

- [Cypress `Cypress.env()` migration guide](https://docs.cypress.io/app/references/migration-guide#Migrating-away-from-Cypressenv)
- [Cypress `expose` API docs](https://on.cypress.io/expose)
- [Issue #375](https://github.com/FRSOURCE/cypress-plugin-visual-regression-diff/issues/375)
42 changes: 29 additions & 13 deletions packages/cypress-plugin-visual-regression-diff/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,9 +158,12 @@ Still got troubles with installation? Have a look at [examples directory of this
## Automatic clean up of unused images

It's useful to remove screenshots generated by the visual regression plugin that are not used by any test anymore.
Enable this feature via env variable and enjoy freed up storage space 🚀:
Enable this feature via expose variable and enjoy freed up storage space 🚀:

```bash
# Cypress 15.10+
npx cypress run --expose "pluginVisualRegressionCleanupUnusedImages=true"
# Cypress <15.10 (deprecated in 15.10, removed in 16)
npx cypress run --env "pluginVisualRegressionCleanupUnusedImages=true"
```

Expand Down Expand Up @@ -216,33 +219,40 @@ cy.matchImage({
});
```

- via [global env configuration](https://docs.cypress.io/guides/guides/environment-variables#Setting). Environment variable names are the same as keys of the configuration object above, but with added `pluginVisualRegression` prefix, e.g.:
- via [global expose configuration](https://on.cypress.io/expose) (Cypress 15.10+). Configuration key names are the same as the keys of the configuration object above, but with added `pluginVisualRegression` prefix, e.g.:

```bash
# Cypress 15.10+
npx cypress run --expose "pluginVisualRegressionUpdateImages=true" --expose "pluginVisualRegressionDiffConfig={\"threshold\":0.01}"
# Cypress <15.10 (deprecated in 15.10, removed in 16)
npx cypress run --env "pluginVisualRegressionUpdateImages=true,pluginVisualRegressionDiffConfig={\"threshold\":0.01}"
```

```ts
// cypress.config.ts
// cypress.config.ts (Cypress 15.10+)
import { defineConfig } from 'cypress';

export default defineConfig({
env: {
expose: {
pluginVisualRegressionUpdateImages: true,
pluginVisualRegressionDiffConfig: { threshold: 0.01 },
},
});
```

```json
// cypress.env.json (https://docs.cypress.io/guides/guides/environment-variables#Option-2-cypress-env-json)
{
"pluginVisualRegressionUpdateImages": true,
"pluginVisualRegressionDiffConfig": { "threshold": 0.01 }
}
```ts
// cypress.config.ts (Cypress <15.10, deprecated in newer versions)
import { defineConfig } from 'cypress';

export default defineConfig({
env: {
pluginVisualRegressionUpdateImages: true,
pluginVisualRegressionDiffConfig: { threshold: 0.01 },
},
});
```

For more ways of setting environment variables [take a look here](https://docs.cypress.io/guides/guides/environment-variables#Setting).
For more ways of setting expose variables [take a look here](https://on.cypress.io/expose).

## FAQ

Expand All @@ -269,9 +279,12 @@ This is typically caused by device pixel ratio differences between headless and
cy.matchImage({ forceDeviceScaleFactor: true });
```

Or set it globally via environment variable:
Or set it globally via expose variable:

```bash
# Cypress 15.10+
npx cypress run --expose "pluginVisualRegressionForceDeviceScaleFactor=true"
# Cypress <15.10 (deprecated in 15.10, removed in 16)
npx cypress run --env "pluginVisualRegressionForceDeviceScaleFactor=true"
```

Expand Down Expand Up @@ -340,9 +353,12 @@ Set `maxDiffThreshold` to `0`:
cy.matchImage({ maxDiffThreshold: 0 });
```

Or globally via an environment variable:
Or globally via an expose variable:

```bash
# Cypress 15.10+
npx cypress run --expose "pluginVisualRegressionMaxDiffThreshold=0"
# Cypress <15.10 (deprecated in 15.10, removed in 16)
npx cypress run --env "pluginVisualRegressionMaxDiffThreshold=0"
```

Expand Down
12 changes: 9 additions & 3 deletions packages/cypress-plugin-visual-regression-diff/src/commands.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { FILE_SUFFIX, LINK_PREFIX, TASK } from './constants';
import { supportsExpose } from './version.utils';
import type pixelmatch from 'pixelmatch';
import * as Base64 from '@frsource/base64';
import type { CompareImagesTaskReturn } from './types';
Expand Down Expand Up @@ -58,9 +59,14 @@ const constructCypressError = (log: Cypress.Log, err: Error) => {
const capitalize = (text: string) =>
text.charAt(0).toUpperCase() + text.slice(1);

const getPluginEnv = <K extends keyof Cypress.MatchImageOptions>(key: K) =>
Cypress.env(`pluginVisualRegression${capitalize(key)}`) as
Cypress.MatchImageOptions[K] | undefined;
const getPluginEnv = <K extends keyof Cypress.MatchImageOptions>(key: K) => {
const envKey = `pluginVisualRegression${capitalize(key)}`;
if (supportsExpose(Cypress.version)) {
return Cypress.expose(envKey) as Cypress.MatchImageOptions[K] | undefined;
}

return Cypress.env(envKey) as Cypress.MatchImageOptions[K] | undefined;
};

const booleanOption = <K extends keyof Cypress.MatchImageOptions, Return>(
options: Cypress.MatchImageOptions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ vi.mock('./afterScreenshot.hook.ts', () => ({
}));

describe('initPlugin', () => {
it('inits hooks', () => {
it('inits hooks (Cypress <15.10, env API)', () => {
const onMock = vi.fn();
initPlugin(onMock, {
version: '13.17.0',
env: { pluginVisualRegressionForceDeviceScaleFactor: false },
} as unknown as Cypress.PluginConfigOptions);

Expand All @@ -22,4 +23,18 @@ describe('initPlugin', () => {
expect(initTaskHook).toBeCalledTimes(1);
expect(initAfterScreenshotHook).toBeCalledTimes(1);
});

it('inits hooks (Cypress 15.10+, expose API)', () => {
const onMock = vi.fn();
initPlugin(onMock, {
version: '15.10.0',
expose: { pluginVisualRegressionForceDeviceScaleFactor: false },
env: {},
} as unknown as Cypress.PluginConfigOptions);

expect(onMock).toBeCalledWith('task', 'task');
expect(onMock).toBeCalledWith('after:screenshot', 'after:screenshot');
expect(initTaskHook).toBeCalledTimes(2);
expect(initAfterScreenshotHook).toBeCalledTimes(2);
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { initTaskHook } from './task.hook';
import { initAfterScreenshotHook } from './afterScreenshot.hook';
import { getPluginConfig } from './version.utils';

/* c8 ignore start */
const initForceDeviceScaleFactor = (on: Cypress.PluginEvents) => {
Expand All @@ -24,7 +25,10 @@ export const initPlugin = (
config: Cypress.PluginConfigOptions,
) => {
/* c8 ignore start */
if (config.env['pluginVisualRegressionForceDeviceScaleFactor'] !== false) {
if (
getPluginConfig(config, 'pluginVisualRegressionForceDeviceScaleFactor') !==
false
) {
initForceDeviceScaleFactor(on);
}
/* c8 ignore stop */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ describe('cleanupImagesTask', () => {

cleanupImagesTask({
projectRoot,
version: '13.17.0',
env: { pluginVisualRegressionCleanupUnusedImages: true },
testingType: 'e2e',
} as unknown as Cypress.PluginConfigOptions);
Expand All @@ -155,6 +156,7 @@ describe('cleanupImagesTask', () => {

cleanupImagesTask({
projectRoot,
version: '13.17.0',
env: { pluginVisualRegressionCleanupUnusedImages: true },
} as unknown as Cypress.PluginConfigOptions);

Expand All @@ -172,6 +174,7 @@ describe('cleanupImagesTask', () => {

cleanupImagesTask({
projectRoot,
version: '13.17.0',
env: { pluginVisualRegressionCleanupUnusedImages: true },
testingType: 'component',
} as unknown as Cypress.PluginConfigOptions);
Expand All @@ -190,6 +193,7 @@ describe('cleanupImagesTask', () => {

cleanupImagesTask({
projectRoot,
version: '13.17.0',
env: { pluginVisualRegressionCleanupUnusedImages: true },
testingType: 'e2e',
} as unknown as Cypress.PluginConfigOptions);
Expand All @@ -206,13 +210,52 @@ describe('cleanupImagesTask', () => {

cleanupImagesTask({
projectRoot,
version: '13.17.0',
env: { pluginVisualRegressionCleanupUnusedImages: true },
testingType: 'e2e',
} as unknown as Cypress.PluginConfigOptions);

expect(existsSync(screenshotPath)).toBe(false);
});
});

describe('with Cypress 15.10+ (expose API)', () => {
it('reads pluginVisualRegressionCleanupUnusedImages from config.expose', async () => {
const { path: projectRoot } = await dir();
const screenshotPath = await writeTmpFixture(
path.join(projectRoot, 'some-file-2_#0.png'),
oldImgFixture,
);

cleanupImagesTask({
projectRoot,
version: '15.10.0',
expose: { pluginVisualRegressionCleanupUnusedImages: true },
env: {},
testingType: 'e2e',
} as unknown as Cypress.PluginConfigOptions);

expect(existsSync(screenshotPath)).toBe(false);
});

it('does not remove images when expose flag is not set', async () => {
const { path: projectRoot } = await dir();
const screenshotPath = await writeTmpFixture(
path.join(projectRoot, 'some-file-2_#0.png'),
oldImgFixture,
);

cleanupImagesTask({
projectRoot,
version: '15.10.0',
expose: {},
env: {},
testingType: 'e2e',
} as unknown as Cypress.PluginConfigOptions);

expect(existsSync(screenshotPath)).toBe(true);
});
});
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ type PixelmatchOptions = NonNullable<Parameters<typeof pixelmatch>[5]>;
import { moveFile } from 'move-file';
import path from 'path';
import { FILE_SUFFIX, TASK } from './constants';
import { getPluginConfig } from './version.utils';
import {
cleanupUnused,
alignImagesToSameSize,
Expand Down Expand Up @@ -47,7 +48,7 @@ export const getScreenshotPathInfoTask = (cfg: {
};

export const cleanupImagesTask = (config: Cypress.PluginConfigOptions) => {
if (config.env['pluginVisualRegressionCleanupUnusedImages']) {
if (getPluginConfig(config, 'pluginVisualRegressionCleanupUnusedImages')) {
cleanupUnused(config);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export const supportsExpose = (version: string): boolean => {
const [major, minor] = version.split('.').map(Number);
return major > 15 || (major === 15 && minor >= 10);
};

export const getPluginConfig = (
config: Cypress.PluginConfigOptions,
key: string,
): unknown => {
if (supportsExpose(config.version ?? '')) {
return config.expose?.[key];
}
return config.env?.[key];
};