@invarn/cibuild 2.0.8 → 2.1.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/cli.cjs +688 -69
- package/dist/src/lib.d.ts +2 -0
- package/dist/src/lib.d.ts.map +1 -1
- package/dist/src/lib.js +1 -0
- package/dist/src/yaml/steps/index.d.ts.map +1 -1
- package/dist/src/yaml/steps/index.js +22 -0
- package/dist/src/yaml/steps/render-post-processor.d.ts +73 -0
- package/dist/src/yaml/steps/render-post-processor.d.ts.map +1 -0
- package/dist/src/yaml/steps/render-post-processor.js +303 -0
- package/dist/src/yaml/steps/render-post-processor.test.d.ts +16 -0
- package/dist/src/yaml/steps/render-post-processor.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/render-post-processor.test.js +238 -0
- package/dist/src/yaml/steps/ui-fidelity-preview-android.d.ts +66 -0
- package/dist/src/yaml/steps/ui-fidelity-preview-android.d.ts.map +1 -0
- package/dist/src/yaml/steps/ui-fidelity-preview-android.js +249 -0
- package/dist/src/yaml/steps/ui-fidelity-preview-android.test.d.ts +13 -0
- package/dist/src/yaml/steps/ui-fidelity-preview-android.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/ui-fidelity-preview-android.test.js +299 -0
- package/dist/src/yaml/steps/ui-fidelity-render-android.d.ts +92 -0
- package/dist/src/yaml/steps/ui-fidelity-render-android.d.ts.map +1 -0
- package/dist/src/yaml/steps/ui-fidelity-render-android.js +726 -0
- package/dist/src/yaml/steps/ui-fidelity-render-android.test.d.ts +2 -0
- package/dist/src/yaml/steps/ui-fidelity-render-android.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/ui-fidelity-render-android.test.js +377 -0
- package/dist/src/yaml/steps/ui-fidelity-render.d.ts +0 -28
- package/dist/src/yaml/steps/ui-fidelity-render.d.ts.map +1 -1
- package/dist/src/yaml/steps/ui-fidelity-render.js +1 -259
- package/dist/src/yaml/steps/ui-fidelity-render.test.js +1 -57
- package/package.json +1 -1
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
* document is { renderer, package_source, error, screens }.
|
|
50
50
|
*/
|
|
51
51
|
import { BaseStepExecutor } from './base.js';
|
|
52
|
+
import { RENDER_SCRIPT_TRIM_STAGE } from './render-post-processor.js';
|
|
52
53
|
/**
|
|
53
54
|
* Default render size in device points. 393x852 is the iPhone-class portrait
|
|
54
55
|
* canvas shared by the iPhone 14 Pro / 15 / 16 — the most common modern
|
|
@@ -986,265 +987,6 @@ function compileAssetCatalogs(packageDir, harnessDir) {
|
|
|
986
987
|
}
|
|
987
988
|
}
|
|
988
989
|
`;
|
|
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
|
-
/**
|
|
1007
|
-
* The CoreGraphics trim/align helper source, run via
|
|
1008
|
-
* `swift <Trim.swift> <in> <out> <aligned> <ref>` (script-interpreter mode).
|
|
1009
|
-
* Crops a PNG to its content bounding box (transparent or uniform-background
|
|
1010
|
-
* margins), writes a reference-sized aligned copy, and prints a TRIMDIMS line.
|
|
1011
|
-
*
|
|
1012
|
-
* Exported so a real-toolchain test can exercise the pixel logic directly --
|
|
1013
|
-
* the fake `swift` in the unit tests cannot, which is why a background-detection
|
|
1014
|
-
* bug shipped twice undetected before a real-pixel guard existed.
|
|
1015
|
-
*/
|
|
1016
|
-
export const TRIM_HELPER_SWIFT_SOURCE = [
|
|
1017
|
-
'import Foundation',
|
|
1018
|
-
'import ImageIO',
|
|
1019
|
-
'import CoreGraphics',
|
|
1020
|
-
'import UniformTypeIdentifiers',
|
|
1021
|
-
'',
|
|
1022
|
-
'func loadCGImage(_ path: String) -> CGImage? {',
|
|
1023
|
-
' guard let src = CGImageSourceCreateWithURL(URL(fileURLWithPath: path) as CFURL, nil),',
|
|
1024
|
-
' let img = CGImageSourceCreateImageAtIndex(src, 0, nil) else { return nil }',
|
|
1025
|
-
' return img',
|
|
1026
|
-
'}',
|
|
1027
|
-
'',
|
|
1028
|
-
'func writePNG(_ image: CGImage, to path: String) -> Bool {',
|
|
1029
|
-
' guard let dest = CGImageDestinationCreateWithURL(URL(fileURLWithPath: path) as CFURL, UTType.png.identifier as CFString, 1, nil) else { return false }',
|
|
1030
|
-
' CGImageDestinationAddImage(dest, image, nil)',
|
|
1031
|
-
' return CGImageDestinationFinalize(dest)',
|
|
1032
|
-
'}',
|
|
1033
|
-
'',
|
|
1034
|
-
'// Redraw an image at an exact pixel size (used to match the reference).',
|
|
1035
|
-
'func resize(_ image: CGImage, toWidth w: Int, height h: Int) -> CGImage? {',
|
|
1036
|
-
' if w <= 0 || h <= 0 { return nil }',
|
|
1037
|
-
' let bytesPerRow = w * 4',
|
|
1038
|
-
' let space = CGColorSpaceCreateDeviceRGB()',
|
|
1039
|
-
' guard let ctx = CGContext(data: nil, width: w, height: h, bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: space, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) else { return nil }',
|
|
1040
|
-
' ctx.interpolationQuality = .high',
|
|
1041
|
-
' ctx.draw(image, in: CGRect(x: 0, y: 0, width: w, height: h))',
|
|
1042
|
-
' return ctx.makeImage()',
|
|
1043
|
-
'}',
|
|
1044
|
-
'',
|
|
1045
|
-
'// Bounding box of content: pixels that are neither transparent nor part of a',
|
|
1046
|
-
'// uniform solid background. Two passes, because the renderer forces the view',
|
|
1047
|
-
'// into a fixed device frame whose margins are transparent: (1) bound the',
|
|
1048
|
-
'// non-transparent pixels, then (2) detect a uniform opaque background from the',
|
|
1049
|
-
'// CORNERS OF THAT OPAQUE BOX -- not the raw frame, whose corners are the',
|
|
1050
|
-
'// transparent device margin -- and bound everything that is not that',
|
|
1051
|
-
'// background. A wrapper that fills the canvas with .background(.white) is thus',
|
|
1052
|
-
'// trimmed to its real content. When the opaque corners disagree, only',
|
|
1053
|
-
'// transparency is trimmed. Top-left image coordinates.',
|
|
1054
|
-
'func contentBoundingBox(_ image: CGImage) -> CGRect? {',
|
|
1055
|
-
' let w = image.width, h = image.height',
|
|
1056
|
-
' if w == 0 || h == 0 { return nil }',
|
|
1057
|
-
' let bytesPerRow = w * 4',
|
|
1058
|
-
' var data = [UInt8](repeating: 0, count: bytesPerRow * h)',
|
|
1059
|
-
' let space = CGColorSpaceCreateDeviceRGB()',
|
|
1060
|
-
' guard let ctx = CGContext(data: &data, width: w, height: h, bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: space, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) else { return nil }',
|
|
1061
|
-
' ctx.draw(image, in: CGRect(x: 0, y: 0, width: w, height: h))',
|
|
1062
|
-
' // Tight tolerance: exact solid fills match, near-colored content does not.',
|
|
1063
|
-
' let tolerance = 4',
|
|
1064
|
-
' func sample(_ x: Int, _ y: Int) -> (Int, Int, Int, Int) {',
|
|
1065
|
-
' let i = y * bytesPerRow + x * 4',
|
|
1066
|
-
' return (Int(data[i]), Int(data[i + 1]), Int(data[i + 2]), Int(data[i + 3]))',
|
|
1067
|
-
' }',
|
|
1068
|
-
' // Pass 1: bounding box of all non-transparent pixels.',
|
|
1069
|
-
' var aMinX = w, aMinY = h, aMaxX = -1, aMaxY = -1',
|
|
1070
|
-
' for y in 0..<h {',
|
|
1071
|
-
' let rowBase = y * bytesPerRow',
|
|
1072
|
-
' for x in 0..<w {',
|
|
1073
|
-
' if data[rowBase + x * 4 + 3] > 0 {',
|
|
1074
|
-
' if x < aMinX { aMinX = x }',
|
|
1075
|
-
' if x > aMaxX { aMaxX = x }',
|
|
1076
|
-
' if y < aMinY { aMinY = y }',
|
|
1077
|
-
' if y > aMaxY { aMaxY = y }',
|
|
1078
|
-
' }',
|
|
1079
|
-
' }',
|
|
1080
|
-
' }',
|
|
1081
|
-
' if aMaxX < 0 { return nil }',
|
|
1082
|
-
' let opaqueBox = CGRect(x: aMinX, y: aMinY, width: aMaxX - aMinX + 1, height: aMaxY - aMinY + 1)',
|
|
1083
|
-
' // Detect a uniform opaque background from the corners of the opaque box.',
|
|
1084
|
-
' let corners = [sample(aMinX, aMinY), sample(aMaxX, aMinY), sample(aMinX, aMaxY), sample(aMaxX, aMaxY)]',
|
|
1085
|
-
' var background: (Int, Int, Int, Int)? = corners[0]',
|
|
1086
|
-
' for c in corners {',
|
|
1087
|
-
' if let bg = background {',
|
|
1088
|
-
' 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 {',
|
|
1089
|
-
' background = nil',
|
|
1090
|
-
' }',
|
|
1091
|
-
' }',
|
|
1092
|
-
' }',
|
|
1093
|
-
' // No uniform background: the opaque box is the content.',
|
|
1094
|
-
' guard let bg = background else { return opaqueBox }',
|
|
1095
|
-
' // Pass 2: within the opaque box, bound everything that is not the background.',
|
|
1096
|
-
' var minX = aMaxX + 1, minY = aMaxY + 1, maxX = aMinX - 1, maxY = aMinY - 1',
|
|
1097
|
-
' for y in aMinY...aMaxY {',
|
|
1098
|
-
' let rowBase = y * bytesPerRow',
|
|
1099
|
-
' for x in aMinX...aMaxX {',
|
|
1100
|
-
' let idx = rowBase + x * 4',
|
|
1101
|
-
' let a = Int(data[idx + 3])',
|
|
1102
|
-
' if a == 0 { continue }',
|
|
1103
|
-
' let r = Int(data[idx]), g = Int(data[idx + 1]), b = Int(data[idx + 2])',
|
|
1104
|
-
' if abs(r - bg.0) <= tolerance && abs(g - bg.1) <= tolerance && abs(b - bg.2) <= tolerance && abs(a - bg.3) <= tolerance { continue }',
|
|
1105
|
-
' if x < minX { minX = x }',
|
|
1106
|
-
' if x > maxX { maxX = x }',
|
|
1107
|
-
' if y < minY { minY = y }',
|
|
1108
|
-
' if y > maxY { maxY = y }',
|
|
1109
|
-
' }',
|
|
1110
|
-
' }',
|
|
1111
|
-
' // All background, no distinct content: keep the opaque box, never crop to nothing.',
|
|
1112
|
-
' if maxX < minX { return opaqueBox }',
|
|
1113
|
-
' return CGRect(x: minX, y: minY, width: maxX - minX + 1, height: maxY - minY + 1)',
|
|
1114
|
-
'}',
|
|
1115
|
-
'',
|
|
1116
|
-
'let arguments = CommandLine.arguments',
|
|
1117
|
-
'guard arguments.count >= 3 else {',
|
|
1118
|
-
' FileHandle.standardError.write(Data("usage: Trim <in.png> <out.png>".utf8))',
|
|
1119
|
-
' exit(64)',
|
|
1120
|
-
'}',
|
|
1121
|
-
'let inputPath = arguments[1]',
|
|
1122
|
-
'let outputPath = arguments[2]',
|
|
1123
|
-
'guard let image = loadCGImage(inputPath) else {',
|
|
1124
|
-
' FileHandle.standardError.write(Data(("could not read image at " + inputPath).utf8))',
|
|
1125
|
-
' exit(65)',
|
|
1126
|
-
'}',
|
|
1127
|
-
'guard let box = contentBoundingBox(image) else {',
|
|
1128
|
-
' FileHandle.standardError.write(Data("no content to trim (image is uniform)".utf8))',
|
|
1129
|
-
' exit(66)',
|
|
1130
|
-
'}',
|
|
1131
|
-
'guard let cropped = image.cropping(to: box) else {',
|
|
1132
|
-
' FileHandle.standardError.write(Data("could not crop image".utf8))',
|
|
1133
|
-
' exit(67)',
|
|
1134
|
-
'}',
|
|
1135
|
-
'let croppedWidth = cropped.width',
|
|
1136
|
-
'let croppedHeight = cropped.height',
|
|
1137
|
-
'guard writePNG(cropped, to: outputPath) else {',
|
|
1138
|
-
' FileHandle.standardError.write(Data(("could not write trimmed PNG to " + outputPath).utf8))',
|
|
1139
|
-
' exit(68)',
|
|
1140
|
-
'}',
|
|
1141
|
-
'',
|
|
1142
|
-
'// Optional aligned output: the trimmed image resized to the reference pixel',
|
|
1143
|
-
'// size, a 1:1 overlay pair. Best-effort; dimensions are reported either way.',
|
|
1144
|
-
'var referenceWidth = 0',
|
|
1145
|
-
'var referenceHeight = 0',
|
|
1146
|
-
'if arguments.count >= 5 {',
|
|
1147
|
-
' let alignedPath = arguments[3]',
|
|
1148
|
-
' let referencePath = arguments[4]',
|
|
1149
|
-
' if let reference = loadCGImage(referencePath) {',
|
|
1150
|
-
' referenceWidth = reference.width',
|
|
1151
|
-
' referenceHeight = reference.height',
|
|
1152
|
-
' if let aligned = resize(cropped, toWidth: referenceWidth, height: referenceHeight) {',
|
|
1153
|
-
' _ = writePNG(aligned, to: alignedPath)',
|
|
1154
|
-
' }',
|
|
1155
|
-
' }',
|
|
1156
|
-
'}',
|
|
1157
|
-
'',
|
|
1158
|
-
'// Report: rendered (trimmed) px, reference px (0 0 when unavailable), then',
|
|
1159
|
-
'// the pre-trim source px so the caller can log how much margin was removed.',
|
|
1160
|
-
'print("TRIMDIMS " + String(croppedWidth) + " " + String(croppedHeight) + " " + String(referenceWidth) + " " + String(referenceHeight) + " " + String(image.width) + " " + String(image.height))'
|
|
1161
|
-
].join('\n') + '\n';
|
|
1162
|
-
const RENDER_SCRIPT_TRIM_STAGE = '\n// ---- render trimming stage (package_source variants) ----\n' +
|
|
1163
|
-
'var TRIM_HELPER_SWIFT = ' +
|
|
1164
|
-
JSON.stringify(TRIM_HELPER_SWIFT_SOURCE) +
|
|
1165
|
-
';\n' +
|
|
1166
|
-
String.raw `
|
|
1167
|
-
// Write the helper into the harness dir once (idempotent across screens).
|
|
1168
|
-
function ensureTrimHelper(harnessDir) {
|
|
1169
|
-
var helperPath = path.join(harnessDir, 'Trim.swift');
|
|
1170
|
-
if (!fs.existsSync(helperPath)) {
|
|
1171
|
-
fs.writeFileSync(helperPath, TRIM_HELPER_SWIFT);
|
|
1172
|
-
}
|
|
1173
|
-
return helperPath;
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
// Round to two decimals, keeping the value a plain number.
|
|
1177
|
-
function roundTwo(value) {
|
|
1178
|
-
return Math.round(value * 100) / 100;
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
// Parse the helper's "TRIMDIMS <rW> <rH> <refW> <refH> <srcW> <srcH>" line
|
|
1182
|
-
// (rendered/reference/source px; last one wins).
|
|
1183
|
-
function parseTrimDims(stdout) {
|
|
1184
|
-
var lines = String(stdout || '').split('\n');
|
|
1185
|
-
for (var i = lines.length - 1; i >= 0; i--) {
|
|
1186
|
-
var parts = lines[i].trim().split(/\s+/);
|
|
1187
|
-
if (parts[0] === 'TRIMDIMS' && parts.length >= 7) {
|
|
1188
|
-
var nums = [
|
|
1189
|
-
Number(parts[1]), Number(parts[2]), Number(parts[3]),
|
|
1190
|
-
Number(parts[4]), Number(parts[5]), Number(parts[6]),
|
|
1191
|
-
];
|
|
1192
|
-
if (nums.every(function (n) { return Number.isFinite(n); })) {
|
|
1193
|
-
return {
|
|
1194
|
-
rendered: [nums[0], nums[1]],
|
|
1195
|
-
reference: [nums[2], nums[3]],
|
|
1196
|
-
source: [nums[4], nums[5]],
|
|
1197
|
-
};
|
|
1198
|
-
}
|
|
1199
|
-
}
|
|
1200
|
-
}
|
|
1201
|
-
return null;
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
// Crop the rendered PNG in place to its content bounding box, write an aligned
|
|
1205
|
-
// copy resized to the reference, and record the dimensions on the entry.
|
|
1206
|
-
// Best-effort: a failure is logged and the untrimmed render is kept.
|
|
1207
|
-
function trimRenderedImage(entry, renderedPath, harnessDir) {
|
|
1208
|
-
var helperPath = ensureTrimHelper(harnessDir);
|
|
1209
|
-
var alignedRelative = 'ui-fidelity/aligned/' + entry.screen + '.png';
|
|
1210
|
-
var alignedPath = path.resolve(artifactsDir, alignedRelative);
|
|
1211
|
-
var referencePath =
|
|
1212
|
-
entry.reference_image_path === null
|
|
1213
|
-
? ''
|
|
1214
|
-
: path.resolve(artifactsDir, entry.reference_image_path);
|
|
1215
|
-
try {
|
|
1216
|
-
fs.mkdirSync(path.dirname(alignedPath), { recursive: true });
|
|
1217
|
-
} catch (mkdirError) {
|
|
1218
|
-
// best effort -- the helper reports a missing output dir itself
|
|
1219
|
-
}
|
|
1220
|
-
var result = runSwift([helperPath, renderedPath, renderedPath, alignedPath, referencePath]);
|
|
1221
|
-
if (result.status !== 0) {
|
|
1222
|
-
logError(
|
|
1223
|
-
'trim failed for ' + renderedPath +
|
|
1224
|
-
' (continuing with the untrimmed render): ' + sanitizeMessage(outputTail(result))
|
|
1225
|
-
);
|
|
1226
|
-
return;
|
|
1227
|
-
}
|
|
1228
|
-
var dims = parseTrimDims(result.stdout);
|
|
1229
|
-
if (dims !== null) {
|
|
1230
|
-
var scale = CONFIG.scale;
|
|
1231
|
-
entry.dimensions = {
|
|
1232
|
-
rendered_px: dims.rendered,
|
|
1233
|
-
logical_pt: [roundTwo(dims.rendered[0] / scale), roundTwo(dims.rendered[1] / scale)],
|
|
1234
|
-
reference_px: dims.reference[0] > 0 && dims.reference[1] > 0 ? dims.reference : null,
|
|
1235
|
-
};
|
|
1236
|
-
// Surface how much margin was removed -- a no-op (source == rendered) means
|
|
1237
|
-
// the view fills an opaque canvas with no transparent or uniform margin.
|
|
1238
|
-
log(
|
|
1239
|
-
'trimmed ' + entry.screen + ': ' + dims.source[0] + 'x' + dims.source[1] +
|
|
1240
|
-
' -> ' + dims.rendered[0] + 'x' + dims.rendered[1]
|
|
1241
|
-
);
|
|
1242
|
-
}
|
|
1243
|
-
if (fs.existsSync(alignedPath)) {
|
|
1244
|
-
entry.aligned_image_path = alignedRelative;
|
|
1245
|
-
}
|
|
1246
|
-
}
|
|
1247
|
-
`;
|
|
1248
990
|
/**
|
|
1249
991
|
* Replaces exactly one occurrence of `anchor` in `source`. Throws when the
|
|
1250
992
|
* anchor is missing or ambiguous, so any drift between the v1 runtime text
|
|
@@ -21,7 +21,7 @@ import { chmodSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSyn
|
|
|
21
21
|
import { tmpdir } from 'node:os';
|
|
22
22
|
import { dirname, join, resolve } from 'node:path';
|
|
23
23
|
import { fileURLToPath } from 'node:url';
|
|
24
|
-
import { DEFAULT_RENDER_SIZE, DEFAULT_SCALE, UiFidelityRenderStepExecutor, generateRenderScript, getRenderScriptInternals, parseRenderSize, parseScale,
|
|
24
|
+
import { DEFAULT_RENDER_SIZE, DEFAULT_SCALE, UiFidelityRenderStepExecutor, generateRenderScript, getRenderScriptInternals, parseRenderSize, parseScale, } from './ui-fidelity-render.js';
|
|
25
25
|
import { clearRegistry, getStepMetadata, hasStep } from './registry.js';
|
|
26
26
|
import { initializeStepRegistry } from './index.js';
|
|
27
27
|
import { testConfig } from './test-config.js';
|
|
@@ -1509,60 +1509,4 @@ describe('committed fixture package', () => {
|
|
|
1509
1509
|
expectNoAbsolutePathLeaks(project, result);
|
|
1510
1510
|
}, 600_000);
|
|
1511
1511
|
});
|
|
1512
|
-
// The fake `swift` cannot exercise the trim helper's CoreGraphics pixel logic,
|
|
1513
|
-
// so the bounding-box behavior is guarded here with the real toolchain. This
|
|
1514
|
-
// is the regression test for the bug where a wrapper that fills the canvas with
|
|
1515
|
-
// an opaque .background(...) was not trimmed to its content: the renderer forces
|
|
1516
|
-
// the view into a fixed device frame whose margins are transparent, so the
|
|
1517
|
-
// helper must detect the solid background from the corners of the opaque region,
|
|
1518
|
-
// not the raw frame.
|
|
1519
|
-
describe('trim helper pixel logic (real toolchain)', () => {
|
|
1520
|
-
const realSwiftTest = process.env.CIBUILD_UI_FIDELITY_REAL_SWIFT === '1' ? test : test.skip;
|
|
1521
|
-
// PNG IHDR carries width/height as big-endian uint32 at byte offsets 16/20.
|
|
1522
|
-
function pngSize(filePath) {
|
|
1523
|
-
const buf = readFileSync(filePath);
|
|
1524
|
-
return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
|
|
1525
|
-
}
|
|
1526
|
-
// Swift program that writes a fixture mimicking a real render: a transparent
|
|
1527
|
-
// device frame, an opaque white wrapper inset within it, and a distinct-color
|
|
1528
|
-
// "content" rectangle inside the white. Crop should land on the content.
|
|
1529
|
-
const MAKE_FIXTURE_SWIFT = [
|
|
1530
|
-
'import Foundation',
|
|
1531
|
-
'import ImageIO',
|
|
1532
|
-
'import CoreGraphics',
|
|
1533
|
-
'import UniformTypeIdentifiers',
|
|
1534
|
-
'let w = 420, h = 320, cs = CGColorSpaceCreateDeviceRGB()',
|
|
1535
|
-
'let c = CGContext(data: nil, width: w, height: h, bitsPerComponent: 8, bytesPerRow: 0, space: cs, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)!',
|
|
1536
|
-
'c.clear(CGRect(x: 0, y: 0, width: w, height: h))',
|
|
1537
|
-
'c.setFillColor(red: 1, green: 1, blue: 1, alpha: 1)',
|
|
1538
|
-
'c.fill(CGRect(x: 20, y: 40, width: 375, height: 200))',
|
|
1539
|
-
'c.setFillColor(red: 1, green: 240.0/255, blue: 238.0/255, alpha: 1)',
|
|
1540
|
-
'c.fill(CGRect(x: 36, y: 56, width: 343, height: 168))',
|
|
1541
|
-
'let out = CommandLine.arguments[1]',
|
|
1542
|
-
'let d = CGImageDestinationCreateWithURL(URL(fileURLWithPath: out) as CFURL, UTType.png.identifier as CFString, 1, nil)!',
|
|
1543
|
-
'CGImageDestinationAddImage(d, c.makeImage()!, nil)',
|
|
1544
|
-
'_ = CGImageDestinationFinalize(d)',
|
|
1545
|
-
].join('\n') + '\n';
|
|
1546
|
-
realSwiftTest('crops an opaque-background wrapper inside a transparent frame to its content', () => {
|
|
1547
|
-
const dir = mkdtempSync(join(tmpdir(), 'ui-fidelity-trim-real-'));
|
|
1548
|
-
tempDirs.push(dir);
|
|
1549
|
-
const makePath = join(dir, 'Make.swift');
|
|
1550
|
-
const trimPath = join(dir, 'Trim.swift');
|
|
1551
|
-
const inputPath = join(dir, 'input.png');
|
|
1552
|
-
const outputPath = join(dir, 'out.png');
|
|
1553
|
-
writeFileSync(makePath, MAKE_FIXTURE_SWIFT);
|
|
1554
|
-
writeFileSync(trimPath, TRIM_HELPER_SWIFT_SOURCE);
|
|
1555
|
-
const make = spawnSync('swift', [makePath, inputPath], { encoding: 'utf-8' });
|
|
1556
|
-
expect(make.status).toBe(0);
|
|
1557
|
-
// 420x320 transparent canvas, 375x200 white wrapper, 343x168 pink card.
|
|
1558
|
-
expect(pngSize(inputPath)).toEqual({ width: 420, height: 320 });
|
|
1559
|
-
const trim = spawnSync('swift', [trimPath, inputPath, outputPath], {
|
|
1560
|
-
encoding: 'utf-8',
|
|
1561
|
-
});
|
|
1562
|
-
expect(trim.status).toBe(0);
|
|
1563
|
-
// The crop lands on the content card, NOT the white wrapper (375x200) and
|
|
1564
|
-
// NOT the raw canvas (420x320).
|
|
1565
|
-
expect(pngSize(outputPath)).toEqual({ width: 343, height: 168 });
|
|
1566
|
-
}, 600_000);
|
|
1567
|
-
});
|
|
1568
1512
|
//# sourceMappingURL=ui-fidelity-render.test.js.map
|