scriptorium 0.0.3 → 0.6.1

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 (292) hide show
  1. checksums.yaml +4 -4
  2. data/README.lt3 +324 -0
  3. data/README.md +3155 -1
  4. data/assets/.DS_Store +0 -0
  5. data/assets/README.md +44 -0
  6. data/assets/back-icon.png +0 -0
  7. data/assets/icons/facebook.svg +1 -0
  8. data/assets/icons/github.svg +1 -0
  9. data/assets/icons/instagram.svg +1 -0
  10. data/assets/icons/reddit.svg +1 -0
  11. data/assets/icons/ui/.DS_Store +0 -0
  12. data/assets/icons/ui/back.png +0 -0
  13. data/assets/icons/ui/copy.png +0 -0
  14. data/assets/icons/ui/down.png +0 -0
  15. data/assets/icons/ui/end.png +0 -0
  16. data/assets/icons/ui/exit.png +0 -0
  17. data/assets/icons/ui/foo +10 -0
  18. data/assets/icons/ui/home.png +0 -0
  19. data/assets/icons/ui/left.png +0 -0
  20. data/assets/icons/ui/next.png +0 -0
  21. data/assets/icons/ui/right.png +0 -0
  22. data/assets/icons/ui/start.png +0 -0
  23. data/assets/icons/ui/up.png +0 -0
  24. data/assets/icons/x.svg +1 -0
  25. data/assets/icons/youtube.svg +1 -0
  26. data/assets/samples/placeholder.svg +9 -0
  27. data/assets/themes/standard/favicon.svg +6 -0
  28. data/bin/scriptorium +1511 -0
  29. data/doc/README.txt +6 -0
  30. data/doc/anti-amnesia/20250727-054000-scriptorium-overview.md +95 -0
  31. data/doc/anti-amnesia/20250727-060000-api-design-tui-planning.md +34 -0
  32. data/doc/anti-amnesia/20250727-061000-runeblog-tui-analysis.md +50 -0
  33. data/doc/anti-amnesia/20250727-123000-anti-amnesia-conventions.md +31 -0
  34. data/doc/anti-amnesia/20250727-154000-livetext-plugin-file-stats.md +73 -0
  35. data/doc/anti-amnesia/20250727-172600-cursor-rbenv-ruby-version-mystery.md +64 -0
  36. data/doc/anti-amnesia/20250727-172600-unified-minitest-framework.md +70 -0
  37. data/doc/anti-amnesia/20250727-172900-ai-cognitive-assessment-capabilities.md +40 -0
  38. data/doc/anti-amnesia/20250727-173000-widget-testing-achievement.md +110 -0
  39. data/doc/anti-amnesia/20250727-180000-post-id-num-refactoring.md +73 -0
  40. data/doc/anti-amnesia/20250728-124243-aaa-syntax-clarification.md +46 -0
  41. data/doc/anti-amnesia/20250728-124421-conversation-summary-concise.md +124 -0
  42. data/doc/anti-amnesia/20250729-190000-scriptorium-tui-testing-complete.md +46 -0
  43. data/doc/anti-amnesia/20250729-200000-scriptorium-tui-testing-edit-file-workflow.md +97 -0
  44. data/doc/anti-amnesia/20250729-210000-reddit-autopost-integration-complete.md +158 -0
  45. data/doc/anti-amnesia/20250729-211500-dependency-management-system.md +211 -0
  46. data/doc/anti-amnesia/20250729-213000-python-virtual-environment-setup.md +141 -0
  47. data/doc/anti-amnesia/20250729-214500-theme-management-commands.md +211 -0
  48. data/doc/anti-amnesia/20250729-215000-version-update-to-0.6.0.md +134 -0
  49. data/doc/anti-amnesia/20250729-220000-user-guide-complete.md +41 -0
  50. data/doc/anti-amnesia/20250804-190500-cognitive-loop-bug.md +45 -0
  51. data/doc/anti-amnesia/20250804-190700-anti-amnesia-timestamping-fix.md +30 -0
  52. data/doc/anti-amnesia/20250804-213700-publishing-test-fix.md +49 -0
  53. data/doc/anti-amnesia/20250804-214400-additional-test-fixes.md +46 -0
  54. data/doc/anti-amnesia/20250804-220000-asset-function-logic-clarification.md +41 -0
  55. data/doc/anti-amnesia/20250806-202032-asset-function-logic-clarification.md +41 -0
  56. data/doc/anti-amnesia/20250807-213025.md +116 -0
  57. data/doc/anti-amnesia/20250813-082428-syntax-highlighting-and-navigation-improvements.md +256 -0
  58. data/doc/banner_svg_config.md +114 -0
  59. data/doc/contrib.lt3 +8 -0
  60. data/doc/dependencies.md +281 -0
  61. data/doc/hacker.lt3 +5 -0
  62. data/doc/reddit_credentials_template.json +8 -0
  63. data/doc/reddit_integration.md +207 -0
  64. data/doc/user.lt3 +38 -0
  65. data/doc/user_guide_section_1.md +137 -0
  66. data/doc/user_guide_section_10.md +515 -0
  67. data/doc/user_guide_section_11.md +708 -0
  68. data/doc/user_guide_section_2.md +233 -0
  69. data/doc/user_guide_section_3.md +5 -0
  70. data/doc/user_guide_section_4.md +221 -0
  71. data/doc/user_guide_section_5.md +243 -0
  72. data/doc/user_guide_section_6.md +147 -0
  73. data/doc/user_guide_section_7.md +311 -0
  74. data/doc/user_guide_section_8.md +224 -0
  75. data/doc/user_guide_section_9.md +375 -0
  76. data/doc/userdoc-toc.txt +88 -0
  77. data/lib/rouge/lexers/livetext.rb +74 -0
  78. data/lib/scriptorium/api.rb +640 -0
  79. data/lib/scriptorium/banner_svg.rb +742 -0
  80. data/lib/scriptorium/contract.rb +33 -0
  81. data/lib/scriptorium/exceptions.rb +170 -1
  82. data/lib/scriptorium/helpers.rb +475 -0
  83. data/lib/scriptorium/post.rb +195 -0
  84. data/lib/scriptorium/reddit.rb +83 -0
  85. data/lib/scriptorium/repo.rb +624 -0
  86. data/lib/scriptorium/standard_files.rb +515 -0
  87. data/lib/scriptorium/syntax_highlighter.rb +234 -0
  88. data/lib/scriptorium/theme.rb +179 -0
  89. data/lib/scriptorium/version.rb +2 -2
  90. data/lib/scriptorium/view.rb +976 -0
  91. data/lib/scriptorium/widgets/featured_posts.rb +149 -0
  92. data/lib/scriptorium/widgets/links.rb +112 -0
  93. data/lib/scriptorium/widgets/pages.rb +133 -0
  94. data/lib/scriptorium/widgets/widget.rb +133 -0
  95. data/lib/scriptorium.rb +21 -40
  96. data/lib/skeleton.rb +8 -2
  97. data/scriptorium.gemspec +15 -4
  98. data/test/README.md +69 -0
  99. data/test/all +43 -0
  100. data/test/api_demo.rb +99 -0
  101. data/test/assets/imagenotfound.jpg +0 -0
  102. data/test/assets/images/.DS_Store +0 -0
  103. data/test/assets/images/README.md +27 -0
  104. data/test/assets/images/odd_aspect.png +0 -0
  105. data/test/assets/images/perfect.png +0 -0
  106. data/test/assets/images/small.png +0 -0
  107. data/test/assets/images/tall.png +0 -0
  108. data/test/assets/images/very_tall.png +0 -0
  109. data/test/assets/images/very_wide.png +0 -0
  110. data/test/assets/images/wide.png +0 -0
  111. data/test/assets/testbanner.jpg +0 -0
  112. data/test/banner_svg/simple_helpers.rb +13 -0
  113. data/test/banner_svg/unit.rb +768 -0
  114. data/test/ed_test.rb +204 -0
  115. data/test/integration/cursor_banner_combinations.rb +193 -0
  116. data/test/integration/cursor_banner_features.rb +374 -0
  117. data/test/integration/integration_test.rb +326 -0
  118. data/test/livetext_plugin_test.rb +229 -0
  119. data/test/manual/asset_mgmt.rb +67 -0
  120. data/test/manual/banner-tests/config.txt +3 -0
  121. data/test/manual/banner-tests/index.html +45 -0
  122. data/test/manual/banner-tests/test01.html +58 -0
  123. data/test/manual/banner-tests/test02.html +58 -0
  124. data/test/manual/banner-tests/test03.html +58 -0
  125. data/test/manual/banner-tests/test04.html +65 -0
  126. data/test/manual/banner-tests/test05.html +65 -0
  127. data/test/manual/banner-tests/test06.html +65 -0
  128. data/test/manual/banner-tests/test07.html +65 -0
  129. data/test/manual/banner-tests/test08.html +59 -0
  130. data/test/manual/banner-tests/test09.html +59 -0
  131. data/test/manual/banner-tests/test10.html +59 -0
  132. data/test/manual/banner-tests/test11.html +59 -0
  133. data/test/manual/banner-tests/test12.html +59 -0
  134. data/test/manual/banner-tests/test13.html +59 -0
  135. data/test/manual/banner-tests/test14.html +59 -0
  136. data/test/manual/banner-tests/test15.html +58 -0
  137. data/test/manual/banner-tests/test16.html +58 -0
  138. data/test/manual/banner-tests/test17.html +58 -0
  139. data/test/manual/banner-tests/test18.html +68 -0
  140. data/test/manual/banner-tests/test19.html +68 -0
  141. data/test/manual/banner-tests/test20.html +68 -0
  142. data/test/manual/banner-tests/test21.html +68 -0
  143. data/test/manual/banner-tests/test22.html +68 -0
  144. data/test/manual/banner-tests/test23.html +68 -0
  145. data/test/manual/banner-tests/test24.html +68 -0
  146. data/test/manual/banner-tests/test25.html +67 -0
  147. data/test/manual/banner_environment.rb +192 -0
  148. data/test/manual/deploy_symlink_demo.rb +142 -0
  149. data/test/manual/environment.rb +67 -0
  150. data/test/manual/make_banner.rb +153 -0
  151. data/test/manual/sample_banner_config.txt +12 -0
  152. data/test/manual/symlink_demo.rb +117 -0
  153. data/test/manual/test1.rb +47 -0
  154. data/test/manual/test2.rb +12 -0
  155. data/test/manual/test3.rb +38 -0
  156. data/test/manual/test4.rb +40 -0
  157. data/test/manual/test5.rb +24 -0
  158. data/test/manual/test6.rb +73 -0
  159. data/test/manual/test_banner_combinations.rb +120 -0
  160. data/test/manual/test_banner_features.rb +306 -0
  161. data/test/manual/test_banner_from_file.rb +150 -0
  162. data/test/manual/test_banner_in_header.rb +35 -0
  163. data/test/manual/test_code_highlighting.rb +68 -0
  164. data/test/manual/test_complex_header.rb +74 -0
  165. data/test/manual/test_empty_header.rb +32 -0
  166. data/test/manual/test_radial_custom.rb +58 -0
  167. data/test/manual/test_radial_large_radius.rb +52 -0
  168. data/test/manual/test_svg_debug.rb +47 -0
  169. data/test/manual/test_syntax_highlighting.rb +147 -0
  170. data/test/pages-demo/config/currentview.txt +1 -0
  171. data/test/pages-demo/views/demo/config/bootstrap_css.txt +5 -0
  172. data/test/pages-demo/views/demo/config/bootstrap_js.txt +4 -0
  173. data/test/pages-demo/views/demo/config/common.js +57 -0
  174. data/test/pages-demo/views/demo/config/footer.txt +1 -0
  175. data/test/pages-demo/views/demo/config/global-head.txt +8 -0
  176. data/test/pages-demo/views/demo/config/header.txt +1 -0
  177. data/test/pages-demo/views/demo/config/layout.txt +1 -0
  178. data/test/pages-demo/views/demo/config/left.txt +1 -0
  179. data/test/pages-demo/views/demo/config/main.txt +1 -0
  180. data/test/pages-demo/views/demo/config/right.txt +1 -0
  181. data/test/pages-demo/views/demo/config.txt +3 -0
  182. data/test/pages-demo/views/demo/output/panes/footer.html +1 -0
  183. data/test/pages-demo/views/demo/output/panes/header.html +1 -0
  184. data/test/pages-demo/views/demo/output/panes/left.html +1 -0
  185. data/test/pages-demo/views/demo/output/panes/main.html +1 -0
  186. data/test/pages-demo/views/demo/output/panes/right.html +1 -0
  187. data/test/rubytext/rubytext_comprehensive_test.rb +307 -0
  188. data/test/rubytext/rubytext_demo_test.rb +42 -0
  189. data/test/rubytext/rubytext_testing_guide.md +277 -0
  190. data/test/run_automated_tests.rb +45 -0
  191. data/test/scriptorium-TEST-1754622690-146/config/bootstrap_css.txt +5 -0
  192. data/test/scriptorium-TEST-1754622690-146/config/bootstrap_js.txt +4 -0
  193. data/test/scriptorium-TEST-1754622690-146/config/common.js +57 -0
  194. data/test/scriptorium-TEST-1754622690-146/config/currentview.txt +1 -0
  195. data/test/scriptorium-TEST-1754622690-146/config/global-head.txt +9 -0
  196. data/test/scriptorium-TEST-1754622690-146/config/last_post_num.txt +1 -0
  197. data/test/scriptorium-TEST-1754622690-146/config/os_helpers.rb +4 -0
  198. data/test/scriptorium-TEST-1754622690-146/config/widgets.txt +3 -0
  199. data/test/scriptorium-TEST-1754622690-146/posts/0001/meta.txt +8 -0
  200. data/test/scriptorium-TEST-1754622690-146/posts/0001/source.lt3 +6 -0
  201. data/test/scriptorium-TEST-1754622690-146/themes/standard/README.txt +1 -0
  202. data/test/scriptorium-TEST-1754622690-146/themes/standard/config.txt +1 -0
  203. data/test/scriptorium-TEST-1754622690-146/themes/standard/initial/post.lt3 +12 -0
  204. data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/footer.txt +2 -0
  205. data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/header.txt +4 -0
  206. data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/left.txt +3 -0
  207. data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/main.txt +5 -0
  208. data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/right.txt +3 -0
  209. data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/gen/text.css +1 -0
  210. data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/layout.txt +5 -0
  211. data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/index.lt3 +1 -0
  212. data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/index_entry.lt3 +14 -0
  213. data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/post.lt3 +13 -0
  214. data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/widget.lt3 +1 -0
  215. data/test/scriptorium-TEST-1754622690-146/views/sample/config/bootstrap_css.txt +5 -0
  216. data/test/scriptorium-TEST-1754622690-146/views/sample/config/bootstrap_js.txt +4 -0
  217. data/test/scriptorium-TEST-1754622690-146/views/sample/config/common.js +57 -0
  218. data/test/scriptorium-TEST-1754622690-146/views/sample/config/deploy.txt +5 -0
  219. data/test/scriptorium-TEST-1754622690-146/views/sample/config/footer.txt +2 -0
  220. data/test/scriptorium-TEST-1754622690-146/views/sample/config/global-head.txt +9 -0
  221. data/test/scriptorium-TEST-1754622690-146/views/sample/config/header.txt +4 -0
  222. data/test/scriptorium-TEST-1754622690-146/views/sample/config/layout.txt +5 -0
  223. data/test/scriptorium-TEST-1754622690-146/views/sample/config/left.txt +3 -0
  224. data/test/scriptorium-TEST-1754622690-146/views/sample/config/main.txt +5 -0
  225. data/test/scriptorium-TEST-1754622690-146/views/sample/config/reddit.txt +10 -0
  226. data/test/scriptorium-TEST-1754622690-146/views/sample/config/right.txt +3 -0
  227. data/test/scriptorium-TEST-1754622690-146/views/sample/config/social.txt +7 -0
  228. data/test/scriptorium-TEST-1754622690-146/views/sample/config/status.txt +7 -0
  229. data/test/scriptorium-TEST-1754622690-146/views/sample/config.txt +3 -0
  230. data/test/scriptorium-TEST-1754622690-146/views/sample/layout/footer.html +3 -0
  231. data/test/scriptorium-TEST-1754622690-146/views/sample/layout/header.html +3 -0
  232. data/test/scriptorium-TEST-1754622690-146/views/sample/layout/left.html +3 -0
  233. data/test/scriptorium-TEST-1754622690-146/views/sample/layout/main.html +3 -0
  234. data/test/scriptorium-TEST-1754622690-146/views/sample/layout/right.html +3 -0
  235. data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/footer.html +1 -0
  236. data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/header.html +1 -0
  237. data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/left.html +1 -0
  238. data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/main.html +1 -0
  239. data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/right.html +1 -0
  240. data/test/staging/.DS_Store +0 -0
  241. data/test/syntax_highlighting_test.lt3 +124 -0
  242. data/test/test_helpers.rb +230 -0
  243. data/test/tui_editor_integration_test.rb +296 -0
  244. data/test/tui_integration_test.rb +637 -0
  245. data/test/unit/api.rb +1056 -0
  246. data/test/unit/asset_management.rb +245 -0
  247. data/test/unit/clipboard_test.rb +60 -0
  248. data/test/unit/contract_test.rb +91 -0
  249. data/test/unit/core.rb +857 -0
  250. data/test/unit/deploy_test.rb +187 -0
  251. data/test/unit/gem_asset_management.rb +189 -0
  252. data/test/unit/livetext_basic.rb +69 -0
  253. data/test/unit/livetext_compatibility.rb +89 -0
  254. data/test/unit/post.rb +244 -0
  255. data/test/unit/read_commented_file_test.rb +276 -0
  256. data/test/unit/reddit_test.rb +235 -0
  257. data/test/unit/repo.rb +548 -0
  258. data/test/unit/social_test.rb +369 -0
  259. data/test/unit/symlink_test.rb +213 -0
  260. data/test/unit/view.rb +431 -0
  261. data/test/unit/widgets.rb +669 -0
  262. data/test/wizard_test.rb +123 -0
  263. data/ui/README.md +67 -0
  264. data/ui/common/lib/ui_common.rb +8 -0
  265. data/ui/rubytext/README.md +191 -0
  266. data/ui/rubytext/bin/scriptorium-rubytext +402 -0
  267. data/ui/rubytext/lib/rubytext_ui.rb +300 -0
  268. data/ui/tui/bin/scriptorium +1420 -0
  269. data/ui/tui/test/tui_test.rb +23 -0
  270. data/ui/web/app/app.rb +1378 -0
  271. data/ui/web/app/error_helpers.rb +150 -0
  272. data/ui/web/app/views/advanced_config.erb +190 -0
  273. data/ui/web/app/views/asset_management.erb +589 -0
  274. data/ui/web/app/views/banner_config.erb +200 -0
  275. data/ui/web/app/views/configure_view.erb +401 -0
  276. data/ui/web/app/views/dashboard.erb +162 -0
  277. data/ui/web/app/views/deploy_config.erb +146 -0
  278. data/ui/web/app/views/edit_pages.erb +195 -0
  279. data/ui/web/app/views/edit_post.erb +54 -0
  280. data/ui/web/app/views/error_page.erb +29 -0
  281. data/ui/web/app/views/header_config.erb +155 -0
  282. data/ui/web/app/views/layout_config.erb +147 -0
  283. data/ui/web/app/views/navbar_config.erb +411 -0
  284. data/ui/web/app/views/view_dashboard.erb +138 -0
  285. data/ui/web/bin/scriptorium-web +153 -0
  286. data/ui/web/test/web_basic_test.rb +38 -0
  287. data/ui/web/test_navbar.txt +7 -0
  288. data/ui/web/tmp/web_server.log +5 -0
  289. data/ui/web/tmp/web_server.pid +1 -0
  290. metadata +359 -7
  291. data/lib/scriptorium/engine.rb +0 -22
  292. data/test/engine/unit.rb +0 -44
