scriptorium 0.6.1 → 0.7.2

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 (358) hide show
  1. checksums.yaml +4 -4
  2. data/assets/icons/social/reddit.png +0 -0
  3. data/assets/icons/social/x-logo.png +0 -0
  4. data/assets/imagenotfound.jpg +0 -0
  5. data/bin/sblog +84 -5
  6. data/bin/scriptorium +1 -0
  7. data/doc/anti-amnesia/20250727-054000-scriptorium-overview.md +0 -1
  8. data/doc/anti-amnesia/20250727-123000-anti-amnesia-conventions.md +0 -29
  9. data/doc/anti-amnesia/20250727-172600-cursor-rbenv-ruby-version-mystery.md +0 -19
  10. data/doc/anti-amnesia/20250727-172900-ai-cognitive-assessment-capabilities.md +1 -1
  11. data/doc/anti-amnesia/20250728-124243-aaa-syntax-clarification.md +1 -1
  12. data/doc/anti-amnesia/20250729-210000-reddit-autopost-integration-complete.md +1 -1
  13. data/doc/anti-amnesia/20250804-190500-cognitive-loop-bug.md +0 -10
  14. data/doc/anti-amnesia/20250804-190700-anti-amnesia-timestamping-fix.md +1 -4
  15. data/doc/anti-amnesia/20250901-211714-codemirror-integration-and-web-tests.md +172 -0
  16. data/doc/anti-amnesia/20250902-002402-backup-restore-system.md +126 -0
  17. data/doc/anti-amnesia/20250907-203339-backup-metadata-implementation.md +66 -0
  18. data/doc/imported/0001-elixir-conf-2014/metadata.txt +7 -0
  19. data/doc/imported/0001-elixir-conf-2014/post.html +37 -0
  20. data/doc/imported/0001-elixir-conf-2014/source.lt3 +22 -0
  21. data/doc/imported/0002-programmers-and-word-processing/metadata.txt +7 -0
  22. data/doc/imported/0002-programmers-and-word-processing/post.html +192 -0
  23. data/doc/imported/0002-programmers-and-word-processing/source.lt3 +146 -0
  24. data/doc/imported/0003-how-to-turn-your-brain-sideways/metadata.txt +7 -0
  25. data/doc/imported/0003-how-to-turn-your-brain-sideways/post.html +60 -0
  26. data/doc/imported/0003-how-to-turn-your-brain-sideways/source.lt3 +40 -0
  27. data/doc/imported/0004-upcoming-lone-star-ruby-conference/metadata.txt +7 -0
  28. data/doc/imported/0004-upcoming-lone-star-ruby-conference/post.html +42 -0
  29. data/doc/imported/0004-upcoming-lone-star-ruby-conference/source.lt3 +24 -0
  30. data/doc/imported/0005-elixir-conf-2015-announced/metadata.txt +7 -0
  31. data/doc/imported/0005-elixir-conf-2015-announced/post.html +30 -0
  32. data/doc/imported/0005-elixir-conf-2015-announced/source.lt3 +16 -0
  33. data/doc/imported/0006-ruby-for-dinosaurs/metadata.txt +7 -0
  34. data/doc/imported/0006-ruby-for-dinosaurs/post.html +43 -0
  35. data/doc/imported/0006-ruby-for-dinosaurs/source.lt3 +27 -0
  36. data/doc/imported/0007-phoenix-isnt-rails/metadata.txt +7 -0
  37. data/doc/imported/0007-phoenix-isnt-rails/post.html +116 -0
  38. data/doc/imported/0007-phoenix-isnt-rails/source.lt3 +87 -0
  39. data/doc/imported/0008-concerning-the-term-monkeypatching/metadata.txt +7 -0
  40. data/doc/imported/0008-concerning-the-term-monkeypatching/post.html +129 -0
  41. data/doc/imported/0008-concerning-the-term-monkeypatching/source.lt3 +92 -0
  42. data/doc/imported/0009-announcement-coming-soon/metadata.txt +7 -0
  43. data/doc/imported/0009-announcement-coming-soon/post.html +33 -0
  44. data/doc/imported/0009-announcement-coming-soon/source.lt3 +19 -0
  45. data/doc/imported/0010-immutable-data-ditching-the-wax-tablet/metadata.txt +7 -0
  46. data/doc/imported/0010-immutable-data-ditching-the-wax-tablet/post.html +175 -0
  47. data/doc/imported/0010-immutable-data-ditching-the-wax-tablet/source.lt3 +139 -0
  48. data/doc/imported/0011-computer-science-as-a-lost-art/metadata.txt +7 -0
  49. data/doc/imported/0011-computer-science-as-a-lost-art/post.html +139 -0
  50. data/doc/imported/0011-computer-science-as-a-lost-art/source.lt3 +104 -0
  51. data/doc/imported/0012-ruby-day-in-turin-italy/metadata.txt +7 -0
  52. data/doc/imported/0012-ruby-day-in-turin-italy/post.html +42 -0
  53. data/doc/imported/0012-ruby-day-in-turin-italy/source.lt3 +24 -0
  54. data/doc/imported/0013-rubyday-was-a-success/metadata.txt +7 -0
  55. data/doc/imported/0013-rubyday-was-a-success/post.html +44 -0
  56. data/doc/imported/0013-rubyday-was-a-success/source.lt3 +27 -0
  57. data/doc/imported/0014-working-on-the-blogging-software/metadata.txt +7 -0
  58. data/doc/imported/0014-working-on-the-blogging-software/post.html +63 -0
  59. data/doc/imported/0014-working-on-the-blogging-software/source.lt3 +41 -0
  60. data/doc/imported/0015-ok-its-not-really-a-lost-art/metadata.txt +7 -0
  61. data/doc/imported/0015-ok-its-not-really-a-lost-art/post.html +172 -0
  62. data/doc/imported/0015-ok-its-not-really-a-lost-art/source.lt3 +134 -0
  63. data/doc/imported/0016-an-in-operator-for-ruby/metadata.txt +7 -0
  64. data/doc/imported/0016-an-in-operator-for-ruby/post.html +155 -0
  65. data/doc/imported/0016-an-in-operator-for-ruby/source.lt3 +106 -0
  66. data/doc/imported/0017-the-forgotten-mathematician/metadata.txt +7 -0
  67. data/doc/imported/0017-the-forgotten-mathematician/post.html +161 -0
  68. data/doc/imported/0017-the-forgotten-mathematician/source.lt3 +119 -0
  69. data/doc/imported/0018-ruby-puns/metadata.txt +7 -0
  70. data/doc/imported/0018-ruby-puns/post.html +46 -0
  71. data/doc/imported/0018-ruby-puns/source.lt3 +28 -0
  72. data/doc/imported/0019-custom-exceptions-via-metaprogramming/metadata.txt +7 -0
  73. data/doc/imported/0019-custom-exceptions-via-metaprogramming/post.html +138 -0
  74. data/doc/imported/0019-custom-exceptions-via-metaprogramming/source.lt3 +101 -0
  75. data/doc/imported/0020-fffff/metadata.txt +7 -0
  76. data/doc/imported/0020-fffff/post.html +24 -0
  77. data/doc/imported/0020-fffff/source.lt3 +12 -0
  78. data/doc/imported/0021-trying-ror-yet-again/metadata.txt +7 -0
  79. data/doc/imported/0021-trying-ror-yet-again/post.html +26 -0
  80. data/doc/imported/0021-trying-ror-yet-again/source.lt3 +12 -0
  81. data/doc/imported/0023-doctor-sleep/metadata.txt +7 -0
  82. data/doc/imported/0023-doctor-sleep/post.html +63 -0
  83. data/doc/imported/0023-doctor-sleep/source.lt3 +44 -0
  84. data/doc/imported/0024-just-a-test/metadata.txt +7 -0
  85. data/doc/imported/0024-just-a-test/post.html +24 -0
  86. data/doc/imported/0024-just-a-test/source.lt3 +12 -0
  87. data/doc/imported/import_summary.txt +98 -0
  88. data/doc/livetext-informal-spec.txt +65 -0
  89. data/doc/myuserdoc/ch-0.lt3 +31 -0
  90. data/doc/myuserdoc/ch-1.lt3 +37 -0
  91. data/doc/myuserdoc/ch-10.lt3 +22 -0
  92. data/doc/myuserdoc/ch-2.lt3 +37 -0
  93. data/doc/myuserdoc/ch-3.lt3 +19 -0
  94. data/doc/myuserdoc/ch-4.lt3 +43 -0
  95. data/doc/myuserdoc/ch-5.lt3 +22 -0
  96. data/doc/myuserdoc/ch-6.lt3 +19 -0
  97. data/doc/myuserdoc/ch-7.lt3 +16 -0
  98. data/doc/myuserdoc/ch-8.lt3 +13 -0
  99. data/doc/myuserdoc/ch-9.lt3 +19 -0
  100. data/doc/myuserdoc/tweak.rb +18 -0
  101. data/doc/{userdoc-toc.txt → myuserdoc/userdoc-toc.txt} +27 -27
  102. data/doc/old-posts/0001-elixir-conf-2014.lt3 +24 -0
  103. data/doc/old-posts/0002-programmers-and-word-processing.lt3 +150 -0
  104. data/doc/old-posts/0003-how-to-turn-your-brain-sideways.lt3 +43 -0
  105. data/doc/old-posts/0004-upcoming-lone-star-ruby-conference.lt3 +26 -0
  106. data/doc/old-posts/0005-elixir-conf-2015-announced.lt3 +17 -0
  107. data/doc/old-posts/0006-ruby-for-dinosaurs.lt3 +30 -0
  108. data/doc/old-posts/0007-phoenix-isnt-rails.lt3 +90 -0
  109. data/doc/old-posts/0008-concerning-the-term-monkeypatching.lt3 +105 -0
  110. data/doc/old-posts/0009-announcement-coming-soon.lt3 +20 -0
  111. data/doc/old-posts/0010-immutable-data-ditching-the-wax-tablet.lt3 +142 -0
  112. data/doc/old-posts/0011-computer-science-as-a-lost-art.lt3 +117 -0
  113. data/doc/old-posts/0012-ruby-day-in-turin-italy.lt3 +26 -0
  114. data/doc/old-posts/0013-rubyday-was-a-success.lt3 +28 -0
  115. data/doc/old-posts/0014-working-on-the-blogging-software.lt3 +42 -0
  116. data/doc/old-posts/0015-ok-its-not-really-a-lost-art.lt3 +137 -0
  117. data/doc/old-posts/0016-an-in-operator-for-ruby.lt3 +142 -0
  118. data/doc/old-posts/0017-the-forgotten-mathematician.lt3 +129 -0
  119. data/doc/old-posts/0018-ruby-puns.lt3 +31 -0
  120. data/doc/old-posts/0019-custom-exceptions-via-metaprogramming.lt3 +116 -0
  121. data/doc/old-posts/0021-trying-ror-yet-again.lt3 +35 -0
  122. data/doc/old-posts/0023-doctor-sleep.lt3 +43 -0
  123. data/doc/old-posts/0024-just-a-test.lt3 +12 -0
  124. data/doc/old-posts/0025-trying-another-post.lt3 +12 -0
  125. data/doc/old-repo +1 -0
  126. data/doc/reddit_integration.md +2 -2
  127. data/doc/user.lt3 +0 -3
  128. data/lib/scriptorium/api.rb +1811 -78
  129. data/lib/scriptorium/banner_svg.rb +55 -68
  130. data/lib/scriptorium/contract.rb +3 -2
  131. data/lib/scriptorium/exceptions.rb +133 -102
  132. data/lib/scriptorium/helpers.rb +282 -82
  133. data/lib/scriptorium/post.rb +81 -17
  134. data/lib/scriptorium/reddit.rb +1 -1
  135. data/lib/scriptorium/repo.rb +478 -164
  136. data/lib/scriptorium/standard_files.rb +30 -396
  137. data/lib/scriptorium/support/common_js/clipboard.js +35 -0
  138. data/lib/scriptorium/support/common_js/content-loader.js +187 -0
  139. data/lib/scriptorium/support/common_js/navigation.js +52 -0
  140. data/lib/scriptorium/support/common_js/syntax-highlighting.js +27 -0
  141. data/lib/scriptorium/support/config/reddit_template.txt +17 -0
  142. data/{test/scriptorium-TEST-1754622690-146/views/sample → lib/scriptorium/support}/config/social.txt +1 -0
  143. data/lib/scriptorium/support/highlight/css.txt +2 -0
  144. data/lib/scriptorium/support/highlight/custom.css +119 -0
  145. data/lib/scriptorium/support/highlight/js.txt +1 -0
  146. data/lib/scriptorium/support/post_index/config.txt +15 -0
  147. data/lib/scriptorium/support/post_index/style.css +55 -0
  148. data/lib/scriptorium/support/templates/index_entry.lt3 +16 -0
  149. data/{test/scriptorium-TEST-1754622690-146/themes/standard/initial/post.lt3 → lib/scriptorium/support/templates/initial_post.lt3} +5 -5
  150. data/lib/scriptorium/support/templates/post.lt3 +104 -0
  151. data/{test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/header.txt → lib/scriptorium/support/theme/header.lt3} +1 -1
  152. data/lib/scriptorium/theme.rb +83 -70
  153. data/lib/scriptorium/version.rb +2 -2
  154. data/lib/scriptorium/view.rb +194 -149
  155. data/lib/scriptorium.rb +24 -1
  156. data/lib/skeleton.rb +4 -1
  157. data/scriptorium.gemspec +2 -1
  158. data/test/WEB_INTEGRATION_README.md +196 -0
  159. data/test/all +40 -0
  160. data/test/banner_svg/unit.rb +267 -35
  161. data/test/config/deployment.txt +5 -0
  162. data/test/integration/integration_test.rb +7 -7
  163. data/test/integration/preview_flow_test.rb +94 -0
  164. data/test/livetext_plugin_test.rb +453 -182
  165. data/test/manual/banner-tests/test01.html +82 -18
  166. data/test/manual/banner-tests/test02.html +82 -18
  167. data/test/manual/banner-tests/test03.html +82 -18
  168. data/test/manual/banner-tests/test04.html +89 -25
  169. data/test/manual/banner-tests/test05.html +89 -25
  170. data/test/manual/banner-tests/test06.html +89 -25
  171. data/test/manual/banner-tests/test07.html +89 -25
  172. data/test/manual/banner-tests/test08.html +82 -18
  173. data/test/manual/banner-tests/test09.html +82 -18
  174. data/test/manual/banner-tests/test10.html +82 -18
  175. data/test/manual/banner-tests/test11.html +82 -18
  176. data/test/manual/banner-tests/test12.html +82 -18
  177. data/test/manual/banner-tests/test13.html +82 -18
  178. data/test/manual/banner-tests/test14.html +82 -18
  179. data/test/manual/banner-tests/test15.html +82 -18
  180. data/test/manual/banner-tests/test16.html +82 -18
  181. data/test/manual/banner-tests/test17.html +82 -18
  182. data/test/manual/banner-tests/test18.html +90 -26
  183. data/test/manual/banner-tests/test19.html +90 -26
  184. data/test/manual/banner-tests/test20.html +90 -26
  185. data/test/manual/banner-tests/test21.html +90 -26
  186. data/test/manual/banner-tests/test22.html +90 -26
  187. data/test/manual/banner-tests/test23.html +90 -26
  188. data/test/manual/banner-tests/test24.html +90 -26
  189. data/test/manual/banner-tests/test25.html +89 -25
  190. data/test/manual/banner_environment.rb +15 -2
  191. data/test/manual/codemirror_demo.html +773 -0
  192. data/test/manual/create_posts_for_web.rb +114 -0
  193. data/test/manual/preview_manual_test.rb +129 -0
  194. data/test/manual/test_banner_features.rb +14 -14
  195. data/test/manual/test_banner_integration.rb +115 -0
  196. data/test/manual/test_banner_radial.rb +87 -0
  197. data/test/manual/test_syntax_highlighting.rb +60 -40
  198. data/test/support/preview_utils.rb +88 -0
  199. data/test/test_gem_assets.rb +48 -0
  200. data/test/test_helpers.rb +10 -0
  201. data/test/tui_editor_integration_test.rb +15 -15
  202. data/test/tui_integration_test.rb +687 -441
  203. data/test/unit/api.rb +757 -37
  204. data/test/unit/asset_management.rb +195 -221
  205. data/test/unit/backup_test.rb +451 -0
  206. data/test/unit/contract_test.rb +1 -23
  207. data/test/unit/core.rb +415 -61
  208. data/test/unit/deploy_config_test.rb +248 -0
  209. data/test/unit/deploy_test.rb +312 -21
  210. data/test/unit/edit_post_test.rb +168 -0
  211. data/test/unit/gem_asset_management.rb +36 -42
  212. data/test/unit/livetext_basic.rb +23 -35
  213. data/test/unit/livetext_compatibility.rb +7 -14
  214. data/test/unit/parse_cmd_test.rb +260 -0
  215. data/test/unit/{symlink_test.rb → permalink_copy_test.rb} +47 -49
  216. data/test/unit/post.rb +91 -26
  217. data/test/unit/post_index_config_test.rb +258 -0
  218. data/test/unit/post_state_helpers_test.rb +137 -0
  219. data/test/unit/read_commented_file_test.rb +8 -6
  220. data/test/unit/repo.rb +75 -54
  221. data/test/unit/social_test.rb +41 -44
  222. data/test/unit/syntax_highlighting.rb +70 -0
  223. data/test/unit/theme_management_test.rb +91 -0
  224. data/test/unit/view.rb +79 -12
  225. data/test/unit/widgets.rb +8 -8
  226. data/test/web_integration_test.rb +231 -0
  227. data/test/web_test_helper.rb +218 -0
  228. data/test/web_workflow_test.rb +527 -0
  229. data/ui/tui/bin/scriptorium +885 -415
  230. data/ui/web/app/app.rb +1398 -176
  231. data/ui/web/app/assets/livetext_mode.js +244 -0
  232. data/ui/web/app/error_helpers.rb +16 -16
  233. data/ui/web/app/views/advanced_config.erb +8 -2
  234. data/ui/web/app/views/asset_management.erb +56 -0
  235. data/ui/web/app/views/backup_management.erb +238 -0
  236. data/ui/web/app/views/config_widget.erb +232 -0
  237. data/ui/web/app/views/dashboard.erb +64 -72
  238. data/ui/web/app/views/deploy_config.erb +3 -0
  239. data/ui/web/app/views/edit_pages.erb +170 -2
  240. data/ui/web/app/views/edit_post.erb +130 -9
  241. data/ui/web/app/views/edit_theme.erb +73 -0
  242. data/ui/web/app/views/edit_theme_file.erb +74 -0
  243. data/ui/web/app/views/theme_management.erb +130 -0
  244. data/ui/web/app/views/view_dashboard.erb +666 -25
  245. data/ui/web/app/views/widgets.erb +249 -0
  246. data/ui/web/bin/scriptorium-web +35 -24
  247. data/ui/web/tmp/timing.log +17 -0
  248. data/ui/web/tmp/web_server.log +0 -5
  249. metadata +190 -116
  250. data/assets/back-icon.png +0 -0
  251. data/assets/icons/facebook.svg +0 -1
  252. data/assets/icons/github.svg +0 -1
  253. data/assets/icons/instagram.svg +0 -1
  254. data/assets/icons/reddit.svg +0 -1
  255. data/assets/icons/x.svg +0 -1
  256. data/assets/icons/youtube.svg +0 -1
  257. data/bin/scriptorium +0 -1511
  258. data/doc/anti-amnesia/20250727-060000-api-design-tui-planning.md +0 -34
  259. data/doc/anti-amnesia/20250727-061000-runeblog-tui-analysis.md +0 -50
  260. data/doc/anti-amnesia/20250727-154000-livetext-plugin-file-stats.md +0 -73
  261. data/doc/anti-amnesia/20250727-172600-unified-minitest-framework.md +0 -70
  262. data/doc/anti-amnesia/20250727-173000-widget-testing-achievement.md +0 -110
  263. data/doc/anti-amnesia/20250727-180000-post-id-num-refactoring.md +0 -73
  264. data/doc/anti-amnesia/20250728-124421-conversation-summary-concise.md +0 -124
  265. data/doc/anti-amnesia/20250729-190000-scriptorium-tui-testing-complete.md +0 -46
  266. data/doc/anti-amnesia/20250729-200000-scriptorium-tui-testing-edit-file-workflow.md +0 -97
  267. data/doc/anti-amnesia/20250729-211500-dependency-management-system.md +0 -211
  268. data/doc/anti-amnesia/20250729-213000-python-virtual-environment-setup.md +0 -141
  269. data/doc/anti-amnesia/20250729-214500-theme-management-commands.md +0 -211
  270. data/doc/anti-amnesia/20250729-215000-version-update-to-0.6.0.md +0 -134
  271. data/doc/anti-amnesia/20250729-220000-user-guide-complete.md +0 -41
  272. data/doc/anti-amnesia/20250804-213700-publishing-test-fix.md +0 -49
  273. data/doc/anti-amnesia/20250804-214400-additional-test-fixes.md +0 -46
  274. data/doc/anti-amnesia/20250804-220000-asset-function-logic-clarification.md +0 -41
  275. data/doc/anti-amnesia/20250806-202032-asset-function-logic-clarification.md +0 -41
  276. data/doc/anti-amnesia/20250813-082428-syntax-highlighting-and-navigation-improvements.md +0 -256
  277. data/lib/scriptorium/syntax_highlighter.rb +0 -234
  278. data/test/manual/deploy_symlink_demo.rb +0 -142
  279. data/test/manual/symlink_demo.rb +0 -117
  280. data/test/manual/test2.rb +0 -12
  281. data/test/manual/test_banner_from_file.rb +0 -150
  282. data/test/manual/test_banner_in_header.rb +0 -35
  283. data/test/manual/test_code_highlighting.rb +0 -68
  284. data/test/manual/test_complex_header.rb +0 -74
  285. data/test/manual/test_empty_header.rb +0 -32
  286. data/test/manual/test_radial_custom.rb +0 -58
  287. data/test/manual/test_radial_large_radius.rb +0 -52
  288. data/test/manual/test_svg_debug.rb +0 -47
  289. data/test/pages-demo/config/currentview.txt +0 -1
  290. data/test/pages-demo/views/demo/config/common.js +0 -57
  291. data/test/pages-demo/views/demo/config/footer.txt +0 -1
  292. data/test/pages-demo/views/demo/config/global-head.txt +0 -8
  293. data/test/pages-demo/views/demo/config/header.txt +0 -1
  294. data/test/pages-demo/views/demo/config/layout.txt +0 -1
  295. data/test/pages-demo/views/demo/config/left.txt +0 -1
  296. data/test/pages-demo/views/demo/config/main.txt +0 -1
  297. data/test/pages-demo/views/demo/config/right.txt +0 -1
  298. data/test/pages-demo/views/demo/config.txt +0 -3
  299. data/test/pages-demo/views/demo/output/panes/footer.html +0 -1
  300. data/test/pages-demo/views/demo/output/panes/header.html +0 -1
  301. data/test/pages-demo/views/demo/output/panes/left.html +0 -1
  302. data/test/pages-demo/views/demo/output/panes/main.html +0 -1
  303. data/test/pages-demo/views/demo/output/panes/right.html +0 -1
  304. data/test/scriptorium-TEST-1754622690-146/config/bootstrap_css.txt +0 -5
  305. data/test/scriptorium-TEST-1754622690-146/config/bootstrap_js.txt +0 -4
  306. data/test/scriptorium-TEST-1754622690-146/config/common.js +0 -57
  307. data/test/scriptorium-TEST-1754622690-146/config/currentview.txt +0 -1
  308. data/test/scriptorium-TEST-1754622690-146/config/global-head.txt +0 -9
  309. data/test/scriptorium-TEST-1754622690-146/config/last_post_num.txt +0 -1
  310. data/test/scriptorium-TEST-1754622690-146/config/os_helpers.rb +0 -4
  311. data/test/scriptorium-TEST-1754622690-146/config/widgets.txt +0 -3
  312. data/test/scriptorium-TEST-1754622690-146/posts/0001/meta.txt +0 -8
  313. data/test/scriptorium-TEST-1754622690-146/posts/0001/source.lt3 +0 -6
  314. data/test/scriptorium-TEST-1754622690-146/themes/standard/README.txt +0 -1
  315. data/test/scriptorium-TEST-1754622690-146/themes/standard/config.txt +0 -1
  316. data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/gen/text.css +0 -1
  317. data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/index.lt3 +0 -1
  318. data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/index_entry.lt3 +0 -14
  319. data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/post.lt3 +0 -13
  320. data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/widget.lt3 +0 -1
  321. data/test/scriptorium-TEST-1754622690-146/views/sample/config/bootstrap_css.txt +0 -5
  322. data/test/scriptorium-TEST-1754622690-146/views/sample/config/bootstrap_js.txt +0 -4
  323. data/test/scriptorium-TEST-1754622690-146/views/sample/config/common.js +0 -57
  324. data/test/scriptorium-TEST-1754622690-146/views/sample/config/deploy.txt +0 -5
  325. data/test/scriptorium-TEST-1754622690-146/views/sample/config/footer.txt +0 -2
  326. data/test/scriptorium-TEST-1754622690-146/views/sample/config/global-head.txt +0 -9
  327. data/test/scriptorium-TEST-1754622690-146/views/sample/config/header.txt +0 -4
  328. data/test/scriptorium-TEST-1754622690-146/views/sample/config/layout.txt +0 -5
  329. data/test/scriptorium-TEST-1754622690-146/views/sample/config/left.txt +0 -3
  330. data/test/scriptorium-TEST-1754622690-146/views/sample/config/main.txt +0 -5
  331. data/test/scriptorium-TEST-1754622690-146/views/sample/config/right.txt +0 -3
  332. data/test/scriptorium-TEST-1754622690-146/views/sample/config/status.txt +0 -7
  333. data/test/scriptorium-TEST-1754622690-146/views/sample/config.txt +0 -3
  334. data/test/scriptorium-TEST-1754622690-146/views/sample/layout/footer.html +0 -3
  335. data/test/scriptorium-TEST-1754622690-146/views/sample/layout/header.html +0 -3
  336. data/test/scriptorium-TEST-1754622690-146/views/sample/layout/left.html +0 -3
  337. data/test/scriptorium-TEST-1754622690-146/views/sample/layout/main.html +0 -3
  338. data/test/scriptorium-TEST-1754622690-146/views/sample/layout/right.html +0 -3
  339. data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/footer.html +0 -1
  340. data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/header.html +0 -1
  341. data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/left.html +0 -1
  342. data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/main.html +0 -1
  343. data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/right.html +0 -1
  344. data/ui/web/tmp/web_server.pid +0 -1
  345. /data/{test/pages-demo/views/demo/config/bootstrap_css.txt → lib/scriptorium/support/bootstrap/css.txt} +0 -0
  346. /data/{test/pages-demo/views/demo/config/bootstrap_js.txt → lib/scriptorium/support/bootstrap/js.txt} +0 -0
  347. /data/{test/scriptorium-TEST-1754622690-146/views/sample → lib/scriptorium/support}/config/reddit.txt +0 -0
  348. /data/{test/scriptorium-TEST-1754622690-146/themes/standard/layout → lib/scriptorium/support/templates}/layout.txt +0 -0
  349. /data/{test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/footer.txt → lib/scriptorium/support/theme/footer.lt3} +0 -0
  350. /data/{test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/left.txt → lib/scriptorium/support/theme/left.lt3} +0 -0
  351. /data/{test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/main.txt → lib/scriptorium/support/theme/main.lt3} +0 -0
  352. /data/{test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/right.txt → lib/scriptorium/support/theme/right.lt3} +0 -0
  353. /data/test/manual/banner-tests/{config.txt → svg.txt} +0 -0
  354. /data/test/manual/{test6.rb → test_advanced_widgets.rb} +0 -0
  355. /data/test/manual/{test1.rb → test_basic_posts.rb} +0 -0
  356. /data/test/manual/{test4.rb → test_layout_widgets.rb} +0 -0
  357. /data/test/manual/{test5.rb → test_pagination.rb} +0 -0
  358. /data/test/manual/{test3.rb → test_random_posts.rb} +0 -0
