@percy/sdk-utils 1.0.0 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,55 @@
1
+ import logger from '@percy/logger';
2
+ import percy from './percy-info.js';
3
+ import request from './request.js'; // Create a socket to connect to a remote logger
4
+
5
+ async function connectRemoteLogger() {
6
+ await logger.remote(async () => {
7
+ let url = percy.address.replace('http', 'ws');
8
+
9
+ if (process.env.__PERCY_BROWSERIFIED__) {
10
+ return new window.WebSocket(url);
11
+ } else {
12
+ /* eslint-disable-next-line import/no-extraneous-dependencies */
13
+ let {
14
+ default: WebSocket
15
+ } = await import('ws');
16
+ let ws = new WebSocket(url); // allow node to exit with an active connection
17
+
18
+ return ws.once('open', () => ws._socket.unref());
19
+ }
20
+ });
21
+ } // Check if Percy is enabled using the healthcheck endpoint
22
+
23
+
24
+ export async function isPercyEnabled() {
25
+ if (percy.enabled == null) {
26
+ let log = logger('utils');
27
+ let error;
28
+
29
+ try {
30
+ let response = await request('/percy/healthcheck');
31
+ percy.version = response.headers['x-percy-core-version'];
32
+ percy.config = response.body.config;
33
+ percy.enabled = true;
34
+ } catch (e) {
35
+ percy.enabled = false;
36
+ error = e;
37
+ }
38
+
39
+ if (percy.enabled && percy.version.major !== 1) {
40
+ log.info('Unsupported Percy CLI version, disabling snapshots');
41
+ log.debug(`Found version: ${percy.version}`);
42
+ percy.enabled = false;
43
+ } else if (!percy.enabled) {
44
+ log.info('Percy is not running, disabling snapshots');
45
+ log.debug(error);
46
+ }
47
+
48
+ if (percy.enabled) {
49
+ await connectRemoteLogger();
50
+ }
51
+ }
52
+
53
+ return percy.enabled;
54
+ }
55
+ export default isPercyEnabled;
@@ -0,0 +1,10 @@
1
+ import request from './request.js';
2
+ const RETRY_ERROR_CODES = ['ECONNRESET', 'ETIMEDOUT'];
3
+ export async function waitForPercyIdle() {
4
+ try {
5
+ return !!(await request('/percy/idle'));
6
+ } catch (e) {
7
+ return RETRY_ERROR_CODES.includes(e.code) && waitForPercyIdle();
8
+ }
9
+ }
10
+ export default waitForPercyIdle;
@@ -0,0 +1,59 @@
1
+ // helper to create a version object from a string
2
+ function toVersion(str) {
3
+ str || (str = '0.0.0');
4
+ return str.split(/\.|-/).reduce((version, part, i) => {
5
+ let v = parseInt(part, 10);
6
+ version[i] = isNaN(v) ? part : v;
7
+ return version;
8
+ }, {
9
+ get major() {
10
+ return this[0] || 0;
11
+ },
12
+
13
+ get minor() {
14
+ return this[1] || 0;
15
+ },
16
+
17
+ get patch() {
18
+ return this[2] || 0;
19
+ },
20
+
21
+ get prerelease() {
22
+ return this[3];
23
+ },
24
+
25
+ get build() {
26
+ return this[4];
27
+ },
28
+
29
+ toString() {
30
+ return str;
31
+ }
32
+
33
+ });
34
+ } // private version cache
35
+
36
+
37
+ let version = toVersion(); // contains local percy info
38
+
39
+ const info = {
40
+ // get or set the CLI API address via the environment
41
+ get address() {
42
+ return process.env.PERCY_SERVER_ADDRESS || 'http://localhost:5338';
43
+ },
44
+
45
+ set address(addr) {
46
+ return process.env.PERCY_SERVER_ADDRESS = addr;
47
+ },
48
+
49
+ // version information
50
+ get version() {
51
+ return version;
52
+ },
53
+
54
+ set version(v) {
55
+ return version = toVersion(v);
56
+ }
57
+
58
+ };
59
+ export default info;
@@ -0,0 +1,17 @@
1
+ import percy from './percy-info.js';
2
+ import request from './request.js'; // Post snapshot data to the snapshot endpoint. If the snapshot endpoint responds with a closed
3
+ // error message, signal that Percy has been disabled.
4
+
5
+ export async function postSnapshot(options, params) {
6
+ let query = params ? `?${new URLSearchParams(params)}` : '';
7
+ await request.post(`/percy/snapshot${query}`, options).catch(err => {
8
+ var _err$response, _err$response$body, _err$response$body$bu;
9
+
10
+ if ((_err$response = err.response) !== null && _err$response !== void 0 && (_err$response$body = _err$response.body) !== null && _err$response$body !== void 0 && (_err$response$body$bu = _err$response$body.build) !== null && _err$response$body$bu !== void 0 && _err$response$body$bu.error) {
11
+ percy.enabled = false;
12
+ } else {
13
+ throw err;
14
+ }
15
+ });
16
+ }
17
+ export default postSnapshot;
@@ -0,0 +1,65 @@
1
+ import percy from './percy-info.js'; // Helper to send a request to the local CLI API
2
+
3
+ export async function request(path, options = {}) {
4
+ let response = await request.fetch(`${percy.address}${path}`, options); // maybe parse response body as json
5
+
6
+ if (typeof response.body === 'string' && response.headers['content-type'] === 'application/json') {
7
+ try {
8
+ response.body = JSON.parse(response.body);
9
+ } catch (e) {}
10
+ } // throw an error if status is not ok
11
+
12
+
13
+ if (!(response.status >= 200 && response.status < 300)) {
14
+ throw Object.assign(new Error(), {
15
+ message: response.body.error || `${response.status} ${response.statusText}`,
16
+ response
17
+ });
18
+ }
19
+
20
+ return response;
21
+ }
22
+
23
+ request.post = function post(url, json) {
24
+ return request(url, {
25
+ method: 'POST',
26
+ body: JSON.stringify(json)
27
+ });
28
+ }; // environment specific implementation
29
+
30
+
31
+ if (process.env.__PERCY_BROWSERIFIED__) {
32
+ // use window.fetch in browsers
33
+ const winFetch = window.fetch;
34
+
35
+ request.fetch = async function fetch(url, options) {
36
+ let response = await winFetch(url, options);
37
+ return {
38
+ status: response.status,
39
+ statusText: response.statusText,
40
+ headers: Object.fromEntries(response.headers.entries()),
41
+ body: await response.text()
42
+ };
43
+ };
44
+ } else {
45
+ // use http.request in node
46
+ request.fetch = async function fetch(url, options) {
47
+ let {
48
+ default: http
49
+ } = await import('http');
50
+ return new Promise((resolve, reject) => {
51
+ http.request(url, options).on('response', response => {
52
+ let body = '';
53
+ response.on('data', chunk => body += chunk.toString());
54
+ response.on('end', () => resolve({
55
+ status: response.statusCode,
56
+ statusText: response.statusMessage,
57
+ headers: response.headers,
58
+ body
59
+ }));
60
+ }).on('error', reject).end(options.body);
61
+ });
62
+ };
63
+ }
64
+
65
+ export default request;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@percy/sdk-utils",
3
- "version": "1.0.0",
3
+ "version": "1.0.3",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -14,10 +14,10 @@
14
14
  "node": ">=14"
