@rettangoli/vt 0.0.14 → 1.0.0-rc2
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 +135 -64
- package/package.json +9 -2
- package/src/capture/capture-scheduler.js +206 -0
- package/src/capture/playwright-runner.js +403 -0
- package/src/capture/result-collector.js +234 -0
- package/src/capture/screenshot-naming.js +13 -0
- package/src/capture/spec-loader.js +117 -0
- package/src/capture/worker-plan.js +66 -0
- package/src/cli/generate-options.js +81 -0
- package/src/cli/generate.js +95 -28
- package/src/cli/report-options.js +43 -0
- package/src/cli/report.js +88 -151
- package/src/cli/templates/index.html +2 -2
- package/src/cli/templates/report.html +4 -4
- package/src/common.js +123 -185
- package/src/createSteps.js +358 -28
- package/src/report/report-model.js +76 -0
- package/src/report/report-render.js +22 -0
- package/src/selector-filter.js +139 -0
- package/src/step-commands.js +33 -0
- package/src/validation.js +304 -0
- package/src/viewport.js +99 -0
- package/docker/Dockerfile +0 -21
- package/docker/build-and-push.sh +0 -16
package/src/cli/report.js
CHANGED
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import crypto from "crypto";
|
|
4
|
-
import { Liquid } from "liquidjs";
|
|
5
4
|
import { cp } from "node:fs/promises";
|
|
6
5
|
import pixelmatch from "pixelmatch";
|
|
7
6
|
import sharp from "sharp";
|
|
7
|
+
import { readYaml } from "../common.js";
|
|
8
|
+
import { validateVtConfig } from "../validation.js";
|
|
9
|
+
import { resolveReportOptions } from "./report-options.js";
|
|
10
|
+
import {
|
|
11
|
+
buildAllRelativePaths,
|
|
12
|
+
toMismatchingItems,
|
|
13
|
+
buildJsonReport,
|
|
14
|
+
} from "../report/report-model.js";
|
|
15
|
+
import { renderHtmlReport } from "../report/report-render.js";
|
|
16
|
+
import {
|
|
17
|
+
filterRelativeScreenshotPathsBySelectors,
|
|
18
|
+
hasSelectors,
|
|
19
|
+
} from "../selector-filter.js";
|
|
20
|
+
|
|
21
|
+
const libraryTemplatesPath = new URL("./templates", import.meta.url).pathname;
|
|
8
22
|
|
|
9
|
-
const libraryTemplatesPath = new URL('./templates', import.meta.url).pathname;
|
|
10
|
-
|
|
11
|
-
// Initialize Liquid engine
|
|
12
|
-
const engine = new Liquid();
|
|
13
|
-
|
|
14
|
-
// Add custom filter to convert string to lowercase and replace spaces with hyphens
|
|
15
|
-
engine.registerFilter("slug", (value) => {
|
|
16
|
-
if (typeof value !== "string") return "";
|
|
17
|
-
return value.toLowerCase().replace(/\s+/g, "-");
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
// Recursively get all files in a directory
|
|
21
23
|
function getAllFiles(dir, fileList = []) {
|
|
22
24
|
const files = fs.readdirSync(dir);
|
|
23
25
|
|
|
@@ -33,58 +35,20 @@ function getAllFiles(dir, fileList = []) {
|
|
|
33
35
|
return fileList;
|
|
34
36
|
}
|
|
35
37
|
|
|
36
|
-
function extractParts(p) {
|
|
37
|
-
const dir = path.dirname(p);
|
|
38
|
-
const filename = path.basename(p, '.webp');
|
|
39
|
-
const lastHyphenIndex = filename.lastIndexOf('-');
|
|
40
|
-
|
|
41
|
-
if (lastHyphenIndex > -1) {
|
|
42
|
-
const suffix = filename.substring(lastHyphenIndex + 1);
|
|
43
|
-
const number = parseInt(suffix);
|
|
44
|
-
|
|
45
|
-
if (!isNaN(number) && String(number) === suffix) {
|
|
46
|
-
const name = path.join(dir, filename.substring(0, lastHyphenIndex));
|
|
47
|
-
return { name, number };
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
// -1 is for the first file (as it will result in the first index when sorting)
|
|
51
|
-
return { name: path.join(dir, filename), number: -1 };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function sortPaths(a, b) {
|
|
55
|
-
const partsA = extractParts(a);
|
|
56
|
-
const partsB = extractParts(b);
|
|
57
|
-
|
|
58
|
-
if (partsA.name < partsB.name) return -1;
|
|
59
|
-
if (partsA.name > partsB.name) return 1;
|
|
60
|
-
|
|
61
|
-
return partsA.number - partsB.number;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
38
|
async function calculateImageHash(imagePath) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
return hash;
|
|
69
|
-
} catch (error) {
|
|
70
|
-
console.error(`Error calculating hash for ${imagePath}:`, error);
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
39
|
+
const imageBuffer = fs.readFileSync(imagePath);
|
|
40
|
+
const hash = crypto.createHash("md5").update(imageBuffer).digest("hex");
|
|
41
|
+
return hash;
|
|
73
42
|
}
|
|
74
43
|
|
|
75
44
|
async function compareImagesMd5(artifactPath, goldPath) {
|
|
76
45
|
try {
|
|
77
46
|
const artifactHash = await calculateImageHash(artifactPath);
|
|
78
47
|
const goldHash = await calculateImageHash(goldPath);
|
|
79
|
-
|
|
80
|
-
if (artifactHash === null || goldHash === null) {
|
|
81
|
-
return { equal: false, error: true };
|
|
82
|
-
}
|
|
83
|
-
|
|
84
48
|
return { equal: artifactHash === goldHash, error: false };
|
|
85
49
|
} catch (error) {
|
|
86
50
|
console.error("Error comparing images:", error);
|
|
87
|
-
return { equal: false, error: true };
|
|
51
|
+
return { equal: false, error: true, message: error.message };
|
|
88
52
|
}
|
|
89
53
|
}
|
|
90
54
|
|
|
@@ -92,7 +56,6 @@ async function compareImagesPixelmatch(artifactPath, goldPath, diffPath, options
|
|
|
92
56
|
const { colorThreshold = 0.1, diffThreshold = 0.3 } = options;
|
|
93
57
|
|
|
94
58
|
try {
|
|
95
|
-
// Load images and convert to raw RGBA using sharp
|
|
96
59
|
const [artifactData, goldData] = await Promise.all([
|
|
97
60
|
sharp(artifactPath).ensureAlpha().raw().toBuffer({ resolveWithObject: true }),
|
|
98
61
|
sharp(goldPath).ensureAlpha().raw().toBuffer({ resolveWithObject: true }),
|
|
@@ -103,12 +66,10 @@ async function compareImagesPixelmatch(artifactPath, goldPath, diffPath, options
|
|
|
103
66
|
const goldHeight = goldData.info.height;
|
|
104
67
|
const totalPixels = width * height;
|
|
105
68
|
|
|
106
|
-
// If dimensions don't match, images are different
|
|
107
69
|
if (width !== goldWidth || height !== goldHeight) {
|
|
108
70
|
return { equal: false, error: false, diffPixels: -1, totalPixels, similarity: 0 };
|
|
109
71
|
}
|
|
110
72
|
|
|
111
|
-
// Create diff image buffer
|
|
112
73
|
const diffBuffer = Buffer.alloc(totalPixels * 4);
|
|
113
74
|
|
|
114
75
|
const diffPixels = pixelmatch(
|
|
@@ -117,14 +78,13 @@ async function compareImagesPixelmatch(artifactPath, goldPath, diffPath, options
|
|
|
117
78
|
diffBuffer,
|
|
118
79
|
width,
|
|
119
80
|
height,
|
|
120
|
-
{ threshold: colorThreshold }
|
|
81
|
+
{ threshold: colorThreshold },
|
|
121
82
|
);
|
|
122
83
|
|
|
123
84
|
const diffPercent = (diffPixels / totalPixels) * 100;
|
|
124
85
|
const similarity = (100 - diffPercent).toFixed(2);
|
|
125
86
|
const equal = diffPercent < diffThreshold;
|
|
126
87
|
|
|
127
|
-
// Save diff image if not equal
|
|
128
88
|
if (!equal && diffPath) {
|
|
129
89
|
fs.mkdirSync(path.dirname(diffPath), { recursive: true });
|
|
130
90
|
await sharp(diffBuffer, { raw: { width, height, channels: 4 } })
|
|
@@ -135,43 +95,39 @@ async function compareImagesPixelmatch(artifactPath, goldPath, diffPath, options
|
|
|
135
95
|
return { equal, error: false, diffPixels, totalPixels, similarity };
|
|
136
96
|
} catch (error) {
|
|
137
97
|
console.error("Error comparing images with pixelmatch:", error);
|
|
138
|
-
return { equal: false, error: true };
|
|
98
|
+
return { equal: false, error: true, message: error.message };
|
|
139
99
|
}
|
|
140
100
|
}
|
|
141
101
|
|
|
142
|
-
async function compareImages(artifactPath, goldPath, method =
|
|
143
|
-
if (method ===
|
|
102
|
+
async function compareImages(artifactPath, goldPath, method = "pixelmatch", diffPath = null, options = {}) {
|
|
103
|
+
if (method === "md5") {
|
|
144
104
|
return compareImagesMd5(artifactPath, goldPath);
|
|
145
105
|
}
|
|
146
106
|
return compareImagesPixelmatch(artifactPath, goldPath, diffPath, options);
|
|
147
107
|
}
|
|
148
108
|
|
|
149
|
-
async function
|
|
109
|
+
async function main(options = {}) {
|
|
110
|
+
const mainConfigPath = "rettangoli.config.yaml";
|
|
111
|
+
let mainConfig;
|
|
150
112
|
try {
|
|
151
|
-
|
|
152
|
-
const templateContent = fs.readFileSync(templatePath, "utf8");
|
|
153
|
-
|
|
154
|
-
// Render the template with the results data
|
|
155
|
-
const renderedHtml = await engine.parseAndRender(templateContent, {
|
|
156
|
-
files: results,
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
// Write the rendered HTML to the output file
|
|
160
|
-
fs.writeFileSync(outputPath, renderedHtml);
|
|
161
|
-
|
|
162
|
-
console.log(`Report generated successfully at ${outputPath}`);
|
|
113
|
+
mainConfig = await readYaml(mainConfigPath);
|
|
163
114
|
} catch (error) {
|
|
164
|
-
|
|
115
|
+
throw new Error(`Unable to read "${mainConfigPath}": ${error.message}`, { cause: error });
|
|
165
116
|
}
|
|
166
|
-
}
|
|
167
117
|
|
|
168
|
-
|
|
118
|
+
const vtConfig = mainConfig?.vt;
|
|
119
|
+
if (!vtConfig) {
|
|
120
|
+
throw new Error(`Invalid "${mainConfigPath}": missing required "vt" section.`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const configData = validateVtConfig(vtConfig, mainConfigPath);
|
|
169
124
|
const {
|
|
170
|
-
vtPath
|
|
171
|
-
compareMethod
|
|
172
|
-
colorThreshold
|
|
173
|
-
diffThreshold
|
|
174
|
-
|
|
125
|
+
vtPath,
|
|
126
|
+
compareMethod,
|
|
127
|
+
colorThreshold,
|
|
128
|
+
diffThreshold,
|
|
129
|
+
selectors,
|
|
130
|
+
} = resolveReportOptions(options, configData);
|
|
175
131
|
|
|
176
132
|
const siteOutputPath = path.join(".rettangoli", "vt", "_site");
|
|
177
133
|
const candidateDir = path.join(siteOutputPath, "candidate");
|
|
@@ -183,7 +139,7 @@ async function main(options = {}) {
|
|
|
183
139
|
const jsonReportPath = path.join(".rettangoli", "vt", "report.json");
|
|
184
140
|
|
|
185
141
|
console.log(`Comparison method: ${compareMethod}`);
|
|
186
|
-
if (compareMethod ===
|
|
142
|
+
if (compareMethod === "pixelmatch") {
|
|
187
143
|
console.log(` color threshold: ${colorThreshold}, diff threshold: ${diffThreshold}%`);
|
|
188
144
|
}
|
|
189
145
|
|
|
@@ -192,52 +148,56 @@ async function main(options = {}) {
|
|
|
192
148
|
fs.mkdirSync(originalReferenceDir, { recursive: true });
|
|
193
149
|
}
|
|
194
150
|
|
|
195
|
-
|
|
196
|
-
if (compareMethod === 'pixelmatch' && !fs.existsSync(diffDir)) {
|
|
151
|
+
if (compareMethod === "pixelmatch" && !fs.existsSync(diffDir)) {
|
|
197
152
|
fs.mkdirSync(diffDir, { recursive: true });
|
|
198
153
|
}
|
|
199
154
|
|
|
200
|
-
// Copy reference directory to _site for web access
|
|
201
155
|
if (fs.existsSync(originalReferenceDir)) {
|
|
202
156
|
console.log("Copying reference directory to _site...");
|
|
203
157
|
await cp(originalReferenceDir, siteReferenceDir, { recursive: true });
|
|
204
158
|
}
|
|
205
159
|
|
|
206
160
|
try {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
161
|
+
if (!fs.existsSync(candidateDir)) {
|
|
162
|
+
throw new Error(`Candidate screenshots directory not found: "${candidateDir}". Run "rtgl vt generate" first.`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const candidateFiles = getAllFiles(candidateDir).filter((file) => file.endsWith(".webp"));
|
|
166
|
+
const referenceFiles = getAllFiles(originalReferenceDir).filter((file) => file.endsWith(".webp"));
|
|
210
167
|
|
|
211
168
|
console.log("Candidate Screenshots:", candidateFiles.length);
|
|
212
169
|
console.log("Reference Screenshots:", referenceFiles.length);
|
|
213
170
|
|
|
214
171
|
const results = [];
|
|
172
|
+
const comparisonErrors = [];
|
|
215
173
|
|
|
216
|
-
// Get relative paths for comparison
|
|
217
174
|
const candidateRelativePaths = candidateFiles.map((file) =>
|
|
218
|
-
path.relative(candidateDir, file)
|
|
175
|
+
path.relative(candidateDir, file),
|
|
219
176
|
);
|
|
220
177
|
const referenceRelativePaths = referenceFiles.map((file) =>
|
|
221
|
-
path.relative(originalReferenceDir, file)
|
|
178
|
+
path.relative(originalReferenceDir, file),
|
|
222
179
|
);
|
|
223
180
|
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
181
|
+
const allPaths = buildAllRelativePaths(candidateRelativePaths, referenceRelativePaths);
|
|
182
|
+
const scopedPaths = filterRelativeScreenshotPathsBySelectors(
|
|
183
|
+
allPaths,
|
|
184
|
+
selectors,
|
|
185
|
+
configData.sections,
|
|
186
|
+
);
|
|
187
|
+
if (hasSelectors(selectors)) {
|
|
188
|
+
const excludedCount = allPaths.length - scopedPaths.length;
|
|
189
|
+
console.log(`Selector scope: ${scopedPaths.length} image(s) selected, ${excludedCount} excluded.`);
|
|
190
|
+
}
|
|
230
191
|
|
|
231
|
-
for (const relativePath of
|
|
192
|
+
for (const relativePath of scopedPaths) {
|
|
232
193
|
const candidatePath = path.join(candidateDir, relativePath);
|
|
233
194
|
const referencePath = path.join(originalReferenceDir, relativePath);
|
|
234
195
|
const siteReferencePath = path.join(siteReferenceDir, relativePath);
|
|
235
|
-
const diffPath = path.join(diffDir, relativePath.replace(
|
|
196
|
+
const diffPath = path.join(diffDir, relativePath.replace(".webp", "-diff.png"));
|
|
236
197
|
|
|
237
198
|
const candidateExists = fs.existsSync(candidatePath);
|
|
238
199
|
const referenceExists = fs.existsSync(referencePath);
|
|
239
200
|
|
|
240
|
-
// Skip if neither file exists (shouldn't happen, but just in case)
|
|
241
201
|
if (!candidateExists && !referenceExists) continue;
|
|
242
202
|
|
|
243
203
|
let equal = true;
|
|
@@ -245,9 +205,7 @@ async function main(options = {}) {
|
|
|
245
205
|
let similarity = null;
|
|
246
206
|
let diffPixels = null;
|
|
247
207
|
|
|
248
|
-
// Compare images if both exist
|
|
249
208
|
if (candidateExists && referenceExists) {
|
|
250
|
-
// Ensure diff directory exists
|
|
251
209
|
const diffDirPath = path.dirname(diffPath);
|
|
252
210
|
if (!fs.existsSync(diffDirPath)) {
|
|
253
211
|
fs.mkdirSync(diffDirPath, { recursive: true });
|
|
@@ -258,20 +216,26 @@ async function main(options = {}) {
|
|
|
258
216
|
referencePath,
|
|
259
217
|
compareMethod,
|
|
260
218
|
diffPath,
|
|
261
|
-
{ colorThreshold, diffThreshold }
|
|
219
|
+
{ colorThreshold, diffThreshold },
|
|
262
220
|
);
|
|
221
|
+
if (comparison.error) {
|
|
222
|
+
comparisonErrors.push(
|
|
223
|
+
`${relativePath}: ${comparison.message || "unknown comparison error"}`,
|
|
224
|
+
);
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
263
227
|
equal = comparison.equal;
|
|
264
228
|
error = comparison.error;
|
|
265
229
|
similarity = comparison.similarity;
|
|
266
230
|
diffPixels = comparison.diffPixels;
|
|
267
231
|
} else {
|
|
268
|
-
equal = false;
|
|
232
|
+
equal = false;
|
|
269
233
|
}
|
|
270
234
|
|
|
271
235
|
if (!error) {
|
|
272
236
|
results.push({
|
|
273
237
|
candidatePath: candidateExists ? candidatePath : null,
|
|
274
|
-
referencePath: referenceExists ? siteReferencePath : null,
|
|
238
|
+
referencePath: referenceExists ? siteReferencePath : null,
|
|
275
239
|
path: relativePath,
|
|
276
240
|
equal: candidateExists && referenceExists ? equal : false,
|
|
277
241
|
similarity,
|
|
@@ -282,28 +246,15 @@ async function main(options = {}) {
|
|
|
282
246
|
}
|
|
283
247
|
}
|
|
284
248
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
(
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
candidatePath: result.candidatePath
|
|
293
|
-
? path.relative(siteOutputPath, result.candidatePath)
|
|
294
|
-
: null,
|
|
295
|
-
referencePath: result.referencePath
|
|
296
|
-
? path.relative(siteOutputPath, result.referencePath)
|
|
297
|
-
: null,
|
|
298
|
-
equal: result.equal,
|
|
299
|
-
similarity: result.similarity,
|
|
300
|
-
diffPixels: result.diffPixels,
|
|
301
|
-
onlyInCandidate: result.onlyInCandidate,
|
|
302
|
-
onlyInReference: result.onlyInReference,
|
|
303
|
-
};
|
|
304
|
-
});
|
|
249
|
+
if (comparisonErrors.length > 0) {
|
|
250
|
+
throw new Error(
|
|
251
|
+
`Image comparison failed for ${comparisonErrors.length} file(s):\n- ${comparisonErrors.join("\n- ")}`,
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const mismatchingItems = toMismatchingItems(results, siteOutputPath);
|
|
305
256
|
console.log("Mismatching Items (JSON):");
|
|
306
|
-
mismatchingItems.forEach(item => {
|
|
257
|
+
mismatchingItems.forEach((item) => {
|
|
307
258
|
const logData = {
|
|
308
259
|
candidatePath: item.candidatePath,
|
|
309
260
|
referencePath: item.referencePath,
|
|
@@ -314,42 +265,28 @@ async function main(options = {}) {
|
|
|
314
265
|
console.log(JSON.stringify(logData, null, 2));
|
|
315
266
|
});
|
|
316
267
|
|
|
317
|
-
|
|
318
|
-
console.log(`\nSummary:`);
|
|
268
|
+
console.log("\nSummary:");
|
|
319
269
|
console.log(`Total images: ${results.length}`);
|
|
320
270
|
console.log(`Mismatched images: ${mismatchingItems.length}`);
|
|
321
271
|
|
|
322
|
-
|
|
323
|
-
await generateReport({
|
|
272
|
+
await renderHtmlReport({
|
|
324
273
|
results: mismatchingItems,
|
|
325
274
|
templatePath,
|
|
326
275
|
outputPath,
|
|
327
276
|
});
|
|
328
277
|
|
|
329
|
-
|
|
330
|
-
const jsonReport = {
|
|
331
|
-
timestamp: new Date().toISOString(),
|
|
278
|
+
const jsonReport = buildJsonReport({
|
|
332
279
|
total: results.length,
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
path: item.candidatePath || item.referencePath,
|
|
336
|
-
candidatePath: item.candidatePath,
|
|
337
|
-
referencePath: item.referencePath,
|
|
338
|
-
equal: item.equal,
|
|
339
|
-
similarity: item.similarity,
|
|
340
|
-
onlyInCandidate: item.onlyInCandidate,
|
|
341
|
-
onlyInReference: item.onlyInReference,
|
|
342
|
-
})),
|
|
343
|
-
};
|
|
280
|
+
mismatchingItems,
|
|
281
|
+
});
|
|
344
282
|
fs.writeFileSync(jsonReportPath, JSON.stringify(jsonReport, null, 2));
|
|
345
283
|
console.log(`JSON report written to ${jsonReportPath}`);
|
|
346
284
|
|
|
347
|
-
if(mismatchingItems.length > 0){
|
|
348
|
-
|
|
349
|
-
process.exit(1);
|
|
285
|
+
if (mismatchingItems.length > 0) {
|
|
286
|
+
throw new Error(`Visual differences found in ${mismatchingItems.length} file(s).`);
|
|
350
287
|
}
|
|
351
288
|
} catch (error) {
|
|
352
|
-
|
|
289
|
+
throw new Error(`Error generating VT report: ${error.message}`, { cause: error });
|
|
353
290
|
}
|
|
354
291
|
}
|
|
355
292
|
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
</rtgl-sidebar>
|
|
53
53
|
</rtgl-view>
|
|
54
54
|
|
|
55
|
-
<rtgl-view id="content" h="100vh" w="
|
|
55
|
+
<rtgl-view id="content" h="100vh" w="1fg" p="lg" g="lg" style="flex-wrap: nowrap;" sv ah="c">
|
|
56
56
|
<rtgl-view w="f" g="xl">
|
|
57
57
|
<rtgl-text s="h2">{{ currentSection.title }} </rtgl-text>
|
|
58
58
|
{% for file in files %}
|
|
@@ -106,4 +106,4 @@
|
|
|
106
106
|
</rtgl-view>
|
|
107
107
|
</body>
|
|
108
108
|
|
|
109
|
-
</html>
|
|
109
|
+
</html>
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
<rtgl-view p="sm">
|
|
31
31
|
</rtgl-view>
|
|
32
32
|
</rtgl-view>
|
|
33
|
-
<rtgl-view h="100vh" w="
|
|
33
|
+
<rtgl-view h="100vh" w="1fg" bgc="su" p="lg" g="lg" style="flex-wrap: nowrap;" sv>
|
|
34
34
|
<rtgl-view>
|
|
35
35
|
<rtgl-text s="tl">Rettangoli Components</rtgl-text>
|
|
36
36
|
<rtgl-text s="sm">{{ files.length }} components failed</rtgl-text>
|
|
@@ -38,11 +38,11 @@
|
|
|
38
38
|
{% for file in files %}
|
|
39
39
|
<rtgl-view w="f">
|
|
40
40
|
<rtgl-view w="f" d="h" g="lg">
|
|
41
|
-
<rtgl-view
|
|
41
|
+
<rtgl-view w="1fg">
|
|
42
42
|
<rtgl-text>{{ file.referencePath }}</rtgl-text>
|
|
43
43
|
<img width="100%" src="/{{ file.referencePath }}">
|
|
44
44
|
</rtgl-view>
|
|
45
|
-
<rtgl-view
|
|
45
|
+
<rtgl-view w="1fg">
|
|
46
46
|
<rtgl-text>{{ file.candidatePath }}</rtgl-text>
|
|
47
47
|
<img width="100%" src="/{{ file.candidatePath }}">
|
|
48
48
|
</rtgl-view>
|
|
@@ -53,4 +53,4 @@
|
|
|
53
53
|
</rtgl-view>
|
|
54
54
|
</body>
|
|
55
55
|
|
|
56
|
-
</html>
|
|
56
|
+
</html>
|