data/test/unit/view.rb CHANGED
@@ -12,7 +12,7 @@ class TestScriptoriumView < Minitest::Test
12
12
  @test_dir = "test/view_test_files"
13
13
  make_dir(@test_dir)
14
14
  # Use a unique test repo path to avoid conflicts
15
- @repo_path = "scriptorium-TEST"
15
+ @repo_path = "scriptorium-TEST-#{Time.now.to_i}-#{rand(1000)}"
16
16
  FileUtils.rm_rf(@repo_path) if Dir.exist?(@repo_path)
17
17
  @repo = Scriptorium::Repo.create(@repo_path, testmode: true)
18
18
  @view = @repo.create_view("test_view", "Test View", "A test view", theme: "standard")
@@ -34,43 +34,43 @@ class TestScriptoriumView < Minitest::Test
34
34
  end
35
35
 
36
36
  def test_002_initialize_with_nil_name
37
- assert_raises(CannotCreateViewNameNil) do
37
+ assert_raises(ViewNameNil) do
38
38
  Scriptorium::View.new(nil, "Title", "Subtitle")
39
39
  end
40
40
  end
41
41
 
42
42
  def test_003_initialize_with_empty_name
43
- assert_raises(CannotCreateViewNameEmpty) do
43
+ assert_raises(ViewNameEmpty) do
44
44
  Scriptorium::View.new("", "Title", "Subtitle")
