@vizzly-testing/cli 0.20.0 → 0.20.1-beta.0
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/dist/api/client.js +134 -0
- package/dist/api/core.js +341 -0
- package/dist/api/endpoints.js +314 -0
- package/dist/api/index.js +19 -0
- package/dist/auth/client.js +91 -0
- package/dist/auth/core.js +176 -0
- package/dist/auth/index.js +30 -0
- package/dist/auth/operations.js +148 -0
- package/dist/cli.js +1 -1
- package/dist/commands/doctor.js +3 -3
- package/dist/commands/finalize.js +41 -15
- package/dist/commands/login.js +7 -6
- package/dist/commands/logout.js +4 -4
- package/dist/commands/project.js +5 -4
- package/dist/commands/run.js +158 -90
- package/dist/commands/status.js +22 -18
- package/dist/commands/tdd.js +105 -78
- package/dist/commands/upload.js +61 -26
- package/dist/commands/whoami.js +4 -4
- package/dist/config/core.js +438 -0
- package/dist/config/index.js +13 -0
- package/dist/config/operations.js +327 -0
- package/dist/index.js +1 -1
- package/dist/project/core.js +295 -0
- package/dist/project/index.js +13 -0
- package/dist/project/operations.js +393 -0
- package/dist/report-generator/core.js +315 -0
- package/dist/report-generator/index.js +8 -0
- package/dist/report-generator/operations.js +196 -0
- package/dist/reporter/reporter-bundle.iife.js +16 -16
- package/dist/screenshot-server/core.js +157 -0
- package/dist/screenshot-server/index.js +11 -0
- package/dist/screenshot-server/operations.js +183 -0
- package/dist/sdk/index.js +3 -2
- package/dist/server/handlers/api-handler.js +14 -5
- package/dist/server/handlers/tdd-handler.js +80 -48
- package/dist/server-manager/core.js +183 -0
- package/dist/server-manager/index.js +81 -0
- package/dist/server-manager/operations.js +208 -0
- package/dist/services/build-manager.js +2 -69
- package/dist/services/index.js +21 -48
- package/dist/services/screenshot-server.js +40 -74
- package/dist/services/server-manager.js +45 -80
- package/dist/services/static-report-generator.js +21 -163
- package/dist/services/test-runner.js +90 -250
- package/dist/services/uploader.js +56 -358
- package/dist/tdd/core/hotspot-coverage.js +112 -0
- package/dist/tdd/core/signature.js +101 -0
- package/dist/tdd/index.js +19 -0
- package/dist/tdd/metadata/baseline-metadata.js +103 -0
- package/dist/tdd/metadata/hotspot-metadata.js +93 -0
- package/dist/tdd/services/baseline-downloader.js +151 -0
- package/dist/tdd/services/baseline-manager.js +166 -0
- package/dist/tdd/services/comparison-service.js +230 -0
- package/dist/tdd/services/hotspot-service.js +71 -0
- package/dist/tdd/services/result-service.js +123 -0
- package/dist/tdd/tdd-service.js +1081 -0
- package/dist/test-runner/core.js +255 -0
- package/dist/test-runner/index.js +13 -0
- package/dist/test-runner/operations.js +483 -0
- package/dist/uploader/core.js +396 -0
- package/dist/uploader/index.js +11 -0
- package/dist/uploader/operations.js +412 -0
- package/package.json +7 -12
- package/dist/services/api-service.js +0 -412
- package/dist/services/auth-service.js +0 -226
- package/dist/services/config-service.js +0 -369
- package/dist/services/html-report-generator.js +0 -455
- package/dist/services/project-service.js +0 -326
- package/dist/services/report-generator/report.css +0 -411
- package/dist/services/report-generator/viewer.js +0 -102
- package/dist/services/tdd-service.js +0 -1437
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comparison Service
|
|
3
|
+
*
|
|
4
|
+
* Wraps honeydiff for image comparison and builds comparison result objects.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { compare } from '@vizzly-testing/honeydiff';
|
|
8
|
+
import { calculateHotspotCoverage } from '../core/hotspot-coverage.js';
|
|
9
|
+
import { generateComparisonId } from '../core/signature.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Compare two images using honeydiff
|
|
13
|
+
*
|
|
14
|
+
* @param {string} baselinePath - Path to baseline image
|
|
15
|
+
* @param {string} currentPath - Path to current image
|
|
16
|
+
* @param {string} diffPath - Path to save diff image
|
|
17
|
+
* @param {Object} options - Comparison options
|
|
18
|
+
* @param {number} options.threshold - CIEDE2000 Delta E threshold (default: 2.0)
|
|
19
|
+
* @param {number} options.minClusterSize - Minimum cluster size (default: 2)
|
|
20
|
+
* @returns {Promise<Object>} Honeydiff result
|
|
21
|
+
* @throws {Error} When honeydiff binary fails (e.g., corrupt images, dimension mismatch)
|
|
22
|
+
*/
|
|
23
|
+
export async function compareImages(baselinePath, currentPath, diffPath, options = {}) {
|
|
24
|
+
let {
|
|
25
|
+
threshold = 2.0,
|
|
26
|
+
minClusterSize = 2
|
|
27
|
+
} = options;
|
|
28
|
+
return compare(baselinePath, currentPath, {
|
|
29
|
+
threshold,
|
|
30
|
+
antialiasing: true,
|
|
31
|
+
diffPath,
|
|
32
|
+
overwrite: true,
|
|
33
|
+
includeClusters: true,
|
|
34
|
+
minClusterSize
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Build a comparison result object for a passing comparison (no diff)
|
|
40
|
+
*
|
|
41
|
+
* @param {Object} params
|
|
42
|
+
* @param {string} params.name - Screenshot name
|
|
43
|
+
* @param {string} params.signature - Screenshot signature
|
|
44
|
+
* @param {string} params.baselinePath - Path to baseline image
|
|
45
|
+
* @param {string} params.currentPath - Path to current image
|
|
46
|
+
* @param {Object} params.properties - Screenshot properties
|
|
47
|
+
* @param {number} params.threshold - Effective threshold used
|
|
48
|
+
* @param {number} params.minClusterSize - Effective minClusterSize used
|
|
49
|
+
* @param {Object} params.honeydiffResult - Result from honeydiff (optional, for metrics)
|
|
50
|
+
* @returns {Object} Comparison result
|
|
51
|
+
*/
|
|
52
|
+
export function buildPassedComparison(params) {
|
|
53
|
+
let {
|
|
54
|
+
name,
|
|
55
|
+
signature,
|
|
56
|
+
baselinePath,
|
|
57
|
+
currentPath,
|
|
58
|
+
properties,
|
|
59
|
+
threshold,
|
|
60
|
+
minClusterSize,
|
|
61
|
+
honeydiffResult
|
|
62
|
+
} = params;
|
|
63
|
+
return {
|
|
64
|
+
id: generateComparisonId(signature),
|
|
65
|
+
name,
|
|
66
|
+
status: 'passed',
|
|
67
|
+
baseline: baselinePath,
|
|
68
|
+
current: currentPath,
|
|
69
|
+
diff: null,
|
|
70
|
+
properties,
|
|
71
|
+
signature,
|
|
72
|
+
threshold,
|
|
73
|
+
minClusterSize,
|
|
74
|
+
totalPixels: honeydiffResult?.totalPixels,
|
|
75
|
+
aaPixelsIgnored: honeydiffResult?.aaPixelsIgnored,
|
|
76
|
+
aaPercentage: honeydiffResult?.aaPercentage
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Build a comparison result object for a new baseline
|
|
82
|
+
*
|
|
83
|
+
* @param {Object} params
|
|
84
|
+
* @param {string} params.name - Screenshot name
|
|
85
|
+
* @param {string} params.signature - Screenshot signature
|
|
86
|
+
* @param {string} params.baselinePath - Path to baseline image
|
|
87
|
+
* @param {string} params.currentPath - Path to current image
|
|
88
|
+
* @param {Object} params.properties - Screenshot properties
|
|
89
|
+
* @returns {Object} Comparison result
|
|
90
|
+
*/
|
|
91
|
+
export function buildNewComparison(params) {
|
|
92
|
+
let {
|
|
93
|
+
name,
|
|
94
|
+
signature,
|
|
95
|
+
baselinePath,
|
|
96
|
+
currentPath,
|
|
97
|
+
properties
|
|
98
|
+
} = params;
|
|
99
|
+
return {
|
|
100
|
+
id: generateComparisonId(signature),
|
|
101
|
+
name,
|
|
102
|
+
status: 'new',
|
|
103
|
+
baseline: baselinePath,
|
|
104
|
+
current: currentPath,
|
|
105
|
+
diff: null,
|
|
106
|
+
properties,
|
|
107
|
+
signature
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Build a comparison result object for a failed comparison (with diff)
|
|
113
|
+
*
|
|
114
|
+
* @param {Object} params
|
|
115
|
+
* @param {string} params.name - Screenshot name
|
|
116
|
+
* @param {string} params.signature - Screenshot signature
|
|
117
|
+
* @param {string} params.baselinePath - Path to baseline image
|
|
118
|
+
* @param {string} params.currentPath - Path to current image
|
|
119
|
+
* @param {string} params.diffPath - Path to diff image
|
|
120
|
+
* @param {Object} params.properties - Screenshot properties
|
|
121
|
+
* @param {number} params.threshold - Effective threshold used
|
|
122
|
+
* @param {number} params.minClusterSize - Effective minClusterSize used
|
|
123
|
+
* @param {Object} params.honeydiffResult - Result from honeydiff
|
|
124
|
+
* @param {Object} params.hotspotAnalysis - Hotspot data for this screenshot (optional)
|
|
125
|
+
* @returns {Object} Comparison result
|
|
126
|
+
*/
|
|
127
|
+
export function buildFailedComparison(params) {
|
|
128
|
+
let {
|
|
129
|
+
name,
|
|
130
|
+
signature,
|
|
131
|
+
baselinePath,
|
|
132
|
+
currentPath,
|
|
133
|
+
diffPath,
|
|
134
|
+
properties,
|
|
135
|
+
threshold,
|
|
136
|
+
minClusterSize,
|
|
137
|
+
honeydiffResult,
|
|
138
|
+
hotspotAnalysis
|
|
139
|
+
} = params;
|
|
140
|
+
|
|
141
|
+
// Calculate hotspot coverage if we have hotspot data
|
|
142
|
+
let hotspotCoverage = null;
|
|
143
|
+
let isHotspotFiltered = false;
|
|
144
|
+
if (hotspotAnalysis && honeydiffResult.diffClusters?.length > 0) {
|
|
145
|
+
hotspotCoverage = calculateHotspotCoverage(honeydiffResult.diffClusters, hotspotAnalysis);
|
|
146
|
+
|
|
147
|
+
// Check if diff should be filtered as hotspot noise
|
|
148
|
+
// Using shouldFilterAsHotspot helper but also checking confidence_score
|
|
149
|
+
// (cloud uses confidence_score >= 70 which is >0.7 when normalized)
|
|
150
|
+
let isHighConfidence = hotspotAnalysis.confidence === 'high' || hotspotAnalysis.confidence_score !== undefined && hotspotAnalysis.confidence_score >= 70;
|
|
151
|
+
if (isHighConfidence && hotspotCoverage.coverage >= 0.8) {
|
|
152
|
+
isHotspotFiltered = true;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
id: generateComparisonId(signature),
|
|
157
|
+
name,
|
|
158
|
+
status: isHotspotFiltered ? 'passed' : 'failed',
|
|
159
|
+
baseline: baselinePath,
|
|
160
|
+
current: currentPath,
|
|
161
|
+
diff: diffPath,
|
|
162
|
+
properties,
|
|
163
|
+
signature,
|
|
164
|
+
threshold,
|
|
165
|
+
minClusterSize,
|
|
166
|
+
diffPercentage: honeydiffResult.diffPercentage,
|
|
167
|
+
diffCount: honeydiffResult.diffPixels,
|
|
168
|
+
reason: isHotspotFiltered ? 'hotspot-filtered' : 'pixel-diff',
|
|
169
|
+
totalPixels: honeydiffResult.totalPixels,
|
|
170
|
+
aaPixelsIgnored: honeydiffResult.aaPixelsIgnored,
|
|
171
|
+
aaPercentage: honeydiffResult.aaPercentage,
|
|
172
|
+
boundingBox: honeydiffResult.boundingBox,
|
|
173
|
+
heightDiff: honeydiffResult.heightDiff,
|
|
174
|
+
intensityStats: honeydiffResult.intensityStats,
|
|
175
|
+
diffClusters: honeydiffResult.diffClusters,
|
|
176
|
+
hotspotAnalysis: hotspotCoverage ? {
|
|
177
|
+
coverage: hotspotCoverage.coverage,
|
|
178
|
+
linesInHotspots: hotspotCoverage.linesInHotspots,
|
|
179
|
+
totalLines: hotspotCoverage.totalLines,
|
|
180
|
+
confidence: hotspotAnalysis?.confidence,
|
|
181
|
+
confidenceScore: hotspotAnalysis?.confidence_score,
|
|
182
|
+
regionCount: hotspotAnalysis?.regions?.length || 0,
|
|
183
|
+
isFiltered: isHotspotFiltered
|
|
184
|
+
} : null
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Build a comparison result object for an error
|
|
190
|
+
*
|
|
191
|
+
* @param {Object} params
|
|
192
|
+
* @param {string} params.name - Screenshot name
|
|
193
|
+
* @param {string} params.signature - Screenshot signature
|
|
194
|
+
* @param {string} params.baselinePath - Path to baseline image
|
|
195
|
+
* @param {string} params.currentPath - Path to current image
|
|
196
|
+
* @param {Object} params.properties - Screenshot properties
|
|
197
|
+
* @param {string} params.errorMessage - Error message
|
|
198
|
+
* @returns {Object} Comparison result
|
|
199
|
+
*/
|
|
200
|
+
export function buildErrorComparison(params) {
|
|
201
|
+
let {
|
|
202
|
+
name,
|
|
203
|
+
signature,
|
|
204
|
+
baselinePath,
|
|
205
|
+
currentPath,
|
|
206
|
+
properties,
|
|
207
|
+
errorMessage
|
|
208
|
+
} = params;
|
|
209
|
+
return {
|
|
210
|
+
id: generateComparisonId(signature),
|
|
211
|
+
name,
|
|
212
|
+
status: 'error',
|
|
213
|
+
baseline: baselinePath,
|
|
214
|
+
current: currentPath,
|
|
215
|
+
diff: null,
|
|
216
|
+
properties,
|
|
217
|
+
signature,
|
|
218
|
+
error: errorMessage
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Check if an error is a dimension mismatch from honeydiff
|
|
224
|
+
*
|
|
225
|
+
* @param {Error} error
|
|
226
|
+
* @returns {boolean}
|
|
227
|
+
*/
|
|
228
|
+
export function isDimensionMismatchError(error) {
|
|
229
|
+
return error.message?.includes("Image dimensions don't match") ?? false;
|
|
230
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hotspot Service
|
|
3
|
+
*
|
|
4
|
+
* Functions for downloading and managing hotspot data from the cloud.
|
|
5
|
+
* Hotspots identify regions that frequently change due to dynamic content.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { saveHotspotMetadata } from '../metadata/hotspot-metadata.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Download hotspots for screenshots from cloud API
|
|
12
|
+
*
|
|
13
|
+
* @param {Object} options
|
|
14
|
+
* @param {Object} options.api - ApiService instance
|
|
15
|
+
* @param {string} options.workingDir - Working directory
|
|
16
|
+
* @param {string[]} options.screenshotNames - Names of screenshots to get hotspots for
|
|
17
|
+
* @returns {Promise<{ success: boolean, count: number, regionCount: number, error?: string }>}
|
|
18
|
+
*/
|
|
19
|
+
export async function downloadHotspots(options) {
|
|
20
|
+
let {
|
|
21
|
+
api,
|
|
22
|
+
workingDir,
|
|
23
|
+
screenshotNames
|
|
24
|
+
} = options;
|
|
25
|
+
if (!screenshotNames || screenshotNames.length === 0) {
|
|
26
|
+
return {
|
|
27
|
+
success: true,
|
|
28
|
+
count: 0,
|
|
29
|
+
regionCount: 0
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
let response = await api.getHotspots(screenshotNames);
|
|
34
|
+
if (!response || !response.hotspots) {
|
|
35
|
+
return {
|
|
36
|
+
success: false,
|
|
37
|
+
error: 'API returned no hotspot data'
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Save hotspots to disk
|
|
42
|
+
saveHotspotMetadata(workingDir, response.hotspots, response.summary);
|
|
43
|
+
|
|
44
|
+
// Calculate stats
|
|
45
|
+
let count = Object.keys(response.hotspots).length;
|
|
46
|
+
let regionCount = Object.values(response.hotspots).reduce((sum, h) => sum + (h.regions?.length || 0), 0);
|
|
47
|
+
return {
|
|
48
|
+
success: true,
|
|
49
|
+
count,
|
|
50
|
+
regionCount
|
|
51
|
+
};
|
|
52
|
+
} catch (error) {
|
|
53
|
+
return {
|
|
54
|
+
success: false,
|
|
55
|
+
error: error.message
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Extract screenshot names from a list of screenshots
|
|
62
|
+
*
|
|
63
|
+
* @param {Array} screenshots - Screenshots with name property
|
|
64
|
+
* @returns {string[]}
|
|
65
|
+
*/
|
|
66
|
+
export function extractScreenshotNames(screenshots) {
|
|
67
|
+
if (!screenshots || !Array.isArray(screenshots)) {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
return screenshots.map(s => s.name).filter(Boolean);
|
|
71
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Result Service
|
|
3
|
+
*
|
|
4
|
+
* Aggregates comparison results and provides summary statistics.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Calculate summary statistics from comparisons
|
|
9
|
+
*
|
|
10
|
+
* @param {Array} comparisons - Array of comparison results
|
|
11
|
+
* @returns {{ total: number, passed: number, failed: number, new: number, errors: number }}
|
|
12
|
+
*/
|
|
13
|
+
export function calculateSummary(comparisons) {
|
|
14
|
+
let passed = 0;
|
|
15
|
+
let failed = 0;
|
|
16
|
+
let newScreenshots = 0;
|
|
17
|
+
let errors = 0;
|
|
18
|
+
for (let c of comparisons) {
|
|
19
|
+
switch (c.status) {
|
|
20
|
+
case 'passed':
|
|
21
|
+
passed++;
|
|
22
|
+
break;
|
|
23
|
+
case 'failed':
|
|
24
|
+
failed++;
|
|
25
|
+
break;
|
|
26
|
+
case 'new':
|
|
27
|
+
newScreenshots++;
|
|
28
|
+
break;
|
|
29
|
+
case 'error':
|
|
30
|
+
errors++;
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
total: comparisons.length,
|
|
36
|
+
passed,
|
|
37
|
+
failed,
|
|
38
|
+
new: newScreenshots,
|
|
39
|
+
errors
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Build complete results object
|
|
45
|
+
*
|
|
46
|
+
* @param {Array} comparisons - Array of comparison results
|
|
47
|
+
* @param {Object} baselineData - Baseline metadata
|
|
48
|
+
* @returns {Object} Results object with summary and comparisons
|
|
49
|
+
*/
|
|
50
|
+
export function buildResults(comparisons, baselineData) {
|
|
51
|
+
let summary = calculateSummary(comparisons);
|
|
52
|
+
return {
|
|
53
|
+
...summary,
|
|
54
|
+
comparisons,
|
|
55
|
+
baseline: baselineData
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get failed comparisons from results
|
|
61
|
+
*
|
|
62
|
+
* @param {Array} comparisons - Array of comparison results
|
|
63
|
+
* @returns {Array} Failed comparisons
|
|
64
|
+
*/
|
|
65
|
+
export function getFailedComparisons(comparisons) {
|
|
66
|
+
return comparisons.filter(c => c.status === 'failed');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get new comparisons from results
|
|
71
|
+
*
|
|
72
|
+
* @param {Array} comparisons - Array of comparison results
|
|
73
|
+
* @returns {Array} New comparisons
|
|
74
|
+
*/
|
|
75
|
+
export function getNewComparisons(comparisons) {
|
|
76
|
+
return comparisons.filter(c => c.status === 'new');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get error comparisons from results
|
|
81
|
+
*
|
|
82
|
+
* @param {Array} comparisons - Array of comparison results
|
|
83
|
+
* @returns {Array} Error comparisons
|
|
84
|
+
*/
|
|
85
|
+
export function getErrorComparisons(comparisons) {
|
|
86
|
+
return comparisons.filter(c => c.status === 'error');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Check if results indicate overall success (no failures or errors)
|
|
91
|
+
*
|
|
92
|
+
* @param {Array} comparisons - Array of comparison results
|
|
93
|
+
* @returns {boolean}
|
|
94
|
+
*/
|
|
95
|
+
export function isSuccessful(comparisons) {
|
|
96
|
+
return !comparisons.some(c => c.status === 'failed' || c.status === 'error');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Find comparison by ID
|
|
101
|
+
*
|
|
102
|
+
* @param {Array} comparisons - Array of comparison results
|
|
103
|
+
* @param {string} id - Comparison ID
|
|
104
|
+
* @returns {Object|null}
|
|
105
|
+
*/
|
|
106
|
+
export function findComparisonById(comparisons, id) {
|
|
107
|
+
return comparisons.find(c => c.id === id) || null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Find comparison by name and signature
|
|
112
|
+
*
|
|
113
|
+
* @param {Array} comparisons - Array of comparison results
|
|
114
|
+
* @param {string} name - Screenshot name
|
|
115
|
+
* @param {string} signature - Screenshot signature (optional)
|
|
116
|
+
* @returns {Object|null}
|
|
117
|
+
*/
|
|
118
|
+
export function findComparison(comparisons, name, signature = null) {
|
|
119
|
+
if (signature) {
|
|
120
|
+
return comparisons.find(c => c.signature === signature) || null;
|
|
121
|
+
}
|
|
122
|
+
return comparisons.find(c => c.name === name) || null;
|
|
123
|
+
}
|