summaryrefslogtreecommitdiff
path: root/test/pdf.zig
diff options
context:
space:
mode:
Diffstat (limited to 'test/pdf.zig')
-rw-r--r--test/pdf.zig161
1 files changed, 161 insertions, 0 deletions
diff --git a/test/pdf.zig b/test/pdf.zig
index a835c6ea..04b2e8b9 100644
--- a/test/pdf.zig
+++ b/test/pdf.zig
@@ -31,6 +31,167 @@ fn hasPdf(pane: *const pardes.Pane) bool {
return if (comptime pdf_enabled) pane.pdf != null else false;
}
+const PdfLinkTests = struct {
+ const Cell = struct { col: u16, row: u16 };
+
+ fn init(tmp: std.testing.TmpDir) !*Pardes {
+ const bytes = try pdf_impl.makeLinksTestPdf(std.testing.allocator);
+ defer std.testing.allocator.free(bytes);
+ try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "links.pdf", .data = bytes });
+ try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "target.txt", .data = "chosen file\n" });
+ var dir_buf: [4096]u8 = undefined;
+ const len = try tmp.dir.realPath(std.testing.io, &dir_buf);
+ const path = try std.fmt.allocPrint(std.testing.allocator, "{s}/links.pdf", .{dir_buf[0..len]});
+ defer std.testing.allocator.free(path);
+ const p = try Pardes.init(std.testing.allocator, .{ .file = path, .cols = 100, .rows = 36 });
+ p.native_images = true;
+ p.presentation.enabled = false;
+ p.panes[0].?.pdf.?.fit = .height;
+ return p;
+ }
+
+ fn cellAt(p: *Pardes, x: f32, y: f32) !Cell {
+ const pane = p.panes[0].?;
+ const rect = p.rects[0];
+ var best: ?Cell = null;
+ var distance: f32 = std.math.inf(f32);
+ for (rect.y..rect.y + rect.h) |row| for (rect.x..rect.x + rect.w) |col| {
+ const point = panes.Pdf.panePointAtPage(p, pane, 0, @intCast(col), @intCast(row), false) orelse continue;
+ const dx = point.x - x;
+ const dy = point.y - y;
+ const d = dx * dx + dy * dy;
+ if (d < distance) {
+ distance = d;
+ best = .{ .col = @intCast(col), .row = @intCast(row) };
+ }
+ };
+ return best orelse error.LinkOutsideViewport;
+ }
+
+ fn textCell(p: *Pardes, text: []const u8) !Cell {
+ var found = try p.panes[0].?.pdf.?.document.search(p.gpa, 0, text);
+ defer found.deinit(p.gpa);
+ if (found.quads.len == 0) return error.MissingLinkFixtureText;
+ const q = found.quads[0].quad;
+ return cellAt(p, (q.ul.x + q.ur.x + q.ll.x + q.lr.x) / 4, (q.ul.y + q.ur.y + q.ll.y + q.lr.y) / 4);
+ }
+
+ fn click(p: *Pardes, cell: Cell) void {
+ p.update(.{ .mouse = .{ .button = config.look_button, .kind = .press, .col = cell.col, .row = cell.row } });
+ p.update(.{ .mouse = .{ .button = config.look_button, .kind = .release, .col = cell.col, .row = cell.row } });
+ }
+
+ fn expectOpened(p: *Pardes, expected: ?[]const u8) !void {
+ var count: usize = 0;
+ while (p.nextEffect()) |effect| {
+ if (effect == .open_link) {
+ count += 1;
+ try std.testing.expectEqualStrings(expected orelse return error.UnexpectedExternalOpen, effect.open_link.slice());
+ }
+ }
+ try std.testing.expectEqual(@as(usize, if (expected != null) 1 else 0), count);
+ }
+
+ test "PDF links activate embedded URLs and blank internal annotations" {
+ if (!pdf_enabled or platform == .web) return;
+ var tmp = std.testing.tmpDir(.{});
+ defer tmp.cleanup();
+ const p = try init(tmp);
+ defer p.deinit();
+ var frame = std.heap.ArenaAllocator.init(p.gpa);
+ defer frame.deinit();
+ _ = try p.render(frame.allocator());
+ while (p.nextEffect()) |_| {}
+ click(p, try textCell(p, "label"));
+ try expectOpened(p, "https://example.com/only");
+ try std.testing.expectEqual(@as(usize, 0), p.panes[0].?.pdf.?.page);
+ click(p, try cellAt(p, 0.3, 0.38));
+ try expectOpened(p, null);
+ try std.testing.expectEqual(@as(usize, 1), p.panes[0].?.pdf.?.page);
+ }
+
+ test "PDF links offer distinct visible destinations and do not duplicate the same URL" {
+ if (!pdf_enabled or platform == .web) return;
+ var tmp = std.testing.tmpDir(.{});
+ defer tmp.cleanup();
+ const p = try init(tmp);
+ defer p.deinit();
+ var frame = std.heap.ArenaAllocator.init(p.gpa);
+ defer frame.deinit();
+ _ = try p.render(frame.allocator());
+ while (p.nextEffect()) |_| {}
+ click(p, try textCell(p, "target.txt"));
+ try expectOpened(p, null);
+ const choices = for (p.panes, 0..) |slot, id| {
+ if (id == 0) continue;
+ if (slot) |pane| if (pane.file) |file| if (file.output != null) break pane;
+ } else return error.MissingPdfLinkChoices;
+ try std.testing.expect(std.mem.indexOf(u8, choices.file.?.content, "https://example.com/embedded") != null);
+ try std.testing.expect(std.mem.indexOf(u8, choices.file.?.content, "target.txt") != null);
+ const choices_id = for (p.panes, 0..) |slot, id| {
+ if (slot == choices) break id;
+ } else return error.MissingPdfLinkChoices;
+ p.lookAt(choices_id, "https://example.com/embedded");
+ try expectOpened(p, "https://example.com/embedded");
+ p.lookAt(choices_id, "target.txt");
+ try std.testing.expectEqualStrings("chosen file\n", p.panes[p.active].?.file.?.content);
+ p.active = 0;
+ _ = try p.render(frame.allocator());
+ click(p, try textCell(p, "https://example.com/same"));
+ try expectOpened(p, "https://example.com/same");
+ }
+
+ fn hover(p: *Pardes, cell: Cell) void {
+ p.update(.{ .mouse = .{ .button = .none, .kind = .motion, .col = cell.col, .row = cell.row } });
+ for (0..(config.look_preview_delay_frames orelse 0)) |_| p.update(.tick);
+ }
+
+ test "PDF links hover clears on leave page and reload while right drag selects text" {
+ if (!pdf_enabled or platform == .web) return;
+ var tmp = std.testing.tmpDir(.{});
+ defer tmp.cleanup();
+ const p = try init(tmp);
+ defer p.deinit();
+ const pane = p.panes[0].?;
+ var frame = std.heap.ArenaAllocator.init(p.gpa);
+ defer frame.deinit();
+ const plain = try p.render(frame.allocator());
+ const plain_revision = plain.images[0].?.native.revision;
+ while (p.nextEffect()) |_| {}
+ const blank = try cellAt(p, 0.3, 0.38);
+ hover(p, blank);
+ const hovered = try p.render(frame.allocator());
+ try std.testing.expectEqual(Surface.PointerShape.link, hovered.pointer_shape);
+ try std.testing.expect(hovered.images[0].?.native.revision != plain_revision);
+ const highlights = panes.Pdf.highlightInput(p, 0, pane);
+ try std.testing.expect(!std.meta.eql(highlights.hover_color, highlights.selection_color));
+ try std.testing.expect(pane.pdf.?.selection == null);
+ try expectOpened(p, null);
+ p.update(.pointer_leave);
+ try std.testing.expectEqual(Surface.PointerShape.arrow, (try p.render(frame.allocator())).pointer_shape);
+ hover(p, blank);
+ panes.Pdf.setPage(p, pane, 1);
+ try std.testing.expectEqual(Surface.PointerShape.arrow, (try p.render(frame.allocator())).pointer_shape);
+ panes.Pdf.setPage(p, pane, 0);
+ _ = try p.render(frame.allocator());
+ hover(p, try cellAt(p, 0.3, 0.38));
+ try std.testing.expect(p.reloadWatchedFile(0, &.{}));
+ try std.testing.expectEqual(Surface.PointerShape.arrow, (try p.render(frame.allocator())).pointer_shape);
+
+ const label = try textCell(p, "label");
+ p.update(.{ .mouse = .{ .button = config.look_button, .kind = .press, .col = label.col, .row = label.row } });
+ p.update(.{ .mouse = .{ .button = config.look_button, .kind = .drag, .col = label.col + 3, .row = label.row } });
+ p.update(.{ .mouse = .{ .button = config.look_button, .kind = .release, .col = label.col + 3, .row = label.row } });
+ try expectOpened(p, null);
+ try std.testing.expect(pane.pdf.?.selection != null);
+ try std.testing.expect(pane.pdf.?.selection_text.len > 0);
+ }
+};
+
+test {
+ _ = PdfLinkTests;
+}
+
test "PDF mounted bytes own their source after the caller frees it" {
if (!pdf_enabled or platform == .web) return;
const gpa = std.testing.allocator;