45
45
  end
46
46
  end
47
47
 
48
48
  def test_004_initialize_with_whitespace_name
49
- assert_raises(CannotCreateViewNameEmpty) do
49
+ assert_raises(ViewNameEmpty) do
50
50
  Scriptorium::View.new(" ", "Title", "Subtitle")
51
51
  end
52
52
  end
53
53
 
54
54
  def test_005_initialize_with_invalid_name_format
55
- assert_raises(CannotCreateViewNameInvalid) do
55
+ assert_raises(ViewNameInvalid) do
56
56
  Scriptorium::View.new("invalid@name", "Title", "Subtitle")
57
57
  end
58
58
  end
59
59
 
60
60
  def test_006_initialize_with_nil_title
61
- assert_raises(CannotCreateViewTitleNil) do
61
+ assert_raises(ViewTitleNil) do
62
62
  Scriptorium::View.new("valid_name", nil, "Subtitle")
63
63
  end
64
64
  end
65
65
 
66
66
  def test_007_initialize_with_empty_title
67
- assert_raises(CannotCreateViewTitleEmpty) do
67
+ assert_raises(ViewTitleEmpty) do
68
68
  Scriptorium::View.new("valid_name", "", "Subtitle")
69
69
  end
70
70
  end
71
71
 
72
72
  def test_008_initialize_with_whitespace_title
