"""An archive button on the card, where the card is (task 47). Archiving used to be one gesture only: drag the card the length of the board onto the activity bar. The card now carries the same action itself — a `⌸` chip at the right-hand end of its footer row, arming on the first click and firing on the second. board.html is a single file with inline JS and no frontend test runner, so most of these are source-level invariants: the ones that, if broken, would give back the thing the task was written against — a second copy of the archivable-stages rule in the page, a bespoke confirmation, an archive that loses the ⌘Z promise, or an armed chip left behind by a card that has gone. python3 -m unittest discover -s tests -v """ from __future__ import annotations import re import sys import unittest from pathlib import Path REPO = Path(__file__).resolve().parents[1] CORE = REPO / "manager" / "core" sys.path.insert(0, str(CORE)) import config # noqa: E402 import taskfiles # noqa: E402 BOARD = CORE / "board.html" TRAY_GLYPH = "⌸" def html() -> str: return BOARD.read_text(encoding="utf-8") class OneAuthorityForTheRule(unittest.TestCase): """Which cards may be archived is the server's answer. The page asks for it rather than keeping a copy that can drift out of step.""" @classmethod def setUpClass(cls): cls.html = html() def test_the_working_stages_are_not_archivable(self): """The rule itself, unchanged: finish or walk a card back, never tidy it away mid-flight.""" self.assertEqual(taskfiles.ARCHIVE_FROM, {"backlog", "to-do", "done"}) for working in ("in-progress", "review"): self.assertIn(working, config.STAGE_DIRS) self.assertNotIn(working, taskfiles.ARCHIVE_FROM) with self.assertRaises(ValueError): taskfiles.archive_task("47-nothing.md", working) def test_the_state_payload_carries_the_set(self): import httpd payload = httpd.state_payload() self.assertIn("archiveFrom", payload) self.assertEqual(set(payload["archiveFrom"]), taskfiles.ARCHIVE_FROM) self.assertEqual(payload["archiveFrom"], [slug for slug, _ in config.STAGES if slug in taskfiles.ARCHIVE_FROM], "sent in board order, so the JSON reads like the board") def test_the_page_holds_no_second_copy(self): for fossil in ("'backlog', 'to-do', 'done'", '"backlog", "to-do", "done"'): self.assertNotIn(fossil, self.html, "the archivable stages are the server's list, " "not a literal in the page") def test_both_gestures_ask_the_same_helper(self): """canArchive() — defined once, reading S.state.archiveFrom, and used by the chip and by the drag-to-the-bar gesture alike.""" body = re.search(r"function canArchive\(task\) \{(.*?)\n\}", self.html, re.S) self.assertIsNotNone(body, "canArchive is gone") self.assertIn("S.state.archiveFrom", body.group(1)) self.assertEqual(len(re.findall(r"canArchive\(task\)", self.html)), 3, "one definition and exactly two callers: the chip " "and the dragstart handler") class TheChipOnTheCard(unittest.TestCase): """A `⌸` chip, last in the footer row, quiet until you are on it.""" @classmethod def setUpClass(cls): cls.html = html() cls.push = re.search(r"if \(canArchive\(task\)\) \{(.*?)\n \}", cls.html, re.S) def rule(self, selector: str) -> str: m = re.search(re.escape(selector) + r"\{([^}]*)\}", self.html) self.assertIsNotNone(m, f"board.html lost its {selector} rule") return m.group(1).replace(" ", "").replace("\n", "") def test_it_wears_the_trays_glyph(self): """The whole point is that the two are visibly one action, so the glyph appears exactly where the tray's does and nowhere else.""" self.assertIsNotNone(self.push, "the archive chip is not pushed") self.assertIn(f"pre: '{TRAY_GLYPH}'", self.push.group(1)) tray = re.search(r"\$\('#tray'\)\.innerHTML =(.*?);", self.html, re.S) self.assertIn(TRAY_GLYPH, tray.group(1), "the tray lost its glyph") self.assertEqual(self.html.count(TRAY_GLYPH), 2, "one glyph, two places: the tray and the card's chip") def test_it_is_the_last_chip_in_the_row(self): chips = self.html.index("const chipRow = chips.length") commands = self.html.index("for (const cmd of (S.state.commands || []))") self.assertLess(commands, self.push.start(), "the archive chip is pushed after every tool chip") self.assertLess(self.push.end(), chips, "…and before the row is rendered") def test_it_sits_at_the_far_end_and_rests_quiet(self): self.assertIn("cls: 'arch'", self.push.group(1)) arch = self.rule(".chip2.arch") self.assertIn("margin-left:auto", arch, "pushed to the right-hand end") self.assertIn("border-color:transparent", arch) self.assertIn("color:var(--dim)", arch) def test_hover_armed_and_busy_all_outrank_the_quiet_rule(self): """`.chip2.arch` is a two-class selector on purpose: every live state is a class plus an element or a pseudo-class, so none of them is dulled back to dim by the resting rule.""" flat = self.html.replace(" ", "").replace("\n", "") for live in ("button.chip2:hover{", "button.chip2.armed{", "button.chip2.busy{"): self.assertIn(live, flat, f"{live} must stay more specific than .chip2.arch") def test_no_chip_where_the_server_would_refuse(self): """Nothing renders it on its own terms: the one condition is canArchive, so in-progress and review cards simply have no chip.""" card = re.search(r"function cardFor\(task\) \{.*?\n\}\n", self.html, re.S).group(0) self.assertEqual(len(re.findall(r"data-arch", card)), 2, "the chip is written once and wired once") self.assertEqual(len(re.findall(r"canArchive\(task\)", card)), 2) class ArmThenFire(unittest.TestCase): """Nothing about archiving invents its own confirmation.""" @classmethod def setUpClass(cls): cls.html = html() def test_it_walks_the_one_state_machine(self): wiring = re.search(r"el\.querySelectorAll\('\[data-arch\]'\).*?\n \}\);", self.html, re.S) self.assertIsNotNone(wiring, "the archive chip is not wired") self.assertIn("wireAction(btn, `${task.file}::archive`", wiring.group(0)) self.assertIn("confirm: 'archive it?'", wiring.group(0)) self.assertIn("busy: 'archiving…'", wiring.group(0)) self.assertIn("archiveCard(task.file, task.stage)", wiring.group(0)) def test_the_label_swaps_in_place_like_every_other_action(self): push_to_row = self.html[self.html.index("if (c.arch) return"):] line = push_to_row.split("\n", 1)[0] self.assertIn("actLabel(c.label, 'archive it?', 'archiving…')", line, "rest / confirm / busy share one grid cell") self.assertIn("::") self.assertIn("delete S.acts[key]", body.group(1)) def test_the_sweep_runs_before_anything_is_drawn(self): head = re.search(r"function render\(\) \{\n(.*?)\n renderTitle", self.html, re.S) self.assertIn("forgetActsOfVanishedCards();", head.group(1)) def test_every_act_key_is_a_task_file(self): """The sweep reads the key's first segment as a filename, so every key wireAction is given must start with one.""" keys = re.findall(r"wireAction\(btn, `([^`]+)`", self.html) self.assertTrue(keys, "no wired actions found") for key in keys: self.assertTrue(key.startswith("${task.file}::"), f"stray act key {key!r}") if __name__ == "__main__": unittest.main()