@applitools/visual-grid-cli-utils 1.21.40-FLD-3978.0 → 1.21.42
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/package.json
CHANGED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Normalizes chained selectors for selectorsToFindRegionsFor
|
|
5
|
+
* Input can be:
|
|
6
|
+
* - A single array (when flag used once): ["sel1", "nt1", "type1", "sel2", "nt2", "type2"]
|
|
7
|
+
* - An array of arrays (when flag used multiple times): [
|
|
8
|
+
* ["sel1", "nt1", "type1", "sel2", "nt2", "type2"],
|
|
9
|
+
* ["sel3", "nt3", "type3", "sel4", "nt4", "type4"]
|
|
10
|
+
* ]
|
|
11
|
+
*
|
|
12
|
+
* Output: Array of chained selector arrays
|
|
13
|
+
* [
|
|
14
|
+
* [{selector, nodeType, type}, ...],
|
|
15
|
+
* [{selector, nodeType, type}, ...]
|
|
16
|
+
* ]
|
|
17
|
+
*/
|
|
18
|
+
function normalizeChainedSelectorsToFindRegionsForArray(chainedSelectorsInput) {
|
|
19
|
+
if (!chainedSelectorsInput) return []
|
|
20
|
+
|
|
21
|
+
// Check if it's an array of arrays (multiple flag usages) or a single array
|
|
22
|
+
const isMultiple = Array.isArray(chainedSelectorsInput[0])
|
|
23
|
+
|
|
24
|
+
const chainedSelectorsArrays = isMultiple ? chainedSelectorsInput : [chainedSelectorsInput]
|
|
25
|
+
|
|
26
|
+
return chainedSelectorsArrays.map(chainedSelectors => {
|
|
27
|
+
const normalized = []
|
|
28
|
+
|
|
29
|
+
// Process in groups of 3: selector, nodeType, type
|
|
30
|
+
for (let i = 0; i < chainedSelectors.length; i += 3) {
|
|
31
|
+
const selector = chainedSelectors[i]
|
|
32
|
+
const nodeType = chainedSelectors[i + 1]
|
|
33
|
+
const type = chainedSelectors[i + 2]
|
|
34
|
+
|
|
35
|
+
if (!selector) {
|
|
36
|
+
throw new Error(`Missing selector at position ${i} in chained-selectors-to-find-regions-for`)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!nodeType) {
|
|
40
|
+
throw new Error(`Missing nodeType at position ${i + 1} for selector "${selector}"`)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!type) {
|
|
44
|
+
throw new Error(`Missing type at position ${i + 2} for selector "${selector}"`)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Validate nodeType
|
|
48
|
+
if (!['iframe', 'shadow-root', 'element'].includes(nodeType)) {
|
|
49
|
+
throw new Error(`Invalid nodeType "${nodeType}" for selector "${selector}". Must be one of: iframe, shadow-root, element`)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Validate type
|
|
53
|
+
if (!['css', 'xpath'].includes(type)) {
|
|
54
|
+
throw new Error(`Invalid type "${type}" for selector "${selector}". Must be one of: css, xpath`)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
normalized.push({
|
|
58
|
+
selector,
|
|
59
|
+
nodeType,
|
|
60
|
+
type,
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Ensure the last item has nodeType 'element'
|
|
65
|
+
if (normalized.length > 0) {
|
|
66
|
+
const lastItem = normalized[normalized.length - 1]
|
|
67
|
+
if (lastItem.nodeType !== 'element') {
|
|
68
|
+
throw new Error(`The last selector in a chained selector must have nodeType 'element', but got '${lastItem.nodeType}'`)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return normalized
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = normalizeChainedSelectorsToFindRegionsForArray
|
|
@@ -3,7 +3,7 @@ const {filterValues} = require('@applitools/functional-commons')
|
|
|
3
3
|
|
|
4
4
|
function parseBrowser(arg) {
|
|
5
5
|
const match =
|
|
6
|
-
/^(chrome|chrome-1|chrome-2|chrome-canary|edgechromium-poc-canary|edgechromium-canary|firefox|firefox-1|firefox-2|firefox-canary|ie11|ie10|edge|safari|safari-1|safari-2|safari-canary|
|
|
6
|
+
/^(chrome|chrome-1|chrome-2|chrome-canary|edgechromium-poc-canary|edgechromium-canary|firefox|firefox-1|firefox-2|firefox-canary|ie11|ie10|edge|safari|safari-1|safari-2|safari-canary|webkit|safari-earlyaccess|ie-vmware|edgechromium|edgechromium-1|edgechromium-2|edgechromium-canary|edgelegacy)(\( *(\d+) *(x|,) *(\d+) *\))?$/.exec(
|
|
7
7
|
arg,
|
|
8
8
|
)
|
|
9
9
|
|
package/src/rerender.js
CHANGED
|
@@ -11,6 +11,7 @@ const {plotDomCaptureToHtml} = require('./plot-dom-capture-to-html')
|
|
|
11
11
|
const {convertBrowserNameToCanary} = require('./commons/parse-browser')
|
|
12
12
|
const parseJson = require('./commons/parse-json.js')
|
|
13
13
|
const normalizeChainedSelectorsArray = require('./commons/normalizeChainedSelectorsArray')
|
|
14
|
+
const normalizeChainedSelectorsToFindRegionsForArray = require('./commons/normalizeChainedSelectorsToFindRegionsForArray')
|
|
14
15
|
|
|
15
16
|
async function main({
|
|
16
17
|
apiKey,
|
|
@@ -46,6 +47,7 @@ async function main({
|
|
|
46
47
|
timeout,
|
|
47
48
|
options: optionsAsString,
|
|
48
49
|
selectorsToFindRegionsFor,
|
|
50
|
+
chainedSelectorsToFindRegionsFor,
|
|
49
51
|
vhsCompatibilityParams,
|
|
50
52
|
globalResourceMapPath,
|
|
51
53
|
originUrl,
|
|
@@ -107,9 +109,19 @@ async function main({
|
|
|
107
109
|
: undefined,
|
|
108
110
|
region: region,
|
|
109
111
|
},
|
|
110
|
-
selectorsToFindRegionsFor:
|
|
111
|
-
|
|
112
|
-
|
|
112
|
+
selectorsToFindRegionsFor: (() => {
|
|
113
|
+
const normalizedRegularSelectors = selectorsToFindRegionsFor
|
|
114
|
+
? normalizeSelectorsToFindRegionsForArray(selectorsToFindRegionsFor)
|
|
115
|
+
: []
|
|
116
|
+
|
|
117
|
+
const normalizedChainedSelectors = chainedSelectorsToFindRegionsFor
|
|
118
|
+
? normalizeChainedSelectorsToFindRegionsForArray(chainedSelectorsToFindRegionsFor)
|
|
119
|
+
: []
|
|
120
|
+
|
|
121
|
+
const combined = [...normalizedRegularSelectors, ...normalizedChainedSelectors]
|
|
122
|
+
|
|
123
|
+
return combined.length > 0 ? combined : undefined
|
|
124
|
+
})(),
|
|
113
125
|
enableMultipleResultsPerSelector: true,
|
|
114
126
|
includeFullPageSize,
|
|
115
127
|
sendDom: rca,
|
package/src/view-rendering.js
CHANGED
|
@@ -1,10 +1,118 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
const {exec} = require('child_process')
|
|
3
|
+
const retry = require('p-retry')
|
|
3
4
|
const createSession = require('./commons/create-session')
|
|
4
5
|
const calculateViewRenderingUrl = require('./commons/view-rendering-url')
|
|
5
6
|
const {fetch, throwErrorFromBadStatus} = require('@applitools/http-commons')
|
|
6
7
|
const fs = require('fs')
|
|
7
8
|
|
|
9
|
+
async function waitForParsedStatus(vgUrl, renderId, authToken, waitTimeout = 5 * 60 * 1000) {
|
|
10
|
+
console.log(`[2/3] Waiting for parsing to complete...`)
|
|
11
|
+
let attemptCount = 0
|
|
12
|
+
const startTime = Date.now()
|
|
13
|
+
return await retry(
|
|
14
|
+
async () => {
|
|
15
|
+
attemptCount++
|
|
16
|
+
const response = await fetch(`${vgUrl}/render-status?render-id=${renderId}`, {
|
|
17
|
+
headers: {'X-Auth-Token': authToken},
|
|
18
|
+
})
|
|
19
|
+
if (!response.ok) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
`failed to get rendering status. Error from Visual Grid: ${await response.text()}`,
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
const {status, error, rdvsMetadata, url} = await response.json()
|
|
25
|
+
if (status === 'error') {
|
|
26
|
+
throw new retry.AbortError(`Rendering failed: ${error}`)
|
|
27
|
+
} else if (status === 'parsed') {
|
|
28
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1)
|
|
29
|
+
console.log(` ✓ Parsing completed (${elapsed}s)`)
|
|
30
|
+
// Return the rdvsMetadata and url from the job
|
|
31
|
+
return {
|
|
32
|
+
rdvsMetadata,
|
|
33
|
+
url,
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
// Only log every 5 attempts to reduce noise
|
|
37
|
+
if (attemptCount % 5 === 0) {
|
|
38
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(0)
|
|
39
|
+
console.log(` Status: ${status} (${elapsed}s elapsed)...`)
|
|
40
|
+
}
|
|
41
|
+
// Still processing (rendering or other status)
|
|
42
|
+
throw new Error(`Status is ${status}, waiting for parsed`)
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
forever: true,
|
|
47
|
+
minTimeout: 1000,
|
|
48
|
+
maxTimeout: 1000,
|
|
49
|
+
maxRetryTime: waitTimeout,
|
|
50
|
+
onFailedAttempt: () => {}, // Suppress default error logging
|
|
51
|
+
},
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function tryNewViewRenderingEndpoint(vgUrl, renderId, authToken, apiKey, serverUrl) {
|
|
56
|
+
try {
|
|
57
|
+
console.log(`[1/3] Starting resource parsing for render: ${renderId}`)
|
|
58
|
+
// Step 1: Call rerender API with parseResourcesOnly flag
|
|
59
|
+
const renderUrl = new URL('./render', vgUrl)
|
|
60
|
+
const {webhook, authToken: sessionAuthToken} = await createSession(apiKey, serverUrl)
|
|
61
|
+
|
|
62
|
+
const rerenderResponse = await fetch(renderUrl, {
|
|
63
|
+
method: 'POST',
|
|
64
|
+
headers: {
|
|
65
|
+
'Content-Type': 'application/json',
|
|
66
|
+
'X-Auth-Token': sessionAuthToken,
|
|
67
|
+
},
|
|
68
|
+
body: JSON.stringify({
|
|
69
|
+
reRenderId: renderId,
|
|
70
|
+
webhook,
|
|
71
|
+
parseResourcesOnly: true,
|
|
72
|
+
agentId: `visual-grid-cli-utils/${require('../package.json').version}`,
|
|
73
|
+
options: {
|
|
74
|
+
parseResourcesOnly: true,
|
|
75
|
+
},
|
|
76
|
+
}),
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
if (!rerenderResponse.ok) {
|
|
80
|
+
const errorBody = await rerenderResponse.text()
|
|
81
|
+
return {
|
|
82
|
+
success: false,
|
|
83
|
+
error: `Rerender API failed - HTTP ${rerenderResponse.status}: ${errorBody}`,
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const {renderId: parseRenderId} = await rerenderResponse.json()
|
|
88
|
+
console.log(` ✓ Parse job created: ${parseRenderId}`)
|
|
89
|
+
|
|
90
|
+
// Step 2: Wait for "parsed" status
|
|
91
|
+
const parsedResult = await waitForParsedStatus(vgUrl, parseRenderId, sessionAuthToken)
|
|
92
|
+
|
|
93
|
+
// Step 3: Check if we have the required data
|
|
94
|
+
if (parsedResult.rdvsMetadata && parsedResult.url) {
|
|
95
|
+
console.log(`[3/3] Building view URL...`)
|
|
96
|
+
console.log(` ✓ Resource tree ready`)
|
|
97
|
+
return {
|
|
98
|
+
success: true,
|
|
99
|
+
url: parsedResult.url,
|
|
100
|
+
rdvsMetadata: parsedResult.rdvsMetadata,
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
return {
|
|
104
|
+
success: false,
|
|
105
|
+
error: `Parsed result missing required fields (rdvsMetadata, url)`,
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} catch (error) {
|
|
109
|
+
return {
|
|
110
|
+
success: false,
|
|
111
|
+
error: error.message || String(error),
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
8
116
|
async function main({
|
|
9
117
|
apiKey,
|
|
10
118
|
browser,
|
|
@@ -24,7 +132,26 @@ async function main({
|
|
|
24
132
|
? {authToken}
|
|
25
133
|
: await createSession(apiKey, serverUrl)
|
|
26
134
|
|
|
27
|
-
const
|
|
135
|
+
const vgUrl = process.env.VG_URL || 'https://render-wus.applitools.com'
|
|
136
|
+
|
|
137
|
+
// Try new view-rendering endpoint with rerender API
|
|
138
|
+
const result = await tryNewViewRenderingEndpoint(
|
|
139
|
+
vgUrl,
|
|
140
|
+
renderId,
|
|
141
|
+
finalAuthToken,
|
|
142
|
+
apiKey,
|
|
143
|
+
serverUrl,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
let viewRenderingUrl
|
|
147
|
+
if (result.success) {
|
|
148
|
+
console.log('\n✓ Successfully generated view URL via new endpoint')
|
|
149
|
+
viewRenderingUrl = result.url
|
|
150
|
+
} else {
|
|
151
|
+
console.log('\n⚠ New endpoint unavailable, using legacy method')
|
|
152
|
+
console.log(` Reason: ${result.error}`)
|
|
153
|
+
viewRenderingUrl = calculateViewRenderingUrl(renderId, finalAuthToken, accountOverride)
|
|
154
|
+
}
|
|
28
155
|
|
|
29
156
|
if (downloadVhsFolder) {
|
|
30
157
|
let response = await fetch(viewRenderingUrl)
|
|
@@ -42,9 +169,12 @@ async function main({
|
|
|
42
169
|
const resource = await response.buffer()
|
|
43
170
|
const fname = `${downloadVhsFolder}/${renderId}.vhs`
|
|
44
171
|
fs.writeFileSync(fname, resource)
|
|
45
|
-
console.log('Wrote file: ' + fname)
|
|
172
|
+
console.log('\n✓ Wrote file: ' + fname)
|
|
46
173
|
} else {
|
|
174
|
+
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
|
|
175
|
+
console.log('View Rendering URL:')
|
|
47
176
|
console.log(viewRenderingUrl)
|
|
177
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
|
|
48
178
|
}
|
|
49
179
|
|
|
50
180
|
if (browser) {
|
|
@@ -189,6 +189,11 @@ async function main(argv, {shouldExitOnError = false} = {}) {
|
|
|
189
189
|
'selectors to find regions for - Specify selectors in the following format: "<selector1>" "xpath || css" <selector2>" "xpath || css"...',
|
|
190
190
|
type: 'array',
|
|
191
191
|
})
|
|
192
|
+
.option('chained-selectors-to-find-regions-for', {
|
|
193
|
+
describe:
|
|
194
|
+
'chained selectors to find regions for (can be used multiple times). Specify each chained selector in the following format: --chained-selectors-to-find-regions-for "<selector1>" "<nodeType>" "<type>" "<selector2>" "<nodeType>" "<type>" ...',
|
|
195
|
+
type: 'array',
|
|
196
|
+
})
|
|
192
197
|
.option('region', {
|
|
193
198
|
describe: 'target a specific region',
|
|
194
199
|
type: 'string',
|