73
- assert_raises(CannotCreateViewTitleEmpty) do
73
+ assert_raises(ViewTitleEmpty) do
74
74
  Scriptorium::View.new("valid_name", " ", "Subtitle")
75
75
  end
76
76
  end
@@ -95,25 +95,25 @@ class TestScriptoriumView < Minitest::Test
95
95
  def test_012_build_widgets_with_valid_widget
96
96
  # This test would need actual widget classes to be available
97
97
  # For now, we'll test the validation logic
98
- assert_raises(CannotBuildWidgetsArgNil) do
98
+ assert_raises(WidgetsArgNil) do
99
99
  @view.build_widgets(nil)
100
100
  end
101
101
  end
102
102
 
103
103
  def test_013_build_widgets_with_empty_arg
104
- assert_raises(CannotBuildWidgetsArgEmpty) do
104
+ assert_raises(WidgetsArgEmpty) do
105
105
  @view.build_widgets("")
106
106
  end
107
107
  end
108
108
 
109
109
  def test_014_build_widgets_with_whitespace_arg
110
- assert_raises(CannotBuildWidgetsArgEmpty) do
110
+ assert_raises(WidgetsArgEmpty) do
111
111
  @view.build_widgets(" ")
112
112
  end
113
113
  end
114
114
 
115
115
  def test_015_build_widgets_with_invalid_widget_name
