diff --git a/package.json b/package.json index 0db2ee765..a7f99e826 100644 --- a/package.json +++ b/package.json @@ -31,13 +31,13 @@ "@babel/eslint-parser": "^7.14.7", "@babel/preset-env": "^7.14.7", "@babel/register": "^7.17.7", - "babel-plugin-transform-import-meta": "^2.2.1", "@rollup/plugin-alias": "^5.1.1", "@rollup/plugin-babel": "^6.0.0", "@rollup/plugin-commonjs": "^21.0.0", "@rollup/plugin-node-resolve": "^15.0.0", "babel-plugin-istanbul": "^7.0.0", "babel-plugin-module-resolver": "^5.0.2", + "babel-plugin-transform-import-meta": "^2.2.1", "cross-env": "^7.0.2", "eslint": "^7.30.0", "eslint-config-standard": "^16.0.2", diff --git a/packages/cli-build/test/finalize.test.js b/packages/cli-build/test/finalize.test.js index 34a257726..f425f83ef 100644 --- a/packages/cli-build/test/finalize.test.js +++ b/packages/cli-build/test/finalize.test.js @@ -35,7 +35,7 @@ describe('percy build:finalize', () => { it('defaults PERCY_PARALLEL_TOTAL to -1', async () => { process.env.PERCY_TOKEN = '<>'; - + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); expect(process.env.PERCY_PARALLEL_TOTAL).toBeUndefined(); await finalize(); expect(process.env.PERCY_PARALLEL_TOTAL).toEqual('-1'); @@ -43,6 +43,7 @@ describe('percy build:finalize', () => { it('gets parallel build info and finalizes all parallel builds', async () => { process.env.PERCY_TOKEN = '<>'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); await finalize(); expect(logger.stderr).toEqual([]); diff --git a/packages/cli-build/test/wait.test.js b/packages/cli-build/test/wait.test.js index 83431943a..e9e64521d 100644 --- a/packages/cli-build/test/wait.test.js +++ b/packages/cli-build/test/wait.test.js @@ -18,12 +18,14 @@ describe('percy build:wait', () => { beforeEach(async () => { process.env.PERCY_TOKEN = '<>'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); await setupTest({ loggerTTY: true }); }); afterEach(() => { delete process.env.PERCY_TOKEN; delete process.env.PERCY_ENABLE; + delete process.env.PERCY_FORCE_PKG_VALUE; }); it('does nothing and logs when percy is not enabled', async () => { @@ -232,7 +234,6 @@ describe('percy build:wait', () => { })]); await expectAsync(wait(['--build=123'])).toBeRejected(); - expect(logger.stdout).toEqual([]); expect(logger.stderr).toEqual(jasmine.arrayContaining([ '[percy] Build #10 failed! https://percy.io/test/test/123', diff --git a/packages/cli-exec/test/exec.test.js b/packages/cli-exec/test/exec.test.js index 9fef56ca2..c67a84cec 100644 --- a/packages/cli-exec/test/exec.test.js +++ b/packages/cli-exec/test/exec.test.js @@ -3,6 +3,7 @@ import exec from '@percy/cli-exec'; describe('percy exec', () => { beforeEach(async () => { process.env.PERCY_TOKEN = '<>'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); jasmine.DEFAULT_TIMEOUT_INTERVAL = 25000; await setupTest(); @@ -10,10 +11,14 @@ describe('percy exec', () => { spyOn(which, 'sync').and.callFake(c => c); spyOn(process, 'exit').and.callFake(c => c); process.env.PERCY_CLIENT_ERROR_LOGS = false; + + // Ensure global.__MOCK_IMPORTS__ is defined + global.__MOCK_IMPORTS__ = global.__MOCK_IMPORTS__ || new Map(); }); afterEach(() => { delete process.env.PERCY_TOKEN; + delete process.env.PERCY_FORCE_PKG_VALUE; delete process.env.PERCY_ENABLE; delete process.env.PERCY_BUILD_ID; delete process.env.PERCY_PARALLEL_TOTAL; @@ -243,9 +248,11 @@ describe('percy exec', () => { let [e, err] = [new EventEmitter(), new Error('spawn error')]; let crossSpawn = () => (setImmediate(() => e.emit('error', err)), e); global.__MOCK_IMPORTS__.set('cross-spawn', { default: crossSpawn }); + let stdinSpy = spyOn(process.stdin, 'pipe').and.resolveTo('some response'); await expectAsync(exec(['--', 'foobar'])).toBeRejected(); + expect(stdinSpy).toHaveBeenCalled(); console.log(logger.stderr); expect(logger.stderr).toEqual(jasmine.arrayContaining([ diff --git a/packages/cli-exec/test/ping.test.js b/packages/cli-exec/test/ping.test.js index d9bfa67b1..0d75fe388 100644 --- a/packages/cli-exec/test/ping.test.js +++ b/packages/cli-exec/test/ping.test.js @@ -6,11 +6,13 @@ describe('percy exec:ping', () => { beforeEach(async () => { process.env.PERCY_TOKEN = '<>'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); await setupTest(); }); afterEach(async () => { delete process.env.PERCY_TOKEN; + delete process.env.PERCY_FORCE_PKG_VALUE; delete process.env.PERCY_ENABLE; delete process.env.PERCY_PARALLEL_TOTAL; delete process.env.PERCY_PARTIAL_BUILD; diff --git a/packages/cli-exec/test/start.test.js b/packages/cli-exec/test/start.test.js index fe6bd123c..6296543bc 100644 --- a/packages/cli-exec/test/start.test.js +++ b/packages/cli-exec/test/start.test.js @@ -14,6 +14,7 @@ describe('percy exec:start', () => { beforeEach(async () => { process.env.PERCY_TOKEN = '<>'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); await setupTest(); started = start(['--quiet']); @@ -117,6 +118,7 @@ describe('percy exec:start', () => { logger.reset(); process.env.PERCY_ENABLE = '0'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); await start(); expect(logger.stdout).toEqual([]); diff --git a/packages/cli-exec/test/stop.test.js b/packages/cli-exec/test/stop.test.js index dd958c5dc..1fe43006e 100644 --- a/packages/cli-exec/test/stop.test.js +++ b/packages/cli-exec/test/stop.test.js @@ -6,11 +6,13 @@ describe('percy exec:stop', () => { beforeEach(async () => { process.env.PERCY_TOKEN = '<>'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); await setupTest(); }); afterEach(async () => { delete process.env.PERCY_TOKEN; + delete process.env.PERCY_FORCE_PKG_VALUE; delete process.env.PERCY_ENABLE; delete process.env.PERCY_PARALLEL_TOTAL; delete process.env.PERCY_PARTIAL_BUILD; diff --git a/packages/cli-snapshot/test/directory.test.js b/packages/cli-snapshot/test/directory.test.js index 22c9ac2a6..dccab72a3 100644 --- a/packages/cli-snapshot/test/directory.test.js +++ b/packages/cli-snapshot/test/directory.test.js @@ -5,6 +5,7 @@ describe('percy snapshot ', () => { beforeEach(async () => { snapshot.packageInformation = { name: '@percy/cli-snapshot' }; process.env.PERCY_TOKEN = '<>'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); await setupTest({ filesystem: { @@ -21,6 +22,7 @@ describe('percy snapshot ', () => { afterEach(() => { delete process.env.PERCY_TOKEN; + delete process.env.PERCY_FORCE_PKG_VALUE; delete process.env.PERCY_ENABLE; delete snapshot.packageInformation; delete process.env.PERCY_CLIENT_ERROR_LOGS; diff --git a/packages/cli-snapshot/test/file.test.js b/packages/cli-snapshot/test/file.test.js index 512e9edf3..7af0eeb17 100644 --- a/packages/cli-snapshot/test/file.test.js +++ b/packages/cli-snapshot/test/file.test.js @@ -8,6 +8,7 @@ describe('percy snapshot ', () => { beforeEach(async () => { snapshot.packageInformation = { name: '@percy/cli-snapshot' }; process.env.PERCY_TOKEN = '<>'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); server = await createTestServer({ default: () => [200, 'text/html', '

Test

'] @@ -44,6 +45,7 @@ describe('percy snapshot ', () => { afterEach(async () => { delete process.env.PERCY_TOKEN; + delete process.env.PERCY_FORCE_PKG_VALUE; delete process.env.PERCY_CLIENT_ERROR_LOGS; delete snapshot.packageInformation; await server.close(); diff --git a/packages/cli-snapshot/test/sitemap.test.js b/packages/cli-snapshot/test/sitemap.test.js index 0b186a0fc..25746b978 100644 --- a/packages/cli-snapshot/test/sitemap.test.js +++ b/packages/cli-snapshot/test/sitemap.test.js @@ -7,6 +7,7 @@ describe('percy snapshot ', () => { beforeEach(async () => { snapshot.packageInformation = { name: '@percy/cli-snapshot' }; process.env.PERCY_TOKEN = '<>'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); await setupTest(); server = await createTestServer({ @@ -33,6 +34,7 @@ describe('percy snapshot ', () => { afterEach(async () => { delete process.env.PERCY_TOKEN; + delete process.env.PERCY_FORCE_PKG_VALUE; delete snapshot.packageInformation; await server.close(); }); diff --git a/packages/cli-upload/test/upload.test.js b/packages/cli-upload/test/upload.test.js index db023c90d..26afc7a30 100644 --- a/packages/cli-upload/test/upload.test.js +++ b/packages/cli-upload/test/upload.test.js @@ -12,6 +12,7 @@ describe('percy upload', () => { upload.packageInformation = { name: '@percy/cli-upload' }; process.env.PERCY_TOKEN = 'web_<>'; process.env.PERCY_CLIENT_ERROR_LOGS = false; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); await setupTest({ filesystem: { 'images/test-1.png': pixel, diff --git a/packages/client/package.json b/packages/client/package.json index 1f98dfcb9..6a8fda25f 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -35,6 +35,7 @@ "dependencies": { "@percy/env": "1.30.6", "@percy/logger": "1.30.6", + "pac-proxy-agent": "^7.0.2", "pako": "^2.1.0" } } diff --git a/packages/client/src/client.js b/packages/client/src/client.js index e3a93fc91..a889c71a1 100644 --- a/packages/client/src/client.js +++ b/packages/client/src/client.js @@ -19,7 +19,7 @@ import { // Default client API URL can be set with an env var for API development const { PERCY_CLIENT_API_URL = 'https://percy.io/api/v1' } = process.env; -const pkg = getPackageJSON(import.meta.url); +let pkg = getPackageJSON(import.meta.url); // minimum polling interval milliseconds const MIN_POLLING_INTERVAL = 1_000; const INVALID_TOKEN_ERROR_MESSAGE = 'Unable to retrieve snapshot details with write access token. Kindly use a full access token for retrieving snapshot details with Synchronous CLI.'; @@ -83,6 +83,10 @@ export class PercyClient { // Stringifies client and environment info. userAgent() { + // forcedPkgValue has been added since when percy package is bundled inside Electron app (LCNC) + // we can't read Percy's package json for package name and version, so we are passing it via env variables + if (this.env.forcedPkgValue) pkg = this.env.forcedPkgValue; + let client = new Set([`Percy/${/\w+$/.exec(this.apiUrl)}`] .concat(`${pkg.name}/${pkg.version}`, ...this.clientInfo) .filter(Boolean)); diff --git a/packages/client/src/proxy.js b/packages/client/src/proxy.js index 4f5d133af..bd16e49c7 100644 --- a/packages/client/src/proxy.js +++ b/packages/client/src/proxy.js @@ -4,10 +4,28 @@ import http from 'http'; import https from 'https'; import logger from '@percy/logger'; import { stripQuotesAndSpaces } from '@percy/env/utils'; +import { PacProxyAgent } from 'pac-proxy-agent'; const CRLF = '\r\n'; const STATUS_REG = /^HTTP\/1.[01] (\d*)/; +// function to create PAC proxy agent +export function createPacAgent(pacUrl, options = {}) { + pacUrl = stripQuotesAndSpaces(pacUrl); + try { + const agent = new PacProxyAgent(pacUrl, { + keepAlive: true, + ...options + }); + + logger('client:proxy').info(`Successfully loaded PAC file from: ${pacUrl}`); + return agent; + } catch (error) { + logger('client:proxy').error(`Failed to load PAC file, error message: ${error.message}, stack: ${error.stack}`); + throw new Error(`Failed to initialize PAC proxy: ${error.message}`); + } +} + // Returns true if the URL hostname matches any patterns export function hostnameMatches(patterns, url) { let subject = new URL(url); @@ -219,11 +237,31 @@ export function proxyAgentFor(url, options) { let { protocol, hostname } = new URL(url); let cachekey = `${protocol}//${hostname}`; - if (!cache.has(cachekey)) { - cache.set(cachekey, protocol === 'https:' - ? new ProxyHttpsAgent(options) - : new ProxyHttpAgent(options)); + // If we already have a cached agent, return it + if (cache.has(cachekey)) { + return cache.get(cachekey); } - return cache.get(cachekey); + try { + let agent; + const pacUrl = process.env.PERCY_PAC_FILE_URL; + + // If PAC URL is provided, use PAC proxy + if (pacUrl) { + logger('client:proxy').info(`Using PAC file from: ${pacUrl}`); + agent = createPacAgent(pacUrl, options); + } else { + // Fall back to other proxy configuration + agent = protocol === 'https:' + ? new ProxyHttpsAgent(options) + : new ProxyHttpAgent(options); + } + + // Cache the created agent + cache.set(cachekey, agent); + return agent; + } catch (error) { + logger('client:proxy').error(`Failed to create proxy agent: ${error.message}`); + throw error; + } } diff --git a/packages/client/src/utils.js b/packages/client/src/utils.js index 9bb0f81fd..369c14a59 100644 --- a/packages/client/src/utils.js +++ b/packages/client/src/utils.js @@ -135,7 +135,9 @@ export async function request(url, options = {}, callback) { let { protocol, hostname, port, pathname, search, hash } = new URL(url); // reference the default export so tests can mock it - let { default: http } = await import(protocol === 'https:' ? 'https' : 'http'); + // bundling cli inside electron or another package fails if we import it + // like this: await import(protocol === 'https:' ? 'https' : 'http'); + let { default: http } = protocol === 'https:' ? await import('https') : await import('http'); let { proxyAgentFor } = await import('./proxy.js'); // automatically stringify body content diff --git a/packages/client/test/client.test.js b/packages/client/test/client.test.js index 5fd2564d7..a723ea5f6 100644 --- a/packages/client/test/client.test.js +++ b/packages/client/test/client.test.js @@ -15,13 +15,21 @@ describe('PercyClient', () => { await logger.mock({ level: 'debug' }); await api.mock(); delete process.env.PERCY_GZIP; - + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); client = new PercyClient({ token: 'PERCY_TOKEN' }); }); describe('#userAgent()', () => { + it('uses default package value when env.forcedPkgValue is not set', () => { + delete process.env.PERCY_FORCE_PKG_VALUE; + client = new PercyClient({ token: 'PERCY_TOKEN' }); + + expect(client.userAgent()).toMatch( + /^Percy\/v1 @percy\/client\/\S+ \(node\/v[\d.]+.*\)$/ + ); + }); it('contains client and environment information', () => { expect(client.userAgent()).toMatch( /^Percy\/v1 @percy\/client\/\S+ \(node\/v[\d.]+.*\)$/ @@ -38,7 +46,7 @@ describe('PercyClient', () => { expect(client.userAgent()).toMatch( /^Percy\/v1 @percy\/client\/\S+ client-info \(env-info; node\/v[\d.]+.*\)$/ ); - expect(logger.stderr.length).toEqual(2); + expect(logger.stderr.length).toBeGreaterThanOrEqual(2); }); it('it logs a debug warning when no info is passed', async () => { @@ -98,6 +106,17 @@ describe('PercyClient', () => { /^Percy\/v1 @percy\/client\/\S+ client-info \(env-info; node\/v[\d.]+.*\)$/ ); }); + + it('uses forced package value when set', () => { + client.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + client = new PercyClient({ + token: 'PERCY_TOKEN' + }); + client.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + expect(client.userAgent()).toMatch( + /^Percy\/v1 @percy\/client\/1.0.0 \(node\/v[\d.]+.*\)$/ + ); + }); }); describe('#get()', () => { diff --git a/packages/client/test/unit/proxy.test.js b/packages/client/test/unit/proxy.test.js new file mode 100644 index 000000000..7bc93a695 --- /dev/null +++ b/packages/client/test/unit/proxy.test.js @@ -0,0 +1,92 @@ +import { ProxyHttpAgent, ProxyHttpsAgent, createPacAgent, getProxy, proxyAgentFor } from '../../src/proxy.js'; +import { PacProxyAgent } from 'pac-proxy-agent'; +import logger from '@percy/logger/test/helpers'; + +describe('proxy', () => { + describe('getProxy', () => { + beforeEach(async () => { + process.env.http_proxy = 'http://proxy.com:8080'; + }); + + it('should return proxy object if proxy is set', () => { + const options = { protocol: 'http:', hostname: 'example.com' }; + const proxy = getProxy(options); + expect(proxy).toBeInstanceOf(Object); + }); + + it('should return undefined if no proxy is set', () => { + delete process.env.http_proxy; + const options = { protocol: 'http:', hostname: 'example.com' }; + expect(getProxy(options)).toBeUndefined(); + }); + }); + + describe('createPacAgent', () => { + it('should create a PAC proxy agent successfully', () => { + const pacUrl = 'http://example.com/proxy.pac'; + const options = { keepAlive: true }; + const agent = createPacAgent(pacUrl, options); + expect(agent).toBeInstanceOf(PacProxyAgent); + }); + + it('should throw an error if PAC proxy agent creation fails', () => { + const pacUrl = 'http://invalid-url/proxy.pac'; + const options = { keepAlive: true }; + createPacAgent(pacUrl, options); + expect(createPacAgent).toThrow(); + }); + }); + + describe('proxyAgentFor', () => { + beforeEach(async () => { + await logger.mock({ level: 'debug' }); + proxyAgentFor.cache?.clear(); + process.env.PERCY_PAC_FILE_URL = 'http://example.com/proxy.pac'; + }); + + afterEach(async () => { + delete process.env.PERCY_PAC_FILE_URL; + }); + + it('should return cached agent if available', () => { + delete process.env.PERCY_PAC_FILE_URL; + const url = 'http://example.com'; + const options = {}; + const agent = new ProxyHttpAgent(options); + proxyAgentFor.cache?.set('http://example.com', agent); + expect(proxyAgentFor(url, options)).toBe(agent); + }); + + it('should create and cache new HTTP agent if not available', () => { + delete process.env.PERCY_PAC_FILE_URL; + const url = 'http://example.com'; + const options = {}; + const agent = proxyAgentFor(url, options); + expect(agent).toBeInstanceOf(ProxyHttpAgent); + expect(proxyAgentFor.cache.get('http://example.com')).toBe(agent); + }); + + it('should create and cache new HTTPS agent if not available', () => { + delete process.env.PERCY_PAC_FILE_URL; + const url = 'https://example.com'; + const options = {}; + const agent = proxyAgentFor(url, options); + expect(agent).toBeInstanceOf(ProxyHttpsAgent); + expect(proxyAgentFor.cache.get('https://example.com')).toBe(agent); + }); + + it('should create PAC proxy agent if PAC URL is provided', () => { + const url = 'http://example.com'; + const options = {}; + const agent = proxyAgentFor(url, options); + expect(agent).toBeInstanceOf(PacProxyAgent); + }); + + it('logs an error and throws when proxy agent creation fails', () => { + const url = 'http://example.com'; + const options = {}; + process.env.PERCY_PAC_FILE_URL = 'invalid-url'; + expect(() => proxyAgentFor(url, options)).toThrowError('Failed to initialize PAC proxy: Invalid URL: invalid-url'); + }); + }); +}); diff --git a/packages/core/src/api.js b/packages/core/src/api.js index 357f6a0a8..23e29f601 100644 --- a/packages/core/src/api.js +++ b/packages/core/src/api.js @@ -1,13 +1,34 @@ import fs from 'fs'; -import path from 'path'; -import { createRequire } from 'module'; +import path, { dirname, resolve } from 'path'; import logger from '@percy/logger'; import { normalize } from '@percy/config/utils'; import { getPackageJSON, Server, percyAutomateRequestHandler, percyBuildEventHandler } from './utils.js'; import WebdriverUtils from '@percy/webdriver-utils'; import { handleSyncJob } from './snapshot.js'; -// need require.resolve until import.meta.resolve can be transpiled -export const PERCY_DOM = createRequire(import.meta.url).resolve('@percy/dom'); +// Previously, we used `createRequire(import.meta.url).resolve` to resolve the path to the module. +// This approach relied on `createRequire`, which is Node.js-specific and less compatible with modern ESM (ECMAScript Module) standards. +// This was leading to hard coded paths when CLI is used as a dependency in another project. +// Now, we use `fileURLToPath` and `path.resolve` to determine the absolute path in a way that's more aligned with ESM conventions. +// This change ensures better compatibility and avoids relying on Node.js-specific APIs that might cause issues in ESM environments. +import { fileURLToPath } from 'url'; +import { createRequire } from 'module'; + +export const getPercyDomPath = (url) => { + try { + return createRequire(url).resolve('@percy/dom'); + } catch (error) { + logger('core:server').warn([ + 'Failed to resolve @percy/dom path using createRequire.', + 'Falling back to using fileURLToPath and path.resolve.' + ].join(' ')); + } + const __filename = fileURLToPath(import.meta.url); + const __dirname = dirname(__filename); + return resolve(__dirname, 'node_modules/@percy/dom'); +}; + +// Resolved path for PERCY_DOM +export const PERCY_DOM = getPercyDomPath(import.meta.url); // Returns a URL encoded string of nested query params function encodeURLSearchParams(subj, prefix) { diff --git a/packages/core/src/percy.js b/packages/core/src/percy.js index 33316b811..f17d9aaae 100644 --- a/packages/core/src/percy.js +++ b/packages/core/src/percy.js @@ -5,12 +5,11 @@ import { getProxy } from '@percy/client/utils'; import Browser from './browser.js'; import Pako from 'pako'; import { - base64encode - , + base64encode, generatePromise, yieldAll, - yieldTo - , redactSecrets, + yieldTo, + redactSecrets, detectSystemProxyAndLog } from './utils.js'; diff --git a/packages/core/test/api.test.js b/packages/core/test/api.test.js index 7cc581a3e..727f74c1d 100644 --- a/packages/core/test/api.test.js +++ b/packages/core/test/api.test.js @@ -3,6 +3,7 @@ import PercyConfig from '@percy/config'; import { logger, setupTest, fs } from './helpers/index.js'; import Percy from '@percy/core'; import WebdriverUtils from '@percy/webdriver-utils'; +import { getPercyDomPath } from '../src/api.js'; describe('API Server', () => { let percy; @@ -14,6 +15,7 @@ describe('API Server', () => { } beforeEach(async () => { + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); await setupTest(); percy = new Percy({ @@ -25,6 +27,7 @@ describe('API Server', () => { afterEach(async () => { percy.stop.and?.callThrough(); await percy.stop(); + delete process.env.PERCY_FORCE_PKG_VALUE; }); it('has a default port', () => { @@ -35,6 +38,11 @@ describe('API Server', () => { expect(percy).toHaveProperty('server.port', 1337); }); + it('should log on createRequire failure', () => { + getPercyDomPath(undefined); + expect(logger.stderr.length).toBeGreaterThan(0); + }); + it('starts a server at the specified port', async () => { await expectAsync(percy.start()).toBeResolved(); await expectAsync(request('/', false)).toBeResolved(); diff --git a/packages/core/test/discovery.test.js b/packages/core/test/discovery.test.js index c99ba2cfe..05ae96dc4 100644 --- a/packages/core/test/discovery.test.js +++ b/packages/core/test/discovery.test.js @@ -35,6 +35,7 @@ describe('Discovery', () => { beforeEach(async () => { captured = []; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); await setupTest(); originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000; @@ -67,6 +68,7 @@ describe('Discovery', () => { afterEach(async () => { await percy?.stop(true); await server.close(); + delete process.env.PERCY_FORCE_PKG_VALUE; jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout; }); diff --git a/packages/core/test/percy.test.js b/packages/core/test/percy.test.js index 4f2f84b75..620ecc7ff 100644 --- a/packages/core/test/percy.test.js +++ b/packages/core/test/percy.test.js @@ -8,6 +8,7 @@ describe('Percy', () => { let percy, server; beforeEach(async () => { + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); await setupTest(); server = await createTestServer({ @@ -28,6 +29,7 @@ describe('Percy', () => { await percy.stop(); await server.close(); delete process.env.PERCY_TOKEN; + delete process.env.PERCY_FORCE_PKG_VALUE; delete process.env.PERCY_CLIENT_ERROR_LOGS; delete process.env.PERCY_IGNORE_TIMEOUT_ERROR; }); diff --git a/packages/core/test/snapshot-multiple.test.js b/packages/core/test/snapshot-multiple.test.js index 8dc71b1eb..af50e9589 100644 --- a/packages/core/test/snapshot-multiple.test.js +++ b/packages/core/test/snapshot-multiple.test.js @@ -7,6 +7,7 @@ describe('Snapshot multiple', () => { beforeEach(async () => { sitemap = ['/']; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); await setupTest(); percy = await Percy.start({ @@ -32,6 +33,7 @@ describe('Snapshot multiple', () => { }); afterEach(async () => { + delete process.env.PERCY_FORCE_PKG_VALUE; await percy.stop(true); await server?.close(); }); diff --git a/packages/core/test/snapshot.test.js b/packages/core/test/snapshot.test.js index 456b9afa3..04d8f372c 100644 --- a/packages/core/test/snapshot.test.js +++ b/packages/core/test/snapshot.test.js @@ -9,6 +9,7 @@ describe('Snapshot', () => { beforeEach(async () => { testDOM = '

Test

'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); await setupTest(); server = await createTestServer({ @@ -33,6 +34,7 @@ describe('Snapshot', () => { afterEach(async () => { delete process.env.PERCY_CLIENT_ERROR_LOGS; + delete process.env.PERCY_FORCE_PKG_VALUE; await percy.stop(true); await server?.close(); }); diff --git a/packages/core/test/utils.test.js b/packages/core/test/utils.test.js index 3457e44dd..b400bfaf5 100644 --- a/packages/core/test/utils.test.js +++ b/packages/core/test/utils.test.js @@ -8,6 +8,7 @@ describe('utils', () => { beforeEach(async () => { log = percyLogger(); logger.reset(true); + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); await logger.mock({ level: 'debug' }); }); diff --git a/packages/env/src/environment.js b/packages/env/src/environment.js index a99e4d331..a62a635aa 100644 --- a/packages/env/src/environment.js +++ b/packages/env/src/environment.js @@ -297,6 +297,17 @@ export class PercyEnv { get token() { return this.vars.PERCY_TOKEN || null; } + + // PERCY_FORCE_PKG_VALUE for forcing package.json values + // to be used as the current environment values in client + get forcedPkgValue() { + try { + let pkg = this.vars.PERCY_FORCE_PKG_VALUE; + return JSON.parse(pkg) || null; + } catch (e) { + return null; + } + } } // cache getters on initial call so subsequent calls are not re-computed diff --git a/packages/env/test/environment.test.js b/packages/env/test/environment.test.js new file mode 100644 index 000000000..c16f27249 --- /dev/null +++ b/packages/env/test/environment.test.js @@ -0,0 +1,25 @@ +import PercyEnv from '@percy/env'; + +describe('PercyEnv', () => { + describe('forcedPkgValue', () => { + it('should return parsed JSON from PERCY_FORCE_PKG_VALUE', () => { + let env = new PercyEnv({ PERCY_FORCE_PKG_VALUE: JSON.stringify({ name: '@percy/client', version: '1.0.0' }) }); + expect(env.forcedPkgValue).toEqual({ name: '@percy/client', version: '1.0.0' }); + }); + + it('should return null if PERCY_FORCE_PKG_VALUE is not set', () => { + let env = new PercyEnv({}); + expect(env.forcedPkgValue).toBeNull(); + }); + + it('should return null if PERCY_FORCE_PKG_VALUE is invalid JSON', () => { + let env = new PercyEnv({ PERCY_FORCE_PKG_VALUE: 'invalid' }); + expect(env.forcedPkgValue).toBeNull(); + }); + + it('should return null if PERCY_FORCE_PKG_VALUE is null', () => { + let env = new PercyEnv({ PERCY_FORCE_PKG_VALUE: null }); + expect(env.forcedPkgValue).toBeNull(); + }); + }); +});