15
15
  },
16
16
  "files": [
17
- "./dist",
18
- "./test/server.js",
19
- "./test/client.js",
20
- "./test/helpers.js"
17
+ "dist",
18
+ "test/server.js",
19
+ "test/client.js",
20
+ "test/helpers.js"
21
21
  ],
22
22
  "main": "./dist/index.js",
23
23
  "browser": "./dist/bundle.js",
@@ -58,7 +58,7 @@
58
58
  }
59
59
  },
60
60
  "dependencies": {
61
- "@percy/logger": "1.0.0"
61
+ "@percy/logger": "1.0.3"
62
62
  },
63
- "gitHead": "6df509421a60144e4f9f5d59dc57a5675372a0b2"
63
+ "gitHead": "a259d5cff0933711bced21a979c577e70765d318"
64
64
  }
package/test/client.js ADDED
@@ -0,0 +1,278 @@
1
+ (function() {
2
+ this["null"] = this["null"] || {};
3
+ this.PercySDKUtils.TestHelpers = (function (logger, utils) {
4
+ 'use strict';
5
+
6
+ const process = (typeof globalThis !== "undefined" && globalThis.process) || {};
7
+ process.env = process.env || {};
8
+ process.env.__PERCY_BROWSERIFIED__ = true;
9
+
10
+ globalThis.process = globalThis.process || process;
11
+
12
+ function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
13
+
14
+ var logger__default = /*#__PURE__*/_interopDefaultLegacy(logger);
15
+ var utils__default = /*#__PURE__*/_interopDefaultLegacy(utils);
16
+
17
+ const {
18
+ assign,
19
+ entries
20
+ } = Object; // matches ansi escape sequences
21
+
22
+ const ANSI_REG = new RegExp('[\\u001B\\u009B][[\\]()#;?]*((?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)' + '|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))', 'g'); // color names by ansi escape code
23
+
24
+ const ANSI_COLORS = {
25
+ '91m': 'red',
26
+ '32m': 'green',
27
+ '93m': 'yellow',
28
+ '34m': 'blue',
29
+ '95m': 'magenta',
30
+ '90m': 'grey'
31
+ }; // colorize each line of a string using an ansi escape sequence
32
+
33
+ const LINE_REG = /^.*$/gm;
34
+
35
+ function colorize(code, str) {
36
+ return str.replace(LINE_REG, line => `\u001b[${code}${line}\u001b[39m`);
37
+ } // map ansi colors to bound colorize functions
38
+
39
+
40
+ entries(ANSI_COLORS).reduce((colors, _ref) => {
41
+ let [code, name] = _ref;
42
+ return assign(colors, {
43
+ [name]: colorize.bind(null, code)
44
+ });
45
+ }, {});
46
+
47
+ const ELAPSED_REG = /\s\S*?\(\d+ms\)\S*/;
48
+ const NEWLINE_REG = /\r\n/g;
49
+ const LASTLINE_REG = /\n$/;
50
+
51
+ function sanitizeLog(str) {
52
+ let {
53
+ ansi,
54
+ elapsed
55
+ } = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
56
+ // normalize line endings
57
+ str = str.replace(NEWLINE_REG, '\n'); // strip ansi colors
58
+
59
+ if (!ansi) str = str.replace(ANSI_REG, ''); // strip elapsed time
60
+
61
+ if (!elapsed) str = str.replace(ELAPSED_REG, ''); // strip trailing line endings
62
+
63
+ return str.replace(LASTLINE_REG, '');
64
+ }
65
+
66
+ function spy(object, method, func) {
67
+ if (object[method].restore) object[method].restore();
68
+ let spy = Object.assign(function spy() {
69
+ for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
70
+ args[_key] = arguments[_key];
71
+ }
72
+
73
+ spy.calls.push(args);
74
+ if (func) return func.apply(this, args);
75
+ }, {
76
+ restore: () => object[method] = spy.originalValue,
77
+ reset: () => (spy.calls.length = 0) || spy,
78
+ originalValue: object[method],
79
+ calls: []
80
+ });
81
+ object[method] = spy;
82
+ return spy;
83
+ }
84
+
85
+ const {
86
+ Logger,
87
+ loglevel
88
+ } = logger__default["default"];
89
+ const helpers$1 = {
90
+ stdout: [],
91
+ stderr: [],
92
+ loglevel,
93
+
94
+ async mock() {
95
+ let options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
96
+ helpers$1.reset();
97
+
98
+ if (process.env.__PERCY_BROWSERIFIED__) {
99
+ spy(Logger.prototype, 'write', function (lvl, msg) {
100
+ let stdio = lvl === 'info' ? 'stdout' : 'stderr';
101
+ helpers$1[stdio].push(sanitizeLog(msg, options));
102
+ return this.write.originalValue.call(this, lvl, msg);
103
+ });
104
+ spy(console, 'log');
105
+ spy(console, 'warn');
106
+ spy(console, 'error');
107
+ } else {
108
+ let {
109
+ Writable
110
+ } = await import('stream');
111
+
112
+ for (let stdio of ['stdout', 'stderr']) {
113
+ Logger[stdio] = Object.assign(new Writable(), {
114
+ columns: options.isTTY ? 100 : null,
115
+ isTTY: options.isTTY,
116
+
117
+ cursorTo() {},
118
+
119
+ clearLine() {},
120
+
121
+ _write(chunk, encoding, callback) {
122
+ helpers$1[stdio].push(sanitizeLog(chunk.toString(), options));
123
+ callback();
124
+ }
125
+
126
+ });
127
+ }
128
+ }
129
+ },
130
+
131
+ reset(soft) {
132
+ if (soft) loglevel('info');else delete Logger.instance;
133
+ helpers$1.stdout.length = 0;
134
+ helpers$1.stderr.length = 0;
135
+
136
+ if (console.log.reset) {
137
+ console.log.reset();
138
+ console.warn.reset();
139
+ console.error.reset();
140
+ }
141
+ },
142
+
143
+ dump() {
144
+ let msgs = Array.from(Logger.instance && Logger.instance.messages || []);
145
+ if (!msgs.length) return;
146
+
147
+ let log = m => process.env.__PERCY_BROWSERIFIED__ ? console.log.and ? console.log.and.originalFn(m) : console.log(m) : process.stderr.write(`${m}\n`);
148
+
149
+ logger__default["default"].loglevel('debug');
150
+ log(logger__default["default"].format('testing', 'warn', '--- DUMPING LOGS ---'));
151
+ msgs.reduce((last, _ref) => {
152
+ let {
153
+ debug,
154
+ level,
155
+ message,
156
+ timestamp
157
+ } = _ref;
158
+ log(logger__default["default"].format(debug, level, message, timestamp - last));
159
+ return timestamp;
160
+ }, msgs[0].timestamp);
161
+ }
162
+
163
+ };
164
+
165
+ const helpers = {
166
+ logger: helpers$1,
167
+
168
+ async setup() {
169
+ utils__default["default"].percy.version = '';
170
+ delete utils__default["default"].percy.config;
171
+ delete utils__default["default"].percy.enabled;
172
+ delete utils__default["default"].percy.domScript;
173
+ delete process.env.PERCY_SERVER_ADDRESS;
174
+ await helpers.call('server.mock');
175
+ await helpers$1.mock();
176
+ },
177
+
178
+ teardown: () => helpers.call('server.close'),
179
+ getRequests: () => helpers.call('server.requests'),
180
+ testReply: (path, reply) => helpers.call('server.reply', path, reply),
181
+ testFailure: function () {
182
+ for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
183
+ args[_key] = arguments[_key];
184
+ }
185
+
186
+ return helpers.call('server.test.failure', ...args);
187
+ },
188
+ testError: path => helpers.call('server.test.error', path),
189
+ testSerialize: fn => !fn ? helpers.call('server.test.serialize') // get
190
+ : helpers.call('server.test.serialize', fn),
191
+ // set
192
+ mockSite: () => helpers.call('site.mock'),
193
+ closeSite: () => helpers.call('site.close')
194
+ };
195
+
196
+ if (process.env.__PERCY_BROWSERIFIED__) {
197
+ helpers.call = async function call(event) {
198
+ for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
199
+ args[_key2 - 1] = arguments[_key2];
200
+ }
201
+
202
+ let {
203
+ socket,
204
+ pending = {}
205
+ } = helpers.call;
206
+
207
+ if (!socket) {
208
+ socket = new window.WebSocket('ws://localhost:5339');
209
+ await new Promise((resolve, reject) => {
210
+ let done = event => {
211
+ clearTimeout(timeoutid);
212
+ socket.onopen = socket.onerror = null;
213
+
214
+ if (event && (event.error || event.type === 'error')) {
215
+ reject(event.error || new Error('Test client connection failed'));
216
+ } else resolve(socket);
217
+ };
218
+
219
+ let timeoutid = setTimeout(done, 1000, {
220
+ error: new Error('Test client connection timed out')
221
+ });
222
+ socket.onopen = socket.onerror = done;
223
+ });
224
+
225
+ socket.onmessage = _ref => {
226
+ let {
227
+ data
228
+ } = _ref;
229
+ let {
230
+ id,
231
+ resolve,
232
+ reject
233
+ } = JSON.parse(data);
234
+ if (!pending[id]) return;
235
+ if (resolve) pending[id].resolve(resolve.result);
236
+ if (reject) pending[id].reject(reject.error);
237
+ };
238
+
239
+ Object.assign(helpers.call, {
240
+ socket,
241
+ pending
242
+ });
243
+ }
244
+
245
+ let id = helpers.call.uid = (helpers.call.uid || 0) + 1;
246
+ args = args.map(a => typeof a === 'function' ? a.toString() : a);
247
+ socket.send(JSON.stringify({
248
+ id,
249
+ event,
250
+ args
251
+ }));
252
+ return (pending[id] = {}).promise = new Promise((resolve, reject) => {
253
+ Object.assign(pending[id], {
254
+ resolve,
255
+ reject
256
+ });
257
+ });
258
+ };
259
+ } else {
260
+ helpers.call = async function call() {
261
+ let {
262
+ context
263
+ } = await import('./server.js');
264
+ helpers.context = helpers.context || (await context());
265
+ return helpers.context.call(...arguments);
266
+ };
267
+ }
268
+
269
+ return helpers;
270
+
271
+ })(PercySDKUtils.logger, PercySDKUtils);
272
+ }).call(window);
273
+
274
+ if (typeof define === "function" && define.amd) {
275
+ define([], () => window.PercySDKUtils.TestHelpers);
276
+ } else if (typeof module === "object" && module.exports) {
277
+ module.exports = window.PercySDKUtils.TestHelpers;
278
+ }
@@ -0,0 +1,80 @@
1
+ import logger from '@percy/logger/test/helpers';
2
+ import utils from '@percy/sdk-utils';
3
+
4
+ export const helpers = {
5
+ logger,
6
+
7
+ async setup() {
8
+ utils.percy.version = '';
9
+ delete utils.percy.config;
10
+ delete utils.percy.enabled;
11
+ delete utils.percy.domScript;
12
+ delete process.env.PERCY_SERVER_ADDRESS;
13
+ await helpers.call('server.mock');
14
+ await logger.mock();
15
+ },
16
+
17
+ teardown: () => helpers.call('server.close'),
18
+ getRequests: () => helpers.call('server.requests'),
19
+ testReply: (path, reply) => helpers.call('server.reply', path, reply),
20
+ testFailure: (...args) => helpers.call('server.test.failure', ...args),
21
+ testError: path => helpers.call('server.test.error', path),
22
+ testSerialize: fn => !fn
23
+ ? helpers.call('server.test.serialize') // get
24
+ : helpers.call('server.test.serialize', fn), // set
25
+ mockSite: () => helpers.call('site.mock'),
26
+ closeSite: () => helpers.call('site.close')
27
+ };
28
+
29
+ if (process.env.__PERCY_BROWSERIFIED__) {
30
+ helpers.call = async function call(event, ...args) {
31
+ let { socket, pending = {} } = helpers.call;
32
+
33
+ if (!socket) {
34
+ socket = new window.WebSocket('ws://localhost:5339');
35
+
36
+ await new Promise((resolve, reject) => {
37
+ let done = event => {
38
+ clearTimeout(timeoutid);
39
+ socket.onopen = socket.onerror = null;
40
+ if (event && (event.error || event.type === 'error')) {
41
+ reject(event.error || new Error('Test client connection failed'));
42
+ } else resolve(socket);
43
+ };
44
+
45
+ let timeoutid = setTimeout(done, 1000, {
46
+ error: new Error('Test client connection timed out')
47
+ });
48
+
49
+ socket.onopen = socket.onerror = done;
50
+ });
51
+
52
+ socket.onmessage = ({ data }) => {
53
+ let { id, resolve, reject } = JSON.parse(data);
54
+ if (!pending[id]) return;
55
+ if (resolve) pending[id].resolve(resolve.result);
56
+ if (reject) pending[id].reject(reject.error);
57
+ };
58
+
59
+ Object.assign(helpers.call, { socket, pending });
60
+ }
61
+
62
+ let id = helpers.call.uid = (helpers.call.uid || 0) + 1;
63
+ args = args.map(a => typeof a === 'function' ? a.toString() : a);
64
+ socket.send(JSON.stringify({ id, event, args }));
65
+
66
+ return ((pending[id] = {}).promise = (
67
+ new Promise((resolve, reject) => {
68
+ Object.assign(pending[id], { resolve, reject });
69
+ })
70
+ ));
71
+ };
72
+ } else {
73
+ helpers.call = async function call() {
74
+ let { context } = await import('./server.js');
75
+ helpers.context = (helpers.context || await context());
76
+ return helpers.context.call(...arguments);
77
+ };
78
+ }
79
+
80
+ export default helpers;