@applitools/visual-grid-cli-utils 1.21.43 → 1.21.45

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.
package/README.md CHANGED
@@ -13,3 +13,20 @@ npm install -g @applitools/visual-grid-cli-utils
13
13
  ## Using it
14
14
 
15
15
  * To get all the options, do `visual-grid-cli-utils --help`
16
+
17
+ ### `view-rendering <render-id> --raw`
18
+
19
+ * Instead of printing the view-rendering URL, `--raw` fetches a time-limited **signed URL to the
20
+ render's raw (unparsed) root-DOM CDT** and prints it together with its expiration.
21
+ * It calls the rendering-api-app `/render` endpoint with the `rawCdtOnly` option; the server
22
+ enforces ownership (you must own the render, or be an admin).
23
+ * Only renders created on or after **2026-06-26** are supported — the raw root-DOM CDT path
24
+ (`rdvsMetadata.rootDomRawResourcePath`) started being recorded with AD-14694. The feature is
25
+ not supported for renders created before then; the command prints a `404` hint explaining this.
26
+
27
+ ```sh
28
+ vg-cli view-rendering <render-id> --raw
29
+ # Raw root-DOM CDT URL:
30
+ # https://storage.googleapis.com/rg-raw-.../<accountId>/<resourceId>?X-Goog-Signature=...
31
+ # Expires at: 2026-06-28T12:00:00.000Z (TTL 900s)
32
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applitools/visual-grid-cli-utils",
3
- "version": "1.21.43",
3
+ "version": "1.21.45",
4
4
  "description": "",
5
5
  "main": "src/visual-grid-cli-utils.js",
6
6
  "engines": {
@@ -0,0 +1,27 @@
1
+ 'use strict'
2
+
3
+ function buildSafariEmulationInfo({
4
+ safariEmulationDeviceName,
5
+ safariEmulationOrientation,
6
+ safariEmulationViewport,
7
+ safariEmulationPixelRatio,
8
+ safariEmulationUserAgent,
9
+ }) {
10
+ const anyFlagSet = !!(
11
+ safariEmulationDeviceName ||
12
+ safariEmulationViewport ||
13
+ safariEmulationPixelRatio !== undefined ||
14
+ safariEmulationUserAgent
15
+ )
16
+ if (!anyFlagSet) return undefined
17
+
18
+ const info = {}
19
+ if (safariEmulationDeviceName) info.deviceName = safariEmulationDeviceName
20
+ if (safariEmulationViewport) info.viewport = safariEmulationViewport
21
+ if (safariEmulationPixelRatio !== undefined) info.pixelRatio = safariEmulationPixelRatio
22
+ if (safariEmulationUserAgent) info.userAgent = safariEmulationUserAgent
23
+ info.screenOrientation = safariEmulationOrientation || 'portrait'
24
+ return info
25
+ }
26
+
27
+ module.exports = buildSafariEmulationInfo
@@ -0,0 +1,34 @@
1
+ 'use strict'
2
+ const {fetch: defaultFetch} = require('@applitools/http-commons')
3
+
4
+ // Fetch a time-limited signed URL for a render's raw (unparsed) root-DOM CDT by calling the
5
+ // rendering-api-app `/render` endpoint with the `rawCdtOnly` flag (AD-14682). The endpoint
6
+ // returns `{ url, expiresAt, ttlSeconds }` and enforces ownership server-side. `fetch` is
7
+ // injectable for testing; it defaults to the shared http-commons fetch.
8
+ async function fetchRawCdtUrl({vgUrl, renderId, authToken, agentId, fetch = defaultFetch}) {
9
+ const renderUrl = new URL('./render', vgUrl)
10
+ const response = await fetch(renderUrl, {
11
+ method: 'POST',
12
+ headers: {
13
+ 'Content-Type': 'application/json',
14
+ 'X-Auth-Token': authToken,
15
+ },
16
+ body: JSON.stringify({
17
+ reRenderId: renderId,
18
+ rawCdtOnly: true,
19
+ agentId,
20
+ options: {
21
+ rawCdtOnly: true,
22
+ },
23
+ }),
24
+ })
25
+
26
+ if (!response.ok) {
27
+ const errorBody = await response.text()
28
+ throw new Error(`Failed to fetch raw CDT URL - HTTP ${response.status}: ${errorBody}`)
29
+ }
30
+
31
+ return response.json()
32
+ }
33
+
34
+ module.exports = fetchRawCdtUrl
@@ -0,0 +1,11 @@
1
+ 'use strict'
2
+
3
+ function parseViewport(arg) {
4
+ const match = /^(\d+)x(\d+)$/i.exec(arg)
5
+ if (!match) {
6
+ throw new Error(`invalid viewport "${arg}". Expected format: WIDTHxHEIGHT (e.g., 393x695)`)
7
+ }
8
+ return {width: parseInt(match[1], 10), height: parseInt(match[2], 10)}
9
+ }
10
+
11
+ module.exports = parseViewport
package/src/job-info.js CHANGED
@@ -3,6 +3,7 @@ const {fetchAsJsonWithJsonBody} = require('@applitools/http-commons')
3
3
  const createSession = require('./commons/create-session')
4
4
  const {convertBrowserNameToCanary} = require('./commons/parse-browser')
5
5
  const parseJson = require('./commons/parse-json.js')
6
+ const buildSafariEmulationInfo = require('./commons/build-safari-emulation-info')
6
7
 
7
8
  async function main({
8
9
  apiKey,
@@ -13,6 +14,11 @@ async function main({
13
14
  native,
14
15
  deviceEmulation,
15
16
  deviceOrientation,
17
+ safariEmulationDeviceName,
18
+ safariEmulationOrientation,
19
+ safariEmulationViewport,
20
+ safariEmulationPixelRatio,
21
+ safariEmulationUserAgent,
16
22
  iosDeviceName,
17
23
  iosDeviceOrientation,
18
24
  androidDeviceName,
@@ -29,6 +35,14 @@ async function main({
29
35
  options.chromeHeadless = headless
30
36
  const vgUrl = process.env.VG_URL || 'https://render-wus.applitools.com'
31
37
 
38
+ const safariEmulationInfo = buildSafariEmulationInfo({
39
+ safariEmulationDeviceName,
40
+ safariEmulationOrientation,
41
+ safariEmulationViewport,
42
+ safariEmulationPixelRatio,
43
+ safariEmulationUserAgent,
44
+ })
45
+
32
46
  const {webhook, authToken} = await createSession(apiKey, serverUrl)
33
47
 
34
48
  const renderInfoUrl = new URL('./job-info', vgUrl)
@@ -38,9 +52,11 @@ async function main({
38
52
  accountOverride,
39
53
  webhook,
40
54
  renderInfo: {
41
- emulationInfo: deviceEmulation
42
- ? {deviceName: deviceEmulation, screenOrientation: deviceOrientation}
43
- : undefined,
55
+ emulationInfo:
56
+ safariEmulationInfo ||
57
+ (deviceEmulation
58
+ ? {deviceName: deviceEmulation, screenOrientation: deviceOrientation}
59
+ : undefined),
44
60
  iosDeviceInfo: iosDeviceName
45
61
  ? {
46
62
  name: iosDeviceName,
@@ -56,8 +72,8 @@ async function main({
56
72
  }
57
73
  : undefined,
58
74
  vhsType: androidDeviceName ? androidVhsType : undefined,
59
- width: deviceEmulation || iosDeviceName ? undefined : width,
60
- height: deviceEmulation || iosDeviceName ? undefined : height,
75
+ width: deviceEmulation || iosDeviceName || safariEmulationInfo ? undefined : width,
76
+ height: deviceEmulation || iosDeviceName || safariEmulationInfo ? undefined : height,
61
77
  target: 'full-page',
62
78
  },
63
79
  agentId: `visual-grid-cli-utils/${require('../package.json').version}`,
package/src/rerender.js CHANGED
@@ -12,6 +12,7 @@ const {convertBrowserNameToCanary} = require('./commons/parse-browser')
12
12
  const parseJson = require('./commons/parse-json.js')
13
13
  const normalizeChainedSelectorsArray = require('./commons/normalizeChainedSelectorsArray')
14
14
  const normalizeChainedSelectorsToFindRegionsForArray = require('./commons/normalizeChainedSelectorsToFindRegionsForArray')
15
+ const buildSafariEmulationInfo = require('./commons/build-safari-emulation-info')
15
16
 
16
17
  async function main({
17
18
  apiKey,
@@ -26,6 +27,11 @@ async function main({
26
27
  headless,
27
28
  deviceEmulation,
28
29
  deviceOrientation,
30
+ safariEmulationDeviceName,
31
+ safariEmulationOrientation,
32
+ safariEmulationViewport,
33
+ safariEmulationPixelRatio,
34
+ safariEmulationUserAgent,
29
35
  androidDeviceName,
30
36
  androidVersion,
31
37
  androidDeviceOrientation,
@@ -64,6 +70,14 @@ async function main({
64
70
  options.chromeHeadless = headless
65
71
  const vgUrl = process.env.VG_URL || 'https://render-wus.applitools.com'
66
72
 
73
+ const safariEmulationInfo = buildSafariEmulationInfo({
74
+ safariEmulationDeviceName,
75
+ safariEmulationOrientation,
76
+ safariEmulationViewport,
77
+ safariEmulationPixelRatio,
78
+ safariEmulationUserAgent,
79
+ })
80
+
67
81
  const finalOutputFile = outputFile || path.join(fs.mkdtempSync(os.tmpdir() + '/'), 'rerender.png')
68
82
  const domSnapshotOutputFile = finalOutputFile.replace(/\.[a-zA-Z0-0]+$/, '.json')
69
83
  const domSnapshotHtml = finalOutputFile.replace(/\.[a-zA-Z0-0]+$/, '.html')
@@ -75,9 +89,11 @@ async function main({
75
89
  accountOverride,
76
90
  webhook,
77
91
  renderInfo: {
78
- emulationInfo: deviceEmulation
79
- ? {deviceName: deviceEmulation, screenOrientation: deviceOrientation}
80
- : undefined,
92
+ emulationInfo:
93
+ safariEmulationInfo ||
94
+ (deviceEmulation
95
+ ? {deviceName: deviceEmulation, screenOrientation: deviceOrientation}
96
+ : undefined),
81
97
  iosDeviceInfo: iosDeviceName
82
98
  ? {
83
99
  name: iosDeviceName,
@@ -99,8 +115,8 @@ async function main({
99
115
  }
100
116
  : undefined,
101
117
  vhsType: androidDeviceName ? androidVhsType : undefined,
102
- width: deviceEmulation || iosDeviceName ? undefined : width,
103
- height: deviceEmulation || iosDeviceName ? undefined : height,
118
+ width: deviceEmulation || iosDeviceName || safariEmulationInfo ? undefined : width,
119
+ height: deviceEmulation || iosDeviceName || safariEmulationInfo ? undefined : height,
104
120
  target,
105
121
  selector: selector
106
122
  ? {selector, type: selector.startsWith('//') ? 'xpath' : 'css'}
@@ -3,6 +3,7 @@ const {exec} = require('child_process')
3
3
  const retry = require('p-retry')
4
4
  const createSession = require('./commons/create-session')
5
5
  const calculateViewRenderingUrl = require('./commons/view-rendering-url')
6
+ const fetchRawCdtUrl = require('./commons/fetch-raw-cdt-url')
6
7
  const {fetch, throwErrorFromBadStatus} = require('@applitools/http-commons')
7
8
  const fs = require('fs')
8
9
 
@@ -52,7 +53,9 @@ async function waitForParsedStatus(vgUrl, renderId, authToken, waitTimeout = 5 *
52
53
  )
53
54
  }
54
55
 
55
- async function tryNewViewRenderingEndpoint(vgUrl, renderId, authToken, apiKey, serverUrl) {
56
+ // `_authToken` is unused this path mints its own session below; the `_` prefix satisfies the
57
+ // no-unused-vars rule (was a pre-existing lint error on this line before the --raw change).
58
+ async function tryNewViewRenderingEndpoint(vgUrl, renderId, _authToken, apiKey, serverUrl) {
56
59
  try {
57
60
  console.log(`[1/3] Starting resource parsing for render: ${renderId}`)
58
61
  // Step 1: Call rerender API with parseResourcesOnly flag
@@ -121,6 +124,7 @@ async function main({
121
124
  serverUrl,
122
125
  accountOverride,
123
126
  downloadVhsFolder,
127
+ raw,
124
128
  }) {
125
129
  if (!apiKey) {
126
130
  throw new Error(
@@ -134,6 +138,43 @@ async function main({
134
138
 
135
139
  const vgUrl = process.env.VG_URL || 'https://render-wus.applitools.com'
136
140
 
141
+ // --raw: fetch a signed URL to the render's raw root-DOM CDT (via the rawCdtOnly /render
142
+ // option, AD-14682) and print it + its expiration, instead of the view-rendering URL.
143
+ if (raw) {
144
+ const agentId = `visual-grid-cli-utils/${require('../package.json').version}`
145
+ let rawCdt
146
+ try {
147
+ rawCdt = await fetchRawCdtUrl({
148
+ vgUrl,
149
+ renderId,
150
+ authToken: finalAuthToken,
151
+ agentId,
152
+ })
153
+ } catch (err) {
154
+ // A 404 most commonly means the render has no stored raw root-DOM CDT path. That path
155
+ // (rdvsMetadata.rootDomRawResourcePath) only started being recorded with AD-14694, so
156
+ // the feature isn't supported for renders created before then — surface a gentle hint.
157
+ if (String(err.message).includes('HTTP 404')) {
158
+ console.log('\n⚠ No raw root-DOM CDT URL is available for this render (404).')
159
+ console.log(
160
+ ' This feature supports renders created on or after 2026-06-26; it is not supported',
161
+ )
162
+ console.log(' for renders created before then.')
163
+ console.log(`\n Server response: ${err.message}`)
164
+ return
165
+ }
166
+ throw err
167
+ }
168
+
169
+ const {url, expiresAt, ttlSeconds} = rawCdt
170
+ console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
171
+ console.log('Raw root-DOM CDT URL:')
172
+ console.log(url)
173
+ console.log(`Expires at: ${expiresAt} (TTL ${ttlSeconds}s)`)
174
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
175
+ return
176
+ }
177
+
137
178
  // Try new view-rendering endpoint with rerender API
138
179
  const result = await tryNewViewRenderingEndpoint(
139
180
  vgUrl,
@@ -2,6 +2,7 @@
2
2
  const yargs = require('yargs')
3
3
  const {parseBrowser} = require('./commons/parse-browser')
4
4
  const parseRegion = require('./commons/parse-region')
5
+ const parseViewport = require('./commons/parse-viewport')
5
6
  const splitApiKey = require('./commons/split-api-key')
6
7
 
7
8
  async function main(argv, {shouldExitOnError = false} = {}) {
@@ -39,6 +40,12 @@ async function main(argv, {shouldExitOnError = false} = {}) {
39
40
  .option('download-vhs-folder', {
40
41
  describe: 'Specify a folder to which download the vhs file (in case of VHS snapshot)',
41
42
  type: 'string',
43
+ })
44
+ .option('raw', {
45
+ describe:
46
+ "fetch a signed URL to the render's raw root-DOM CDT (and its expiration) instead of the view-rendering url",
47
+ default: false,
48
+ type: 'boolean',
42
49
  }),
43
50
  )
44
51
  .command('view-resource <hash>', 'view resource url', (yargs) =>
@@ -128,6 +135,29 @@ async function main(argv, {shouldExitOnError = false} = {}) {
128
135
  choices: ['portrait', 'landscape'],
129
136
  default: 'portrait',
130
137
  })
138
+ .option('safari-emulation-device-name', {
139
+ describe: 'the device name to use for Safari emulation (e.g., "iPhone 16")',
140
+ type: 'string',
141
+ })
142
+ .option('safari-emulation-orientation', {
143
+ describe: 'portrait or landscape for Safari emulation',
144
+ type: 'string',
145
+ choices: ['portrait', 'landscape'],
146
+ default: 'portrait',
147
+ })
148
+ .option('safari-emulation-viewport', {
149
+ describe: 'viewport for Safari emulation, in WIDTHxHEIGHT format (e.g., 393x695)',
150
+ type: 'string',
151
+ coerce: parseViewport,
152
+ })
153
+ .option('safari-emulation-pixel-ratio', {
154
+ describe: 'device pixel ratio for Safari emulation (e.g., 3)',
155
+ type: 'number',
156
+ })
157
+ .option('safari-emulation-user-agent', {
158
+ describe: 'user-agent string for Safari emulation',
159
+ type: 'string',
160
+ })
131
161
  .option('android-device-name', {
132
162
  describe: 'the name of the Android device to use',
133
163
  type: 'string',
@@ -491,6 +521,29 @@ async function main(argv, {shouldExitOnError = false} = {}) {
491
521
  choices: ['portrait', 'landscape'],
492
522
  default: 'portrait',
493
523
  })
524
+ .option('safari-emulation-device-name', {
525
+ describe: 'the device name to use for Safari emulation (e.g., "iPhone 16")',
526
+ type: 'string',
527
+ })
528
+ .option('safari-emulation-orientation', {
529
+ describe: 'portrait or landscape for Safari emulation',
530
+ type: 'string',
531
+ choices: ['portrait', 'landscape'],
532
+ default: 'portrait',
533
+ })
534
+ .option('safari-emulation-viewport', {
535
+ describe: 'viewport for Safari emulation, in WIDTHxHEIGHT format (e.g., 393x695)',
536
+ type: 'string',
537
+ coerce: parseViewport,
538
+ })
539
+ .option('safari-emulation-pixel-ratio', {
540
+ describe: 'device pixel ratio for Safari emulation (e.g., 3)',
541
+ type: 'number',
542
+ })
543
+ .option('safari-emulation-user-agent', {
544
+ describe: 'user-agent string for Safari emulation',
545
+ type: 'string',
546
+ })
494
547
  .option('ios-device-name', {
495
548
  describe: 'the name of the iOS device to use',
496
549
  type: 'string',
@@ -810,7 +863,7 @@ async function main(argv, {shouldExitOnError = false} = {}) {
810
863
 
811
864
  const args = /**@type {object}*/ (commandLineOptions.parse())
812
865
 
813
- if (args._ && args._[0]) {
866
+ if (args._ && args._[0] && !args.help) {
814
867
  try {
815
868
  return await require(`./${args._[0]}`)(normalizeArgs(args))
816
869
  } catch (err) {