| Rooms and prefs come back with you a4be36c nandi yesterday | 1 | ## What survives a restart, and what deliberately does not. |
| 2 | ## |
| 3 | ## Every test here points `XDG_CONFIG_HOME` at a directory of its own, set |
| 4 | ## before `frq/store` is touched at all: the alternative is a suite that reads |
| 5 | ## and writes the config of whoever runs it, which would both lie about the |
| 6 | ## result and cost them their room list. |
| 7 | |
| Find out who somebody is 4b72ad1 nandi yesterday | 8 | import std/[json, os, tables, times, unittest] |
| Rooms and prefs come back with you a4be36c nandi yesterday | 9 | |
| 10 | let sandbox = getTempDir() / "frq-tstore-" & $epochTime() |
| 11 | putEnv("XDG_CONFIG_HOME", sandbox) |
| 12 | |
| 13 | import frq/[cells, clock, model, rooms, store, reducer] |
| 14 | |
| 15 | proc clean() = |
| 16 | removeDir(sandbox) |
| 17 | app = initState() |
| 18 | |
| 19 | suite "the config directory": |
| 20 | test "is under XDG_CONFIG_HOME where there is one": |
| 21 | check configDir() == sandbox / "frq" |
| 22 | |
| 23 | suite "a saved session": |
| 24 | setup: clean() |
| 25 | test "round-trips, and says whether there was one": |
| 26 | check loadSession()[1] == false |
| 27 | check saveSession(SavedSession(brokerToken: "durable", handle: "alice.uk", |
| 28 | did: "did:plc:a", nick: "alice")) |
| 29 | let (s, had) = loadSession() |
| 30 | check had |
| 31 | check s.brokerToken == "durable" |
| 32 | check s.did == "did:plc:a" |
| 33 | test "with no broker token is not a session": |
| 34 | # The token is the whole point of saving one; the handle beside it is |
| 35 | # only there to say whose it is. |
| 36 | check saveSession(SavedSession(handle: "alice.uk")) |
| 37 | check loadSession()[1] == false |
| Four more modules into Nim: members, glyphs, emoji, store 3975b4b nandi yesterday | 38 | test "is written so nobody else can read it": |
| Rooms and prefs come back with you a4be36c nandi yesterday | 39 | check saveSession(SavedSession(brokerToken: "durable")) |
| 40 | check getFilePermissions(sessionFile()) == {fpUserRead, fpUserWrite} |
| 41 | test "and restore brings it back as the Bluesky mode": |
| 42 | check saveSession(SavedSession(brokerToken: "durable", handle: "alice.uk", |
| 43 | nick: "alice")) |
| 44 | restore() |
| 45 | check app.brokerToken == "durable" |
| 46 | check app.authMode == amBluesky |
| 47 | check app.formHandle == "alice.uk" |
| 48 | check app.formNick == "alice" |
| 49 | |
| 50 | suite "the rooms file": |
| 51 | setup: clean() |
| 52 | |
| 53 | proc saved(): seq[SavedRoom] = |
| 54 | var rooms: OrderedTable[string, Room] |
| 55 | for (name, accessed, id, at) in [("#a", 100'i64, "m1", 900'i64), |
| 56 | ("#b", 300'i64, "m2", 800'i64), |
| 57 | ("carol", 200'i64, "", 0'i64)]: |
| 58 | var r = initRoom(name) |
| 59 | r.accessed = accessed |
| 60 | r.lastReadId = id |
| 61 | r.lastReadAt = at |
| 62 | r.messages = @[Message(id: "x", frm: "bob", text: "hi", at: 1000)] |
| 63 | rooms[name] = r |
| 64 | check saveRooms(rooms) |
| 65 | loadRooms() |
| 66 | |
| 67 | test "keeps the name and the marker, and not the messages": |
| 68 | # The list is the part worth keeping; the lines in it come from the |
| 69 | # server, and a client that saved them would be a second copy to go |
| 70 | # stale. |
| 71 | let rs = saved() |
| 72 | check rs.len == 3 |
| 73 | check rs[0].name == "#a" |
| 74 | check rs[0].lastReadId == "m1" |
| 75 | check rs[0].lastReadAt == 900 |
| 76 | |
| 77 | test "restore brings them back empty, unjoined, with their markers": |
| 78 | discard saved() |
| 79 | restore() |
| 80 | check app.rooms.len == 3 |
| 81 | check app.rooms["#a"].messages.len == 0 |
| 82 | check not app.rooms["#a"].joined |
| 83 | check app.rooms["#a"].lastReadId == "m1" |
| 84 | check app.rooms["#a"].accessed == 100 |
| 85 | check app.rooms["#b"].accessed == 300 |
| 86 | |
| 87 | test "and the most recently used is still top of the list": |
| 88 | discard saved() |
| 89 | restore() |
| 90 | check channelList(app.rooms, "")[0].name == "#b" |
| 91 | |
| 92 | test "a record with no marker is caught up, not unread from the start": |
| 93 | # An older frq's file, or one that lost it. The alternative announces a |
| 94 | # hundred lines the reader has already seen. |
| 95 | discard saved() |
| 96 | restore() |
| 97 | let before = nowMs() |
| 98 | check app.rooms["carol"].lastReadAt >= before - 5000 |
| 99 | check app.rooms["carol"].lastReadAt > 0 |
| 100 | |
| 101 | test "a room the reader has closed stays closed": |
| 102 | discard saved() |
| 103 | restore() |
| 104 | dispatch(%*{"id": "room.leave:#a"}) |
| 105 | app = initState() |
| 106 | restore() |
| 107 | check not app.rooms.hasKey("#a") |
| 108 | check app.rooms.hasKey("#b") |
| 109 | |
| 110 | suite "the prefs file": |
| 111 | setup: clean() |
| A row is named for its message, not its place 4083860 nandi yesterday | 112 | test "the display toggles outlive the run": |
| Rooms and prefs come back with you a4be36c nandi yesterday | 113 | restore() |
| 114 | let wasJoinPart = app.hideJoinPart |
| 115 | dispatch(%*{"id": "join-part.toggle"}) |
| A row is named for its message, not its place 4083860 nandi yesterday | 116 | dispatch(%*{"id": "chat-list.toggle"}) |
| Rooms and prefs come back with you a4be36c nandi yesterday | 117 | app = initState() |
| 118 | restore() |
| 119 | check app.hideJoinPart == not wasJoinPart |
| A row is named for its message, not its place 4083860 nandi yesterday | 120 | check app.hideChatList |
| 121 | |
| 122 | test "but the member list does not": |
| 123 | # On a narrow window the people panel is the pane rather than something |
| 124 | # beside it, so a remembered "on" opens a room and shows a list of names |
| 125 | # instead of the room. |
| 126 | restore() |
| 127 | dispatch(%*{"id": "users.toggle"}) |
| Rooms and prefs come back with you a4be36c nandi yesterday | 128 | check app.showUsers |
| A row is named for its message, not its place 4083860 nandi yesterday | 129 | app = initState() |
| 130 | restore() |
| 131 | check not app.showUsers |
| Rooms and prefs come back with you a4be36c nandi yesterday | 132 | test "the overview is not one of them": |
| 133 | # It is a way of looking at the moment you are in rather than a |
| 134 | # preference, and reopening into it would answer a question nobody asked |
| 135 | # twice. |
| 136 | restore() |
| 137 | dispatch(%*{"id": "overview.toggle"}) |
| 138 | app = initState() |
| 139 | restore() |
| 140 | check not app.overview |
| 141 | test "and a prefs file that is not there is a first run, not an error": |
| 142 | check loadPrefs().len == 0 |
| 143 | restore() |
| 144 | check not app.hideJoinPart |