@invarn/cibuild 2.0.4 → 2.0.6
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.
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ui-fidelity-render.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/ui-fidelity-render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAEpE;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC;IACnC;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,mFAAmF;IACnF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6DAA6D;IAC7D,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CACzB;AAED;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,YAAY,CAAC;AAE7C,mFAAmF;AACnF,eAAO,MAAM,aAAa,IAAI,CAAC;AAE/B,iDAAiD;AACjD,eAAO,MAAM,eAAe,6BAA8B,CAAC;AAC3D,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC;AAE7D,sEAAsE;AACtE,eAAO,MAAM,sBAAsB,EAAE,aAAsB,CAAC;AAE5D,+EAA+E;AAC/E,eAAO,MAAM,wBAAwB,mBAAmB,CAAC;AAEzD;;;;;;;GAOG;AACH,eAAO,MAAM,2BAA2B,QAAmB,CAAC;AAE5D,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,CAWzD;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAQzD;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,kBAAkB;IACjC,2EAA2E;IAC3E,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,yEAAyE;IACzE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,aAAa,CAAC;CAC/B;AAED,4DAA4D;AAC5D,MAAM,WAAW,uBAAuB;IACtC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf;AAED,gFAAgF;AAChF,MAAM,WAAW,qBAAqB;IACpC,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAC1C,2BAA2B,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,uBAAuB,GAAG,MAAM,CAAC;IACzF,kBAAkB,CAAC,OAAO,EAAE,uBAAuB,GAAG,MAAM,CAAC;IAC7D,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,uBAAuB,GAAG,MAAM,CAAC;CAC/E;
|
|
1
|
+
{"version":3,"file":"ui-fidelity-render.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/ui-fidelity-render.ts"],"names":[],"mappings":"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"}
|
|
@@ -986,6 +986,240 @@ function compileAssetCatalogs(packageDir, harnessDir) {
|
|
|
986
986
|
}
|
|
987
987
|
}
|
|
988
988
|
`;
|
|
989
|
+
/**
|
|
990
|
+
* Runtime support for trimming the rendered output (package_source variants).
|
|
991
|
+
*
|
|
992
|
+
* The render entry forces the view into a fixed device-sized frame, so a
|
|
993
|
+
* component smaller than the canvas renders with transparent margins. After a
|
|
994
|
+
* screen renders, crop the PNG in place to the bounding box of its
|
|
995
|
+
* non-transparent pixels, so the rendered artifact is the component itself,
|
|
996
|
+
* tight -- no canvas, no background ambiguity for the human comparing screens.
|
|
997
|
+
*
|
|
998
|
+
* The crop is done by a small CoreGraphics helper run in Swift's
|
|
999
|
+
* script-interpreter mode (so it needs no extra harness target and reuses the
|
|
1000
|
+
* one `swift` the runner already has). Best-effort: a trim failure is logged
|
|
1001
|
+
* (warn-and-continue) and the untrimmed render is kept -- a cosmetic post-step
|
|
1002
|
+
* never fails an otherwise good render. This is prep, not scoring: it produces
|
|
1003
|
+
* a tighter image for the human's judgment and computes nothing about
|
|
1004
|
+
* fidelity.
|
|
1005
|
+
*/
|
|
1006
|
+
const RENDER_SCRIPT_TRIM_STAGE = String.raw `
|
|
1007
|
+
// ---- render trimming stage (package_source variants) ----
|
|
1008
|
+
|
|
1009
|
+
// CoreGraphics helper, run via 'swift <Trim.swift> <in.png> <out.png>'
|
|
1010
|
+
// (script-interpreter mode). Crops a PNG to the bounding box of its
|
|
1011
|
+
// non-transparent pixels and writes it to the output path, which may equal the
|
|
1012
|
+
// input path (in-place trim).
|
|
1013
|
+
var TRIM_HELPER_SWIFT = [
|
|
1014
|
+
'import Foundation',
|
|
1015
|
+
'import ImageIO',
|
|
1016
|
+
'import CoreGraphics',
|
|
1017
|
+
'import UniformTypeIdentifiers',
|
|
1018
|
+
'',
|
|
1019
|
+
'func loadCGImage(_ path: String) -> CGImage? {',
|
|
1020
|
+
' guard let src = CGImageSourceCreateWithURL(URL(fileURLWithPath: path) as CFURL, nil),',
|
|
1021
|
+
' let img = CGImageSourceCreateImageAtIndex(src, 0, nil) else { return nil }',
|
|
1022
|
+
' return img',
|
|
1023
|
+
'}',
|
|
1024
|
+
'',
|
|
1025
|
+
'func writePNG(_ image: CGImage, to path: String) -> Bool {',
|
|
1026
|
+
' guard let dest = CGImageDestinationCreateWithURL(URL(fileURLWithPath: path) as CFURL, UTType.png.identifier as CFString, 1, nil) else { return false }',
|
|
1027
|
+
' CGImageDestinationAddImage(dest, image, nil)',
|
|
1028
|
+
' return CGImageDestinationFinalize(dest)',
|
|
1029
|
+
'}',
|
|
1030
|
+
'',
|
|
1031
|
+
'// Redraw an image at an exact pixel size (used to match the reference).',
|
|
1032
|
+
'func resize(_ image: CGImage, toWidth w: Int, height h: Int) -> CGImage? {',
|
|
1033
|
+
' if w <= 0 || h <= 0 { return nil }',
|
|
1034
|
+
' let bytesPerRow = w * 4',
|
|
1035
|
+
' let space = CGColorSpaceCreateDeviceRGB()',
|
|
1036
|
+
' guard let ctx = CGContext(data: nil, width: w, height: h, bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: space, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) else { return nil }',
|
|
1037
|
+
' ctx.interpolationQuality = .high',
|
|
1038
|
+
' ctx.draw(image, in: CGRect(x: 0, y: 0, width: w, height: h))',
|
|
1039
|
+
' return ctx.makeImage()',
|
|
1040
|
+
'}',
|
|
1041
|
+
'',
|
|
1042
|
+
'// Bounding box of content: pixels that are neither transparent nor part of a',
|
|
1043
|
+
'// uniform solid background. The background is detected from the four corners',
|
|
1044
|
+
'// (opaque and agreeing within a tight tolerance); when the corners disagree,',
|
|
1045
|
+
'// only transparency is trimmed. Top-left image coordinates.',
|
|
1046
|
+
'func contentBoundingBox(_ image: CGImage) -> CGRect? {',
|
|
1047
|
+
' let w = image.width, h = image.height',
|
|
1048
|
+
' if w == 0 || h == 0 { return nil }',
|
|
1049
|
+
' let bytesPerRow = w * 4',
|
|
1050
|
+
' var data = [UInt8](repeating: 0, count: bytesPerRow * h)',
|
|
1051
|
+
' let space = CGColorSpaceCreateDeviceRGB()',
|
|
1052
|
+
' guard let ctx = CGContext(data: &data, width: w, height: h, bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: space, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) else { return nil }',
|
|
1053
|
+
' ctx.draw(image, in: CGRect(x: 0, y: 0, width: w, height: h))',
|
|
1054
|
+
' // Tight tolerance: exact solid fills match, near-colored content does not.',
|
|
1055
|
+
' let tolerance = 4',
|
|
1056
|
+
' func sample(_ x: Int, _ y: Int) -> (Int, Int, Int, Int) {',
|
|
1057
|
+
' let i = y * bytesPerRow + x * 4',
|
|
1058
|
+
' return (Int(data[i]), Int(data[i + 1]), Int(data[i + 2]), Int(data[i + 3]))',
|
|
1059
|
+
' }',
|
|
1060
|
+
' // Detect a uniform opaque background shared by all four corners.',
|
|
1061
|
+
' let corners = [sample(0, 0), sample(w - 1, 0), sample(0, h - 1), sample(w - 1, h - 1)]',
|
|
1062
|
+
' var background: (Int, Int, Int, Int)? = corners[0]',
|
|
1063
|
+
' for c in corners {',
|
|
1064
|
+
' if let bg = background {',
|
|
1065
|
+
' if c.3 == 0 || abs(c.0 - bg.0) > tolerance || abs(c.1 - bg.1) > tolerance || abs(c.2 - bg.2) > tolerance || abs(c.3 - bg.3) > tolerance {',
|
|
1066
|
+
' background = nil',
|
|
1067
|
+
' }',
|
|
1068
|
+
' }',
|
|
1069
|
+
' }',
|
|
1070
|
+
' var minX = w, minY = h, maxX = -1, maxY = -1',
|
|
1071
|
+
' for y in 0..<h {',
|
|
1072
|
+
' let rowBase = y * bytesPerRow',
|
|
1073
|
+
' for x in 0..<w {',
|
|
1074
|
+
' let idx = rowBase + x * 4',
|
|
1075
|
+
' let a = Int(data[idx + 3])',
|
|
1076
|
+
' var isMargin = a == 0',
|
|
1077
|
+
' if !isMargin, let bg = background {',
|
|
1078
|
+
' let r = Int(data[idx]), g = Int(data[idx + 1]), b = Int(data[idx + 2])',
|
|
1079
|
+
' if abs(r - bg.0) <= tolerance && abs(g - bg.1) <= tolerance && abs(b - bg.2) <= tolerance && abs(a - bg.3) <= tolerance {',
|
|
1080
|
+
' isMargin = true',
|
|
1081
|
+
' }',
|
|
1082
|
+
' }',
|
|
1083
|
+
' if !isMargin {',
|
|
1084
|
+
' if x < minX { minX = x }',
|
|
1085
|
+
' if x > maxX { maxX = x }',
|
|
1086
|
+
' if y < minY { minY = y }',
|
|
1087
|
+
' if y > maxY { maxY = y }',
|
|
1088
|
+
' }',
|
|
1089
|
+
' }',
|
|
1090
|
+
' }',
|
|
1091
|
+
' if maxX < 0 { return nil }',
|
|
1092
|
+
' return CGRect(x: minX, y: minY, width: maxX - minX + 1, height: maxY - minY + 1)',
|
|
1093
|
+
'}',
|
|
1094
|
+
'',
|
|
1095
|
+
'let arguments = CommandLine.arguments',
|
|
1096
|
+
'guard arguments.count >= 3 else {',
|
|
1097
|
+
' FileHandle.standardError.write(Data("usage: Trim <in.png> <out.png>".utf8))',
|
|
1098
|
+
' exit(64)',
|
|
1099
|
+
'}',
|
|
1100
|
+
'let inputPath = arguments[1]',
|
|
1101
|
+
'let outputPath = arguments[2]',
|
|
1102
|
+
'guard let image = loadCGImage(inputPath) else {',
|
|
1103
|
+
' FileHandle.standardError.write(Data(("could not read image at " + inputPath).utf8))',
|
|
1104
|
+
' exit(65)',
|
|
1105
|
+
'}',
|
|
1106
|
+
'guard let box = contentBoundingBox(image) else {',
|
|
1107
|
+
' FileHandle.standardError.write(Data("no content to trim (image is uniform)".utf8))',
|
|
1108
|
+
' exit(66)',
|
|
1109
|
+
'}',
|
|
1110
|
+
'guard let cropped = image.cropping(to: box) else {',
|
|
1111
|
+
' FileHandle.standardError.write(Data("could not crop image".utf8))',
|
|
1112
|
+
' exit(67)',
|
|
1113
|
+
'}',
|
|
1114
|
+
'let croppedWidth = cropped.width',
|
|
1115
|
+
'let croppedHeight = cropped.height',
|
|
1116
|
+
'guard writePNG(cropped, to: outputPath) else {',
|
|
1117
|
+
' FileHandle.standardError.write(Data(("could not write trimmed PNG to " + outputPath).utf8))',
|
|
1118
|
+
' exit(68)',
|
|
1119
|
+
'}',
|
|
1120
|
+
'',
|
|
1121
|
+
'// Optional aligned output: the trimmed image resized to the reference pixel',
|
|
1122
|
+
'// size, a 1:1 overlay pair. Best-effort; dimensions are reported either way.',
|
|
1123
|
+
'var referenceWidth = 0',
|
|
1124
|
+
'var referenceHeight = 0',
|
|
1125
|
+
'if arguments.count >= 5 {',
|
|
1126
|
+
' let alignedPath = arguments[3]',
|
|
1127
|
+
' let referencePath = arguments[4]',
|
|
1128
|
+
' if let reference = loadCGImage(referencePath) {',
|
|
1129
|
+
' referenceWidth = reference.width',
|
|
1130
|
+
' referenceHeight = reference.height',
|
|
1131
|
+
' if let aligned = resize(cropped, toWidth: referenceWidth, height: referenceHeight) {',
|
|
1132
|
+
' _ = writePNG(aligned, to: alignedPath)',
|
|
1133
|
+
' }',
|
|
1134
|
+
' }',
|
|
1135
|
+
'}',
|
|
1136
|
+
'',
|
|
1137
|
+
'// Report: rendered (trimmed) px, reference px (0 0 when unavailable), then',
|
|
1138
|
+
'// the pre-trim source px so the caller can log how much margin was removed.',
|
|
1139
|
+
'print("TRIMDIMS " + String(croppedWidth) + " " + String(croppedHeight) + " " + String(referenceWidth) + " " + String(referenceHeight) + " " + String(image.width) + " " + String(image.height))'
|
|
1140
|
+
].join('\n') + '\n';
|
|
1141
|
+
|
|
1142
|
+
// Write the helper into the harness dir once (idempotent across screens).
|
|
1143
|
+
function ensureTrimHelper(harnessDir) {
|
|
1144
|
+
var helperPath = path.join(harnessDir, 'Trim.swift');
|
|
1145
|
+
if (!fs.existsSync(helperPath)) {
|
|
1146
|
+
fs.writeFileSync(helperPath, TRIM_HELPER_SWIFT);
|
|
1147
|
+
}
|
|
1148
|
+
return helperPath;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// Round to two decimals, keeping the value a plain number.
|
|
1152
|
+
function roundTwo(value) {
|
|
1153
|
+
return Math.round(value * 100) / 100;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// Parse the helper's "TRIMDIMS <rW> <rH> <refW> <refH> <srcW> <srcH>" line
|
|
1157
|
+
// (rendered/reference/source px; last one wins).
|
|
1158
|
+
function parseTrimDims(stdout) {
|
|
1159
|
+
var lines = String(stdout || '').split('\n');
|
|
1160
|
+
for (var i = lines.length - 1; i >= 0; i--) {
|
|
1161
|
+
var parts = lines[i].trim().split(/\s+/);
|
|
1162
|
+
if (parts[0] === 'TRIMDIMS' && parts.length >= 7) {
|
|
1163
|
+
var nums = [
|
|
1164
|
+
Number(parts[1]), Number(parts[2]), Number(parts[3]),
|
|
1165
|
+
Number(parts[4]), Number(parts[5]), Number(parts[6]),
|
|
1166
|
+
];
|
|
1167
|
+
if (nums.every(function (n) { return Number.isFinite(n); })) {
|
|
1168
|
+
return {
|
|
1169
|
+
rendered: [nums[0], nums[1]],
|
|
1170
|
+
reference: [nums[2], nums[3]],
|
|
1171
|
+
source: [nums[4], nums[5]],
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
return null;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// Crop the rendered PNG in place to its content bounding box, write an aligned
|
|
1180
|
+
// copy resized to the reference, and record the dimensions on the entry.
|
|
1181
|
+
// Best-effort: a failure is logged and the untrimmed render is kept.
|
|
1182
|
+
function trimRenderedImage(entry, renderedPath, harnessDir) {
|
|
1183
|
+
var helperPath = ensureTrimHelper(harnessDir);
|
|
1184
|
+
var alignedRelative = 'ui-fidelity/aligned/' + entry.screen + '.png';
|
|
1185
|
+
var alignedPath = path.resolve(artifactsDir, alignedRelative);
|
|
1186
|
+
var referencePath =
|
|
1187
|
+
entry.reference_image_path === null
|
|
1188
|
+
? ''
|
|
1189
|
+
: path.resolve(artifactsDir, entry.reference_image_path);
|
|
1190
|
+
try {
|
|
1191
|
+
fs.mkdirSync(path.dirname(alignedPath), { recursive: true });
|
|
1192
|
+
} catch (mkdirError) {
|
|
1193
|
+
// best effort -- the helper reports a missing output dir itself
|
|
1194
|
+
}
|
|
1195
|
+
var result = runSwift([helperPath, renderedPath, renderedPath, alignedPath, referencePath]);
|
|
1196
|
+
if (result.status !== 0) {
|
|
1197
|
+
logError(
|
|
1198
|
+
'trim failed for ' + renderedPath +
|
|
1199
|
+
' (continuing with the untrimmed render): ' + sanitizeMessage(outputTail(result))
|
|
1200
|
+
);
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
var dims = parseTrimDims(result.stdout);
|
|
1204
|
+
if (dims !== null) {
|
|
1205
|
+
var scale = CONFIG.scale;
|
|
1206
|
+
entry.dimensions = {
|
|
1207
|
+
rendered_px: dims.rendered,
|
|
1208
|
+
logical_pt: [roundTwo(dims.rendered[0] / scale), roundTwo(dims.rendered[1] / scale)],
|
|
1209
|
+
reference_px: dims.reference[0] > 0 && dims.reference[1] > 0 ? dims.reference : null,
|
|
1210
|
+
};
|
|
1211
|
+
// Surface how much margin was removed -- a no-op (source == rendered) means
|
|
1212
|
+
// the view fills an opaque canvas with no transparent or uniform margin.
|
|
1213
|
+
log(
|
|
1214
|
+
'trimmed ' + entry.screen + ': ' + dims.source[0] + 'x' + dims.source[1] +
|
|
1215
|
+
' -> ' + dims.rendered[0] + 'x' + dims.rendered[1]
|
|
1216
|
+
);
|
|
1217
|
+
}
|
|
1218
|
+
if (fs.existsSync(alignedPath)) {
|
|
1219
|
+
entry.aligned_image_path = alignedRelative;
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
`;
|
|
989
1223
|
/**
|
|
990
1224
|
* Replaces exactly one occurrence of `anchor` in `source`. Throws when the
|
|
991
1225
|
* anchor is missing or ambiguous, so any drift between the v1 runtime text
|
|
@@ -1093,6 +1327,21 @@ function buildPackageSourceMain() {
|
|
|
1093
1327
|
main = replaceOnce(main, 'function renderWithHarness(renderable, packagePath) {', RENDER_SCRIPT_ASSET_CATALOG_STAGE + '\nfunction renderWithHarness(renderable, packagePath) {');
|
|
1094
1328
|
main = replaceOnce(main, ' renderable.forEach(function (entry) {', ' compileAssetCatalogs(packagePath, harnessDir);\n\n' +
|
|
1095
1329
|
' renderable.forEach(function (entry) {');
|
|
1330
|
+
// Render-trimming stage: define the helpers ahead of renderWithHarness, then
|
|
1331
|
+
// crop each rendered PNG to its content bounding box right after it renders.
|
|
1332
|
+
main = replaceOnce(main, 'function renderWithHarness(renderable, packagePath) {', RENDER_SCRIPT_TRIM_STAGE + '\nfunction renderWithHarness(renderable, packagePath) {');
|
|
1333
|
+
main = replaceOnce(main, " log('rendered ' + entry.screen + ' -> ' + relative);", " log('rendered ' + entry.screen + ' -> ' + relative);\n" +
|
|
1334
|
+
' trimRenderedImage(entry, outputPath, harnessDir);');
|
|
1335
|
+
// Per-screen result entries gain the trimmed-render fields: the aligned
|
|
1336
|
+
// (reference-sized) image and the rendered/logical/reference dimensions.
|
|
1337
|
+
// They stay null until a screen renders and is trimmed.
|
|
1338
|
+
main = replaceOnce(main, ' reference_image_path: null,\n' +
|
|
1339
|
+
' rendered_image_path: null,\n' +
|
|
1340
|
+
' };', ' reference_image_path: null,\n' +
|
|
1341
|
+
' rendered_image_path: null,\n' +
|
|
1342
|
+
' aligned_image_path: null,\n' +
|
|
1343
|
+
' dimensions: null,\n' +
|
|
1344
|
+
' };');
|
|
1096
1345
|
return main;
|
|
1097
1346
|
}
|
|
1098
1347
|
const RENDER_SCRIPT_MAIN_WITH_PACKAGE_SOURCE = buildPackageSourceMain();
|
|
@@ -111,6 +111,23 @@ const FAKE_SWIFT_LINES = [
|
|
|
111
111
|
" printf '\\x89PNG\\r\\n\\x1a\\n' > \"$out\"",
|
|
112
112
|
' printf \'fake-render:%s\' "$screen" >> "$out"',
|
|
113
113
|
' exit 0 ;;',
|
|
114
|
+
' *.swift)',
|
|
115
|
+
// Script-interpreter mode (swift <Trim.swift> <in> <out> <aligned> <ref>):
|
|
116
|
+
// stands in for the CoreGraphics trim helper. Overwrites <out> with a PNG
|
|
117
|
+
// marked "trimmed:<in basename>" and writes an "aligned:<in basename>" PNG so
|
|
118
|
+
// tests can prove both outputs. Emits a fixed TRIMDIMS line (rendered
|
|
119
|
+
// 320x640, reference 160x320, source 400x800). FAKE_TRIM_FAIL exercises
|
|
120
|
+
// warn-and-continue.
|
|
121
|
+
' if [ -n "$FAKE_TRIM_FAIL" ]; then echo "trim: simulated failure" >&2; exit 1; fi',
|
|
122
|
+
' in_png="$1"; out_png="$2"; aligned_png="$3"',
|
|
123
|
+
" printf '\\x89PNG\\r\\n\\x1a\\n' > \"$out_png\"",
|
|
124
|
+
' printf \'trimmed:%s\' "$(basename "$in_png")" >> "$out_png"',
|
|
125
|
+
' if [ -n "$aligned_png" ]; then',
|
|
126
|
+
" printf '\\x89PNG\\r\\n\\x1a\\n' > \"$aligned_png\"",
|
|
127
|
+
' printf \'aligned:%s\' "$(basename "$in_png")" >> "$aligned_png"',
|
|
128
|
+
' fi',
|
|
129
|
+
' echo "TRIMDIMS 320 640 160 320 400 800"',
|
|
130
|
+
' exit 0 ;;',
|
|
114
131
|
' *)',
|
|
115
132
|
' exit 0 ;;',
|
|
116
133
|
'esac',
|
|
@@ -896,6 +913,12 @@ describe('render script runtime (package_source: repo)', () => {
|
|
|
896
913
|
error: null,
|
|
897
914
|
reference_image_path: 'ui-fidelity/references/HomeView.png',
|
|
898
915
|
rendered_image_path: 'ui-fidelity/rendered/HomeView.png',
|
|
916
|
+
aligned_image_path: 'ui-fidelity/aligned/HomeView.png',
|
|
917
|
+
dimensions: {
|
|
918
|
+
rendered_px: [320, 640],
|
|
919
|
+
logical_pt: [160, 320],
|
|
920
|
+
reference_px: [160, 320],
|
|
921
|
+
},
|
|
899
922
|
},
|
|
900
923
|
],
|
|
901
924
|
});
|
|
@@ -965,6 +988,82 @@ describe('render script runtime (package_source: repo)', () => {
|
|
|
965
988
|
expect(run.stderr).toContain('asset-catalog compile failed');
|
|
966
989
|
});
|
|
967
990
|
});
|
|
991
|
+
describe('render output trimming (package_source variants)', () => {
|
|
992
|
+
// The renderer forces the view into a fixed device-sized frame, so a small
|
|
993
|
+
// component lands in a large canvas with transparent margins. After each
|
|
994
|
+
// screen renders, the trim helper crops the PNG in place to its content
|
|
995
|
+
// bounding box so the rendered artifact IS the component, tight.
|
|
996
|
+
function readArtifact(project, relative) {
|
|
997
|
+
return readFileSync(artifact(project, relative), 'utf-8');
|
|
998
|
+
}
|
|
999
|
+
test('trims the rendered PNG in place via the Swift helper', async () => {
|
|
1000
|
+
const project = makeProject({ screens: { HomeView: 'home.png' } });
|
|
1001
|
+
writeReference(project, 'home.png');
|
|
1002
|
+
const script = await buildScript(project, { package_source: 'repo' });
|
|
1003
|
+
const logPath = join(project.dir, 'swift-invocations.log');
|
|
1004
|
+
const run = runScript(project, script, { FAKE_SWIFT_LOG: logPath });
|
|
1005
|
+
expect(run.status).toBe(0);
|
|
1006
|
+
// The helper is invoked in script mode with the rendered PNG as in and out
|
|
1007
|
+
// (in-place overwrite), after the render run.
|
|
1008
|
+
const invocations = readFileSync(logPath, 'utf-8').trim().split('\n');
|
|
1009
|
+
const trimCall = invocations.find((c) => c.includes('Trim.swift'));
|
|
1010
|
+
expect(trimCall).toBeTruthy();
|
|
1011
|
+
expect(trimCall).toContain('ui-fidelity/rendered/HomeView.png');
|
|
1012
|
+
// The rendered file now holds the trim helper's output, not the raw render.
|
|
1013
|
+
expect(isPng(artifact(project, 'ui-fidelity/rendered/HomeView.png'))).toBe(true);
|
|
1014
|
+
expect(readArtifact(project, 'ui-fidelity/rendered/HomeView.png')).toContain('trimmed:HomeView.png');
|
|
1015
|
+
});
|
|
1016
|
+
test('writes an aligned image and records rendered/logical/reference dimensions', async () => {
|
|
1017
|
+
const project = makeProject({ screens: { HomeView: 'home.png' } });
|
|
1018
|
+
writeReference(project, 'home.png');
|
|
1019
|
+
const run = runScript(project, await buildScript(project, { package_source: 'repo' }));
|
|
1020
|
+
expect(run.status).toBe(0);
|
|
1021
|
+
// The aligned (reference-sized) image is a separate artifact.
|
|
1022
|
+
expect(isPng(artifact(project, 'ui-fidelity/aligned/HomeView.png'))).toBe(true);
|
|
1023
|
+
expect(readArtifact(project, 'ui-fidelity/aligned/HomeView.png')).toContain('aligned:HomeView.png');
|
|
1024
|
+
// Dimensions are explicit: rendered px from the helper, logical points
|
|
1025
|
+
// derived from the scale (320/2, 640/2), reference px from the helper.
|
|
1026
|
+
const screen = readResult(project).screens[0];
|
|
1027
|
+
expect(screen.aligned_image_path).toBe('ui-fidelity/aligned/HomeView.png');
|
|
1028
|
+
expect(screen.dimensions).toEqual({
|
|
1029
|
+
rendered_px: [320, 640],
|
|
1030
|
+
logical_pt: [160, 320],
|
|
1031
|
+
reference_px: [160, 320],
|
|
1032
|
+
});
|
|
1033
|
+
});
|
|
1034
|
+
test('logs how much margin the trim removed (source -> rendered)', async () => {
|
|
1035
|
+
const project = makeProject({ screens: { HomeView: 'home.png' } });
|
|
1036
|
+
writeReference(project, 'home.png');
|
|
1037
|
+
const run = runScript(project, await buildScript(project, { package_source: 'repo' }));
|
|
1038
|
+
expect(run.status).toBe(0);
|
|
1039
|
+
// The pre-trim source size and the trimmed size are both surfaced, so a
|
|
1040
|
+
// no-op trim (an opaque-canvas wrapper) is visible in the build log.
|
|
1041
|
+
expect(run.stdout).toContain('trimmed HomeView: 400x800 -> 320x640');
|
|
1042
|
+
});
|
|
1043
|
+
test('a failing trim warns but still renders (warn-and-continue)', async () => {
|
|
1044
|
+
const project = makeProject({ screens: { HomeView: 'home.png' } });
|
|
1045
|
+
writeReference(project, 'home.png');
|
|
1046
|
+
const script = await buildScript(project, { package_source: 'repo' });
|
|
1047
|
+
const run = runScript(project, script, { FAKE_TRIM_FAIL: '1' });
|
|
1048
|
+
// A trim failure never fails the step: the untrimmed render survives, with
|
|
1049
|
+
// no aligned image and no dimensions.
|
|
1050
|
+
expect(run.status).toBe(0);
|
|
1051
|
+
expect(isPng(artifact(project, 'ui-fidelity/rendered/HomeView.png'))).toBe(true);
|
|
1052
|
+
const screen = readResult(project).screens[0];
|
|
1053
|
+
expect(screen.status).toBe('rendered');
|
|
1054
|
+
expect(screen.aligned_image_path).toBeNull();
|
|
1055
|
+
expect(screen.dimensions).toBeNull();
|
|
1056
|
+
expect(run.stderr).toContain('trim failed');
|
|
1057
|
+
});
|
|
1058
|
+
test('trims renders shipped via package_source: inputs too', async () => {
|
|
1059
|
+
const project = makeProject({ screens: { HomeView: 'home.png' } });
|
|
1060
|
+
writeReference(project, 'home.png');
|
|
1061
|
+
stageArchive(project, shippedPackageEntries('pkg'));
|
|
1062
|
+
const run = runScript(project, await buildInputsScript({ target: 'FixtureViews' }));
|
|
1063
|
+
expect(run.status).toBe(0);
|
|
1064
|
+
expect(readArtifact(project, 'ui-fidelity/rendered/HomeView.png')).toContain('trimmed:HomeView.png');
|
|
1065
|
+
});
|
|
1066
|
+
});
|
|
968
1067
|
describe('render script runtime (package_source: inputs)', () => {
|
|
969
1068
|
test('renders every screen from a shipped archive with a single package root', async () => {
|
|
970
1069
|
const project = makeProject({
|
|
@@ -993,6 +1092,12 @@ describe('render script runtime (package_source: inputs)', () => {
|
|
|
993
1092
|
error: null,
|
|
994
1093
|
reference_image_path: 'ui-fidelity/references/HomeView.png',
|
|
995
1094
|
rendered_image_path: 'ui-fidelity/rendered/HomeView.png',
|
|
1095
|
+
aligned_image_path: 'ui-fidelity/aligned/HomeView.png',
|
|
1096
|
+
dimensions: {
|
|
1097
|
+
rendered_px: [320, 640],
|
|
1098
|
+
logical_pt: [160, 320],
|
|
1099
|
+
reference_px: [160, 320],
|
|
1100
|
+
},
|
|
996
1101
|
},
|
|
997
1102
|
{
|
|
998
1103
|
screen: 'SettingsView',
|
|
@@ -1000,6 +1105,12 @@ describe('render script runtime (package_source: inputs)', () => {
|
|
|
1000
1105
|
error: null,
|
|
1001
1106
|
reference_image_path: 'ui-fidelity/references/SettingsView.png',
|
|
1002
1107
|
rendered_image_path: 'ui-fidelity/rendered/SettingsView.png',
|
|
1108
|
+
aligned_image_path: 'ui-fidelity/aligned/SettingsView.png',
|
|
1109
|
+
dimensions: {
|
|
1110
|
+
rendered_px: [320, 640],
|
|
1111
|
+
logical_pt: [160, 320],
|
|
1112
|
+
reference_px: [160, 320],
|
|
1113
|
+
},
|
|
1003
1114
|
},
|
|
1004
1115
|
],
|
|
1005
1116
|
});
|