116
- assert_raises(CannotBuildWidgetNameInvalid) do
116
+ assert_raises(WidgetNameInvalid) do
117
117
  @view.build_widgets("invalid@widget")
118
118
  end
119
119
  end
@@ -428,4 +428,71 @@ class TestScriptoriumView < Minitest::Test
428
428
  output_permissions = File.stat(output_file).mode & 0777
429
429
  assert output_permissions > 0, "Expected reasonable file permissions"
430
430
  end
431
+
432
+ # ========================================
433
+ # View Validation Error Tests
434
+ # ========================================
435
+
436
+ def test_036_view_target_validation_exceptions
437
+ # Test that exception classes exist
438
+ assert ViewTargetNil
439
+ assert ViewTargetEmpty
440
+ assert ViewTargetInvalid
441
+ end
442
+
443
+ def test_037_view_creation_exceptions
444
+ # Test that exception classes exist
445
+ assert CannotCreateView
446
+
447
+ # Test actual exception raising for view creation failures
448
+ # This is harder to test since we'd need to simulate file system failures
449
+ # For now, just verify the class exists
450
+ assert CannotCreateView
451
+ end
452
+
453
+ def test_038_widget_building_exceptions
454
+ # Test that exception classes exist
455
+ assert CannotBuildWidget
456
+ assert WidgetsArgNil
457
+ assert WidgetsArgEmpty
458
+ assert WidgetNameNil
459
+ assert WidgetNameInvalid
460
+
461
+ # Test actual exception raising for widget building
462
+ assert_raises(WidgetsArgNil) do
463
+ @view.build_widgets(nil)
464
+ end
465
+
466
+ assert_raises(WidgetsArgEmpty) do
467
+ @view.build_widgets("")
468
+ end
469
+
470
+ assert_raises(WidgetNameInvalid) do
471
+ @view.build_widgets("invalid@widget")
472
+ end
473
+ end
474
+
475
+ def test_039_view_target_validation_scenarios
476
+ # Test actual exception raising for view target validation
477
+ # These would need methods that actually raise these exceptions
478
+ # For now, just verify the classes exist
479
+ assert ViewTargetNil
480
+ assert ViewTargetEmpty
481
+ assert ViewTargetInvalid
482
+
483
+ # Test actual exception raising for view target validation
484
+ # We can test these by calling lookup_view with invalid targets
485
+ assert_raises(ViewTargetNil) do
486
+ @repo.lookup_view(nil)
487
+ end
488
+
489
+ assert_raises(ViewTargetEmpty) do
490
+ @repo.lookup_view("")
491
+ end
492
+
493
+ assert_raises(ViewTargetInvalid) do
494
+ @repo.lookup_view("invalid@name")
495
+ end
496
+ end
497
+
431
498
  end
