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: 2 additions & 0 deletions packages/sdk-utils/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import postBuildEvents from './post-build-event.js';
import flushSnapshots from './flush-snapshots.js';
import captureAutomateScreenshot from './post-screenshot.js';
import getResponsiveWidths from './get-responsive-widths.js';
import mergeSnapshotOptions from './merge-snapshot-options.js';
import {
waitForReadyScript,
getReadinessConfig,
Expand Down Expand Up @@ -47,6 +48,7 @@ export {
captureAutomateScreenshot,
postBuildEvents,
getResponsiveWidths,
mergeSnapshotOptions,
DEFAULT_MAX_IFRAME_DEPTH,
HARD_MAX_IFRAME_DEPTH,
clampIframeDepth,
Expand Down
34 changes: 34 additions & 0 deletions packages/sdk-utils/src/merge-snapshot-options.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import percy from './percy-info.js';

function isPlainObject(value) {
return !!value && typeof value === 'object' && !Array.isArray(value);
}

// Recursively merge `override` onto `base`. Plain (non-array) objects are merged
// key-by-key so overriding one nested key keeps the base's sibling keys; arrays,
// scalars, null and functions from `override` replace the base value wholesale.
function deepMerge(base, override) {
const result = { ...base };
for (const key of Object.keys(override)) {
const baseVal = base[key];
const overrideVal = override[key];
result[key] = isPlainObject(baseVal) && isPlainObject(overrideVal)
? deepMerge(baseVal, overrideVal)
: overrideVal;
}
return result;
}

// Merges .percy.yml config snapshot options with per-snapshot options.
// Per-snapshot options take priority over config options.
//
// The merge is deep: nested objects (e.g. `discovery`) are merged recursively so
// a per-snapshot override of one nested key does not drop the config's sibling
// nested keys. At the leaves, per-snapshot values win; arrays are replaced, not
// concatenated.
export function mergeSnapshotOptions(options = {}) {
const configOptions = percy?.config?.snapshot || {};
return deepMerge(configOptions, options);
}

export default mergeSnapshotOptions;
69 changes: 69 additions & 0 deletions packages/sdk-utils/test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -936,4 +936,73 @@ describe('SDK Utils', () => {
expect(result).toBe(null);
});
});

describe('mergeSnapshotOptions(options)', () => {
let { mergeSnapshotOptions } = utils;

beforeEach(async () => {
await helpers.setupTest();
await utils.isPercyEnabled();
});

it('merges config snapshot options with per-snapshot options', () => {
const result = mergeSnapshotOptions({ enableJavaScript: true });
expect(result.enableJavaScript).toBe(true);
expect(result.widths).toEqual([375, 1280]);
});

it('gives per-snapshot options priority over config', () => {
const result = mergeSnapshotOptions({ widths: [768] });
expect(result.widths).toEqual([768]);
});

it('returns config options when no per-snapshot options are provided', () => {
const result = mergeSnapshotOptions();
expect(result.widths).toEqual([375, 1280]);
});

it('returns empty object when config.snapshot is undefined and no options given', () => {
const savedConfig = utils.percy.config;
utils.percy.config = { ...savedConfig, snapshot: undefined };

const result = mergeSnapshotOptions();
expect(result).toEqual({});

utils.percy.config = savedConfig;
});

it('returns only per-snapshot options when config.snapshot is undefined', () => {
const savedConfig = utils.percy.config;
utils.percy.config = { ...savedConfig, snapshot: undefined };

const result = mergeSnapshotOptions({ enableJavaScript: true });
expect(result).toEqual({ enableJavaScript: true });

utils.percy.config = savedConfig;
});

it('deep-merges nested objects, keeping config sibling keys not overridden', () => {
const savedConfig = utils.percy.config;
utils.percy.config = {
...savedConfig,
snapshot: { discovery: { networkIdleTimeout: 50, disableCache: false } }
};

const result = mergeSnapshotOptions({ discovery: { disableCache: true } });
// per-snapshot wins on the overridden nested key, config sibling key survives
expect(result.discovery).toEqual({ networkIdleTimeout: 50, disableCache: true });

utils.percy.config = savedConfig;
});

it('replaces (does not concatenate) arrays from per-snapshot options', () => {
const savedConfig = utils.percy.config;
utils.percy.config = { ...savedConfig, snapshot: { widths: [375, 1280] } };

const result = mergeSnapshotOptions({ widths: [768] });
expect(result.widths).toEqual([768]);

utils.percy.config = savedConfig;
});
});
});
Loading