t4lists.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. """The T4 lists bundle (goal 41 task 4): the tasks/goals/projects/stacks
  2. list pages on the Spry conversion — faceted status chips with counts on
  3. /tasks (default view = open work) and /goals, the create-form expanders
  4. replaced by native <dialog> modals (openAppDialog helper in app.js),
  5. the goals Tasks column as readable badges, and the projects list's
  6. folded repo cell. Markup-level: http_get_page fetches the compiled
  7. page; the counts are cross-checked against the slot data the chips
  8. count over."""
  9. from harness import * # noqa: F401,F403 — the shared wire helpers
  10. def run(ctx):
  11. auth, fake, fake_id, fake_token = ctx.auth, ctx.fake, ctx.fake_id, ctx.fake_token
  12. OPEN = ("queued", "assigned", "working", "blocked", "suspended")
  13. def status_counts():
  14. """Per-status task counts from the /tasks slot (the chip
  15. numbers count exactly these rows)."""
  16. rows = slot_public(auth, "/tasks", "tasks")["tasks"]
  17. counts = {}
  18. for row in rows:
  19. counts[row["status"]] = counts.get(row["status"], 0) + 1
  20. counts["all"] = len(rows)
  21. counts["open"] = sum(n for s, n in counts.items()
  22. if s in OPEN)
  23. return counts
  24. def seed_tasks():
  25. conn = db()
  26. for i, status in enumerate(("queued", "working", "blocked", "done",
  27. "done", "failed", "draft")):
  28. conn.execute(
  29. "insert into tasks (project_id, title, status, priority,"
  30. " created_at) values (1, ?, ?, 5, strftime('%s','now'))",
  31. ("T4 chip probe %d" % i, status))
  32. conn.commit()
  33. conn.close()
  34. print("== /tasks: faceted status chips with counts ==")
  35. seed_tasks()
  36. counts = status_counts()
  37. check(counts["open"] == 3 and counts["all"] >= 7,
  38. "the fixture shapes the counts (open %d / all %d)"
  39. % (counts["open"], counts["all"]))
  40. tasks_page = http_get_page(auth, "/tasks")
  41. check("taskChipClass" in tasks_page and "taskStatusCount" in tasks_page
  42. and "TASK_OPEN_STATUSES" in tasks_page,
  43. "the tasks page ships the chip engine (class/count/open-set)")
  44. check(tasks_page.index("goal-strip") < tasks_page.index("<table"),
  45. "the chip strip sits above the queue table")
  46. check("window.setTaskStatusFilter('open')" in tasks_page
  47. and "window.setTaskStatusFilter('done')" in tasks_page,
  48. "every chip drives the shared setTaskFilter engine")
  49. # The default view is open work: the status filter initialises to
  50. # 'open' and taskRowVisible understands the open set.
  51. check("taskFilters.status = 'open'" in tasks_page
  52. and "TASK_OPEN_STATUSES.indexOf(row.status) === -1" in tasks_page,
  53. "the default filter is the open-work set (queued..suspended)")
  54. check("filter-project" in tasks_page and "filter-goal" in tasks_page,
  55. "the project and goal selects stay (chips drive the status axis)")
  56. check('id="filter-status"' not in tasks_page,
  57. "the old status select is gone")
  58. print("== /tasks: the create form lives in a dialog ==")
  59. check('id="task-create-dialog"' in tasks_page
  60. and "<dialog" in tasks_page,
  61. "the create form is a native dialog")
  62. check(tasks_page.index("<dialog") > tasks_page.index("</table>"),
  63. "the dialog markup sits after the table, opened by the button")
  64. check('onclick="window.openAppDialog(\'task-create-dialog\')"' in tasks_page,
  65. "the + New task button opens the dialog through the shared helper")
  66. # The form inside the dialog is the SAME form: same action, same
  67. # fields (the B6b branch fields, the mode select, auto-enqueue).
  68. dialog_html = tasks_page[tasks_page.index('id="task-create-dialog"'):]
  69. check('stm-action="tasks.create"' in dialog_html
  70. and 'name="task_branch"' in dialog_html
  71. and 'name="branch_from"' in dialog_html
  72. and 'id="task-mode"' in dialog_html
  73. and 'id="task-enqueue-after"' in dialog_html
  74. and "No project (standalone)" in dialog_html,
  75. "the dialog carries the unchanged create form (action + fields)")
  76. check('class="dialog-titlebar"' in dialog_html
  77. and 'class="dialog-footer"' in dialog_html,
  78. "the dialog uses Spry's dialog classes (titlebar/footer)")
  79. # Semantics still work end to end: create through the action the
  80. # dialog form posts (new tasks land in draft).
  81. before = counts["draft"]
  82. act(auth, slot_public(auth, "/tasks", "tasks")["create"], {
  83. "project_id": "1", "title": "T4 dialog create task", "priority": "5"})
  84. after = status_counts()
  85. check(after["draft"] == before + 1,
  86. "the dialog form's action still creates (draft %d -> %d)"
  87. % (before, after["draft"]))
  88. print("== app.js: the shared dialog helper ==")
  89. status, app_js = http("GET", "/_statum/resource/app.js")
  90. check(status == 200 and "window.openAppDialog" in app_js
  91. and "window.closeAppDialog" in app_js,
  92. "the shared dialog helper ships in app.js")
  93. check("onNotifyHandler" in app_js and "dialog.close()" in app_js,
  94. "the helper closes the dialog on the action's notify")
  95. print("== /goals: chips + readable task badges + create dialog ==")
  96. goals_page = http_get_page(auth, "/goals")
  97. check("goalStatusCount" in goals_page and "goalChipClass" in goals_page
  98. and "setGoalStatusFilter" in goals_page,
  99. "the goals list ships the same chip engine")
  100. check("goalRowVisible" in goals_page,
  101. "the goals rows filter client-side like the queue")
  102. # The four-count muted string is gone; badges carry the shape.
  103. check("draft / " not in goals_page
  104. and "running_tasks + " in goals_page,
  105. "the Tasks column renders the total badge from the counts")
  106. check('badge-warning" stm-if="goal.running_tasks &gt; 0"' in goals_page,
  107. "running tasks surface as a warning badge")
  108. check('id="goal-create-dialog"' in goals_page
  109. and 'stm-action="goals.create"' in goals_page,
  110. "the goal create form lives in a dialog")
  111. goal_dialog = goals_page[goals_page.index('id="goal-create-dialog"'):]
  112. check('id="goal-brief"' in goal_dialog and "textarea" in goal_dialog
  113. and "required" in goal_dialog,
  114. "the required brief textarea is inside the dialog (visible on open)")
  115. print("== /projects: the folded repo cell + create dialog ==")
  116. projects_page = http_get_page(auth, "/projects")
  117. check('id="project-create-dialog"' in projects_page
  118. and 'stm-action="projects.create"' in projects_page
  119. and 'onclick="window.openAppDialog(\'project-create-dialog\')"' in projects_page,
  120. "the project create form lives in a dialog behind the button")
  121. check("<th>Repo</th>" in projects_page and "<th>Git URL</th>" not in projects_page,
  122. "the Git URL column folded into one Repo cell")
  123. check("repo-url" in projects_page and "truncate" in projects_page,
  124. "the repo cell truncates the clone URL (branch under it)")
  125. print("== /stacks: the create dialog ==")
  126. stacks_page = http_get_page(auth, "/stacks")
  127. check('id="stack-create-dialog"' in stacks_page
  128. and 'stm-action="stacks.create"' in stacks_page
  129. and 'onclick="window.openAppDialog(\'stack-create-dialog\')"' in stacks_page,
  130. "the stack create form lives in a dialog behind the button")
  131. check("<details>" not in stacks_page and "<details>" not in projects_page,
  132. "no create expander remains on stacks or projects")