data/test/unit/widgets.rb CHANGED
@@ -179,9 +179,9 @@ class TestScriptoriumWidgets < Minitest::Test
179
179
 
180
180
  def test_008_list_widget_load_data_missing_file
181
181
  # Should raise error when list.txt doesn't exist
182
- assert_raises(CannotReadFileNotFound) do
183
- TestListWidget2.new(@repo, @view)
184
- end
182
+ assert_raises(ReadFileNotFound) do
183
+ TestListWidget2.new(@repo, @view)
184
+ end
185
185
  end
186
186
 
187
187
  def test_009_list_widget_load_data_empty_file
@@ -309,9 +309,9 @@ class TestScriptoriumWidgets < Minitest::Test
309
309
 
310
310
  def test_016_links_widget_missing_list_file
311
311
  # Should raise error when list.txt doesn't exist
312
- assert_raises(CannotReadFileNotFound) do
313
- Scriptorium::Widget::Links.new(@repo, @view)
314
- end
312
+ assert_raises(ReadFileNotFound) do
313
+ Scriptorium::Widget::Links.new(@repo, @view)
314
+ end
315
315
  end
316
316
 
317
317
  # ========================================
@@ -355,13 +355,13 @@ class TestScriptoriumWidgets < Minitest::Test
355
355
  end
356
356
 
357
357
  def test_019_build_widgets_invalid_widget_name
358
- assert_raises(NameError) do
358
+ assert_raises(CannotBuildWidget) do
359
359
  @view.build_widgets("nonexistent")
360
360
  end
361
361
  end
362
362
 
363
363
  def test_020_build_widgets_with_special_characters
364
- assert_raises(CannotBuildWidgetNameInvalid) do
364
+ assert_raises(WidgetNameInvalid) do
365
365
  @view.build_widgets("invalid@widget")
366
366
  end
367
367
  end
