summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rw-r--r--test/macos-snapshots/context.zig109
-rw-r--r--test/macos-snapshots/rendering-parity.golden14
-rw-r--r--test/macos-snapshots/rendering-parity.snap54
-rw-r--r--test/macos-snapshots/tag-ink.golden1
-rw-r--r--test/macos-snapshots/tag-ink.snap8
-rw-r--r--test/macos_e2e.swift185
6 files changed, 365 insertions, 6 deletions
diff --git a/test/macos-snapshots/context.zig b/test/macos-snapshots/context.zig
new file mode 100644
index 00000000..ff8f8553
--- /dev/null
+++ b/test/macos-snapshots/context.zig
@@ -0,0 +1,109 @@
+const Workspace = struct {
+ const enabled = true;
+
+ fn draw(
+ value: u32,
+ ) u32 {
+ var result = value;
+ result +%= 1;
+ result +%= 2;
+ result +%= 3;
+ result +%= 4;
+ result +%= 5;
+ result +%= 6;
+ result +%= 7;
+ result +%= 8;
+ result +%= 9;
+ result +%= 10;
+ result +%= 11;
+ result +%= 12;
+ result +%= 13;
+ result +%= 14;
+ result +%= 15;
+ result +%= 16;
+ result +%= 17;
+ result +%= 18;
+ result +%= 19;
+ result +%= 20;
+ result +%= 21;
+ result +%= 22;
+ result +%= 23;
+ result +%= 24;
+ result +%= 25;
+ result +%= 26;
+ result +%= 27;
+ result +%= 28;
+ result +%= 29;
+ result +%= 30;
+ result +%= 31;
+ result +%= 32;
+ result +%= 33;
+ result +%= 34;
+ result +%= 35;
+ result +%= 36;
+ result +%= 37;
+ result +%= 38;
+ result +%= 39;
+ result +%= 40;
+ result +%= 41;
+ result +%= 42;
+ result +%= 43;
+ result +%= 44;
+ result +%= 45;
+ result +%= 46;
+ result +%= 47;
+ result +%= 48;
+ result +%= 49;
+ result +%= 50;
+ result +%= 51;
+ result +%= 52;
+ result +%= 53;
+ result +%= 54;
+ result +%= 55;
+ result +%= 56;
+ result +%= 57;
+ result +%= 58;
+ result +%= 59;
+ result +%= 60;
+ result +%= 61;
+ result +%= 62;
+ result +%= 63;
+ result +%= 64;
+ result +%= 65;
+ result +%= 66;
+ result +%= 67;
+ result +%= 68;
+ result +%= 69;
+ result +%= 70;
+ result +%= 71;
+ result +%= 72;
+ result +%= 73;
+ result +%= 74;
+ result +%= 75;
+ result +%= 76;
+ result +%= 77;
+ result +%= 78;
+ result +%= 79;
+ result +%= 80;
+ result +%= 81;
+ result +%= 82;
+ result +%= 83;
+ result +%= 84;
+ result +%= 85;
+ result +%= 86;
+ result +%= 87;
+ result +%= 88;
+ result +%= 89;
+ result +%= 90;
+ result +%= 91;
+ result +%= 92;
+ result +%= 93;
+ result +%= 94;
+ result +%= 95;
+ result +%= 96;
+ result +%= 97;
+ result +%= 98;
+ result +%= 99;
+ return result;
+ }
+};
diff --git a/test/macos-snapshots/rendering-parity.golden b/test/macos-snapshots/rendering-parity.golden
new file mode 100644
index 00000000..536f6800
--- /dev/null
+++ b/test/macos-snapshots/rendering-parity.golden
@@ -0,0 +1,14 @@
+== backgrounds tags-100 uniform
+== backgrounds tags-bottom uniform
+== backgrounds tags-71 uniform
+== backgrounds tags-0 uniform
+== backgrounds invalid-stays-0 uniform
+== context context-100 compact separators verified
+== context context-71 compact separators verified
+== pdf pdf-100 native page opacity verified
+== pdf pdf-0 native page opacity verified
+== pdf pdf-71 native page opacity verified
+== pdf pdf-fit-height native page opacity verified
+== pdf pdf-full-tint native page opacity verified
+== pdf pdf-original-colors native page opacity verified
+== pdf pdf-scrolled native page opacity verified
diff --git a/test/macos-snapshots/rendering-parity.snap b/test/macos-snapshots/rendering-parity.snap
new file mode 100644
index 00000000..cdd940c8
--- /dev/null
+++ b/test/macos-snapshots/rendering-parity.snap
@@ -0,0 +1,54 @@
+start 40 110
+native-metrics
+wait 8000 $
+command Theme forge
+command TaglineSize 70
+stable 700 20000
+check-backgrounds tags-100
+command Tagbottom
+stable 700 20000
+check-backgrounds tags-bottom
+command Tagbottom
+stable 700 20000
+command WindowOpacity 71
+stable 700 20000
+check-backgrounds tags-71
+command WindowOpacity 0
+stable 700 20000
+check-backgrounds tags-0
+command WindowOpacity 101
+stable 700 20000
+check-backgrounds invalid-stays-0
+command WindowOpacity 100
+open-fixture context.zig:55
+command TreeContext on
+stable 700 20000
+capture context-initial
+check-context context-100
+command WindowOpacity 71
+stable 700 20000
+check-context context-71
+command WindowOpacity 100
+open-fixture ../../docs/9p.pdf
+stable 1000 20000
+check-pdf pdf-100
+command WindowOpacity 0
+stable 700 20000
+check-pdf pdf-0
+command WindowOpacity 71
+stable 700 20000
+check-pdf pdf-71
+command WindowOpacity 100
+command PdfFit
+stable 700 20000
+check-pdf pdf-fit-height
+command PdfTint
+stable 700 20000
+check-pdf pdf-full-tint
+command PdfTint
+stable 700 20000
+check-pdf pdf-original-colors
+command PdfFit
+scroll 6 10 30
+stable 700 20000
+check-pdf pdf-scrolled
diff --git a/test/macos-snapshots/tag-ink.golden b/test/macos-snapshots/tag-ink.golden
new file mode 100644
index 00000000..dfb0704d
--- /dev/null
+++ b/test/macos-snapshots/tag-ink.golden
@@ -0,0 +1 @@
+== tag-ink forge-tags visible
diff --git a/test/macos-snapshots/tag-ink.snap b/test/macos-snapshots/tag-ink.snap
new file mode 100644
index 00000000..e9e4c8e2
--- /dev/null
+++ b/test/macos-snapshots/tag-ink.snap
@@ -0,0 +1,8 @@
+start 24 80
+wait 8000 Newcol Joincol
+wait 8000 $
+stable 700 20000
+command Theme forge
+command TaglineSize 70
+stable 700 20000
+draw-tag-ink forge-tags
diff --git a/test/macos_e2e.swift b/test/macos_e2e.swift
index 43627a7e..5427c3ad 100644
--- a/test/macos_e2e.swift
+++ b/test/macos_e2e.swift
@@ -92,6 +92,8 @@ private final class Driver: PardesViewDelegate {
private var window: NSWindow?
private var view: PardesView?
private var booted = false
+ private var nativeMetrics = false
+ private let scriptDirectory: String
private var observer: NSObjectProtocol?
private var output = ""
private var nextAnimationFrame: Date?
@@ -101,7 +103,8 @@ private final class Driver: PardesViewDelegate {
/// The script's own file name, for error messages.
private let scriptName: String
- init(scriptName: String) {
+ init(scriptName: String, scriptDirectory: String) {
+ self.scriptDirectory = scriptDirectory
self.scriptName = scriptName
// The app answers this notification with pump(); so does the harness,
// and for the same reason — the view calls into the core and never
@@ -232,11 +235,17 @@ private final class Driver: PardesViewDelegate {
let grid = view.gridSize
// Points, not backing-store pixels. The app passes physical pixels
// because the native PDF placement path measures in them; a golden must
- // not change when the same script runs on a Retina display, and nothing
- // here places a PDF.
- let cellW = UInt16(max(1, min(view.cellWidth.rounded(), CGFloat(UInt16.max))))
- let cellH = UInt16(max(1, min(view.cellHeight.rounded(), CGFloat(UInt16.max))))
+ // not change when the same grid script runs on a Retina display. Pixel
+ // parity scripts opt into native metrics below, including PDF placement.
+ let scale: CGFloat = nativeMetrics ? (window?.backingScaleFactor ?? 1) : 1
+ let cellW = UInt16(max(1, min((view.cellWidth * scale).rounded(), CGFloat(UInt16.max))))
+ let cellH = UInt16(max(1, min((view.cellHeight * scale).rounded(), CGFloat(UInt16.max))))
pardes_resize(grid.cols, grid.rows, cellW, cellH)
+ if nativeMetrics {
+ pardes_row_metrics(cellW, cellH,
+ UInt16(max(1, (view.taglineCellWidth * scale).rounded())),
+ UInt16(max(1, (view.taglineBandHeight * scale).rounded())))
+ }
pump()
}
@@ -498,6 +507,34 @@ private final class Driver: PardesViewDelegate {
output += "== draw \(label) \(w)x\(h) nonblank\n"
}
+ /// Forge's topbar must contain light ink, not CoreText's default black.
+ /// Restrict the sample to text, excluding grips, pane bodies and cursors.
+ private func drawTagInk(_ label: String) throws {
+ let view = try live()
+ let bounds = view.bounds
+ guard let rep = view.bitmapImageRepForCachingDisplay(in: bounds) else {
+ throw ScriptError(message: "draw-tag-ink: no bitmap")
+ }
+ view.cacheDisplay(in: bounds, to: rep)
+ let scale = CGFloat(rep.pixelsWide) / bounds.width
+ var brightest: CGFloat = 0
+ for y in 0..<Int(view.cellHeight * scale) {
+ for x in Int(3 * view.cellWidth * scale)..<Int(25 * view.cellWidth * scale) {
+ if let color = rep.colorAt(x: x, y: y)?.usingColorSpace(.sRGB) {
+ brightest = max(brightest, min(color.redComponent, color.greenComponent, color.blueComponent))
+ }
+ }
+ }
+ guard brightest > 0.5 else {
+ throw ScriptError(message: "draw-tag-ink: Forge topbar has no light foreground ink")
+ }
+ if let directory = ProcessInfo.processInfo.environment["PARDES_TEST_CAPTURE_DIR"],
+ let png = rep.representation(using: .png, properties: [:]) {
+ try png.write(to: URL(fileURLWithPath: directory).appendingPathComponent(label + ".png"))
+ }
+ output += "== tag-ink \(label) visible\n"
+ }
+
/// The alpha range of a drawn frame, recorded into the golden.
///
/// The contract WindowOpacity states (shaders/ui.frag.glsl, and
@@ -543,6 +580,122 @@ private final class Driver: PardesViewDelegate {
output += "== draw-opacity \(label) \(w)x\(h) alpha \(lowest)..\(highest)\n"
}
+ private func renderedCapture(_ label: String) throws -> NSBitmapImageRep {
+ let view = try live()
+ guard let rep = view.bitmapImageRepForCachingDisplay(in: view.bounds) else {
+ throw ScriptError(message: "capture: no bitmap")
+ }
+ view.cacheDisplay(in: view.bounds, to: rep)
+ if let directory = ProcessInfo.processInfo.environment["PARDES_TEST_CAPTURE_DIR"],
+ let png = rep.representation(using: .png, properties: [:]) {
+ try png.write(to: URL(fileURLWithPath: directory).appendingPathComponent(label + ".png"))
+ }
+ return rep
+ }
+
+ // colorAt labels components as calibrated RGB even for a Display P3
+ // bitmap. Reattach the bitmap's actual profile before comparing sRGB.
+ private func sample(_ rep: NSBitmapImageRep, x: Int, y: Int) -> NSColor? {
+ guard let raw = rep.colorAt(x: x, y: y) else { return nil }
+ let components = [raw.redComponent, raw.greenComponent, raw.blueComponent, raw.alphaComponent]
+ return NSColor(colorSpace: rep.colorSpace, components: components, count: 4).usingColorSpace(.sRGB)
+ }
+
+ private func checkBackgrounds(_ label: String) throws {
+ let view = try live(), rep = try renderedCapture(label)
+ let scale = CGFloat(rep.pixelsWide) / view.bounds.width
+ func check(x: CGFloat, y: CGFloat, rgb: UInt32, name: String) throws {
+ guard let c = sample(rep, x: Int(x * scale), y: Int(y * scale)) else {
+ throw ScriptError(message: "\(name): no pixel")
+ }
+ guard abs(c.alphaComponent - view.backgroundAlpha) < 2.0 / 255 else {
+ throw ScriptError(message: "\(name): background alpha \(c.alphaComponent), expected \(view.backgroundAlpha)")
+ }
+ if view.backgroundAlpha > 0 {
+ let actual = [c.redComponent, c.greenComponent, c.blueComponent]
+ let expected = [CGFloat((rgb >> 16) & 255), CGFloat((rgb >> 8) & 255), CGFloat(rgb & 255)]
+ guard zip(actual, expected).allSatisfy({ abs($0 * 255 - $1) < 4 }) else {
+ throw ScriptError(message: "\(name): tag background does not cover the full row: \(actual) vs \(expected)")
+ }
+ }
+ }
+ for i in 0..<pardes_tag_layer_limit() {
+ guard pardes_tag_layer_value(i, 0) > 0 else { continue }
+ let x = CGFloat(pardes_tag_layer_value(i, 4) + pardes_tag_layer_value(i, 6)) * view.cellWidth - 2 / scale
+ let top = CGFloat(pardes_tag_layer_value(i, 5)) * view.cellHeight
+ let height = Int(view.cellHeight * scale)
+ let kind = pardes_tag_layer_value(i, 1)
+ let bottom = kind == 2 && pardes_tag_layer_value(i, 11) != 0
+ for pixel in 0..<height {
+ var rgb = pardes_tag_layer_value(i, 10)
+ if kind != 0 && pixel == (bottom ? 0 : height - 1) { rgb = 0x475263 }
+ if pardes_tag_layer_value(i, 5) == UInt32(PARDES_TOPBAR_H) && pixel == 0 { rgb = 0x475263 }
+ try check(x: x, y: top + (CGFloat(pixel) + 0.5) / scale,
+ rgb: rgb, name: "tag \(i), pixel \(pixel)")
+ }
+ }
+ try check(x: view.bounds.width - 2 * view.cellWidth,
+ y: view.bounds.height - 2 * view.cellHeight,
+ rgb: view.themeBG ?? pardesDefaultBG, name: "body")
+ output += "== backgrounds \(label) uniform\n"
+ }
+
+ private func checkContext(_ label: String) throws {
+ let view = try live(), rep = try renderedCapture(label)
+ let scale = CGFloat(rep.pixelsWide) / view.bounds.width
+ var headers = 0, separators = 0
+ for i in 0..<pardes_body_layer_limit() {
+ let count = Int(pardes_body_layer_value(i, 2))
+ guard count > 0 else { continue }
+ headers += count
+ let x = CGFloat(pardes_body_layer_value(i, 3) + pardes_body_layer_value(i, 5)) * view.cellWidth - 2 / scale
+ let top = CGFloat(pardes_body_layer_value(i, 4)) * view.cellHeight
+ for row in 0..<count where pardes_body_layer_value(i, UInt32(11 + row)) != 0 {
+ separators += 1
+ let y = top + CGFloat(row + 1) * view.taglineBandHeight - 0.5 / scale
+ guard let c = sample(rep, x: Int(x * scale), y: Int(y * scale)) else {
+ throw ScriptError(message: "context: no separator pixel")
+ }
+ let rgb: UInt32 = 0x475263 // Forge border, also used by the Linux chrome overlay.
+ guard abs(c.redComponent * 255 - CGFloat((rgb >> 16) & 255)) < 4,
+ abs(c.greenComponent * 255 - CGFloat((rgb >> 8) & 255)) < 4,
+ abs(c.blueComponent * 255 - CGFloat(rgb & 255)) < 4,
+ abs(c.alphaComponent - view.backgroundAlpha) < 2.0 / 255 else {
+ throw ScriptError(message: "context: separator is misplaced or has accumulated opacity")
+ }
+ }
+ }
+ guard headers >= 2, separators > 0 else {
+ throw ScriptError(message: "context: expected nested compact declarations, got \(headers) rows")
+ }
+ output += "== context \(label) compact separators verified\n"
+ }
+
+ private func checkPDF(_ label: String) throws {
+ let view = try live(), rep = try renderedCapture(label)
+ guard nativeMetrics, pardes_frame_images() > 0, let images = pardes_frame_image_list() else {
+ throw ScriptError(message: "PDF: no native image placements")
+ }
+ let scale = window?.backingScaleFactor ?? 1
+ let image = images[0]
+ let x = CGFloat(image.cell_x) * view.cellWidth + CGFloat(image.dst_x) / scale
+ let y = CGFloat(image.cell_y) * view.cellHeight + (CGFloat(image.dst_y) + CGFloat(image.offset_y)) / scale
+ let width = CGFloat(image.dst_w) / scale, height = CGFloat(image.dst_h) / scale
+ guard width > view.cellWidth * 4, height > view.cellHeight * 2,
+ y >= CGFloat(image.cell_y) * view.cellHeight - 1,
+ x + width <= view.bounds.width + 1, y + height <= view.bounds.height + 1 else {
+ throw ScriptError(message: "PDF: placement is outside its physical-pixel viewport")
+ }
+ let backing = CGFloat(rep.pixelsWide) / view.bounds.width
+ var matched = 0
+ for fraction in [CGFloat(0.2), 0.4, 0.6, 0.8] {
+ if let c = rep.colorAt(x: Int((x + width * fraction) * backing),
+ y: Int((y + height * fraction) * backing)), abs(c.alphaComponent - view.backgroundAlpha) < 2.0 / 255 { matched += 1 }
+ }
+ guard matched == 4 else { throw ScriptError(message: "PDF: raster opacity does not match WindowOpacity") }
+ output += "== pdf \(label) native page opacity verified\n"
+ }
+
// MARK: - the script
func run(source: String) throws -> String {
@@ -569,6 +722,23 @@ private final class Driver: PardesViewDelegate {
let args = rest.split(separator: " ").map(String.init)
switch cmd {
+ case "native-metrics":
+ nativeMetrics = true
+ pardesViewDidResize(try live())
+ case "open-fixture":
+ _ = try live()
+ let path = URL(fileURLWithPath: scriptDirectory).appendingPathComponent(rest).standardized.path
+ let command = "Look " + path
+ pardes_command(command, command.utf8.count)
+ pump()
+ case "check-backgrounds":
+ try checkBackgrounds(rest)
+ case "check-context":
+ try checkContext(rest)
+ case "check-pdf":
+ try checkPDF(rest)
+ case "capture":
+ _ = try renderedCapture(rest)
case "start":
let rows = try int(args, 0, "start rows")
let cols = try int(args, 1, "start cols")
@@ -882,6 +1052,9 @@ private final class Driver: PardesViewDelegate {
// place it can be checked: the core's cell buffer says nothing about
// alpha, and `draw` deliberately compares channels without knowing
// which one is which.
+ case "draw-tag-ink":
+ try drawTagInk(rest)
+
case "draw-opacity":
guard !rest.isEmpty else { throw ScriptError(message: "draw-opacity needs a label") }
try drawOpacity(rest)
@@ -1125,7 +1298,7 @@ private func runOne(scriptPath: String, update: Bool) -> Bool {
let goldenPath = String(scriptPath.dropLast(".snap".count)) + ".golden"
let actualPath = String(scriptPath.dropLast(".snap".count)) + ".actual"
- let driver = Driver(scriptName: name)
+ let driver = Driver(scriptName: name, scriptDirectory: (scriptPath as NSString).deletingLastPathComponent)
// pardes_deinit is mandatory, not tidiness: the ABI is a singleton, so a
// core left standing makes the NEXT script's pardes_init return 1.
defer { driver.shutdown() }