data/test/unit/api.rb ADDED
@@ -0,0 +1,1056 @@
1
+ # test/unit/api.rb
2
+
3
+
4
+ require 'minitest/autorun'
5
+ require 'open3'
6
+ require_relative '../../lib/scriptorium'
7
+ require_relative '../test_helpers'
8
+
9
+ class TestScriptoriumAPI < Minitest::Test
10
+ include Scriptorium::Exceptions
11
+ include Scriptorium::Helpers
12
+
13
+
14
+
15
+ def teardown
16
+ FileUtils.rm_rf(@test_dir) if Dir.exist?(@test_dir)
17
+ @api = nil
18
+ end
19
+
20
+ def setup
21
+ @test_dir = "test/scriptorium-TEST"
22
+ # Clean up any existing test directory first
23
+ FileUtils.rm_rf(@test_dir) if Dir.exist?(@test_dir)
24
+ @api = Scriptorium::API.new(testmode: true)
25
+ @api.create_repo(@test_dir)
26
+ end
27
+
28
+ # Basic API functionality tests
29
+
30
+ def test_001_api_initialization
31
+ assert_instance_of Scriptorium::API, @api
32
+ assert_instance_of Scriptorium::Repo, @api.repo
33
+ refute_nil @api.current_view
34
+ assert_equal "sample", @api.current_view.name
35
+ end
36
+
37
+ def test_002_create_view
38
+ @api.create_view("test_view", "Test View", "A test view")
39
+
40
+ assert_equal "test_view", @api.current_view.name
41
+ assert_equal "Test View", @api.current_view.title
42
+ assert_equal "A test view", @api.current_view.subtitle
43
+ end
44
+
45
+ def test_003_create_post
46
+ @api.create_view("test_view", "Test View")
47
+ post = @api.create_post("Test Post", "Test body", tags: ["test"])
48
+
49
+ assert_instance_of Scriptorium::Post, post
50
+ assert_equal "Test Post", post.title
51
+ assert_equal "test", post.tags
52
+ end
53
+
54
+ def test_004_posts
55
+ @api.create_view("test_view", "Test View")
56
+ @api.create_post("Post 1", "Body 1")
57
+ @api.create_post("Post 2", "Body 2")
58
+
59
+ posts = @api.posts
60
+ assert_equal 2, posts.length
61
+ # Posts might not be in creation order, so check both exist
62
+ titles = posts.map(&:title)
63
+ assert_includes titles, "Post 1"
64
+ assert_includes titles, "Post 2"
65
+ end
66
+
67
+ def test_005_post
68
+ @api.create_view("test_view", "Test View")
69
+ created_post = @api.create_post("Test Post", "Test body")
70
+
71
+ retrieved_post = @api.post(created_post.id)
72
+ assert_equal created_post.id, retrieved_post.id
73
+ assert_equal "Test Post", retrieved_post.title
74
+ end
75
+
76
+ # New API methods tests
77
+ def test_006_views
78
+ @api.create_view("view1", "View 1")
79
+ @api.create_view("view2", "View 2")
80
+
81
+ views = @api.views.map(&:name)
82
+ assert_includes views, "view1"
83
+ assert_includes views, "view2"
84
+ end
85
+
86
+ def test_007_post_attrs
87
+ @api.create_view("test_view", "Test View")
88
+ post = @api.create_post("Test Post", "Test body", tags: ["test", "demo"])
89
+
90
+ attrs = @api.post_attrs(post.id, :title, :tags)
91
+ assert_equal ["Test Post", "test, demo"], attrs
92
+ end
93
+
94
+ def test_008_post_attrs_with_post_object
95
+ @api.create_view("test_view", "Test View")
96
+ post = @api.create_post("Test Post", "Test body", tags: ["test"])
97
+
98
+ attrs = @api.post_attrs(post, :title, :tags)
99
+ assert_equal ["Test Post", "test"], attrs
100
+ end
101
+
102
+ def test_009_views_for
103
+ @api.create_view("test_view", "Test View")
104
+ post = @api.create_post("Test Post", "Test body")
105
+
106
+ views = @api.views_for(post)
107
+ assert_equal ["test_view"], views
108
+ end
109
+
110
+ def test_010_views_for_with_id
111
+ @api.create_view("test_view", "Test View")
112
+ post = @api.create_post("Test Post", "Test body")
113
+
114
+ views = @api.views_for(post.id)
115
+ assert_equal ["test_view"], views
116
+ end
117
+
118
+ def test_011_apply_theme
119
+ @api.create_view("test_view", "Test View")
120
+
121
+ # Should not raise an error
122
+ @api.apply_theme("standard")
123
+ assert_equal "standard", @api.current_view.theme
124
+ end
125
+
126
+
127
+
128
+ # Empty methods tests (should not raise errors)
129
+ def test_012_empty_methods
130
+ assert_equal [], @api.drafts
131
+ # Widgets are now available from widgets.txt
132
+ widgets = @api.widgets_available
133
+ assert_includes widgets, "links"
134
+ assert_includes widgets, "pages"
135
+
136
+ # These should not raise errors
137
+ # @api.delete_draft("some_path") # This would fail since it's not a valid draft path
138
+ # @api.delete_post(1) # This would fail since post doesn't exist
139
+ # @api.generate_widget("some_widget") # This would fail since no current view set
140
+ @api.select_posts { |p| true }
141
+ @api.search_posts(title: "query")
142
+ # @api.unlink_post(nil, nil) # This would fail since no current view and invalid post
143
+ # @api.generate_all # This would fail since no current view set
144
+ end
145
+
146
+ def test_013_drafts
147
+ drafts = @api.drafts
148
+ assert_instance_of Array, drafts
149
+ # Should return empty array if no drafts directory exists
150
+ end
151
+
152
+ def test_014_themes_available
153
+ themes = @api.themes_available
154
+ assert_instance_of Array, themes
155
+ assert_includes themes, "standard" # Should have the standard theme
156
+ end
157
+
158
+ def test_015_widgets_available
159
+ widgets = @api.widgets_available
160
+ assert_instance_of Array, widgets
161
+ # Should return available widgets from widgets.txt
162
+ assert_includes widgets, "links"
163
+ assert_includes widgets, "pages"
164
+ end
165
+
166
+ def test_016_generate_view
167
+ @api.create_view("test_view", "Test View")
168
+
169
+ # Should not raise an error
170
+ @api.generate_view
171
+ end
172
+
173
+ def test_017_generate_view_with_specific_view
174
+ @api.create_view("view1", "View 1")
175
+ @api.create_view("view2", "View 2")
176
+
177
+ # Should not raise an error
178
+ @api.generate_view("view1")
179
+ end
180
+
181
+
182
+
183
+ # Error handling tests
184
+ def test_018_create_post_without_view
185
+ # Clear the current view directly
186
+ @api.repo.instance_variable_set(:@current_view, nil)
187
+
188
+ assert_raises(RuntimeError) do
189
+ @api.create_post("Test Post", "Test body")
190
+ end
191
+ end
192
+
193
+ def test_019_safe_delete_post
194
+ @api.create_view("test_view", "Test View")
195
+ post = @api.create_post("Test Post", "Test body")
196
+
197
+ # Initially visible
198
+ posts = @api.posts
199
+ assert_equal 1, posts.length
200
+
201
+ # Delete the post
202
+ @api.delete_post(post.id)
203
+
204
+ # Should not appear in posts list
205
+ posts = @api.posts
206
+ assert_equal 0, posts.length
207
+
208
+ # But post object still exists and can be retrieved
209
+ retrieved_post = @api.post(post.id)
210
+ assert_equal "Test Post", retrieved_post.title
211
+ end
212
+
213
+ def test_020_undelete_post
214
+ @api.create_view("test_view", "Test View")
215
+ post = @api.create_post("Test Post", "Test body")
216
+
217
+ # Delete the post
218
+ @api.delete_post(post.id)
219
+ assert_equal 0, @api.posts.length
220
+
221
+ # Undelete the post
222
+ @api.undelete_post(post.id)
223
+
224
+ # Should appear in posts list again
225
+ posts = @api.posts
226
+ assert_equal 1, posts.length
227
+ assert_equal "Test Post", posts[0].title
228
+ end
229
+
230
+ def test_021_update_post
231
+ @api.create_view("test_view", "Test View")
232
+ @api.create_view("other_view", "Other View")
233
+ post = @api.create_post("Test Post", "Test body", views: ["test_view", "other_view"])
234
+
235
+ # Update the views field
236
+ result = @api.update_post(post.id, {views: ["test_view"]})
237
+ assert result
238
+
239
+ # Check that the source file was updated
240
+ source_file = post.dir/"source.lt3"
241
+ content = read_file(source_file)
242
+ assert_includes content, ".views test_view"
243
+ assert_includes content, "# updated views"
244
+ end
245
+
246
+ def test_022_update_post_preserves_comments
247
+ @api.create_view("test_view", "Test View")
248
+ post = @api.create_post("Test Post", "Test body", views: ["test_view"])
249
+
250
+ # Manually add a comment to the source file
251
+ source_file = post.dir/"source.lt3"
252
+ lines = read_file(source_file, lines: true, chomp: false)
253
+ lines.map! do |line|
254
+ if line.strip.start_with?('.views')
255
+ ".views test_view # original comment\n"
256
+ else
257
+ line
258
+ end
259
+ end
260
+ write_file(source_file, lines.join)
261
+
262
+ # Update the views field
263
+ result = @api.update_post(post.id, {views: ["new_view"]})
264
+ assert result
265
+
266
+ # Check that original comment is preserved
267
+ content = read_file(source_file)
268
+ assert_includes content, "# original comment"
269
+ assert_includes content, "# updated views"
270
+ end
271
+
272
+ def test_023_update_post_multiple_fields
273
+ @api.create_view("test_view", "Test View")
274
+ post = @api.create_post("Test Post", "Test body", views: ["test_view"])
275
+
276
+ # Update multiple fields at once
277
+ result = @api.update_post(post.id, {
278
+ title: "Updated Title",
279
+ tags: ["new", "tags"]
280
+ })
281
+ assert result
282
+
283
+ # Check that both fields were updated
284
+ source_file = post.dir/"source.lt3"
285
+ content = read_file(source_file)
286
+ assert_includes content, ".title Updated Title"
287
+ assert_includes content, ".tags new, tags"
288
+ assert_includes content, "# updated title"
289
+ assert_includes content, "# updated tags"
290
+ end
291
+
292
+
293
+
294
+ def test_024_unlink_post
295
+ @api.create_view("test_view", "Test View")
296
+ @api.create_view("other_view", "Other View")
297
+ post = @api.create_post("Test Post", "Test body", views: ["test_view", "other_view"])
298
+
299
+ # Initially post should be in both views
300
+ assert_includes post.views, "test_view"
301
+ assert_includes post.views, "other_view"
302
+
303
+ # Unlink from current view
304
+ result = @api.unlink_post(post.id)
305
+ assert result
306
+
307
+ # Post should now only be in test_view (since we unlinked from other_view)
308
+ updated_post = @api.post(post.id)
309
+ updated_views = updated_post.views.strip.split(/\s+/)
310
+ assert_includes updated_views, "test_view"
311
+ refute_includes updated_views, "other_view"
312
+ end
313
+
314
+ def test_025_link_post
315
+ @api.create_view("test_view", "Test View")
316
+ @api.create_view("other_view", "Other View")
317
+ post = @api.create_post("Test Post", "Test body", views: ["test_view"])
318
+
319
+ # Initially post should only be in test_view
320
+ assert_includes post.views, "test_view"
321
+ refute_includes post.views, "other_view"
322
+
323
+ # Link to other_view
324
+ result = @api.link_post(post.id, "other_view")
325
+ assert result
326
+
327
+ # Post should now be in both views
328
+ updated_post = @api.post(post.id)
329
+ updated_views = updated_post.views.strip.split(/\s+/)
330
+ assert_includes updated_views, "test_view"
331
+ assert_includes updated_views, "other_view"
332
+ end
333
+
334
+ def test_026_link_post_current_view
335
+ @api.create_view("test_view", "Test View")
336
+ @api.create_view("other_view", "Other View")
337
+ post = @api.create_post("Test Post", "Test body", views: ["other_view"])
338
+
339
+ # Initially post should only be in other_view
340
+ assert_includes post.views, "other_view"
341
+ refute_includes post.views, "test_view"
342
+
343
+ # Set current view to test_view
344
+ @api.view("test_view")
345
+
346
+ # Link to current view (test_view)
347
+ result = @api.link_post(post.id)
348
+ assert result
349
+
350
+ # Post should now be in both views
351
+ updated_post = @api.post(post.id)
352
+ updated_views = updated_post.views.strip.split(/\s+/)
353
+ assert_includes updated_views, "test_view"
354
+ assert_includes updated_views, "other_view"
355
+ end
356
+
357
+ def test_027_link_post_duplicate
358
+ @api.create_view("test_view", "Test View")
359
+ post = @api.create_post("Test Post", "Test body", views: ["test_view"])
360
+
361
+ # Initially post should be in test_view
362
+ assert_includes post.views, "test_view"
363
+
364
+ # Try to link to the same view (should not add duplicate)
365
+ result = @api.link_post(post.id, "test_view")
366
+ assert result
367
+
368
+ # Post should still only be in test_view (no duplicates)
369
+ updated_post = @api.post(post.id)
370
+ updated_views = updated_post.views.strip.split(/\s+/)
371
+ assert_equal ["test_view"], updated_views
372
+ end
373
+
374
+ def test_028_post_add_view
375
+ @api.create_view("test_view", "Test View")
376
+ @api.create_view("other_view", "Other View")
377
+ post = @api.create_post("Test Post", "Test body", views: ["test_view"])
378
+
379
+ # Initially post should only be in test_view
380
+ assert_includes post.views, "test_view"
381
+ refute_includes post.views, "other_view"
382
+
383
+ # Add other_view to the post
384
+ result = @api.post_add_view(post.id, "other_view")
385
+ assert result
386
+
387
+ # Post should now be in both views
388
+ updated_post = @api.post(post.id)
389
+ updated_views = updated_post.views.strip.split(/\s+/)
390
+ assert_includes updated_views, "test_view"
391
+ assert_includes updated_views, "other_view"
392
+ end
393
+
394
+ def test_029_post_add_view_with_view_object
395
+ @api.create_view("test_view", "Test View")
396
+ @api.create_view("other_view", "Other View")
397
+ other_view = @api.view("other_view") # Get the View object
398
+ post = @api.create_post("Test Post", "Test body", views: ["test_view"])
399
+
400
+ # Add other_view to the post using View object
401
+ result = @api.post_add_view(post.id, other_view)
402
+ assert result
403
+
404
+ # Post should now be in both views
405
+ updated_post = @api.post(post.id)
406
+ updated_views = updated_post.views.strip.split(/\s+/)
407
+ assert_includes updated_views, "test_view"
408
+ assert_includes updated_views, "other_view"
409
+ end
410
+
411
+ def test_030_post_remove_view
412
+ @api.create_view("test_view", "Test View")
413
+ @api.create_view("other_view", "Other View")
414
+ post = @api.create_post("Test Post", "Test body", views: ["test_view", "other_view"])
415
+
416
+ # Initially post should be in both views
417
+ assert_includes post.views, "test_view"
418
+ assert_includes post.views, "other_view"
419
+
420
+ # Remove other_view from the post
421
+ result = @api.post_remove_view(post.id, "other_view")
422
+ assert result
423
+
424
+ # Post should now only be in test_view
425
+ updated_post = @api.post(post.id)
426
+ updated_views = updated_post.views.strip.split(/\s+/)
427
+ assert_includes updated_views, "test_view"
428
+ refute_includes updated_views, "other_view"
429
+ end
430
+
431
+ def test_031_post_remove_view_with_view_object
432
+ @api.create_view("test_view", "Test View")
433
+ @api.create_view("other_view", "Other View")
434
+ other_view = @api.view("other_view") # Get the View object
435
+ post = @api.create_post("Test Post", "Test body", views: ["test_view", "other_view"])
436
+
437
+ # Remove other_view from the post using View object
438
+ result = @api.post_remove_view(post.id, other_view)
439
+ assert result
440
+
441
+ # Post should now only be in test_view
442
+ updated_post = @api.post(post.id)
443
+ updated_views = updated_post.views.strip.split(/\s+/)
444
+ assert_includes updated_views, "test_view"
445
+ refute_includes updated_views, "other_view"
446
+ end
447
+
448
+ def test_032_update_post_blurb
449
+ @api.create_view("test_view", "Test View")
450
+ post = @api.create_post("Test Post", "Test body")
451
+
452
+ # Manually add a blurb line to the source file
453
+ source_file = post.dir/"source.lt3"
454
+ lines = read_file(source_file, lines: true, chomp: false)
455
+ lines.insert(-2, ".blurb This is just a short intro to this post.\n") # Insert before the body
456
+ write_file(source_file, lines.join)
457
+
458
+ # Update the blurb
459
+ result = @api.update_post(post.id, {blurb: "Updated blurb for this post"})
460
+ assert result
461
+
462
+ # Check that the blurb was updated
463
+ content = read_file(source_file)
464
+ assert_includes content, ".blurb Updated blurb for this post"
465
+ assert_includes content, "# updated blurb"
466
+ end
467
+
468
+ def test_033_delete_draft
469
+ @api.create_view("test_view", "Test View")
470
+
471
+ # Create a draft
472
+ draft_path = @api.draft(title: "Test Draft", body: "Test body")
473
+
474
+ # Verify draft exists
475
+ drafts = @api.drafts
476
+ assert_equal 1, drafts.length
477
+ assert_equal draft_path, drafts.first[:path]
478
+
479
+ # Delete the draft
480
+ result = @api.delete_draft(draft_path)
481
+ assert result
482
+
483
+ # Verify draft is gone
484
+ drafts = @api.drafts
485
+ assert_equal 0, drafts.length
486
+ end
487
+
488
+ def test_034_delete_draft_invalid_path
489
+ @api.create_view("test_view", "Test View")
490
+
491
+ # Test with non-draft file
492
+ assert_raises(RuntimeError) do
493
+ @api.delete_draft("not-a-draft.txt")
494
+ end
495
+
496
+ # Test with non-existent file
497
+ assert_raises(RuntimeError) do
498
+ @api.delete_draft("nonexistent-draft.lt3")
499
+ end
500
+ end
501
+
502
+ def test_035_generate_view
503
+ @api.create_view("test_view", "Test View")
504
+ @api.create_post("Test Post", "Test body")
505
+
506
+ # Should not raise an error
507
+ result = @api.generate_view
508
+ assert result
509
+ end
510
+
511
+
512
+
513
+ def test_036_generate_widget
514
+ @api.create_view("test_view", "Test View")
515
+
516
+ # Create the widget directory and sample data
517
+ widget_dir = @api.repo.root/:views/"test_view"/:widgets/"links"
518
+ make_dir(widget_dir)
519
+ write_file(widget_dir/"list.txt", "https://example.com, Example Link")
520
+
521
+ # Should not raise an error for a valid widget
522
+ result = @api.generate_widget("links")
523
+ assert result
524
+
525
+ # Verify the widget files were created
526
+ assert File.exist?(widget_dir/"links-card.html")
527
+ end
528
+
529
+
530
+
531
+ def test_037_generate_widget_invalid_name
532
+ @api.create_view("test_view", "Test View")
533
+
534
+ # Test with invalid widget name
535
+ assert_raises(RuntimeError) do
536
+ @api.generate_widget("invalid-widget")
537
+ end
538
+
539
+ # Test with nil
540
+ assert_raises(RuntimeError) do
541
+ @api.generate_widget(nil)
542
+ end
543
+
544
+ # Test with empty string
545
+ assert_raises(RuntimeError) do
546
+ @api.generate_widget("")
547
+ end
548
+ end
549
+
550
+ def test_038_generate_widget_nonexistent
551
+ @api.create_view("test_view", "Test View")
552
+
553
+ # Test with non-existent widget class
554
+ assert_raises(RuntimeError) do
555
+ @api.generate_widget("nonexistent")
556
+ end
557
+ end
558
+
559
+ def test_039_select_posts
560
+ @api.create_view("test_view", "Test View")
561
+ @api.create_view("other_view", "Other View")
562
+
563
+ # Create posts in different views
564
+ post1 = @api.create_post("Post 1", "Body 1", views: ["test_view"])
565
+ post2 = @api.create_post("Post 2", "Body 2", views: ["other_view"])
566
+ post3 = @api.create_post("Post 3", "Body 3", views: ["test_view", "other_view"])
567
+
568
+ # Test filtering by view
569
+ test_view_posts = @api.select_posts { |post| post.views.include?("test_view") }
570
+ assert_equal 2, test_view_posts.length
571
+ assert_includes test_view_posts.map(&:title), "Post 1"
572
+ assert_includes test_view_posts.map(&:title), "Post 3"
573
+
574
+ # Test filtering by title
575
+ title_posts = @api.select_posts { |post| post.title.include?("Post 2") }
576
+ assert_equal 1, title_posts.length
577
+ assert_equal "Post 2", title_posts.first.title
578
+ end
579
+
580
+ def test_040_search_posts
581
+ @api.create_view("test_view", "Test View")
582
+
583
+ # Create posts with different content
584
+ post1 = @api.create_post("Ruby Programming", "Learn Ruby basics", tags: "ruby, programming")
585
+ post2 = @api.create_post("Python Guide", "Python vs Ruby", tags: "python, comparison")
586
+ post3 = @api.create_post("Scriptorium API", "API documentation", tags: "api, scriptorium")
587
+
588
+ # Test title search with regex
589
+ ruby_posts = @api.search_posts(title: /Ruby/)
590
+ assert_equal 1, ruby_posts.length
591
+ assert_equal "Ruby Programming", ruby_posts.first.title
592
+
593
+ # Test body search with string
594
+ body_posts = @api.search_posts(body: "Ruby")
595
+ assert_equal 2, body_posts.length
596
+ assert_includes body_posts.map(&:title), "Ruby Programming"
597
+ assert_includes body_posts.map(&:title), "Python Guide"
598
+
599
+ # Test tags search
600
+ api_posts = @api.search_posts(tags: "api")
601
+ assert_equal 1, api_posts.length
602
+ assert_equal "Scriptorium API", api_posts.first.title
603
+
604
+ # Test multiple criteria (AND)
605
+ ruby_api_posts = @api.search_posts(title: /Ruby/, body: "basics")
606
+ assert_equal 1, ruby_api_posts.length
607
+ assert_equal "Ruby Programming", ruby_api_posts.first.title
608
+ end
609
+
610
+ def test_041_search_posts_with_blurb
611
+ @api.create_view("test_view", "Test View")
612
+ post = @api.create_post("Test Post", "Test body", blurb: "This is a test blurb for searching.")
613
+
614
+ # Test blurb search
615
+ blurb_posts = @api.search_posts(blurb: "test blurb")
616
+ assert_equal 1, blurb_posts.length
617
+ assert_equal "Test Post", blurb_posts.first.title
618
+
619
+ # Test blurb search with regex
620
+ regex_posts = @api.search_posts(blurb: /blurb.*search/)
621
+ assert_equal 1, regex_posts.length
622
+ assert_equal "Test Post", regex_posts.first.title
623
+ end
624
+
625
+ def test_042_search_posts_unknown_field
626
+ @api.create_view("test_view", "Test View")
627
+
628
+ # Create a post so the search actually processes something
629
+ @api.create_post("Test Post", "Test body")
630
+
631
+ assert_raises(RuntimeError) do
632
+ @api.search_posts(unknown_field: "value")
633
+ end
634
+ end
635
+
636
+ def test_043_unlink_post_specific_view
637
+ @api.create_view("test_view", "Test View")
638
+ @api.create_view("other_view", "Other View")
639
+ post = @api.create_post("Test Post", "Test body", views: ["test_view", "other_view"])
640
+
641
+ # Unlink from specific view
642
+ result = @api.unlink_post(post.id, "other_view")
643
+ assert result
644
+
645
+ # Post should now only be in test_view
646
+ updated_post = @api.post(post.id)
647
+ assert_includes updated_post.views, "test_view"
648
+ refute_includes updated_post.views, "other_view"
649
+ end
650
+
651
+ def test_044_post_add_tag
652
+ @api.create_view("test_view", "Test View")
653
+ post = @api.create_post("Test Post", "Test body", tags: ["ruby"])
654
+
655
+ # Initially post should only have ruby tag
656
+ assert_includes post.tags, "ruby"
657
+ refute_includes post.tags, "scriptorium"
658
+
659
+ # Add scriptorium tag to the post
660
+ result = @api.post_add_tag(post.id, "scriptorium")
661
+ assert result
662
+
663
+ # Post should now have both tags
664
+ updated_post = @api.post(post.id)
665
+ updated_tags = updated_post.tags.strip.split(/,\s*/)
666
+ assert_includes updated_tags, "ruby"
667
+ assert_includes updated_tags, "scriptorium"
668
+ end
669
+
670
+ def test_045_post_add_tag_duplicate
671
+ @api.create_view("test_view", "Test View")
672
+ post = @api.create_post("Test Post", "Test body", tags: ["ruby"])
673
+
674
+ # Initially post should have ruby tag
675
+ assert_includes post.tags, "ruby"
676
+
677
+ # Try to add the same tag (should not add duplicate)
678
+ result = @api.post_add_tag(post.id, "ruby")
679
+ assert result
680
+
681
+ # Post should still only have ruby tag (no duplicates)
682
+ updated_post = @api.post(post.id)
683
+ updated_tags = updated_post.tags.strip.split(/,\s*/)
684
+ assert_equal ["ruby"], updated_tags
685
+ end
686
+
687
+ def test_046_post_remove_tag
688
+ @api.create_view("test_view", "Test View")
689
+ post = @api.create_post("Test Post", "Test body", tags: ["ruby", "scriptorium"])
690
+
691
+ # Initially post should have both tags
692
+ assert_includes post.tags, "ruby"
693
+ assert_includes post.tags, "scriptorium"
694
+
695
+ # Remove scriptorium tag from the post
696
+ result = @api.post_remove_tag(post.id, "scriptorium")
697
+ assert result
698
+
699
+ # Post should now only have ruby tag
700
+ updated_post = @api.post(post.id)
701
+ updated_tags = updated_post.tags.strip.split(/,\s*/)
702
+ assert_includes updated_tags, "ruby"
703
+ refute_includes updated_tags, "scriptorium"
704
+ end
705
+
706
+ def test_047_post_remove_tag_nonexistent
707
+ @api.create_view("test_view", "Test View")
708
+ post = @api.create_post("Test Post", "Test body", tags: ["ruby"])
709
+
710
+ # Initially post should have ruby tag
711
+ assert_includes post.tags, "ruby"
712
+
713
+ # Try to remove a tag that doesn't exist
714
+ result = @api.post_remove_tag(post.id, "nonexistent")
715
+ assert result # Should succeed even if tag doesn't exist
716
+
717
+ # Post should still have ruby tag
718
+ updated_post = @api.post(post.id)
719
+ updated_tags = updated_post.tags.strip.split(/,\s*/)
720
+ assert_includes updated_tags, "ruby"
721
+ refute_includes updated_tags, "nonexistent"
722
+ end
723
+
724
+ # edit_file tests
725
+ def test_048_edit_file_validation_nil_path
726
+ assert_raises(CannotEditFilePathNil) do
727
+ @api.edit_file(nil)
728
+ end
729
+ end
730
+
731
+ def test_049_edit_file_validation_empty_path
732
+ assert_raises(CannotEditFilePathEmpty) do
733
+ @api.edit_file("")
734
+ end
735
+ end
736
+
737
+ def test_050_edit_file_validation_whitespace_path
738
+ assert_raises(CannotEditFilePathEmpty) do
739
+ @api.edit_file(" ")
740
+ end
741
+ end
742
+
743
+ def test_051_edit_file_uses_editor_from_env
744
+ # Mock ENV to return a specific editor
745
+ ENV.stub :[], "nano" do
746
+ # Mock system! to verify it's called with the right editor
747
+ mock_system = Minitest::Mock.new
748
+ mock_system.expect :call, true, ["nano", "/path/to/file"]
749
+
750
+ @api.stub :system!, mock_system do
751
+ @api.edit_file("/path/to/file")
752
+ end
753
+
754
+ mock_system.verify
755
+ end
756
+ end
757
+
758
+ def test_052_edit_file_uses_vim_fallback
759
+ # Mock ENV to return nil (no EDITOR set)
760
+ ENV.stub :[], nil do
761
+ # Mock system to return true (vim available)
762
+ @api.stub :system, true do
763
+ # Mock Open3.popen3 to return a mock process
764
+ mock_process = Minitest::Mock.new
765
+ mock_process.expect :pid, 123
766
+ mock_process.expect :wait, nil
767
+
768
+ Open3.stub :popen3, mock_process do
769
+ @api.edit_file("test.txt")
770
+ end
771
+ end
772
+ end
773
+ end
774
+
775
+ # Convenience file editing method tests
776
+
777
+ def test_053_edit_layout
778
+ @api.create_view("test_view", "Test View")
779
+
780
+ # Mock edit_file to track calls
781
+ called_path = nil
782
+ @api.stub :edit_file, ->(path) { called_path = path } do
783
+ @api.edit_layout
784
+ assert_equal "views/test_view/layout.txt", called_path
785
+ end
786
+ end
787
+
788
+ def test_054_edit_layout_with_specific_view
789
+ @api.create_view("test_view", "Test View")
790
+ @api.create_view("other_view", "Other View")
791
+
792
+ # Mock edit_file to track calls
793
+ called_path = nil
794
+ @api.stub :edit_file, ->(path) { called_path = path } do
795
+ @api.edit_layout("other_view")
796
+ assert_equal "views/other_view/layout.txt", called_path
797
+ end
798
+ end
799
+
800
+ def test_055_edit_layout_no_view
801
+ # Clear the current view
802
+ @api.repo.instance_variable_set(:@current_view, nil)
803
+
804
+ assert_raises(RuntimeError, "No view specified and no current view set") do
805
+ @api.edit_layout
806
+ end
807
+ end
808
+
809
+ def test_065_edit_config
810
+ @api.create_view("test_view", "Test View")
811
+
812
+ # Mock edit_file to track calls
813
+ called_path = nil
814
+ @api.stub :edit_file, ->(path) { called_path = path } do
815
+ @api.edit_config
816
+ assert_equal "views/test_view/config.txt", called_path
817
+ end
818
+ end
819
+
820
+ def test_066_edit_config_with_specific_view
821
+ @api.create_view("test_view", "Test View")
822
+ @api.create_view("other_view", "Other View")
823
+
824
+ # Mock edit_file to track calls
825
+ called_path = nil
826
+ @api.stub :edit_file, ->(path) { called_path = path } do
827
+ @api.edit_config("other_view")
828
+ assert_equal "views/other_view/config.txt", called_path
829
+ end
830
+ end
831
+
832
+ def test_067_edit_config_no_view
833
+ # Clear the current view
834
+ @api.repo.instance_variable_set(:@current_view, nil)
835
+
836
+ assert_raises(RuntimeError, "No view specified and no current view set") do
837
+ @api.edit_config
838
+ end
839
+ end
840
+
841
+ def test_056_edit_widget_data
842
+ @api.create_view("test_view", "Test View")
843
+
844
+ # Mock edit_file to track calls
845
+ called_path = nil
846
+ @api.stub :edit_file, ->(path) { called_path = path } do
847
+ @api.edit_widget_data(nil, "links")
848
+ assert_equal "views/test_view/widgets/links/list.txt", called_path
849
+ end
850
+ end
851
+
852
+ def test_057_edit_widget_data_with_specific_view
853
+ @api.create_view("test_view", "Test View")
854
+ @api.create_view("other_view", "Other View")
855
+
856
+ # Mock edit_file to track calls
857
+ called_path = nil
858
+ @api.stub :edit_file, ->(path) { called_path = path } do
859
+ @api.edit_widget_data("other_view", "news")
860
+ assert_equal "views/other_view/widgets/news/list.txt", called_path
861
+ end
862
+ end
863
+
864
+ def test_058_edit_widget_data_nil_widget
865
+ @api.create_view("test_view", "Test View")
866
+
867
+ assert_raises(RuntimeError, "Widget name cannot be nil") do
868
+ @api.edit_widget_data(nil, nil)
869
+ end
870
+ end
871
+
872
+ def test_060_edit_repo_config
873
+ # Mock edit_file to track calls
874
+ called_path = nil
875
+ @api.stub :edit_file, ->(path) { called_path = path } do
876
+ @api.edit_repo_config
877
+ assert_equal "config/repo.txt", called_path
878
+ end
879
+ end
880
+
881
+ def test_061_edit_deploy_config
882
+ # Mock edit_file to track calls
883
+ called_path = nil
884
+ @api.stub :edit_file, ->(path) { called_path = path } do
885
+ @api.edit_deploy_config
886
+ assert_equal "config/deploy.txt", called_path
887
+ end
888
+ end
889
+
890
+ def test_062_edit_post_with_source
891
+ @api.create_view("test_view", "Test View")
892
+ post = @api.create_post("Test Post", "Test body")
893
+
894
+ # Create source.lt3 file to test smart selection
895
+ source_path = "posts/#{post.num}/source.lt3"
896
+ write_file(source_path, "Test source content")
897
+
898
+ # Mock edit_file to track calls
899
+ called_path = nil
900
+ @api.stub :edit_file, ->(path) { called_path = path } do
901
+ @api.edit_post(post.id)
902
+ assert_equal source_path, called_path
903
+ end
904
+ end
905
+
906
+ def test_063_edit_post_without_source
907
+ @api.create_view("test_view", "Test View")
908
+ post = @api.create_post("Test Post", "Test body")
909
+
910
+ # Ensure source.lt3 doesn't exist, only body.html
911
+ source_path = "posts/#{post.num}/source.lt3"
912
+ File.delete(source_path) if File.exist?(source_path)
913
+
914
+ # Mock edit_file to track calls
915
+ called_path = nil
916
+ @api.stub :edit_file, ->(path) { called_path = path } do
917
+ @api.edit_post(post.id)
918
+ assert_equal "posts/#{post.num}/body.html", called_path
919
+ end
920
+ end
921
+
922
+ def test_064_edit_post_nonexistent
923
+ assert_raises(NoMethodError) do
924
+ @api.edit_post(999)
925
+ end
926
+ end
927
+
928
+ # Publication system tests
929
+
930
+
931
+ def test_068_publish_post
932
+ @api.create_view("test_view", "Test View")
933
+ post = @api.create_post("Test Post", "Test body")
934
+
935
+ # Initially unpublished
936
+ refute @api.post_published?(post.id)
937
+
938
+ # Publish the post
939
+ published_post = @api.publish_post(post.id)
940
+
941
+ # Should now be published
942
+ assert @api.post_published?(post.id)
943
+ assert_equal "Test Post", published_post.title
944
+
945
+ # Should have generated the post
946
+ assert File.exist?("#{@test_dir}/posts/#{post.num}/body.html")
947
+ end
948
+
949
+ def test_069_publish_post_already_published
950
+ @api.create_view("test_view", "Test View")
951
+ post = @api.create_post("Test Post", "Test body")
952
+
953
+ # Publish once
954
+ @api.publish_post(post.id)
955
+
956
+ # Try to publish again
957
+ assert_raises(RuntimeError, "Post #{post.id} is already published") do
958
+ @api.publish_post(post.id)
959
+ end
960
+ end
961
+
962
+ def test_070_publish_post_nonexistent
963
+ assert_raises(RequiredFileNotFound) do
964
+ @api.publish_post(999)
965
+ end
966
+ end
967
+
968
+ def test_071_post_published_status
969
+ @api.create_view("test_view", "Test View")
970
+ post = @api.create_post("Test Post", "Test body")
971
+
972
+ # Initially unpublished
973
+ refute @api.post_published?(post.id)
974
+
975
+ # Publish
976
+ @api.publish_post(post.id)
977
+
978
+ # Now published
979
+ assert @api.post_published?(post.id)
980
+ end
981
+
982
+ def test_072_get_published_posts
983
+ @api.create_view("test_view", "Test View")
984
+
985
+ # Create multiple posts
986
+ post1 = @api.create_post("Post 1", "Body 1")
987
+ post2 = @api.create_post("Post 2", "Body 2")
988
+ post3 = @api.create_post("Post 3", "Body 3")
989
+
990
+ # Initially no published posts
991
+ published_posts = @api.get_published_posts
992
+ assert_equal 0, published_posts.length
993
+
994
+ # Publish two posts
995
+ @api.publish_post(post1.id)
996
+ @api.publish_post(post3.id)
997
+
998
+ # Should have 2 published posts
999
+ published_posts = @api.get_published_posts
1000
+ assert_equal 2, published_posts.length
1001
+ assert_includes published_posts.map(&:id), post1.id
1002
+ assert_includes published_posts.map(&:id), post3.id
1003
+ refute_includes published_posts.map(&:id), post2.id
1004
+ end
1005
+
1006
+ def test_073_get_published_posts_with_view
1007
+ @api.create_view("test_view", "Test View")
1008
+ @api.create_view("other_view", "Other View")
1009
+
1010
+ # Create posts in different views
1011
+ post1 = @api.create_post("Post 1", "Body 1", views: "test_view")
1012
+ post2 = @api.create_post("Post 2", "Body 2", views: "other_view")
1013
+
1014
+ # Publish both
1015
+ @api.publish_post(post1.id)
1016
+ @api.publish_post(post2.id)
1017
+
1018
+ # Get published posts for specific view
1019
+ test_view_posts = @api.get_published_posts("test_view")
1020
+ assert_equal 1, test_view_posts.length
1021
+ assert_equal post1.id, test_view_posts.first.id
1022
+
1023
+ other_view_posts = @api.get_published_posts("other_view")
1024
+ assert_equal 1, other_view_posts.length
1025
+ assert_equal post2.id, other_view_posts.first.id
1026
+ end
1027
+
1028
+ def test_074_create_post_with_generation
1029
+ @api.create_view("test_view", "Test View")
1030
+ post = @api.create_post("Test Post", "Test body")
1031
+
1032
+ # Post should exist and be generated by default
1033
+ assert File.exist?("#{@test_dir}/posts/#{post.num}/source.lt3")
1034
+ assert File.exist?("#{@test_dir}/posts/#{post.num}/meta.txt")
1035
+ assert File.exist?("#{@test_dir}/posts/#{post.num}/body.html")
1036
+
1037
+ # Should not be published
1038
+ refute @api.post_published?(post.id)
1039
+ end
1040
+
1041
+ def test_075_create_post_with_generation_default
1042
+ @api.create_view("test_view", "Test View")
1043
+ post = @api.create_post("Test Post", "Test body")
1044
+
1045
+ # Post should be generated by default (backward compatibility)
1046
+ assert File.exist?("#{@test_dir}/posts/#{post.num}/body.html")
1047
+
1048
+ # Should NOT be published (generation and publication are separate)
1049
+ # Check the metadata file directly to be sure
1050
+ metadata_file = "#{@test_dir}/posts/#{post.num}/meta.txt"
1051
+ assert File.exist?(metadata_file)
1052
+ metadata_content = read_file(metadata_file)
1053
+ assert_match /post\.published\s+no/, metadata_content
1054
+ refute @api.post_published?(post.id)
1055
+ end
1056
+ end