@@ -0,0 +1,231 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'minitest/autorun'
4
+ require_relative '../lib/scriptorium'
5
+ require_relative 'web_test_helper'
6
+
7
+ class WebIntegrationTest < Minitest::Test
8
+ include Scriptorium::Helpers
9
+ include WebTestHelper
10
+
11
+ # Test repository path and web server config are now in WebTestHelper
12
+
13
+ def setup
14
+ cleanup_test_repo
15
+ # Don't create repo here - let the web app create it interactively
16
+ # Disable DBC contracts in tests
17
+ ENV['DBC_DISABLED'] = 'true'
18
+
19
+ # rbenv hack to ensure correct Ruby version
20
+ ENV['PATH'] = "#{ENV['HOME']}/.rbenv/shims:#{ENV['PATH']}"
21
+ ENV['RBENV_VERSION'] = '3.2.3'
22
+ end
23
+
24
+ def teardown
25
+ cleanup_test_repo
26
+ stop_web_server
27
+ end
28
+
29
+ # Test basic web app functionality
30
+ def test_001_basic_web_interaction
31
+ start_web_server
32
+
33
+ # Test that web app starts and responds
34
+ response = get("/status")
35
+ assert_response_success(response, "Status endpoint should work")
36
+
37
+ # Verify test mode is working
38
+ data = parse_json_response(response)
39
+ assert data, "Should have status data"
40
+ assert_nil data["current_view"], "Should not have current view (test repo empty)"
41
+ assert_equal false, data["repo_loaded"], "Should not have repo loaded (test repo empty)"
42
+
43
+ # Basic web interaction working
44
+ end
45
+
46
+ # Test view creation workflow via web
47
+ def test_002_view_creation_workflow
48
+ start_web_server
49
+
50
+ # Test view creation form
51
+ response = get("/")
52
+ assert_response_success(response, "Dashboard should load")
53
+ assert_includes_concise response, "Create Repository", "Should show create repository option"
54
+
55
+ # Test repository creation (this would be a POST request in real usage)
56
+ # For now, we'll test that the form elements exist
57
+ assert_includes_concise response, "create_repo", "Should have repository creation form"
58
+
59
+ # View creation workflow accessible
60
+ end
61
+
62
+ # Test asset management via web
63
+ def test_003_asset_management_web
64
+ start_web_server
65
+
66
+ # Setup test environment first
67
+ setup_test_environment
68
+
69
+ # Test asset management page
70
+ response = get("/asset_management")
71
+ assert_response_success(response, "Asset management page should load")
72
+ assert_includes_concise response, "Asset Management", "Should show asset management interface"
73
+
74
+ # Asset management accessible
75
+ end
76
+
77
+ # Test view configuration via web
78
+ def test_004_view_configuration_web
79
+ start_web_server
80
+
81
+ # Setup test environment first
82
+ setup_test_environment
83
+
84
+ # Test view configuration page
85
+ response = get("/advanced_config")
86
+ assert_response_success(response, "Advanced config page should load")
87
+ assert_includes_concise response, "Advanced Configuration", "Should show configuration interface"
88
+
89
+ # View configuration accessible
90
+ end
91
+
92
+ # Test deployment configuration via web
93
+ def test_005_deployment_configuration_web
94
+ start_web_server
95
+
96
+ # Setup test environment first
97
+ setup_test_environment
98
+
99
+ # Test deployment configuration page
100
+ response = get("/deploy_config")
101
+ assert_response_success(response, "Deploy config page should load")
102
+ assert_includes_concise response, "Deployment Configuration", "Should show deployment interface"
103
+
104
+ # Deployment configuration accessible
105
+ end
106
+
107
+ # Test view generation via web
108
+ def test_006_view_generation_web
109
+ start_web_server
110
+
111
+ # Setup test environment first
112
+ setup_test_environment
113
+
114
+ # Test view generation
115
+ response = post("/generate_view", { 'view_name' => 'test-view' })
116
+ assert_response_success(response, "View generation should succeed")
117
+
118
+ # Check that the generated HTML has valid JavaScript
119
+ index_file = "ui/web/scriptorium-TEST/views/test-view/output/index.html"
120
+ assert File.exist?(index_file), "Index file should exist after generation"
121
+
122
+ html_content = File.read(index_file)
123
+
124
+ # Check that load_main function is defined
125
+ assert_match /function load_main/, html_content, "load_main function should be defined"
126
+
127
+ # Check that the SVG script loads properly (functional test)
128
+ assert_match /console\.log\('SVG script loaded'\)/, html_content,
129
+ "Should have SVG script loaded message"
130
+ end
131
+
132
+ # Test generated HTML has clickable posts
133
+ def test_007_generated_html_has_clickable_posts
134
+ start_web_server
135
+
136
+ # Setup test environment first
137
+ setup_test_environment
138
+
139
+ # Create a test post
140
+ post("/create_post", {
141
+ title: "Test Post",
142
+ body: "This is a test post body",
143
+ views: "test-view",
144
+ blurb: "Test post blurb"
145
+ })
146
+
147
+ # Generate the view first
148
+ post("/generate_view", { 'view_name' => 'test-view' })
149
+
150
+ # Check the generated HTML
151
+ index_file = "ui/web/scriptorium-TEST/views/test-view/output/index.html"
152
+ html_content = File.read(index_file)
153
+
154
+ # Should have onclick handlers for posts
155
+ assert_match /onclick="load_main\('index\.html\?post=/, html_content,
156
+ "Should have onclick handlers for posts"
157
+
158
+ # Should have post files
159
+ posts_dir = "ui/web/scriptorium-TEST/views/test-view/output/posts"
160
+ assert Dir.exist?(posts_dir), "Posts directory should exist"
161
+
162
+ post_files = Dir.glob("#{posts_dir}/*.html")
163
+ assert post_files.length > 0, "Should have generated post files"
164
+ end
165
+
166
+ def test_008_collapsible_create_post_form
167
+ start_web_server
168
+ setup_test_environment
169
+
170
+ # Get the view dashboard
171
+ response = get("/view/test-view")
172
+ assert_response_success(response, "View dashboard should load")
173
+
174
+ # Check that the form is hidden by default
175
+ assert_match(/id="createPostSection".*style="[^"]*display: none[^"]*"/, response.body, "Create post form should be hidden by default")
176
+
177
+ # Check that the Create Post button exists
178
+ assert_match(/id="createPostButton".*Create Post/, response.body, "Create Post button should exist")
179
+
180
+ # Check that the form has Edit and Cancel buttons
181
+ assert_match(/button.*Edit/, response.body, "Form should have Edit button")
182
+ assert_match(/button.*Cancel/, response.body, "Form should have Cancel button")
183
+
184
+ # Check that view checkboxes exist
185
+ assert_match(/name="views\[\]"/, response.body, "Form should have view checkboxes")
186
+ end
187
+
188
+ def test_009_create_post_with_modal_redirect
189
+ start_web_server
190
+ setup_test_environment
191
+
192
+ # Create a post with the new form
193
+ response = post("/create_post", {
194
+ title: "Modal Test Post",
195
+ blurb: "Test post for modal",
196
+ tags: "test, modal",
197
+ views: ["test-view"]
198
+ })
199
+
200
+ # Should redirect to view with edit_post parameter
201
+ assert_response_redirect(response, "Should redirect after creating post")
202
+ assert_match(/edit_post=/, response['Location'], "Should redirect with edit_post parameter")
203
+ assert_match(/view\/test-view/, response['Location'], "Should redirect to view dashboard")
204
+ end
205
+
206
+ def test_010_post_content_api_endpoint
207
+ start_web_server
208
+ setup_test_environment
209
+
210
+ # Create a post first
211
+ create_response = post("/create_post", {
212
+ title: "API Test Post",
213
+ blurb: "Test post for API",
214
+ views: ["test-view"]
215
+ })
216
+
217
+ # Extract post ID from redirect
218
+ redirect_location = create_response['Location']
219
+ post_id = redirect_location.match(/edit_post=(\d+)/)[1]
220
+
221
+ # Test the API endpoint
222
+ api_response = get("/api/post_content/#{post_id}")
223
+ assert_response_success(api_response, "API endpoint should work")
224
+
225
+ # Should return the post content
226
+ assert_match(/API Test Post/, api_response.body, "API should return post content")
227
+ end
228
+
229
+ private
230
+ # All helper methods are now in WebTestHelper
231
+ end
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'net/http'
4
+ require 'json'
5
+ require 'fileutils'
6
+
7
+ module WebTestHelper
8
+ # Web server configuration
9
+ WEB_SERVER_PORT = 4567
10
+ WEB_SERVER_URL = "http://localhost:#{WEB_SERVER_PORT}"
11
+
12
+ # Test repository path
13
+ TEST_REPO_PATH = "ui/web/scriptorium-TEST"
14
+
15
+ # Start web server in test mode
16
+ def start_web_server
17
+ # Check if server is already running
18
+ return if server_running?
19
+
20
+ # Kill any existing server on the port first
21
+ system("lsof -ti:#{WEB_SERVER_PORT} | xargs kill -9 2>/dev/null")
22
+ sleep(1) # Give it time to release the port
23
+
24
+ # Start the server
25
+ system("rbenv exec ruby ui/web/bin/scriptorium-web start --test")
26
+ wait_for_server
27
+ end
28
+
29
+ # Check if server is already running
30
+ def server_running?
31
+ begin
32
+ response = Net::HTTP.get_response(URI("#{WEB_SERVER_URL}/status"))
33
+ response.code == "200"
34
+ rescue
35
+ false
36
+ end
37
+ end
38
+
39
+ # Stop web server
40
+ def stop_web_server
41
+ system("rbenv exec ruby ui/web/bin/scriptorium-web stop")
42
+ end
43
+
44
+ # Wait for server to be ready
45
+ def wait_for_server(timeout = 15)
46
+ start_time = Time.now
47
+ last_error = nil
48
+
49
+ while Time.now - start_time < timeout
50
+ begin
51
+ response = Net::HTTP.get_response(URI("#{WEB_SERVER_URL}/status"))
52
+ if response.code == "200"
53
+ return true
54
+ end
55
+ rescue => e
56
+ last_error = e
57
+ # Server not ready yet
58
+ end
59
+ sleep 0.5
60
+ end
61
+
62
+ # If we get here, the server didn't start
63
+ error_msg = "Web server did not start within #{timeout} seconds"
64
+ if last_error
65
+ error_msg += " (last error: #{last_error.message})"
66
+ end
67
+ flunk error_msg
68
+ end
69
+
70
+ # Make GET request
71
+ def get(path, params = {})
72
+ uri = URI("#{WEB_SERVER_URL}#{path}")
73
+ uri.query = URI.encode_www_form(params) unless params.empty?
74
+
75
+ response = Net::HTTP.get_response(uri)
76
+ response
77
+ rescue => e
78
+ flunk "GET request failed: #{e.message}"
79
+ end
80
+
81
+ # Make POST request
82
+ def post(path, data = {})
83
+ uri = URI("#{WEB_SERVER_URL}#{path}")
84
+
85
+ http = Net::HTTP.new(uri.host, uri.port)
86
+ request = Net::HTTP::Post.new(uri)
87
+ request.set_form_data(data)
88
+
89
+ response = http.request(request)
90
+ response
91
+ rescue => e
92
+ flunk "POST request failed: #{e.message}"
93
+ end
94
+
95
+ # Make PUT request
96
+ def put(path, data = {})
97
+ uri = URI("#{WEB_SERVER_URL}#{path}")
98
+
99
+ http = Net::HTTP.new(uri.host, uri.port)
100
+ request = Net::HTTP::Put.new(uri)
101
+ request.set_form_data(data)
102
+
103
+ response = http.request(request)
104
+ response
105
+ rescue => e
106
+ flunk "PUT request failed: #{e.message}"
107
+ end
108
+
109
+ # Make DELETE request
110
+ def delete(path)
111
+ uri = URI("#{WEB_SERVER_URL}#{path}")
112
+
113
+ http = Net::HTTP.new(uri.host, uri.port)
114
+ request = Net::HTTP::Delete.new(uri)
115
+
116
+ response = http.request(request)
117
+ response
118
+ rescue => e
119
+ flunk "DELETE request failed: #{e.message}"
120
+ end
121
+
122
+ # Assert response is successful (200)
123
+ def assert_response_success(response, description)
124
+ assert response, "Response should exist"
125
+ assert_equal "200", response.code, "#{description}: Expected 200, got #{response.code}"
126
+ end
127
+
128
+ # Assert response is redirect (302 or 303)
129
+ def assert_response_redirect(response, description)
130
+ assert response, "Response should exist"
131
+ assert ["302", "303"].include?(response.code), "#{description}: Expected 302 or 303, got #{response.code}"
132
+ end
133
+
134
+ # Assert response is not found (404)
135
+ def assert_response_not_found(response, description)
136
+ assert response, "Response should exist"
137
+ assert_equal "404", response.code, "#{description}: Expected 404, got #{response.code}"
138
+ end
139
+
140
+ # Assert that response body includes text, with concise error message
141
+ def assert_includes_concise(response, text, description)
142
+ unless response.body.include?(text)
143
+ # Show response size and a snippet for debugging
144
+ size = response.body.size
145
+ snippet = response.body[0, 300].gsub(/\s+/, ' ').strip
146
+ flunk "#{description} - Expected to find '#{text}' in response (#{size} bytes). Response snippet: #{snippet}..."
147
+ end
148
+ end
149
+
150
+ # Assert that a string includes text, with concise error message
151
+ def assert_includes_concise_string(content, text, description)
152
+ unless content.include?(text)
153
+ # Show content size and a snippet for debugging
154
+ size = content.size
155
+ snippet = content[0, 300].gsub(/\s+/, ' ').strip
156
+ flunk "#{description} - Expected to find '#{text}' in content (#{size} bytes). Content snippet: #{snippet}..."
157
+ end
158
+ end
159
+
160
+ # Parse JSON response
161
+ def parse_json_response(response)
162
+ return nil unless response.body
163
+ JSON.parse(response.body)
164
+ rescue JSON::ParserError => e
165
+ flunk "Failed to parse JSON response: #{e.message}"
166
+ end
167
+
168
+ # Clean up test repository
169
+ def cleanup_test_repo
170
+ FileUtils.rm_rf(TEST_REPO_PATH) if Dir.exist?(TEST_REPO_PATH)
171
+ # Also clean up backup directory for test repo
172
+ backup_dir = File.dirname(TEST_REPO_PATH) + "/backup-scriptorium-TEST"
173
+ FileUtils.rm_rf(backup_dir) if Dir.exist?(backup_dir)
174
+ end
175
+
176
+ # Setup basic test environment (repo + view)
177
+ def setup_test_environment
178
+ # Create repository
179
+ post("/create_repo", {})
180
+
181
+ # Create test view
182
+ post("/create_view", {
183
+ name: "test-view",
184
+ title: "Test View",
185
+ subtitle: "A test view for web testing"
186
+ })
187
+ end
188
+
189
+ # Wait for a specific condition with timeout
190
+ def wait_for_condition(timeout = 10, &block)
191
+ start_time = Time.now
192
+ while Time.now - start_time < timeout
193
+ if block.call
194
+ return true
195
+ end
196
+ sleep 0.5
197
+ end
198
+ flunk "Condition not met within #{timeout} seconds"
199
+ end
200
+
201
+ # Check if web app is in test mode
202
+ def assert_test_mode_active
203
+ response = get("/status")
204
+ data = parse_json_response(response)
205
+ assert data, "Should have status data"
206
+ assert_nil data["current_view"], "Should not have current view (test mode active)"
207
+ assert_equal false, data["repo_loaded"], "Should not have repo loaded (test mode active)"
208
+ end
209
+
210
+ # Verify test repository path is being used
211
+ def assert_test_repository_path
212
+ # This would require checking the web app's internal state
213
+ # For now, we verify by checking that no production data is visible
214
+ response = get("/")
215
+ assert_response_success(response, "Dashboard should load")
216
+ assert_includes_concise response, "No repository found", "Should show no repository (test mode)"
217
+ end
218
+ end