116 lines
6.2 KiB
Python
116 lines
6.2 KiB
Python
from __future__ import annotations
|
|||
|
|
|
||
|
|
import json
|
||
|
|
import subprocess
|
||
|
|
import unittest
|
||
|
|
|
||
|
|
from hub_support import POPUP
|
||
|
|
|
||
|
|
POPUP_PROBE = """
|
||
|
|
const P = require(process.argv[1]);
|
||
|
|
let seed = 11;
|
||
|
|
const next = () => {
|
||
|
|
seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0;
|
||
|
|
return seed / 4294967296;
|
||
|
|
};
|
||
|
|
const node = (left, top) => ({left, top, width: 20, height: 20});
|
||
|
|
const wide = {width: 1366, height: 768};
|
||
|
|
const phone = {width: 390, height: 844};
|
||
|
|
const random = Array.from({length: 600}, () => {
|
||
|
|
const view = {width: 300 + Math.round(next() * 1700), height: 300 + Math.round(next() * 900)};
|
||
|
|
const size = {width: Math.min(440, view.width - 2 * P.GAP), height: Math.round(80 + next() * (view.height - 2 * P.GAP - 80))};
|
||
|
|
const anchor = {left: Math.round(next() * view.width) - 10, top: Math.round(next() * view.height) - 10, width: 20, height: 20};
|
||
|
|
return {view, size, anchor, at: P.place(anchor, size, view)};
|
||
|
|
});
|
||
|
|
const nodes = [
|
||
|
|
{id: "s1", label: "Skills session"}, {id: "s2", label: "Stage"}, {id: "agent:Stage@/a", label: "Stage"}, {id: "agent:Stage@/b", label: "Stage"},
|
||
|
|
{id: "b1", label: "feature/stage-fix"}, {id: "b2", label: "main"}, {id: "b3", label: "main"}, {id: "x", label: "Backstage"},
|
||
|
|
];
|
||
|
|
const ids = (list) => list.map((entry) => entry.id);
|
||
|
|
console.log(JSON.stringify({
|
||
|
|
constants: [P.GAP, P.REACH, P.MIN],
|
||
|
|
right: P.place(node(100, 300), {width: 440, height: 300}, wide),
|
||
|
|
left: P.place(node(1200, 300), {width: 440, height: 300}, wide),
|
||
|
|
below: P.place(node(185, 100), {width: 358, height: 300}, phone),
|
||
|
|
above: P.place(node(185, 700), {width: 358, height: 300}, phone),
|
||
|
|
capped: P.place(node(185, 400), {width: 358, height: 700}, phone),
|
||
|
|
covered: P.place(node(185, 180), {width: 358, height: 368}, {width: 390, height: 400}),
|
||
|
|
centre: P.place(null, {width: 440, height: 300}, wide),
|
||
|
|
random,
|
||
|
|
hits: {
|
||
|
|
stage: ids(P.hits(nodes, "sta", 8)),
|
||
|
|
main: ids(P.hits(nodes, " MAIN ", 8)),
|
||
|
|
blank: P.hits(nodes, " ", 8),
|
||
|
|
limit: ids(P.hits(nodes, "a", 2)),
|
||
|
|
none: P.hits(nodes, "zzz", 8),
|
||
|
|
same: P.hits(nodes, "stage", 8)[0] === nodes[1],
|
||
|
|
},
|
||
|
|
}));
|
||
|
|
"""
|
||
|
|
SLACK = 0.06
|
||
|
|
|
||
|
|
|
||
|
|
class TestPopupModule(unittest.TestCase):
|
||
|
|
@classmethod
|
||
|
|
def setUpClass(cls):
|
||
|
|
done = subprocess.run(["node", "-e", POPUP_PROBE, str(POPUP)], capture_output=True, text=True, timeout=60, check=True)
|
||
|
|
cls.data = json.loads(done.stdout)
|
||
|
|
cls.gap, cls.reach, cls.min = cls.data["constants"]
|
||
|
|
|
||
|
|
def test_the_popup_sits_right_of_the_node_and_the_leader_spans_the_gap_between_them(self):
|
||
|
|
self.assertEqual(self.data["right"], {"left": 160, "top": 160, "height": None, "line": {"x1": 120, "y1": 310, "x2": 160, "y2": 310}})
|
||
|
|
self.assertEqual(self.data["right"]["line"]["x2"] - self.data["right"]["line"]["x1"], self.reach)
|
||
|
|
|
||
|
|
def test_a_node_near_the_right_edge_gets_the_popup_on_its_left(self):
|
||
|
|
self.assertEqual(self.data["left"], {"left": 720, "top": 160, "height": None, "line": {"x1": 1200, "y1": 310, "x2": 1160, "y2": 310}})
|
||
|
|
|
||
|
|
def test_on_a_phone_the_popup_goes_below_or_above_the_node(self):
|
||
|
|
self.assertEqual(self.data["below"], {"left": 16, "top": 160, "height": None, "line": {"x1": 195, "y1": 120, "x2": 195, "y2": 160}})
|
||
|
|
self.assertEqual(self.data["above"], {"left": 16, "top": 360, "height": None, "line": {"x1": 195, "y1": 700, "x2": 195, "y2": 660}})
|
||
|
|
|
||
|
|
def test_a_popup_too_tall_for_either_side_is_capped_to_the_larger_room_and_keeps_its_leader(self):
|
||
|
|
self.assertEqual(self.data["capped"], {"left": 16, "top": 460, "height": 368, "line": {"x1": 195, "y1": 420, "x2": 195, "y2": 460}})
|
||
|
|
|
||
|
|
def test_without_room_the_popup_covers_the_node_and_draws_no_leader(self):
|
||
|
|
self.assertEqual(self.data["covered"], {"left": 16, "top": 16, "height": None, "line": None})
|
||
|
|
|
||
|
|
def test_a_popup_without_a_node_is_centred_without_a_leader(self):
|
||
|
|
self.assertEqual(self.data["centre"], {"left": 463, "top": 234, "height": None, "line": None})
|
||
|
|
|
||
|
|
def test_every_placement_stays_in_the_viewport_and_every_leader_runs_from_the_node_edge_to_the_popup_edge(self):
|
||
|
|
leaders = 0
|
||
|
|
for case in self.data["random"]:
|
||
|
|
view, size, anchor, at = case["view"], case["size"], case["anchor"], case["at"]
|
||
|
|
height = size["height"] if at["height"] is None else at["height"]
|
||
|
|
box = (at["left"], at["top"], at["left"] + size["width"], at["top"] + height)
|
||
|
|
with self.subTest(case=case):
|
||
|
|
self.assertGreaterEqual(min(box[0], box[1]), self.gap - SLACK)
|
||
|
|
self.assertLessEqual(box[2], view["width"] - self.gap + SLACK)
|
||
|
|
self.assertLessEqual(box[3], view["height"] - self.gap + SLACK)
|
||
|
|
if at["height"] is not None:
|
||
|
|
self.assertGreaterEqual(at["height"], self.min - SLACK)
|
||
|
|
self.assertLess(at["height"], size["height"])
|
||
|
|
rect = (anchor["left"], anchor["top"], anchor["left"] + anchor["width"], anchor["top"] + anchor["height"])
|
||
|
|
line = at["line"]
|
||
|
|
if line is None:
|
||
|
|
self.assertTrue(box[0] <= rect[2] + SLACK and rect[0] <= box[2] + SLACK and box[1] <= rect[3] + SLACK and rect[1] <= box[3] + SLACK, "no leader although the popup misses the node")
|
||
|
|
continue
|
||
|
|
leaders += 1
|
||
|
|
self.assertTrue(any(abs(line["x2"] - edge) <= SLACK for edge in (box[0], box[2])) or any(abs(line["y2"] - edge) <= SLACK for edge in (box[1], box[3])))
|
||
|
|
self.assertTrue(any(abs(line["x1"] - edge) <= SLACK for edge in (rect[0], rect[2])) or any(abs(line["y1"] - edge) <= SLACK for edge in (rect[1], rect[3])))
|
||
|
|
self.assertTrue(rect[0] - SLACK <= line["x1"] <= rect[2] + SLACK and rect[1] - SLACK <= line["y1"] <= rect[3] + SLACK)
|
||
|
|
self.assertGreater(leaders, len(self.data["random"]) * 0.9)
|
||
|
|
|
||
|
|
def test_search_hits_are_one_per_label_prefix_first_case_insensitive_and_limited(self):
|
||
|
|
hits = self.data["hits"]
|
||
|
|
self.assertEqual(hits["stage"], ["s2", "b1", "x"])
|
||
|
|
self.assertEqual(hits["main"], ["b2"])
|
||
|
|
self.assertEqual(hits["limit"], ["s2", "b1"])
|
||
|
|
self.assertTrue(hits["same"])
|
||
|
|
|
||
|
|
def test_a_blank_or_unknown_query_finds_nothing(self):
|
||
|
|
self.assertEqual((self.data["hits"]["blank"], self.data["hits"]["none"]), ([], []))
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
unittest.main()
|