theme.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. """The theme bundle (goal 41 task 2): the account-settings appearance
  2. picker backed by Spry's built-in themes. The SetThemeAction persists
  3. the choice per user in user_preferences and writes the DEVICE-scope
  4. "ui" slot ({theme, mode}) the client applies as data-theme/data-mode;
  5. login seeds that slot from the stored preferences."""
  6. from harness import * # noqa: F401,F403
  7. def run(ctx):
  8. admin = ctx.auth
  9. def login_frames(username, password):
  10. """The login action's FULL directive chain (the harness login()
  11. only keeps the auth slot; the ui seed rides the same chain)."""
  12. frames = directives("/login")
  13. content = json.loads(frames[0]["snapshot_frame"]["content"])
  14. action = content["public"]["login"]
  15. status, body = http(action.get("method", "POST"), action["uri"],
  16. {"username": username, "password": password})
  17. if status != 200:
  18. raise SystemExit("login -> HTTP %d: %s" % (status, body[:300]))
  19. return json.loads(body)
  20. print("== profile page: the appearance card ==")
  21. profile = slot_public(admin, "/profile", "profile")
  22. check(profile.get("set_theme") is not None,
  23. "profile carries the set_theme action")
  24. check(profile.get("theme", "") == "" and profile.get("mode") == "auto",
  25. "no stored preference reads as core theme + auto mode")
  26. page = http_get_page(admin, "/profile")
  27. check("Appearance" in page, "profile page shows the Appearance card")
  28. for label in ("Spry (default)", "Airy", "Dwarf", "Formal", "Glassy",
  29. "H2O", "Living Sky", "Modern"):
  30. check(label in page, "theme choice offered: %s" % label)
  31. for label in ("Auto (follow system)", "Light", "Dark"):
  32. check(label in page, "mode choice offered: %s" % label)
  33. check("remembered for this account" in page,
  34. "the card hints the choice is remembered")
  35. print("== the seven theme stylesheets are served ==")
  36. for slug in ("airy", "dwarf", "formal", "glassy", "h2o",
  37. "living-sky", "modern"):
  38. status, _ = http("GET", "/_statum/resource/spry-theme-%s.css" % slug)
  39. check(status == 200, "spry-theme-%s.css served (HTTP %d)" % (slug, status))
  40. status, _ = http("GET", "/_statum/resource/spry.css")
  41. check(status == 200, "spry.css served (HTTP %d)" % status)
  42. print("== saving a theme persists + answers the ui slot ==")
  43. result = act(admin, profile["set_theme"], {"theme": "dwarf", "mode": "dark"})
  44. check("saved" in (action_notify(result) or ""),
  45. "set_theme accepts dwarf/dark")
  46. ui = [d for d in result
  47. if d["type"] == "set"
  48. and json.loads(d["snapshot_frame"]["content"])["slot"]["type"] == "ui"]
  49. check(len(ui) == 1, "the action answers a ui slot")
  50. ui_public = json.loads(ui[0]["snapshot_frame"]["content"])["public"]
  51. check(ui_public.get("theme") == "dwarf" and ui_public.get("mode") == "dark",
  52. "ui slot public data carries {theme, mode}")
  53. ui_scope = json.loads(ui[0]["snapshot_frame"]["content"])["slot"]
  54. check(ui_scope.get("scope") == "device",
  55. "the ui slot is DEVICE scope (cached across sessions)")
  56. conn = db()
  57. try:
  58. rows = conn.execute(
  59. "select key, value from user_preferences").fetchall()
  60. finally:
  61. conn.close()
  62. check(dict(rows).get("theme") == "dwarf" and dict(rows).get("mode") == "dark",
  63. "user_preferences stores theme=dwarf mode=dark: %s" % rows)
  64. print("== the saved choice reflects back on the profile ==")
  65. profile = slot_public(admin, "/profile", "profile")
  66. check(profile.get("theme") == "dwarf" and profile.get("mode") == "dark",
  67. "profile reads the stored choice back")
  68. print("== invalid values are refused, nothing stored ==")
  69. result = act(admin, profile["set_theme"], {"theme": "gruvbox", "mode": "auto"})
  70. check("Unknown theme" in (action_notify(result) or ""),
  71. "an unknown theme slug is refused with a friendly error")
  72. result = act(admin, profile["set_theme"], {"theme": "airy", "mode": "sepia"})
  73. check("Unknown colour mode" in (action_notify(result) or ""),
  74. "an unknown colour mode is refused")
  75. conn = db()
  76. try:
  77. rows = conn.execute(
  78. "select key, value from user_preferences").fetchall()
  79. finally:
  80. conn.close()
  81. check(dict(rows).get("theme") == "dwarf",
  82. "the refused slugs wrote nothing: %s" % rows)
  83. print("== core default + upsert overwrite ==")
  84. result = act(admin, profile["set_theme"], {"theme": "", "mode": "light"})
  85. check("saved" in (action_notify(result) or ""), "core default + light accepted")
  86. conn = db()
  87. try:
  88. rows = dict(conn.execute(
  89. "select key, value from user_preferences").fetchall())
  90. finally:
  91. conn.close()
  92. check(rows.get("theme") == "" and rows.get("mode") == "light",
  93. "the upsert replaced both rows in place: %s" % rows)
  94. profile = slot_public(admin, "/profile", "profile")
  95. check(profile.get("theme") == "" and profile.get("mode") == "light",
  96. "profile reads the core default back")
  97. print("== login seeds the ui slot from stored prefs ==")
  98. # The seed rides the LOGIN response's directive chain (the same
  99. # builder Spry's login action returns, extended by our subclass) -
  100. # so assert it there, not on a later entrypoint.
  101. frames = login_frames("admin", "adminpass123")
  102. seeded = [d for d in frames
  103. if d["type"] == "set"
  104. and json.loads(d["snapshot_frame"]["content"])["slot"]["type"] == "ui"]
  105. check(len(seeded) == 1, "the login response answers a ui slot")
  106. seeded_public = json.loads(seeded[0]["snapshot_frame"]["content"])["public"]
  107. check(seeded_public.get("theme") == "" and seeded_public.get("mode") == "light",
  108. "login seeds the ui slot from the stored preferences")