simcord 2.0.2__tar.gz → 2.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. simcord-2.2.0/.github/workflows/ci.yml +13 -0
  2. {simcord-2.0.2 → simcord-2.2.0}/.github/workflows/release.yml +4 -46
  3. simcord-2.2.0/.github/workflows/verify.yml +99 -0
  4. {simcord-2.0.2 → simcord-2.2.0}/.gitignore +5 -0
  5. {simcord-2.0.2 → simcord-2.2.0}/CHANGELOG.md +32 -0
  6. {simcord-2.0.2 → simcord-2.2.0}/CONTRIBUTING.md +12 -0
  7. {simcord-2.0.2 → simcord-2.2.0}/PKG-INFO +27 -5
  8. {simcord-2.0.2 → simcord-2.2.0}/README.md +15 -4
  9. {simcord-2.0.2 → simcord-2.2.0}/docs/api.md +35 -0
  10. {simcord-2.0.2 → simcord-2.2.0}/docs/architecture.md +15 -0
  11. {simcord-2.0.2 → simcord-2.2.0}/docs/concepts.md +12 -7
  12. {simcord-2.0.2 → simcord-2.2.0}/docs/cookbook.md +26 -1
  13. {simcord-2.0.2 → simcord-2.2.0}/docs/faq.md +8 -0
  14. {simcord-2.0.2 → simcord-2.2.0}/docs/guides/ai-coding-agents.md +14 -0
  15. {simcord-2.0.2 → simcord-2.2.0}/docs/guides/components.md +7 -0
  16. {simcord-2.0.2 → simcord-2.2.0}/docs/guides/diagnostics.md +17 -8
  17. simcord-2.2.0/docs/guides/preview.md +398 -0
  18. {simcord-2.0.2 → simcord-2.2.0}/docs/guides/time-control.md +4 -1
  19. {simcord-2.0.2 → simcord-2.2.0}/docs/index.md +6 -0
  20. {simcord-2.0.2 → simcord-2.2.0}/docs/installation.md +27 -5
  21. {simcord-2.0.2 → simcord-2.2.0}/docs/llms.txt +1 -0
  22. {simcord-2.0.2 → simcord-2.2.0}/docs/parity-matrix.md +9 -4
  23. {simcord-2.0.2 → simcord-2.2.0}/docs/quickstart.md +4 -4
  24. {simcord-2.0.2 → simcord-2.2.0}/docs/stability.md +17 -2
  25. simcord-2.2.0/examples/README.md +53 -0
  26. {simcord-2.0.2 → simcord-2.2.0}/examples/bot.py +13 -0
  27. simcord-2.2.0/examples/preview_example.py +78 -0
  28. {simcord-2.0.2 → simcord-2.2.0}/examples/test_bot.py +13 -0
  29. {simcord-2.0.2 → simcord-2.2.0}/mkdocs.yml +1 -0
  30. {simcord-2.0.2 → simcord-2.2.0}/pyproject.toml +9 -2
  31. simcord-2.2.0/scripts/capture_visual_reference.py +457 -0
  32. {simcord-2.0.2 → simcord-2.2.0}/scripts/check_discoverability.py +2 -0
  33. simcord-2.2.0/scripts/check_package.py +47 -0
  34. simcord-2.2.0/scripts/compare_visual_reference.py +588 -0
  35. simcord-2.2.0/scripts/discord_reference_bot.py +242 -0
  36. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/__init__.py +21 -1
  37. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/_dpy_internals.py +155 -10
  38. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/actors.py +17 -13
  39. simcord-2.2.0/src/simcord/backend/access.py +71 -0
  40. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/models/interaction.py +1 -0
  41. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/models/user.py +2 -0
  42. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/ops/base.py +3 -2
  43. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/serializers.py +1 -1
  44. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/builders.py +5 -0
  45. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/env.py +391 -48
  46. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/http/router.py +6 -2
  47. simcord-2.2.0/src/simcord/preview/__init__.py +398 -0
  48. simcord-2.2.0/src/simcord/preview/_actions.py +673 -0
  49. simcord-2.2.0/src/simcord/preview/_assets.py +209 -0
  50. simcord-2.2.0/src/simcord/preview/_capture.py +644 -0
  51. simcord-2.2.0/src/simcord/preview/_markdown.py +145 -0
  52. simcord-2.2.0/src/simcord/preview/_media.py +167 -0
  53. simcord-2.2.0/src/simcord/preview/_pages.py +343 -0
  54. simcord-2.2.0/src/simcord/preview/_server.py +287 -0
  55. simcord-2.2.0/src/simcord/preview/_snapshot.py +750 -0
  56. simcord-2.2.0/src/simcord/preview/protocol.schema.json +59 -0
  57. simcord-2.2.0/src/simcord/preview/static/app.js +885 -0
  58. simcord-2.2.0/src/simcord/preview/static/components.js +859 -0
  59. simcord-2.2.0/src/simcord/preview/static/fonts/OFL-1.1-Noto-Arabic.txt +93 -0
  60. simcord-2.2.0/src/simcord/preview/static/fonts/OFL-1.1-Noto-CJK.txt +92 -0
  61. simcord-2.2.0/src/simcord/preview/static/fonts/OFL-1.1-Noto-Devanagari.txt +93 -0
  62. simcord-2.2.0/src/simcord/preview/static/fonts/OFL-1.1-Noto-Emoji.txt +92 -0
  63. simcord-2.2.0/src/simcord/preview/static/fonts/OFL-1.1-Noto-Hebrew.txt +93 -0
  64. simcord-2.2.0/src/simcord/preview/static/fonts/OFL-1.1-Noto.txt +93 -0
  65. simcord-2.2.0/src/simcord/preview/static/fonts/manifest.json +202 -0
  66. simcord-2.2.0/src/simcord/preview/static/fonts/noto-color-emoji-v2.051.ttf +0 -0
  67. simcord-2.2.0/src/simcord/preview/static/fonts/noto-sans-arabic-v2.012.ttf +0 -0
  68. simcord-2.2.0/src/simcord/preview/static/fonts/noto-sans-devanagari-v2.007.ttf +0 -0
  69. simcord-2.2.0/src/simcord/preview/static/fonts/noto-sans-hebrew-v3.001.ttf +0 -0
  70. simcord-2.2.0/src/simcord/preview/static/fonts/noto-sans-latin-italic-v2.015.ttf +0 -0
  71. simcord-2.2.0/src/simcord/preview/static/fonts/noto-sans-latin-v2.015.ttf +0 -0
  72. simcord-2.2.0/src/simcord/preview/static/fonts/noto-sans-mono-v2.014.ttf +0 -0
  73. simcord-2.2.0/src/simcord/preview/static/fonts/noto-sans-sc-v2.004.ttf +0 -0
  74. simcord-2.2.0/src/simcord/preview/static/index.html +27 -0
  75. simcord-2.2.0/src/simcord/preview/static/preview.css +993 -0
  76. simcord-2.2.0/src/simcord/types.py +17 -0
  77. simcord-2.2.0/tests/fixtures/preview/__init__.py +1 -0
  78. simcord-2.2.0/tests/fixtures/preview/catalog.py +483 -0
  79. simcord-2.2.0/tests/fixtures/preview/coverage.json +9596 -0
  80. simcord-2.2.0/tests/fixtures/preview/measurements.json +346 -0
  81. simcord-2.2.0/tests/fixtures/preview/profiles.json +87 -0
  82. simcord-2.2.0/tests/fixtures/preview/states.json +137 -0
  83. simcord-2.2.0/tests/integration/preview_helpers.py +122 -0
  84. {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_app_commands.py +7 -0
  85. {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_bot_users_and_webhooks.py +3 -0
  86. {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_diagnostics.py +12 -3
  87. {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_lifecycle.py +37 -0
  88. {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_misc_branches.py +1 -1
  89. {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_moderation.py +23 -0
  90. simcord-2.2.0/tests/integration/test_preview.py +620 -0
  91. simcord-2.2.0/tests/integration/test_preview_additional.py +673 -0
  92. simcord-2.2.0/tests/integration/test_preview_capture.py +350 -0
  93. simcord-2.2.0/tests/integration/test_preview_capture_dx.py +296 -0
  94. simcord-2.2.0/tests/integration/test_preview_coverage.py +327 -0
  95. simcord-2.2.0/tests/integration/test_preview_dx.py +177 -0
  96. simcord-2.2.0/tests/integration/test_preview_edges.py +445 -0
  97. simcord-2.2.0/tests/integration/test_preview_fixes.py +638 -0
  98. simcord-2.2.0/tests/integration/test_preview_fonts.py +143 -0
  99. simcord-2.2.0/tests/integration/test_preview_security.py +208 -0
  100. {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_settlement_contract.py +273 -0
  101. simcord-2.2.0/tests/integration/test_time.py +189 -0
  102. simcord-2.2.0/tests/unit/test_dpy_internals.py +543 -0
  103. simcord-2.2.0/tests/unit/test_lazy_import.py +33 -0
  104. simcord-2.2.0/tests/unit/test_router.py +97 -0
  105. {simcord-2.0.2 → simcord-2.2.0}/uv.lock +402 -2
  106. simcord-2.0.2/.github/workflows/ci.yml +0 -59
  107. simcord-2.0.2/examples/README.md +0 -29
  108. simcord-2.0.2/tests/integration/test_time.py +0 -38
  109. simcord-2.0.2/tests/unit/test_router.py +0 -39
  110. {simcord-2.0.2 → simcord-2.2.0}/.github/ISSUE_TEMPLATE/bug.md +0 -0
  111. {simcord-2.0.2 → simcord-2.2.0}/.github/ISSUE_TEMPLATE/feature.md +0 -0
  112. {simcord-2.0.2 → simcord-2.2.0}/.github/ISSUE_TEMPLATE/parity-gap.md +0 -0
  113. {simcord-2.0.2 → simcord-2.2.0}/.github/dependabot.yml +0 -0
  114. {simcord-2.0.2 → simcord-2.2.0}/.github/workflows/dpy-master.yml +0 -0
  115. {simcord-2.0.2 → simcord-2.2.0}/.pre-commit-config.yaml +0 -0
  116. {simcord-2.0.2 → simcord-2.2.0}/.python-version +0 -0
  117. {simcord-2.0.2 → simcord-2.2.0}/.readthedocs.yaml +0 -0
  118. {simcord-2.0.2 → simcord-2.2.0}/CODE_OF_CONDUCT.md +0 -0
  119. {simcord-2.0.2 → simcord-2.2.0}/LICENSE +0 -0
  120. {simcord-2.0.2 → simcord-2.2.0}/SECURITY.md +0 -0
  121. {simcord-2.0.2 → simcord-2.2.0}/benchmarks/_helpers.py +0 -0
  122. {simcord-2.0.2 → simcord-2.2.0}/benchmarks/test_dispatch_benchmarks.py +0 -0
  123. {simcord-2.0.2 → simcord-2.2.0}/benchmarks/test_perf_guards.py +0 -0
  124. {simcord-2.0.2 → simcord-2.2.0}/changes/README.md +0 -0
  125. {simcord-2.0.2 → simcord-2.2.0}/docs/assets/simcord-terminal-demo.gif +0 -0
  126. {simcord-2.0.2 → simcord-2.2.0}/docs/assets/simcord-transparent.webp +0 -0
  127. {simcord-2.0.2 → simcord-2.2.0}/docs/assets/social-card.png +0 -0
  128. {simcord-2.0.2 → simcord-2.2.0}/docs/changelog.md +0 -0
  129. {simcord-2.0.2 → simcord-2.2.0}/docs/guides/fixtures.md +0 -0
  130. {simcord-2.0.2 → simcord-2.2.0}/docs/guides/intents.md +0 -0
  131. {simcord-2.0.2 → simcord-2.2.0}/docs/guides/interactions.md +0 -0
  132. {simcord-2.0.2 → simcord-2.2.0}/docs/guides/messages.md +0 -0
  133. {simcord-2.0.2 → simcord-2.2.0}/docs/guides/mocks-vs-simulation.md +0 -0
  134. {simcord-2.0.2 → simcord-2.2.0}/docs/guides/moderation-and-guild.md +0 -0
  135. {simcord-2.0.2 → simcord-2.2.0}/docs/guides/permissions.md +0 -0
  136. {simcord-2.0.2 → simcord-2.2.0}/docs/guides/test-without-token.md +0 -0
  137. {simcord-2.0.2 → simcord-2.2.0}/docs/guides/testing-discord-py-bots.md +0 -0
  138. {simcord-2.0.2 → simcord-2.2.0}/docs/guides/testing-slash-commands.md +0 -0
  139. {simcord-2.0.2 → simcord-2.2.0}/docs/guides/threads-reactions-dms.md +0 -0
  140. {simcord-2.0.2 → simcord-2.2.0}/docs/performance.md +0 -0
  141. {simcord-2.0.2 → simcord-2.2.0}/docs/robots.txt +0 -0
  142. {simcord-2.0.2 → simcord-2.2.0}/examples/__init__.py +0 -0
  143. {simcord-2.0.2 → simcord-2.2.0}/examples/conftest.py +0 -0
  144. {simcord-2.0.2 → simcord-2.2.0}/overrides/main.html +0 -0
  145. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/asserts.py +0 -0
  146. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/__init__.py +0 -0
  147. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/cdn.py +0 -0
  148. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/errors.py +0 -0
  149. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/models/__init__.py +0 -0
  150. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/models/auditlog.py +0 -0
  151. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/models/automod.py +0 -0
  152. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/models/channel.py +0 -0
  153. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/models/expression.py +0 -0
  154. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/models/guild.py +0 -0
  155. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/models/invite.py +0 -0
  156. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/models/member.py +0 -0
  157. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/models/message.py +0 -0
  158. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/models/role.py +0 -0
  159. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/models/scheduled_event.py +0 -0
  160. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/models/stage_instance.py +0 -0
  161. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/models/voice.py +0 -0
  162. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/models/webhook.py +0 -0
  163. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/ops/__init__.py +0 -0
  164. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/ops/audit.py +0 -0
  165. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/ops/channels.py +0 -0
  166. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/ops/commands.py +0 -0
  167. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/ops/expressions.py +0 -0
  168. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/ops/guilds.py +0 -0
  169. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/ops/invites.py +0 -0
  170. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/ops/messages.py +0 -0
  171. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/ops/permissions.py +0 -0
  172. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/ops/polls.py +0 -0
  173. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/ops/reactions.py +0 -0
  174. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/ops/scheduled_events.py +0 -0
  175. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/ops/voice.py +0 -0
  176. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/ops/webhooks.py +0 -0
  177. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/permissions.py +0 -0
  178. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/state.py +0 -0
  179. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/components.py +0 -0
  180. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/enums.py +0 -0
  181. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/gateway.py +0 -0
  182. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/http/__init__.py +0 -0
  183. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/http/_helpers.py +0 -0
  184. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/http/client.py +0 -0
  185. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/http/routes/__init__.py +0 -0
  186. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/http/routes/application.py +0 -0
  187. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/http/routes/auditlogs.py +0 -0
  188. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/http/routes/automod.py +0 -0
  189. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/http/routes/channels.py +0 -0
  190. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/http/routes/commands.py +0 -0
  191. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/http/routes/expressions.py +0 -0
  192. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/http/routes/guilds.py +0 -0
  193. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/http/routes/interactions.py +0 -0
  194. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/http/routes/invites.py +0 -0
  195. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/http/routes/messages.py +0 -0
  196. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/http/routes/reactions.py +0 -0
  197. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/http/routes/scheduled_events.py +0 -0
  198. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/http/routes/stage_instances.py +0 -0
  199. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/intents.py +0 -0
  200. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/interactions.py +0 -0
  201. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/parity.py +0 -0
  202. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/py.typed +0 -0
  203. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/pytest_plugin.py +0 -0
  204. {simcord-2.0.2 → simcord-2.2.0}/src/simcord/results.py +0 -0
  205. {simcord-2.0.2 → simcord-2.2.0}/tests/conformance/_world.py +0 -0
  206. {simcord-2.0.2 → simcord-2.2.0}/tests/conformance/test_field_honesty.py +0 -0
  207. {simcord-2.0.2 → simcord-2.2.0}/tests/conformance/test_field_honesty_fuzz.py +0 -0
  208. {simcord-2.0.2 → simcord-2.2.0}/tests/conformance/test_route_coverage.py +0 -0
  209. {simcord-2.0.2 → simcord-2.2.0}/tests/conformance/test_serializers.py +0 -0
  210. {simcord-2.0.2 → simcord-2.2.0}/tests/conftest.py +0 -0
  211. {simcord-2.0.2 → simcord-2.2.0}/tests/fixtures/__init__.py +0 -0
  212. {simcord-2.0.2 → simcord-2.2.0}/tests/fixtures/sample_bot/__init__.py +0 -0
  213. {simcord-2.0.2 → simcord-2.2.0}/tests/fixtures/sample_bot/events.py +0 -0
  214. {simcord-2.0.2 → simcord-2.2.0}/tests/fixtures/sample_bot/general.py +0 -0
  215. {simcord-2.0.2 → simcord-2.2.0}/tests/fixtures/sample_bot/interactions.py +0 -0
  216. {simcord-2.0.2 → simcord-2.2.0}/tests/fixtures/sample_bot/moderation.py +0 -0
  217. {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_application_emojis.py +0 -0
  218. {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_asserts.py +0 -0
  219. {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_audit_logs.py +0 -0
  220. {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_automod.py +0 -0
  221. {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_channels.py +0 -0
  222. {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_command_permissions.py +0 -0
  223. {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_common_bot_sweep.py +0 -0
  224. {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_components_v2_interactions.py +0 -0
  225. {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_components_v2_messages.py +0 -0
  226. {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_coverage_sweep.py +0 -0
  227. {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_edge_cases.py +0 -0
  228. {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_error_capture_and_edits.py +0 -0
  229. {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_expressions.py +0 -0
  230. {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_forum.py +0 -0
  231. {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_gateway_and_automod_exempt.py +0 -0
  232. {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_guilds.py +0 -0
  233. {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_intents.py +0 -0
  234. {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_interaction_followups.py +0 -0
  235. {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_interactions.py +0 -0
  236. {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_invites.py +0 -0
  237. {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_members.py +0 -0
  238. {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_message_routes.py +0 -0
  239. {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_messages.py +0 -0
  240. {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_polls.py +0 -0
  241. {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_reactions.py +0 -0
  242. {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_roles_and_ordering.py +0 -0
  243. {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_scheduled_events.py +0 -0
  244. {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_setup_hook_suspension.py +0 -0
  245. {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_sharding.py +0 -0
  246. {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_stage_instances.py +0 -0
  247. {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_threads.py +0 -0
  248. {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_transport_edges.py +0 -0
  249. {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_voice_state.py +0 -0
  250. {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_world_helpers.py +0 -0
  251. {simcord-2.0.2 → simcord-2.2.0}/tests/unit/test_assert_helpers.py +0 -0
  252. {simcord-2.0.2 → simcord-2.2.0}/tests/unit/test_backend_lookup_errors.py +0 -0
  253. {simcord-2.0.2 → simcord-2.2.0}/tests/unit/test_components_v2_validation.py +0 -0
  254. {simcord-2.0.2 → simcord-2.2.0}/tests/unit/test_errors.py +0 -0
  255. {simcord-2.0.2 → simcord-2.2.0}/tests/unit/test_intent_rules.py +0 -0
  256. {simcord-2.0.2 → simcord-2.2.0}/tests/unit/test_parity.py +0 -0
  257. {simcord-2.0.2 → simcord-2.2.0}/tests/unit/test_parity_tooling.py +0 -0
  258. {simcord-2.0.2 → simcord-2.2.0}/tests/unit/test_permission_validators.py +0 -0
  259. {simcord-2.0.2 → simcord-2.2.0}/tests/unit/test_permissions.py +0 -0
  260. {simcord-2.0.2 → simcord-2.2.0}/tests/unit/test_pytest_plugin.py +0 -0
  261. {simcord-2.0.2 → simcord-2.2.0}/tests/unit/test_version.py +0 -0
@@ -0,0 +1,13 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [master]
6
+ pull_request:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ verify:
13
+ uses: ./.github/workflows/verify.yml
@@ -32,54 +32,12 @@ jobs:
32
32
  print(f"Releasing simcord {project_version}")
33
33
  PY
34
34
 
35
- lint:
36
- needs: version
37
- runs-on: ubuntu-latest
38
- steps:
39
- - uses: actions/checkout@v7
40
- - uses: astral-sh/setup-uv@v7
41
- - run: uv sync --extra dev
42
- - run: uv run ruff check src tests examples benchmarks scripts
43
- - run: uv run ruff format --check src tests examples benchmarks scripts
44
- - run: uv run pyright src
45
-
46
- test:
47
- needs: version
48
- runs-on: ubuntu-latest
49
- strategy:
50
- fail-fast: false
51
- matrix:
52
- python-version: ["3.11", "3.12", "3.13", "3.14"]
53
- steps:
54
- - uses: actions/checkout@v7
55
- - uses: astral-sh/setup-uv@v7
56
- with:
57
- python-version: ${{ matrix.python-version }}
58
- - run: uv sync --extra dev
59
- - run: uv run coverage run -m pytest tests examples -q
60
- - run: uv run coverage report
61
-
62
- docs:
63
- needs: version
64
- runs-on: ubuntu-latest
65
- steps:
66
- - uses: actions/checkout@v7
67
- - uses: astral-sh/setup-uv@v7
68
- - run: uv sync --extra docs
69
- - run: uv run mkdocs build --strict
70
- - run: uv run python scripts/check_discoverability.py site
71
-
72
- benchmark:
73
- needs: version
74
- runs-on: ubuntu-latest
75
- steps:
76
- - uses: actions/checkout@v7
77
- - uses: astral-sh/setup-uv@v7
78
- - run: uv sync --extra bench
79
- - run: uv run pytest benchmarks
35
+ verify:
36
+ needs: [version]
37
+ uses: ./.github/workflows/verify.yml
80
38
 
81
39
  build:
82
- needs: [version, lint, test, docs, benchmark]
40
+ needs: [version, verify]
83
41
  runs-on: ubuntu-latest
84
42
  steps:
85
43
  - uses: actions/checkout@v7
@@ -0,0 +1,99 @@
1
+ name: Verify
2
+
3
+ on:
4
+ workflow_call:
5
+
6
+ permissions:
7
+ contents: read
8
+
9
+ jobs:
10
+ lint:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v7
14
+ - uses: astral-sh/setup-uv@v7
15
+ # screenshot extra pulls in playwright/PIL/markdown-it so pyright resolves
16
+ # the lazy optional imports in simcord.preview with real type info.
17
+ - run: uv sync --extra dev --extra screenshot
18
+ - run: uv run ruff check src tests examples benchmarks scripts
19
+ - run: uv run ruff format --check src tests examples benchmarks scripts
20
+ - run: uv run pyright src
21
+
22
+ test:
23
+ runs-on: ubuntu-latest
24
+ strategy:
25
+ fail-fast: false
26
+ matrix:
27
+ python-version: ["3.11", "3.12", "3.13", "3.14"]
28
+ steps:
29
+ - uses: actions/checkout@v7
30
+ - uses: astral-sh/setup-uv@v7
31
+ with:
32
+ python-version: ${{ matrix.python-version }}
33
+ # preview extra adds markdown-it + Pillow so non-browser preview tests run;
34
+ # playwright stays absent so capture tests skip via importorskip.
35
+ - run: uv sync --extra dev --extra preview
36
+ - run: uv run coverage run -m pytest tests examples -q
37
+ - run: uv run coverage report
38
+
39
+ docs:
40
+ runs-on: ubuntu-latest
41
+ steps:
42
+ - uses: actions/checkout@v7
43
+ - uses: astral-sh/setup-uv@v7
44
+ - run: uv sync --extra docs
45
+ - run: uv run mkdocs build --strict
46
+ - run: uv run python scripts/check_discoverability.py site
47
+
48
+ preview-package:
49
+ name: Preview package + pinned Linux Chromium
50
+ runs-on: ubuntu-22.04
51
+ timeout-minutes: 20
52
+ steps:
53
+ - uses: actions/checkout@v7
54
+ - uses: astral-sh/setup-uv@v7
55
+ with:
56
+ python-version: "3.12"
57
+ - run: uv sync --extra screenshot --extra pytest
58
+ - run: uv run python -m playwright install --with-deps chromium
59
+ - run: uv run pytest tests/integration/test_preview*.py -q
60
+ - run: uv build
61
+ - run: uv run python scripts/check_package.py dist
62
+ - name: Install the wheel outside the checkout
63
+ shell: bash
64
+ run: |
65
+ set -euo pipefail
66
+ tmp="$(mktemp -d)"
67
+ mkdir -p package-smoke-artifacts
68
+ uv venv --python 3.12 "$tmp/venv"
69
+ wheel="$(printf '%s\n' "$GITHUB_WORKSPACE"/dist/*.whl)"
70
+ uv pip install --python "$tmp/venv/bin/python" \
71
+ "simcord[screenshot] @ file://$wheel"
72
+ "$tmp/venv/bin/python" -m playwright install --with-deps chromium
73
+ cp examples/bot.py examples/preview_example.py "$tmp/"
74
+ (
75
+ cd "$tmp"
76
+ "$tmp/venv/bin/python" preview_example.py > "$GITHUB_WORKSPACE/package-smoke-artifacts/preview-report.json"
77
+ )
78
+ cp "$tmp/preview-example.png" package-smoke-artifacts/
79
+ - uses: actions/upload-artifact@v7
80
+ if: failure()
81
+ with:
82
+ name: preview-package-failure
83
+ path: package-smoke-artifacts/
84
+ if-no-files-found: ignore
85
+
86
+ benchmark:
87
+ runs-on: ubuntu-latest
88
+ steps:
89
+ - uses: actions/checkout@v7
90
+ - uses: astral-sh/setup-uv@v7
91
+ # The perf guards (absolute budget + same-run scaling ratio) fail the build
92
+ # on a catastrophic regression; the JSON is uploaded as a non-gating trend.
93
+ - run: uv sync --extra bench
94
+ - run: uv run pytest benchmarks --benchmark-json=benchmark.json
95
+ - uses: actions/upload-artifact@v7
96
+ if: always()
97
+ with:
98
+ name: benchmark-results
99
+ path: benchmark.json
@@ -24,3 +24,8 @@ htmlcov/
24
24
  PLAN.md
25
25
  .claude/
26
26
  site/
27
+
28
+ docs/plans/
29
+
30
+ # Local Discord visual references (never committed or packaged)
31
+ .discord-reference-captures/
@@ -4,6 +4,38 @@ This changelog is generated with [towncrier](https://towncrier.readthedocs.io/).
4
4
 
5
5
  <!-- towncrier release notes start -->
6
6
 
7
+ ## 2.2.0 (2026-09-23)
8
+
9
+ ### Features
10
+
11
+ - Add an optional local component preview with real callback interactions, authorized viewer contexts, offline media assets, and deterministic Playwright screenshot reports. The base package remains lazy and networkless; install `simcord[preview]` for the bridge or `simcord[screenshot]` plus Chromium for capture.
12
+ - Breaking in SimCord 2.2: remove `Env.http_log`; use `Env.http_requests` and its `HttpLogEntry` records instead.
13
+ - Extend the local component preview: `env.preview(port=...)` pins the loopback port for stable capability URLs and pre-created SSH forwards, `await preview.snapshot()` returns the detached JSON projection as the structured/agent read surface, `screenshot(path=None)` captures in memory with PNG bytes on `PreviewCapture.png`, `window.simcordPreview` exposes `viewerId`/`targetId`, localhost origins are accepted, and internal bridge methods are now private by default.
14
+ - Hardened the local preview with deterministic presentation diagnostics, generation-scoped media readiness, modal focus isolation, safe page release, and strict action-envelope validation.
15
+ - Preview snapshots now publish protocol 2: `messageIndex` is picker-only summary data, `messages` maps authorized IDs to full projections, and `targetId` replaces `selected`. Component and modal controls use scoped stable control keys, mutating browser actions carry an explicit target and published revision, and `presentation_time` is deterministic and timezone-aware. Snapshot consumers must migrate; protocol 1 keys are not supported.
16
+ - Settlement now recognizes intentional timer-bounded waits — store-registered View/Modal expiry timers, `wait_for` timeouts on listener waits, `discord.ext.tasks` loop intervals, and wake-up timers scheduled inside `env.external_wait` — and makes them virtual-only: they fire only via `env.advance_time()` or their awaited input, never the wall clock, so they no longer burn real settle time or expire mid-operation. Migration: tests that relied on a View expiring on the wall clock without `advance_time` must now call `await env.advance_time(<timeout>)`.
17
+
18
+ ### Documentation
19
+
20
+ - Improve the preview demo onboarding: `examples/preview_example.py` gains a `--keep-open` flag
21
+ that keeps the session live for clicking (it calls `preview.wait_closed()`), the URL and
22
+ human-facing notes move to stderr so stdout carries only the JSON capture report, the
23
+ quickstart's next-steps list links to the preview guide, and CONTRIBUTING documents which
24
+ optional extras unlock the preview tests, example, and type-checking.
25
+
26
+ ### Miscellaneous
27
+
28
+ - Removed redundant code in the preview snapshot projection.
29
+ - The local component preview renders the Discord dark theme only: the `theme=` option and the browser theme toggle were removed. `profile.theme` still reports `"dark"` in snapshots and capture reports.
30
+
31
+
32
+ ## 2.1.0 (2026-09-19)
33
+
34
+ ### Features
35
+
36
+ - Add `Env.http_requests` records with query parameters and audit reasons while preserving the deprecated live `Env.http_log` tuple list for 2.x compatibility.
37
+
38
+
7
39
  ## 2.0.2 (2026-09-18)
8
40
 
9
41
  ### Bug fixes
@@ -19,6 +19,18 @@ uv run pyright src
19
19
  The pre-commit hooks mirror the CI `lint` job (ruff on commit, pyright on push)
20
20
  using the project's own pinned tools, so a clean commit means a green CI lint.
21
21
 
22
+ Optional extras layer on top of `dev` depending on what you work on:
23
+
24
+ | Extra | Unlocks |
25
+ | --- | --- |
26
+ | `preview` | `markdown-it-py` + `Pillow`, so the non-browser preview tests run; browser capture tests still skip via `importorskip`. |
27
+ | `screenshot` | Playwright as well — the full preview suite, `examples/preview_example.py`, and real types for pyright's check of the lazy optional imports. |
28
+
29
+ ```bash
30
+ uv sync --extra dev --extra screenshot # matches the CI lint job
31
+ uv run playwright install --with-deps chromium # needed once for browser capture tests + the example
32
+ ```
33
+
22
34
  ## Project layout
23
35
 
24
36
  - `src/simcord/backend/`: the virtual Discord with dataclass models, wire-format
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: simcord
3
- Version: 2.0.2
3
+ Version: 2.2.0
4
4
  Summary: Offline discord.py bot testing with pytest. No network, token, or test server.
5
5
  Project-URL: Homepage, https://github.com/SilentHacks/simcord
6
6
  Project-URL: Documentation, https://simcord.readthedocs.io/
@@ -30,6 +30,7 @@ Requires-Dist: pytest>=8; extra == 'bench'
30
30
  Provides-Extra: dev
31
31
  Requires-Dist: coverage[toml]>=7; extra == 'dev'
32
32
  Requires-Dist: hypothesis>=6; extra == 'dev'
33
+ Requires-Dist: jsonschema>=4; extra == 'dev'
33
34
  Requires-Dist: pre-commit>=3; extra == 'dev'
34
35
  Requires-Dist: pyright>=1.1; extra == 'dev'
35
36
  Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
@@ -40,9 +41,19 @@ Requires-Dist: towncrier>=24; extra == 'dev'
40
41
  Provides-Extra: docs
41
42
  Requires-Dist: mkdocs-material>=9; extra == 'docs'
42
43
  Requires-Dist: mkdocstrings[python]>=0.26; extra == 'docs'
44
+ Provides-Extra: preview
45
+ Requires-Dist: aiohttp>=3.9; extra == 'preview'
46
+ Requires-Dist: markdown-it-py>=3; extra == 'preview'
47
+ Requires-Dist: pillow>=10; extra == 'preview'
43
48
  Provides-Extra: pytest
49
+ Requires-Dist: jsonschema>=4; extra == 'pytest'
44
50
  Requires-Dist: pytest-asyncio>=0.24; extra == 'pytest'
45
51
  Requires-Dist: pytest>=8; extra == 'pytest'
52
+ Provides-Extra: screenshot
53
+ Requires-Dist: aiohttp>=3.9; extra == 'screenshot'
54
+ Requires-Dist: markdown-it-py>=3; extra == 'screenshot'
55
+ Requires-Dist: pillow>=10; extra == 'screenshot'
56
+ Requires-Dist: playwright>=1.45; extra == 'screenshot'
46
57
  Description-Content-Type: text/markdown
47
58
 
48
59
  <div align="center">
@@ -116,9 +127,14 @@ uv add --dev "simcord[pytest]"
116
127
  ```
117
128
 
118
129
  Requires **Python >=3.11** (tested on **3.11–3.14**) and **discord.py >=2.7.1,<3**.
119
- The locked CI matrix tests discord.py 2.7.1; a separate weekly workflow runs against
120
- upstream `master`, rather than continuously testing every released 2.x version. No
121
- dependencies beyond discord.py itself.
130
+ The locked CI matrix tests discord.py 2.7.1; a separate weekly workflow checks upstream
131
+ `master`, rather than continuously testing every released 2.x version. The base simulation
132
+ adds no runtime dependency beyond discord.py.
133
+
134
+ For an authenticated local browser page showing real component callbacks, install
135
+ `simcord[preview]`; for deterministic PNGs also run `playwright install chromium` after installing
136
+ `simcord[screenshot]`. See the [component preview and screenshots guide](https://simcord.readthedocs.io/en/latest/guides/preview/)
137
+ for lifecycle, access, limits, SSH forwarding, diagnostics, and fidelity boundaries.
122
138
 
123
139
  ## Quickstart
124
140
 
@@ -228,6 +244,7 @@ See [Core concepts](https://simcord.readthedocs.io/en/latest/concepts/) for the
228
244
  | **Polls** | `vote`, `remove_vote` | Poll answers and results. |
229
245
  | **Voice & events** | `join_voice`, `leave_voice`, `set_voice`, `subscribe_event` | Voice state, scheduled-event subscriptions. |
230
246
  | **DMs** | `send_dm` | Direct-message channels and flows. |
247
+ | **Browser preview** | `env.preview`, `preview.refresh`, `preview.screenshot` | Loopback component pages, real callbacks, authorized viewers, explicit refresh, and immutable capture diagnostics. |
231
248
 
232
249
  Responses come back as a rich [`InteractionResult`](https://simcord.readthedocs.io/en/latest/api/)
233
250
  exposing `acknowledged`, `deferred`, `ephemeral`, `response`, `followups` and `modal`. Threads,
@@ -263,7 +280,11 @@ When something goes wrong, the `env` tells you what happened:
263
280
 
264
281
  - `env.transcript()`: the ordered log of gateway events and REST calls, auto-attached to
265
282
  failing pytest tests.
266
- - `env.http_log`: every REST request the bot made, to assert on or inspect.
283
+ - `env.http_requests`: structured records for every attempted REST request (`method`, `path`,
284
+ `params`, `json`, `reason`). Values are discord.py transport arguments, not wire-normalized
285
+ strings or headers; uploaded files are not captured. JSON bodies retain their supplied
286
+ JSON-compatible shape, including top-level arrays. `params` and `json` are detached deep
287
+ snapshots, so later source-container mutations do not change a record.
267
288
  - `env.errors`: exceptions the bot swallowed.
268
289
  - `env.inject_error("POST", "/channels/*/messages", status=500)`: make matching REST calls
269
290
  fail, to test your bot's error handling.
@@ -313,6 +334,7 @@ More in the [architecture docs](https://simcord.readthedocs.io/en/latest/archite
313
334
  | 🍳 [Recipes](https://simcord.readthedocs.io/en/latest/cookbook/) | Copy-paste patterns for common cases. |
314
335
  | 📋 [Parity matrix](https://simcord.readthedocs.io/en/latest/parity-matrix/) | Exactly what's implemented. |
315
336
  | 🔖 [API reference](https://simcord.readthedocs.io/en/latest/api/) | Every public object and verb. |
337
+ | 🖥️ [Component preview](https://simcord.readthedocs.io/en/latest/guides/preview/) | Real browser callbacks, page-local viewers, offline assets, and deterministic screenshot reports. |
316
338
 
317
339
  ## Contributing
318
340
 
@@ -69,9 +69,14 @@ uv add --dev "simcord[pytest]"
69
69
  ```
70
70
 
71
71
  Requires **Python >=3.11** (tested on **3.11–3.14**) and **discord.py >=2.7.1,<3**.
72
- The locked CI matrix tests discord.py 2.7.1; a separate weekly workflow runs against
73
- upstream `master`, rather than continuously testing every released 2.x version. No
74
- dependencies beyond discord.py itself.
72
+ The locked CI matrix tests discord.py 2.7.1; a separate weekly workflow checks upstream
73
+ `master`, rather than continuously testing every released 2.x version. The base simulation
74
+ adds no runtime dependency beyond discord.py.
75
+
76
+ For an authenticated local browser page showing real component callbacks, install
77
+ `simcord[preview]`; for deterministic PNGs also run `playwright install chromium` after installing
78
+ `simcord[screenshot]`. See the [component preview and screenshots guide](https://simcord.readthedocs.io/en/latest/guides/preview/)
79
+ for lifecycle, access, limits, SSH forwarding, diagnostics, and fidelity boundaries.
75
80
 
76
81
  ## Quickstart
77
82
 
@@ -181,6 +186,7 @@ See [Core concepts](https://simcord.readthedocs.io/en/latest/concepts/) for the
181
186
  | **Polls** | `vote`, `remove_vote` | Poll answers and results. |
182
187
  | **Voice & events** | `join_voice`, `leave_voice`, `set_voice`, `subscribe_event` | Voice state, scheduled-event subscriptions. |
183
188
  | **DMs** | `send_dm` | Direct-message channels and flows. |
189
+ | **Browser preview** | `env.preview`, `preview.refresh`, `preview.screenshot` | Loopback component pages, real callbacks, authorized viewers, explicit refresh, and immutable capture diagnostics. |
184
190
 
185
191
  Responses come back as a rich [`InteractionResult`](https://simcord.readthedocs.io/en/latest/api/)
186
192
  exposing `acknowledged`, `deferred`, `ephemeral`, `response`, `followups` and `modal`. Threads,
@@ -216,7 +222,11 @@ When something goes wrong, the `env` tells you what happened:
216
222
 
217
223
  - `env.transcript()`: the ordered log of gateway events and REST calls, auto-attached to
218
224
  failing pytest tests.
219
- - `env.http_log`: every REST request the bot made, to assert on or inspect.
225
+ - `env.http_requests`: structured records for every attempted REST request (`method`, `path`,
226
+ `params`, `json`, `reason`). Values are discord.py transport arguments, not wire-normalized
227
+ strings or headers; uploaded files are not captured. JSON bodies retain their supplied
228
+ JSON-compatible shape, including top-level arrays. `params` and `json` are detached deep
229
+ snapshots, so later source-container mutations do not change a record.
220
230
  - `env.errors`: exceptions the bot swallowed.
221
231
  - `env.inject_error("POST", "/channels/*/messages", status=500)`: make matching REST calls
222
232
  fail, to test your bot's error handling.
@@ -266,6 +276,7 @@ More in the [architecture docs](https://simcord.readthedocs.io/en/latest/archite
266
276
  | 🍳 [Recipes](https://simcord.readthedocs.io/en/latest/cookbook/) | Copy-paste patterns for common cases. |
267
277
  | 📋 [Parity matrix](https://simcord.readthedocs.io/en/latest/parity-matrix/) | Exactly what's implemented. |
268
278
  | 🔖 [API reference](https://simcord.readthedocs.io/en/latest/api/) | Every public object and verb. |
279
+ | 🖥️ [Component preview](https://simcord.readthedocs.io/en/latest/guides/preview/) | Real browser callbacks, page-local viewers, offline assets, and deterministic screenshot reports. |
269
280
 
270
281
  ## Contributing
271
282
 
@@ -26,6 +26,17 @@ top-level `simcord` package.
26
26
  Use `await env.external_wait(awaitable, reason="...")` for one explicitly scoped external
27
27
  input wait. Unknown waits remain active and produce a diagnostic timeout.
28
28
 
29
+
30
+ ## Request observability
31
+
32
+ ::: simcord.HttpLogEntry
33
+
34
+ `Env.http_requests` exposes these records for transport-level assertions. The `params` and
35
+ `reason` fields contain discord.py transport arguments, not wire-normalized query strings or
36
+ encoded headers; uploaded files are not captured. The `json` field preserves the supplied
37
+ JSON-compatible shape, including top-level arrays. `params` and `json` are detached deep
38
+ snapshots, so later mutations to source containers do not alter a record.
39
+
29
40
  ## Builders
30
41
 
31
42
  Synchronous, omnipotent handles for arranging the virtual Discord. Returned by `env`/`guild`
@@ -46,6 +57,30 @@ The simulated human that drives your bot. Created by `guild.add_member(...)`. Se
46
57
 
47
58
  ::: simcord.MemberActor
48
59
 
60
+ ## Local preview
61
+
62
+ The optional [`Env.preview`](guides/preview.md#start-and-stop-a-session) context manager serves
63
+ viewer-authorized messages and real component callbacks from a loopback browser. It is not a
64
+ Discord connection. Install `simcord[preview]` for the bridge, or
65
+ `simcord[screenshot]` and `playwright install --with-deps chromium` for managed PNG capture.
66
+ `port=` pins the loopback origin so a pre-created forward can use the same origin on both ends;
67
+ it never makes the capability-bearing URL stable or safe to log. Keep `preview.url` secret.
68
+
69
+ ::: simcord.preview.Preview
70
+
71
+ ::: simcord.preview.Preview.screenshot
72
+ options:
73
+ heading_level: 4
74
+
75
+ ::: simcord.preview.PreviewCapture
76
+
77
+ `Preview.snapshot()` returns the detached JSON projection the bundled page renders — the structured
78
+ read surface for non-visual checks and text-only tooling. `PreviewCapture` is an immutable report.
79
+ Its `ready`, `complete`, and `calibrated` fields are independent; inspect `diagnostics`, `action`,
80
+ `profile`, and `geometry` rather than inferring success from a PNG path. When `screenshot()` is
81
+ called with `path=None`, `PreviewCapture.path` is `None` and `PreviewCapture.png` carries the PNG
82
+ bytes in memory. Internal `/api/*` payloads and DOM/CSS names are not extension APIs.
83
+
49
84
  ## Results
50
85
 
51
86
  Returned by the interaction verbs (`slash`, `context_menu`, `click`, `select`,
@@ -63,6 +63,21 @@ test ──► builders/actors ──► virtual backend (single source of truth
63
63
  discord.py's master branch to catch drift early.
64
64
  - **Deterministic snowflakes** with valid embedded timestamps, from a fixed virtual epoch.
65
65
 
66
+ ## Optional browser presentation
67
+
68
+ `Env.preview(...)` adds a local presentation layer above the same backend; it is not another
69
+ Discord transport. An `aiohttp` server binds to `127.0.0.1` only, serves packaged ES modules/CSS,
70
+ and exposes viewer-filtered snapshots and closed action envelopes. A random capability in the URL
71
+ fragment plus page context headers authorize reads and real actor callbacks. Python owns the
72
+ session and default presentation; each browser page and managed screenshot has an independent
73
+ viewer/focus/generation, so one tab cannot retarget another.
74
+
75
+ Preview polling is explicit and revision-based rather than websocket-driven. `refresh()` settles
76
+ the environment and republishes current projections; it does not make background mutations live.
77
+ Optional Pillow media work is bounded and outside Env's loop. Optional Playwright capture pins a
78
+ settled projection and reports geometry, readiness, completeness, calibration, and diagnostics.
79
+ Base imports do not allocate any preview server, worker, or browser.
80
+
66
81
  ## What it will never do
67
82
 
68
83
  Connect to Discord. There is no "integration mode"; automating a real client violates
@@ -23,8 +23,7 @@ async def test_something(simcord_env):
23
23
  ```
24
24
 
25
25
  The `Env` owns the virtual backend, tracks the bot's background tasks, captures errors,
26
- holds the virtual clock, and exposes the diagnostics (`env.http_log`, `env.transcript()`).
27
- A fresh `Env` is created for each test, so tests never leak state into each other.
26
+ holds the virtual clock, and exposes the diagnostics (`env.http_requests`, `env.transcript()`).
28
27
 
29
28
  !!! info "One environment per event loop"
30
29
  `Env` monkeypatches `loop.create_task` and `time.monotonic` while it's live so it can
@@ -95,10 +94,16 @@ assert channel.last_message.content == "Pong!" # the reply is already here
95
94
  ```
96
95
 
97
96
  Bot-owned work stays bot-owned across parking, timeout, cancellation, later actions, startup,
98
- and restart. Recognized waits — `Client.wait_for`, View/Modal completion, composed
99
- `gather`/`shield`/`wait`/`TaskGroup`, explicit `env.external_wait(...)`, and verified
100
- far-future sleeps — may remain. Unknown waits remain active and time out. Use a reasoned,
101
- scoped declaration for external input:
97
+ and restart. Recognized waits — `Client.wait_for` (including its `timeout=`), View/Modal
98
+ completion, store-registered View/Modal expiry timers, `discord.ext.tasks` loop intervals,
99
+ composed `gather`/`shield`/`wait`/`TaskGroup`, explicit `env.external_wait(...)`, and verified
100
+ far-future sleeps — may remain. A timer whose only job is resuming a recognized wait — a
101
+ `wait_for` deadline on a listener, a View/Modal expiry, a loop interval, a wake-up timer
102
+ scheduled inside `external_wait` — is virtual: it fires only via `env.advance_time()` or the
103
+ awaited input, never the wall clock. Verified far-future sleeps keep their real fallback, and
104
+ a View sent with no dispatchable items never registers (its expiry keeps real-time behavior).
105
+ Unknown waits remain active and time out. Use a reasoned, scoped declaration for external
106
+ input:
102
107
 
103
108
  ```python
104
109
  await env.external_wait(stop.wait(), reason="wait for the next message")
@@ -132,7 +137,7 @@ channel.history(viewer=mod) # ephemeral-aware: what `mod` would see
132
137
  channel.pinned_messages() # list[discord.Message]
133
138
  guild.get_ban(target) # ban record | None
134
139
  env.errors # exceptions the bot swallowed
135
- env.http_log # every REST call the bot made
140
+ env.http_requests # structured REST request records
136
141
  ```
137
142
 
138
143
  Interaction verbs return a richer [`InteractionResult`](api.md#simcord.InteractionResult) capturing
@@ -246,7 +246,11 @@ async def test_no_double_post(simcord_env):
246
246
 
247
247
  await alice.send(channel, "!announce")
248
248
 
249
- posts = [c for c in simcord_env.http_log if c[0] == "POST" and "/messages" in c[1]]
249
+ posts = [
250
+ entry
251
+ for entry in simcord_env.http_requests
252
+ if entry.method == "POST" and "/messages" in entry.path
253
+ ]
250
254
  assert len(posts) == 1
251
255
  ```
252
256
 
@@ -265,6 +269,27 @@ async def test_reaction_role(simcord_env):
265
269
  assert any(r.name == "Gamer" for r in alice.member.roles)
266
270
  ```
267
271
 
272
+ ## Preview a component message and capture a screenshot
273
+
274
+ ```python
275
+ async def test_panel_preview(simcord_env):
276
+ channel = simcord_env.create_guild().create_text_channel("roles")
277
+ alice = simcord_env.guild.add_member(simcord_env.create_user("alice"))
278
+ await alice.slash(channel, "panel")
279
+
280
+ async with simcord_env.preview(channel, viewers=[alice]) as preview:
281
+ capture = await preview.screenshot("panel.png")
282
+ assert capture.complete and capture.ready
283
+ snapshot = await preview.snapshot()
284
+ target = snapshot["messages"].get(snapshot["targetId"])
285
+ assert target is not None
286
+ ```
287
+
288
+ Reach for a preview when assertions alone aren't enough: the screenshot verifies what a user would
289
+ actually see, and `snapshot()` returns the structured projection for agent or text-only checks. It
290
+ requires the `simcord[preview]` extra (plus `simcord[screenshot]` and Chromium for `screenshot()`);
291
+ see the [preview guide](guides/preview.md) for lifecycle, viewer access, and resource limits.
292
+
268
293
  ## See also
269
294
 
270
295
  - [Messages](guides/messages.md) · [Slash commands](guides/interactions.md) ·
@@ -27,6 +27,14 @@ No. The library makes no outbound network requests and collects no usage or runt
27
27
  telemetry. Package hosts and source platforms may publish their own aggregate statistics,
28
28
  but SimCord does not transmit data from your tests.
29
29
 
30
+ ## Does the component preview connect to Discord?
31
+
32
+ No. The optional preview uses a loopback-only `aiohttp` server and an authenticated capability;
33
+ it does not need a bot token or a Discord connection. Install `simcord[preview]` for the browser
34
+ bridge, and `simcord[screenshot]` plus `playwright install chromium` for managed screenshots.
35
+ The [preview guide](guides/preview.md) documents forwarding, access checks, limits, and
36
+ incomplete/uncalibrated captures.
37
+
30
38
  ## Is this a mock library?
31
39
 
32
40
  It's more than a mock. It is a **simulator**. A mock returns canned responses; SimCord
@@ -111,6 +111,20 @@ all required repository gates before reporting completion.
111
111
  - Do not test source text, call counts, or private plumbing when behavior is observable.
112
112
  - Do not connect CI to a Discord test guild.
113
113
 
114
+ ## Use the local component preview
115
+
116
+ When a change affects a panel, modal, or other component presentation, install
117
+ `simcord[preview]` and use [Component preview and screenshots](preview.md). A browser click
118
+ dispatches the real callback; do not replace it with a DOM mock. Use `await preview.refresh()` after
119
+ Python-side mutations, wait for `window.simcordPreview.ready`, and inspect `lastAction`,
120
+ `complete`, `calibration`, and `diagnostics` separately. Prefer `await preview.snapshot()` for
121
+ assertions and text-only agents: it settles, republishes, and returns the protocol-2 projection dict
122
+ (`messages[targetId]`, `messageIndex`, `timeline`, `diagnostics`, `lastAction`, …) without a browser.
123
+ Browser tooling can read `viewerId`/`targetId` on `window.simcordPreview` alongside `ready`.
124
+ Pass `port=` to `env.preview` for a repeatable capability URL across reruns and pre-created SSH
125
+ forwards. Managed captures require `simcord[screenshot]` plus `playwright install --with-deps chromium`;
126
+ never put the capability-bearing `preview.url` in agent logs or artifacts.
127
+
114
128
  ## Machine-readable documentation
115
129
 
116
130
  SimCord publishes a curated [`llms.txt`](../llms.txt) index for agent navigation. It is an optional convenience, not a replacement for HTML documentation, `robots.txt`, sitemaps, or ordinary search indexing.
@@ -165,6 +165,13 @@ Pass a freshly built client — re-running the same instance would re-execute `s
165
165
  - [Slash commands](interactions.md) — invoking commands and the interaction lifecycle.
166
166
  - [Time control](time-control.md) — firing view timeouts and cooldowns instantly.
167
167
  - [Recipes](../cookbook.md) — a reusable paginator test, among others.
168
+
169
+ ## Inspect the result in a browser
170
+
171
+ For an authenticated local page that renders the same real callback flow, see
172
+ [Component preview and screenshots](preview.md). The preview is optional, keeps Python and browser
173
+ presentations independent, and labels stale, incomplete, and uncalibrated output explicitly.
174
+
168
175
  ## Settlement and external input
169
176
 
170
177
  View/Modal completion waits are recognized by settlement. If a component flow waits on a
@@ -107,19 +107,28 @@ The [pytest plugin attaches this to failing tests automatically](fixtures.md#the
107
107
  so a failure shows you exactly what the bot did, in order — usually enough to diagnose
108
108
  without a debugger.
109
109
 
110
- ## The HTTP log (`env.http_log`)
110
+ ## HTTP request records (`env.http_requests`)
111
111
 
112
- For fine-grained assertions, `env.http_log` is the list of every REST call the bot made, as
113
- `(method, path, json_body)` tuples:
112
+ For fine-grained assertions, `env.http_requests` is the list of every REST call the bot made,
113
+ as [`simcord.HttpLogEntry`](../api.md#simcord.HttpLogEntry) records:
114
114
 
115
115
  ```python
116
- posts = [c for c in simcord_env.http_log if c[0] == "POST" and "/messages" in c[1]]
117
- assert len(posts) == 1 # the bot sent exactly one message
118
- assert posts[0][2]["content"] == "Pong!"
116
+ posts = [
117
+ entry
118
+ for entry in simcord_env.http_requests
119
+ if entry.method == "POST" and "/messages" in entry.path
120
+ ]
121
+ assert len(posts) == 1
122
+ assert posts[0].json["content"] == "Pong!"
119
123
  ```
120
124
 
121
- This is how you assert the bot **didn't** do something (e.g. didn't double-post), or made
122
- exactly the calls you expect.
125
+ Each record exposes `method`, `path`, `params`, `json`, and `reason`. These values are the
126
+ arguments discord.py passed to its transport: query parameters are not wire-normalized
127
+ strings, and `reason` is not an encoded HTTP header value. Uploaded files are not captured.
128
+ The `json` field preserves every supplied JSON-compatible shape, including top-level arrays.
129
+ `params` and `json` are detached deep snapshots, so later mutations to source containers do
130
+ not alter a record. The request record is appended before fault injection and route handling,
131
+ so failed and unimplemented requests remain observable.
123
132
 
124
133
  ## Injecting API failures
125
134