also looking at this
test(window): make "every window can be reopened" a property the build checks
#76
4 files
+432
-23
| @@ -0,0 +1,160 @@ | ||
| 1 | //! Which windows craft opened. | |
| 2 | //! | |
| 3 | //! The Dock's reopen event arrives with no argument: AppKit hands over the | |
| 4 | //! application and craft has to work out, from `[NSApp windows]`, which of them | |
| 5 | //! are its own. That list also holds the offscreen windows AppKit keeps for | |
| 6 | //! menus, tooltips and its own bookkeeping, and ordering one of those front | |
| 7 | //! puts an empty frame on screen. | |
| 8 | //! | |
| 9 | //! ## Why this is a list and not a test | |
| 10 | //! | |
| 11 | //! The first version asked the window: is your content view a `WKWebView`? | |
| 12 | //! That is true of craft's plain window and false of three others — | |
| 13 | //! `--web-sidebar-material` installs a backdrop container, and both sidebar | |
| 14 | //! constructors install a container or a split view controller. All three kept | |
| 15 | //! their closed window alive, so reopen found them, skipped them, and left the | |
| 16 | //! app activated with nothing on screen: exactly the dead end the reopen | |
| 17 | //! handler exists to remove. | |
| 18 | //! | |
| 19 | //! Nothing caught it. It compiles, every test passes, and it fails only against | |
| 20 | //! a real reopen event on a window style no test constructs. | |
| 21 | //! | |
| 22 | //! Recording the answer instead of inferring it fixes that, and it does | |
| 23 | //! something else worth more: it turns an untestable property into a | |
| 24 | //! checkable one. "Does this window look like ours?" can only be answered by | |
| 25 | //! having the window. "Did every constructor register?" is a property of the | |
| 26 | //! source, and `test/window_lifecycle_test.zig` checks it. | |
| 27 | ||
| 28 | const std = ; | |
| 29 | ||
| 30 | /// An opaque window handle — `@intFromPtr(NSWindow)` at the call site. Kept | |
| 31 | /// opaque so this module stays free of Objective-C and can be tested without | |
| 32 | /// AppKit. | |
| 33 | pub const Handle = usize; | |
| 34 | ||
| 35 | /// Craft opens one window today; #67 opens more. Far past any real app, and | |
| 36 | /// fixed so registration needs no allocator on the window-creation path. | |
| 37 | pub const capacity = 16; | |
| 38 | ||
| 39 | var windows: [capacity]Handle = @splat(0); | |
| 40 | ||
| 41 | /// Record a window craft created. Idempotent. | |
| 42 | /// | |
| 43 | /// Returns false if the table was full and the window was not recorded — the | |
| 44 | /// caller is expected to say so rather than let a window silently become | |
| 45 | /// unreopenable. | |
| 46 | pub fn remember(handle: Handle) bool { | |
| 47 | if (handle == 0) return false; | |
| 48 | for (&windows) |*slot| { | |
| 49 | if (slot.* == handle) return true; | |
| 50 | if (slot.* == 0) { | |
| 51 | slot.* = handle; | |
| 52 | return true; | |
| 53 | } | |
| 54 | } | |
| 55 | return false; | |
| 56 | } | |
| 57 | ||
| 58 | /// Whether craft opened this window. | |
| 59 | pub fn isKnown(handle: Handle) bool { | |
| 60 | if (handle == 0) return false; | |
| 61 | for (windows) |known| { | |
| 62 | if (known == handle) return true; | |
| 63 | } | |
| 64 | return false; | |
| 65 | } | |
| 66 | ||
| 67 | /// Drop a window. For real teardown, whenever #67 introduces some. | |
| 68 | pub fn forget(handle: Handle) void { | |
| 69 | for (&windows) |*slot| { | |
| 70 | if (slot.* == handle) slot.* = 0; | |
| 71 | } | |
| 72 | } | |
| 73 | ||
| 74 | pub fn count() usize { | |
| 75 | var n: usize = 0; | |
| 76 | for (windows) |w| { | |
| 77 | if (w != 0) n += 1; | |
| 78 | } | |
| 79 | return n; | |
| 80 | } | |
| 81 | ||
| 82 | pub fn resetForTesting() void { | |
| 83 | windows = @splat(0); | |
| 84 | } | |
| 85 | ||
| 86 | const testing = std.testing; | |
| 87 | ||
| 88 | test "a window craft opened is recognised" { | |
| 89 | resetForTesting(); | |
| 90 | try testing.expect(remember(0x1000)); | |
| 91 | try testing.expect(isKnown(0x1000)); | |
| 92 | } | |
| 93 | ||
| 94 | test "a window craft did not open is not" { | |
| 95 | // The offscreen windows AppKit keeps for menus and tooltips land here. | |
| 96 | // Ordering one of those front puts an empty frame on screen. | |
| 97 | resetForTesting(); | |
| 98 | _ = remember(0x1000); | |
| 99 | try testing.expect(!isKnown(0x2000)); | |
| 100 | } | |
| 101 | ||
| 102 | test "registering twice does not consume two slots" { | |
| 103 | resetForTesting(); | |
| 104 | try testing.expect(remember(0x1000)); | |
| 105 | try testing.expect(remember(0x1000)); | |
| 106 | try testing.expectEqual(@as(usize, 1), count()); | |
| 107 | } | |
| 108 | ||
| 109 | test "a null window is neither recorded nor recognised" { | |
| 110 | // `contentView` and friends return nil often enough that zero has to mean | |
| 111 | // nothing rather than becoming a real entry that matches every other nil. | |
| 112 | resetForTesting(); | |
| 113 | try testing.expect(!remember(0)); | |
| 114 | try testing.expect(!isKnown(0)); | |
| 115 | try testing.expectEqual(@as(usize, 0), count()); | |
| 116 | } | |
| 117 | ||
| 118 | test "every window up to capacity is recorded" { | |
| 119 | resetForTesting(); | |
| 120 | var i: usize = 1; | |
| 121 | while (i <= capacity) : (i += 1) { | |
| 122 | try testing.expect(remember(i * 0x100)); | |
| 123 | } | |
| 124 | i = 1; | |
| 125 | while (i <= capacity) : (i += 1) { | |
| 126 | try testing.expect(isKnown(i * 0x100)); | |
| 127 | } | |
| 128 | try testing.expectEqual(capacity, count()); | |
| 129 | } | |
| 130 | ||
| 131 | test "past capacity the failure is reported, not swallowed" { | |
| 132 | // The caller logs this. A window that quietly stopped being reopenable is | |
| 133 | // the bug this whole module exists to prevent, so it must not be the | |
| 134 | // silent outcome of a full table. | |
| 135 | resetForTesting(); | |
| 136 | var i: usize = 1; | |
| 137 | while (i <= capacity) : (i += 1) _ = remember(i * 0x100); | |
| 138 | try testing.expect(!remember(0xDEAD)); | |
| 139 | try testing.expect(!isKnown(0xDEAD)); | |
| 140 | } | |
| 141 | ||
| 142 | test "forgetting frees the slot for the next window" { | |
| 143 | resetForTesting(); | |
| 144 | var i: usize = 1; | |
| 145 | while (i <= capacity) : (i += 1) _ = remember(i * 0x100); | |
| 146 | try testing.expect(!remember(0xDEAD)); | |
| 147 | ||
| 148 | forget(0x100); | |
| 149 | try testing.expect(!isKnown(0x100)); | |
| 150 | try testing.expect(remember(0xDEAD)); | |
| 151 | try testing.expect(isKnown(0xDEAD)); | |
| 152 | } | |
| 153 | ||
| 154 | test "forgetting a window that was never known changes nothing" { | |
| 155 | resetForTesting(); | |
| 156 | _ = remember(0x1000); | |
| 157 | forget(0x2000); | |
| 158 | try testing.expect(isKnown(0x1000)); | |
| 159 | try testing.expectEqual(@as(usize, 1), count()); | |
| 160 | } | |