From fad41afc6426cd19597430170fd1e79ac805b21f Mon Sep 17 00:00:00 2001 From: shantanuk-browserstack <131665162+shantanuk-browserstack@users.noreply.github.com> Date: Fri, 21 Jun 2024 12:48:49 +0530 Subject: [PATCH] tags support (#1625) --- packages/cli-command/src/flags.js | 13 +- packages/cli-command/test/command.test.js | 23 ++-- packages/cli-command/test/flags.test.js | 44 +++++- packages/cli-command/test/help.test.js | 159 ++++++++++++---------- packages/cli-upload/test/upload.test.js | 1 + packages/client/src/client.js | 15 +- packages/client/src/utils.js | 11 ++ packages/client/test/client.test.js | 52 ++++++- packages/client/test/tagsList.test.js | 32 +++++ packages/core/src/config.js | 7 + packages/core/src/percy.js | 10 +- packages/core/src/snapshot.js | 2 +- 12 files changed, 271 insertions(+), 98 deletions(-) create mode 100644 packages/client/test/tagsList.test.js diff --git a/packages/cli-command/src/flags.js b/packages/cli-command/src/flags.js index 99b97be5d..56b7d7f9f 100644 --- a/packages/cli-command/src/flags.js +++ b/packages/cli-command/src/flags.js @@ -102,11 +102,22 @@ export const debug = { group: 'Percy' }; +export const labels = { + name: 'labels', + description: 'Associates labels to the build (ex: --labels=dev,prod )', + group: 'Global', + type: 'string', + parse: String, + percyrc: 'labels', + short: 'l' +}; + // Group constants export const GLOBAL = [ verbose, quiet, - silent + silent, + labels ]; export const PERCY = [ diff --git a/packages/cli-command/test/command.test.js b/packages/cli-command/test/command.test.js index ce3e0d6d4..6508acecc 100644 --- a/packages/cli-command/test/command.test.js +++ b/packages/cli-command/test/command.test.js @@ -65,17 +65,18 @@ describe('Command', () => { await test(['--help']); expect(logger.stdout).toEqual([dedent` - Usage: - $ test [options] - - Options: - --verbose [level] Replaces common flag (default: "debug") - -q, --qux Replaces common short flag - - Global options: - --quiet Log errors only - -s, --silent Log nothing - -h, --help Display command help + Usage: + $ test [options] + + Options: + --verbose [level] Replaces common flag (default: "debug") + -q, --qux Replaces common short flag + + Global options: + --quiet Log errors only + -s, --silent Log nothing + -l, --labels Associates labels to the build (ex: --labels=dev,prod ) + -h, --help Display command help ` + '\n']); }); diff --git a/packages/cli-command/test/flags.test.js b/packages/cli-command/test/flags.test.js index 877ac4cba..d0c21c87e 100644 --- a/packages/cli-command/test/flags.test.js +++ b/packages/cli-command/test/flags.test.js @@ -51,11 +51,49 @@ describe('Built-in flags:', () => { }); }); + describe('--labels', () => { + it('sets labels to the given flag labels', async () => { + test = command('percy', { + percy: {} + }, ({ percy }) => { + test.percy = percy; + }); + + await test(['--labels=tag1,tag2']); + + expect(test.percy.labels).toBe('tag1,tag2'); + }); + + it('sets labels to the given config labels', async () => { + test = command('percy', { + percy: { labels: 'tag3,tag4' } + }, ({ percy }) => { + test.percy = percy; + }); + + await test(); + + expect(test.percy.labels).toBe('tag3,tag4'); + }); + + it('sets labels to the given flag labels when both are present', async () => { + test = command('percy', { + percy: { labels: 'tag3,tag4' } + }, ({ percy }) => { + test.percy = percy; + }); + + await test(['--labels=tag1,tag2']); + + expect(test.percy.labels).toBe('tag1,tag2'); + }); + }); + describe('Percy flags:', () => { const expectedMinPercyFlags = jasmine.stringContaining(dedent` Percy options: - -c, --config Config file path - -d, --dry-run Print snapshot names only + -c, --config Config file path + -d, --dry-run Print snapshot names only `); const expectedAllPercyFlags = jasmine.stringContaining(dedent` @@ -99,7 +137,7 @@ describe('Built-in flags:', () => { expect(logger.stdout).not.toEqual([expectedAllPercyFlags]); expect(logger.stdout).toEqual([expectedMinPercyFlags]); expect(logger.stdout).toEqual([jasmine.stringContaining( - ' -P, --port [number] Local CLI server port (default: 5338)' + ' -P, --port [number] Local CLI server port (default: 5338)' )]); }); diff --git a/packages/cli-command/test/help.test.js b/packages/cli-command/test/help.test.js index 1b61142ec..cc4ff93cc 100644 --- a/packages/cli-command/test/help.test.js +++ b/packages/cli-command/test/help.test.js @@ -22,14 +22,15 @@ describe('Help output', () => { $ foo Commands: - bar:baz [options] Foo bar baz - help [command] Display command help + bar:baz [options] Foo bar baz + help [command] Display command help Global options: - -v, --verbose Log everything - -q, --quiet Log errors only - -s, --silent Log nothing - -h, --help Display command help + -v, --verbose Log everything + -q, --quiet Log errors only + -s, --silent Log nothing + -l, --labels Associates labels to the build (ex: --labels=dev,prod ) + -h, --help Display command help ` + '\n']); }); @@ -96,34 +97,35 @@ describe('Help output', () => { expect(logger.stderr).toEqual([]); expect(logger.stdout).toEqual([dedent` - Command description - - Usage: - $ test [options] [second] - - Arguments: - first First command argument - second Second command argument (default: "2") - - Commands: - sub [options] - sub:nested [options] Nested description - help [command] Display command help - - Options: - -o, --one Command flag 1 - --two Command flag 2 - --other-flag - - Global options: - -v, --verbose Log everything - -q, --quiet Log errors only - -s, --silent Log nothing - -h, --help Display command help - - Examples: - $ test --one - $ test -o --two 2 + Command description + + Usage: + $ test [options] [second] + + Arguments: + first First command argument + second Second command argument (default: "2") + + Commands: + sub [options] + sub:nested [options] Nested description + help [command] Display command help + + Options: + -o, --one Command flag 1 + --two Command flag 2 + --other-flag + + Global options: + -v, --verbose Log everything + -q, --quiet Log errors only + -s, --silent Log nothing + -l, --labels Associates labels to the build (ex: --labels=dev,prod ) + -h, --help Display command help + + Examples: + $ test --one + $ test -o --two 2 ` + '\n']); logger.reset(); @@ -151,6 +153,7 @@ describe('Help output', () => { -v, --verbose Log everything -q, --quiet Log errors only -s, --silent Log nothing + -l, --labels Associates labels to the build (ex: --labels=dev,prod ) -h, --help Display command help ` + '\n']); @@ -173,6 +176,7 @@ describe('Help output', () => { -v, --verbose Log everything -q, --quiet Log errors only -s, --silent Log nothing + -l, --labels Associates labels to the build (ex: --labels=dev,prod ) -h, --help Display command help Examples: @@ -191,10 +195,11 @@ describe('Help output', () => { $ test sub:nested:deep [options] Global options: - -v, --verbose Log everything - -q, --quiet Log errors only - -s, --silent Log nothing - -h, --help Display command help + -v, --verbose Log everything + -q, --quiet Log errors only + -s, --silent Log nothing + -l, --labels Associates labels to the build (ex: --labels=dev,prod ) + -h, --help Display command help ` + '\n']); }); @@ -311,20 +316,21 @@ describe('Help output', () => { $ test Arguments: - arg (default: "foo") + arg (default: "foo") Commands: command [options] - help [command] Display command help + help [command] Display command help Options: --flag Global options: - -v, --verbose Log everything - -q, --quiet Log errors only - -s, --silent Log nothing - -h, --help Display command help + -v, --verbose Log everything + -q, --quiet Log errors only + -s, --silent Log nothing + -l, --labels Associates labels to the build (ex: --labels=dev,prod ) + -h, --help Display command help ` + '\n']); }); @@ -349,49 +355,52 @@ describe('Help output', () => { $ test [options] Options: - --help Custom help - --version Custom version + --help Custom help + --version Custom version Global options: - -v, --verbose Log everything - -q, --quiet Log errors only - -s, --silent Log nothing - -h Display command help - -V Display version + -v, --verbose Log everything + -q, --quiet Log errors only + -s, --silent Log nothing + -l, --labels Associates labels to the build (ex: --labels=dev,prod ) + -h Display command help + -V Display version ` + '\n']); logger.reset(); await test(false, true); expect(logger.stdout).toEqual([dedent` - Usage: - $ test [options] - - Options: - -h Custom help - -V Custom version - - Global options: - -v, --verbose Log everything - -q, --quiet Log errors only - -s, --silent Log nothing - --help Display command help - --version Display version + Usage: + $ test [options] + + Options: + -h Custom help + -V Custom version + + Global options: + -v, --verbose Log everything + -q, --quiet Log errors only + -s, --silent Log nothing + -l, --labels Associates labels to the build (ex: --labels=dev,prod ) + --help Display command help + --version Display version ` + '\n']); logger.reset(); await test(true, true); expect(logger.stdout).toEqual([dedent` - Usage: - $ test [options] - - Options: - -h, --help Custom help - -V, --version Custom version - - Global options: - -v, --verbose Log everything - -q, --quiet Log errors only - -s, --silent Log nothing + Usage: + $ test [options] + + Options: + -h, --help Custom help + -V, --version Custom version + + Global options: + -v, --verbose Log everything + -q, --quiet Log errors only + -s, --silent Log nothing + -l, --labels Associates labels to the build (ex: --labels=dev,prod ) ` + '\n']); }); diff --git a/packages/cli-upload/test/upload.test.js b/packages/cli-upload/test/upload.test.js index f52ea4233..58f705a25 100644 --- a/packages/cli-upload/test/upload.test.js +++ b/packages/cli-upload/test/upload.test.js @@ -89,6 +89,7 @@ describe('percy upload', () => { scope: null, sync: false, 'test-case': null, + tags: [], 'scope-options': {}, 'minimum-height': 10, 'enable-javascript': null, diff --git a/packages/client/src/client.js b/packages/client/src/client.js index 89b831d44..dc237595f 100644 --- a/packages/client/src/client.js +++ b/packages/client/src/client.js @@ -12,7 +12,8 @@ import { base64encode, getPackageJSON, waitForTimeout, - validateTiles + validateTiles, + tagsList } from './utils.js'; // Default client API URL can be set with an env var for API development @@ -54,10 +55,11 @@ export class PercyClient { clientInfo, environmentInfo, config, + labels, // versioned api url apiUrl = PERCY_CLIENT_API_URL } = {}) { - Object.assign(this, { token, config: config || {}, apiUrl }); + Object.assign(this, { token, config: config || {}, apiUrl, labels: labels }); this.addClientInfo(clientInfo); this.addEnvironmentInfo(environmentInfo); this.buildType = null; @@ -134,6 +136,8 @@ export class PercyClient { async createBuild({ resources = [], projectType } = {}) { this.log.debug('Creating a new build...'); + let tagsArr = tagsList(this.labels); + return this.post('builds', { data: { type: 'builds', @@ -152,7 +156,8 @@ export class PercyClient { 'pull-request-number': this.env.pullRequest, 'parallel-nonce': this.env.parallel.nonce, 'parallel-total-shards': this.env.parallel.total, - partial: this.env.partial + partial: this.env.partial, + tags: tagsArr }, relationships: { resources: { @@ -367,6 +372,7 @@ export class PercyClient { environmentInfo, sync, testCase, + labels, thTestCaseExecutionId, resources = [] } = {}) { @@ -378,6 +384,8 @@ export class PercyClient { this.log.warn('Warning: Missing `clientInfo` and/or `environmentInfo` properties'); } + let tagsArr = tagsList(labels); + this.log.debug(`Creating snapshot: ${name}...`); for (let resource of resources) { @@ -394,6 +402,7 @@ export class PercyClient { scope: scope || null, sync: !!sync, 'test-case': testCase || null, + tags: tagsArr, 'scope-options': scopeOptions || {}, 'minimum-height': minHeight || null, 'enable-javascript': enableJavaScript || null, diff --git a/packages/client/src/utils.js b/packages/client/src/utils.js index 2a248ebda..88385a90c 100644 --- a/packages/client/src/utils.js +++ b/packages/client/src/utils.js @@ -213,6 +213,17 @@ export function validateTiles(tiles) { return true; } +// convert tags comma-separated-names to array of objects for POST request +export function tagsList(tags) { + let tagsArr = []; + if (typeof tags !== 'undefined' && tags !== null && typeof tags === 'string') { + let tagNamesArray = tags.split(','); + tagsArr = tagNamesArray.map(name => ({ id: null, name: name.trim() })); + } + + return tagsArr; +} + export { hostnameMatches, ProxyHttpAgent, diff --git a/packages/client/test/client.test.js b/packages/client/test/client.test.js index 70e7cf695..338faba13 100644 --- a/packages/client/test/client.test.js +++ b/packages/client/test/client.test.js @@ -164,7 +164,8 @@ describe('PercyClient', () => { 'pull-request-number': client.env.pullRequest, 'parallel-nonce': client.env.parallel.nonce, 'parallel-total-shards': client.env.parallel.total, - partial: client.env.partial + partial: client.env.partial, + tags: [] } })); }); @@ -197,7 +198,8 @@ describe('PercyClient', () => { 'pull-request-number': client.env.pullRequest, 'parallel-nonce': client.env.parallel.nonce, 'parallel-total-shards': client.env.parallel.total, - partial: client.env.partial + partial: client.env.partial, + tags: [] } })); }); @@ -277,7 +279,46 @@ describe('PercyClient', () => { 'pull-request-number': client.env.pullRequest, 'parallel-nonce': client.env.parallel.nonce, 'parallel-total-shards': client.env.parallel.total, - partial: client.env.partial + partial: client.env.partial, + tags: [] + } + })); + }); + + it('creates a new build with tags', async () => { + client = new PercyClient({ + token: 'PERCY_TOKEN', + labels: 'tag1,tag2' + }); + await expectAsync(client.createBuild({ projectType: 'web' })).toBeResolvedTo({ + data: { + id: '123', + attributes: { + 'build-number': 1, + 'web-url': 'https://percy.io/test/test/123' + } + } + }); + + expect(api.requests['/builds'][0].body.data) + .toEqual(jasmine.objectContaining({ + attributes: { + branch: client.env.git.branch, + type: 'web', + 'target-branch': client.env.target.branch, + 'target-commit-sha': client.env.target.commit, + 'commit-sha': client.env.git.sha, + 'commit-committed-at': client.env.git.committedAt, + 'commit-author-name': client.env.git.authorName, + 'commit-author-email': client.env.git.authorEmail, + 'commit-committer-name': client.env.git.committerName, + 'commit-committer-email': client.env.git.committerEmail, + 'commit-message': client.env.git.message, + 'pull-request-number': client.env.pullRequest, + 'parallel-nonce': client.env.parallel.nonce, + 'parallel-total-shards': client.env.parallel.total, + partial: client.env.partial, + tags: [{ id: null, name: 'tag1' }, { id: null, name: 'tag2' }] } })); }); @@ -678,6 +719,7 @@ describe('PercyClient', () => { scope: '#main', sync: true, testCase: 'foo test case', + labels: 'tag 1,tag 2', scopeOptions: { scroll: true }, minHeight: 1000, enableJavaScript: true, @@ -715,6 +757,7 @@ describe('PercyClient', () => { scope: '#main', sync: true, 'test-case': 'foo test case', + tags: [{ id: null, name: 'tag 1' }, { id: null, name: 'tag 2' }], 'minimum-height': 1000, 'scope-options': { scroll: true }, 'enable-javascript': true, @@ -762,6 +805,7 @@ describe('PercyClient', () => { scope: null, sync: false, 'test-case': null, + tags: [], 'scope-options': {}, 'minimum-height': null, 'enable-javascript': null, @@ -831,6 +875,7 @@ describe('PercyClient', () => { scope: null, sync: true, 'test-case': null, + tags: [], 'scope-options': {}, 'enable-javascript': null, 'minimum-height': null, @@ -1340,6 +1385,7 @@ describe('PercyClient', () => { name: 'test snapshot name', scope: null, 'test-case': null, + tags: [], 'scope-options': {}, 'enable-javascript': null, 'minimum-height': null, diff --git a/packages/client/test/tagsList.test.js b/packages/client/test/tagsList.test.js new file mode 100644 index 000000000..b0f149a7a --- /dev/null +++ b/packages/client/test/tagsList.test.js @@ -0,0 +1,32 @@ +import { tagsList } from '@percy/client/utils'; + +describe('tagsList', () => { + it('should return empty array when tags is undefined', () => { + let tags; + const actual = tagsList(tags); + expect(actual).toEqual([]); + }); + + it('should return empty array when tags is null', () => { + let tags = null; + const actual = tagsList(tags); + expect(actual).toEqual([]); + }); + + it('should return empty array when tags is not string', () => { + let tags = 123; + const actual = tagsList(tags); + expect(actual).toEqual([]); + }); + + it('should return array of tag objects when tags-names are passed', () => { + let tags = 'tag1,tag2,tag3'; + const expected = [ + { id: null, name: 'tag1' }, + { id: null, name: 'tag2' }, + { id: null, name: 'tag3' } + ]; + const actual = tagsList(tags); + expect(actual).toEqual(expected); + }); +}); diff --git a/packages/core/src/config.js b/packages/core/src/config.js index 7cb95f94c..9e9393933 100644 --- a/packages/core/src/config.js +++ b/packages/core/src/config.js @@ -9,6 +9,9 @@ export const configSchema = { }, token: { type: 'string' + }, + labels: { + type: 'string' } } }, @@ -74,6 +77,9 @@ export const configSchema = { testCase: { type: 'string' }, + labels: { + type: 'string' + }, thTestCaseExecutionId: { type: 'string' }, @@ -274,6 +280,7 @@ export const snapshotSchema = { enableLayout: { $ref: '/config/snapshot#/properties/enableLayout' }, sync: { $ref: '/config/snapshot#/properties/sync' }, testCase: { $ref: '/config/snapshot#/properties/testCase' }, + labels: { $ref: '/config/snapshot#/properties/labels' }, thTestCaseExecutionId: { $ref: '/config/snapshot#/properties/thTestCaseExecutionId' }, reshuffleInvalidTags: { $ref: '/config/snapshot#/properties/reshuffleInvalidTags' }, scopeOptions: { $ref: '/config/snapshot#/properties/scopeOptions' }, diff --git a/packages/core/src/percy.js b/packages/core/src/percy.js index 49544af9a..c01bc9d2a 100644 --- a/packages/core/src/percy.js +++ b/packages/core/src/percy.js @@ -58,6 +58,7 @@ export class Percy { // implies `skipUploads` and `skipDiscovery` dryRun, // implies `dryRun`, silent logs, and adds extra api endpoints + labels, testing, // configuration filepath config: configFile, @@ -91,8 +92,9 @@ export class Percy { this.skipDiscovery = this.dryRun || !!skipDiscovery; this.delayUploads = this.skipUploads || !!delayUploads; this.deferUploads = this.skipUploads || !!deferUploads; + this.labels = labels; - this.client = new PercyClient({ token, clientInfo, environmentInfo, config }); + this.client = new PercyClient({ token, clientInfo, environmentInfo, config, labels }); if (server) this.server = createPercyServer(this, port); this.browser = new Browser(this); @@ -359,6 +361,12 @@ export class Percy { } // validate comparison uploads and warn about any errors + + // we are having two similar attrs in options: tags & tag + // tags: is used as labels and is string comma-separated like "tag1,tag" + // tag: is comparison-tag used by app-percy & poa and this is used to create a comparison-tag in BE + // its format is object like {name: "", os:"", os_version:"", device:""} + // DO NOT GET CONFUSED!!! :) if ('tag' in options || 'tiles' in options) { // throw when missing required snapshot or tag name if (!options.name) throw new Error('Missing required snapshot name'); diff --git a/packages/core/src/snapshot.js b/packages/core/src/snapshot.js index 49e1ea6f2..23dc184ee 100644 --- a/packages/core/src/snapshot.js +++ b/packages/core/src/snapshot.js @@ -105,7 +105,7 @@ function getSnapshotOptions(options, { config, meta }) { return PercyConfig.merge([{ widths: configSchema.snapshot.properties.widths.default, discovery: { allowedHostnames: [validURL(options.url).hostname] }, - meta: { ...meta, snapshot: { name: options.name, testCase: options.testCase } } + meta: { ...meta, snapshot: { name: options.name, testCase: options.testCase, labels: options.labels } } }, config.snapshot, { // only specific discovery options are used per-snapshot discovery: {