source_monitor 0.2.0 → 0.3.0
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.
- checksums.yaml +4 -4
- data/.claude/agents/rails-concern.md +464 -0
- data/.claude/agents/rails-controller.md +424 -0
- data/.claude/agents/rails-hotwire.md +446 -0
- data/.claude/agents/rails-implement.md +374 -0
- data/.claude/agents/rails-job.md +334 -0
- data/.claude/agents/rails-lint.md +294 -0
- data/.claude/agents/rails-mailer.md +371 -0
- data/.claude/agents/rails-migration.md +449 -0
- data/.claude/agents/rails-model.md +420 -0
- data/.claude/agents/rails-policy.md +443 -0
- data/.claude/agents/rails-presenter.md +427 -0
- data/.claude/agents/rails-query.md +412 -0
- data/.claude/agents/rails-review.md +490 -0
- data/.claude/agents/rails-service.md +458 -0
- data/.claude/agents/rails-state-records.md +465 -0
- data/.claude/agents/rails-tdd.md +314 -0
- data/.claude/agents/rails-test.md +441 -0
- data/.claude/agents/rails-view-component.md +418 -0
- data/.claude/hooks/block-secrets.sh +52 -0
- data/.claude/settings.json +85 -0
- data/.claude/skills/action-cable-patterns/SKILL.md +296 -0
- data/.claude/skills/action-mailer-patterns/SKILL.md +295 -0
- data/.claude/skills/active-storage-setup/SKILL.md +311 -0
- data/.claude/skills/api-versioning/SKILL.md +294 -0
- data/.claude/skills/authentication-flow/SKILL.md +335 -0
- data/.claude/skills/authentication-flow/reference/current.md +248 -0
- data/.claude/skills/authentication-flow/reference/passwordless.md +253 -0
- data/.claude/skills/authentication-flow/reference/sessions.md +201 -0
- data/.claude/skills/authorization-pundit/SKILL.md +462 -0
- data/.claude/skills/caching-strategies/SKILL.md +350 -0
- data/.claude/skills/database-migrations/SKILL.md +354 -0
- data/.claude/skills/form-object-patterns/SKILL.md +399 -0
- data/.claude/skills/hotwire-patterns/SKILL.md +247 -0
- data/.claude/skills/hotwire-patterns/reference/stimulus.md +307 -0
- data/.claude/skills/hotwire-patterns/reference/tailwind-integration.md +112 -0
- data/.claude/skills/hotwire-patterns/reference/turbo-frames.md +158 -0
- data/.claude/skills/hotwire-patterns/reference/turbo-streams.md +218 -0
- data/.claude/skills/i18n-patterns/SKILL.md +320 -0
- data/.claude/skills/install/SKILL.md +367 -0
- data/.claude/skills/performance-optimization/SKILL.md +311 -0
- data/.claude/skills/rails-architecture/SKILL.md +259 -0
- data/.claude/skills/rails-architecture/reference/error-handling.md +333 -0
- data/.claude/skills/rails-architecture/reference/event-tracking.md +142 -0
- data/.claude/skills/rails-architecture/reference/layer-interactions.md +417 -0
- data/.claude/skills/rails-architecture/reference/multi-tenancy.md +152 -0
- data/.claude/skills/rails-architecture/reference/query-patterns.md +342 -0
- data/.claude/skills/rails-architecture/reference/service-patterns.md +286 -0
- data/.claude/skills/rails-architecture/reference/state-records.md +250 -0
- data/.claude/skills/rails-architecture/reference/testing-strategy.md +326 -0
- data/.claude/skills/rails-concern/SKILL.md +399 -0
- data/.claude/skills/rails-controller/SKILL.md +336 -0
- data/.claude/skills/rails-model-generator/SKILL.md +321 -0
- data/.claude/skills/rails-model-generator/reference/validations.md +298 -0
- data/.claude/skills/rails-presenter/SKILL.md +274 -0
- data/.claude/skills/rails-query-object/SKILL.md +289 -0
- data/.claude/skills/rails-service-object/SKILL.md +349 -0
- data/.claude/skills/solid-queue-setup/SKILL.md +307 -0
- data/.claude/skills/tdd-cycle/SKILL.md +359 -0
- data/.claude/skills/viewcomponent-patterns/SKILL.md +333 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +2 -0
- data/.ruby-version +1 -1
- data/.vbw-planning/.notification-log.jsonl +192 -0
- data/.vbw-planning/.session-log.jsonl +871 -0
- data/.vbw-planning/PROJECT.md +51 -0
- data/.vbw-planning/REQUIREMENTS.md +50 -0
- data/.vbw-planning/SHIPPED.md +28 -0
- data/.vbw-planning/codebase/ARCHITECTURE.md +147 -0
- data/.vbw-planning/codebase/CONCERNS.md +99 -0
- data/.vbw-planning/codebase/CONVENTIONS.md +97 -0
- data/.vbw-planning/codebase/DEPENDENCIES.md +100 -0
- data/.vbw-planning/codebase/INDEX.md +86 -0
- data/.vbw-planning/codebase/META.md +42 -0
- data/.vbw-planning/codebase/PATTERNS.md +262 -0
- data/.vbw-planning/codebase/STACK.md +101 -0
- data/.vbw-planning/codebase/STRUCTURE.md +324 -0
- data/.vbw-planning/codebase/TESTING.md +154 -0
- data/.vbw-planning/config.json +12 -0
- data/.vbw-planning/discovery.json +24 -0
- data/.vbw-planning/milestones/default/ROADMAP.md +115 -0
- data/.vbw-planning/milestones/default/STATE.md +83 -0
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01-SUMMARY.md +56 -0
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01.md +187 -0
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02-SUMMARY.md +64 -0
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02.md +137 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01-SUMMARY.md +67 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01.md +142 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02-SUMMARY.md +64 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02.md +138 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03-SUMMARY.md +85 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03.md +147 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04-SUMMARY.md +63 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04.md +129 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05-SUMMARY.md +74 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05.md +154 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION-wave1.md +303 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION.md +510 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01-SUMMARY.md +61 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01.md +161 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02-SUMMARY.md +66 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02.md +132 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03-SUMMARY.md +59 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03.md +171 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04-SUMMARY.md +56 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04.md +152 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/04-CONTEXT.md +33 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01-SUMMARY.md +42 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01.md +119 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02-SUMMARY.md +52 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02.md +195 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03-SUMMARY.md +79 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03.md +130 -0
- data/CHANGELOG.md +28 -0
- data/CLAUDE.md +179 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +114 -101
- data/Rakefile +2 -0
- data/app/assets/builds/source_monitor/application.css +2076 -0
- data/app/assets/builds/source_monitor/application.js +2758 -0
- data/app/assets/builds/source_monitor/application.js.map +7 -0
- data/app/controllers/source_monitor/application_controller.rb +2 -0
- data/app/controllers/source_monitor/health_controller.rb +2 -0
- data/app/controllers/source_monitor/import_sessions/bulk_configuration.rb +106 -0
- data/app/controllers/source_monitor/import_sessions/entry_annotation.rb +187 -0
- data/app/controllers/source_monitor/import_sessions/health_check_management.rb +112 -0
- data/app/controllers/source_monitor/import_sessions/opml_parser.rb +130 -0
- data/app/controllers/source_monitor/import_sessions_controller.rb +6 -507
- data/app/controllers/source_monitor/items_controller.rb +2 -0
- data/app/controllers/source_monitor/sources_controller.rb +0 -14
- data/app/helpers/source_monitor/application_helper.rb +4 -112
- data/app/helpers/source_monitor/health_badge_helper.rb +69 -0
- data/app/helpers/source_monitor/table_sort_helper.rb +53 -0
- data/app/jobs/source_monitor/application_job.rb +2 -0
- data/app/models/source_monitor/application_record.rb +2 -0
- data/app/models/source_monitor/log_entry.rb +0 -2
- data/config/coverage_baseline.json +217 -1862
- data/config/routes.rb +2 -0
- data/db/migrate/20251009103000_add_feed_content_readability_to_sources.rb +2 -0
- data/db/migrate/20251014171659_add_performance_indexes.rb +2 -0
- data/db/migrate/20251014172525_add_fetch_status_check_constraint.rb +2 -0
- data/db/migrate/20251108120116_refresh_fetch_status_constraint.rb +2 -0
- data/db/migrate/20260210204022_add_composite_index_to_log_entries.rb +17 -0
- data/lib/source_monitor/assets/bundler.rb +2 -0
- data/lib/source_monitor/assets.rb +2 -0
- data/lib/source_monitor/configuration/authentication_settings.rb +62 -0
- data/lib/source_monitor/configuration/events.rb +60 -0
- data/lib/source_monitor/configuration/fetching_settings.rb +27 -0
- data/lib/source_monitor/configuration/health_settings.rb +27 -0
- data/lib/source_monitor/configuration/http_settings.rb +43 -0
- data/lib/source_monitor/configuration/model_definition.rb +108 -0
- data/lib/source_monitor/configuration/models.rb +36 -0
- data/lib/source_monitor/configuration/realtime_settings.rb +95 -0
- data/lib/source_monitor/configuration/retention_settings.rb +45 -0
- data/lib/source_monitor/configuration/scraper_registry.rb +67 -0
- data/lib/source_monitor/configuration/scraping_settings.rb +39 -0
- data/lib/source_monitor/configuration/validation_definition.rb +32 -0
- data/lib/source_monitor/configuration.rb +12 -579
- data/lib/source_monitor/dashboard/queries/recent_activity_query.rb +138 -0
- data/lib/source_monitor/dashboard/queries/stats_query.rb +71 -0
- data/lib/source_monitor/dashboard/queries.rb +2 -195
- data/lib/source_monitor/engine.rb +2 -0
- data/lib/source_monitor/fetching/feed_fetcher/adaptive_interval.rb +141 -0
- data/lib/source_monitor/fetching/feed_fetcher/entry_processor.rb +89 -0
- data/lib/source_monitor/fetching/feed_fetcher/source_updater.rb +200 -0
- data/lib/source_monitor/fetching/feed_fetcher.rb +37 -379
- data/lib/source_monitor/items/item_creator/content_extractor.rb +113 -0
- data/lib/source_monitor/items/item_creator/entry_parser/media_extraction.rb +96 -0
- data/lib/source_monitor/items/item_creator/entry_parser.rb +294 -0
- data/lib/source_monitor/items/item_creator.rb +28 -455
- data/lib/source_monitor/setup/bundle_installer.rb +2 -0
- data/lib/source_monitor/setup/cli.rb +2 -0
- data/lib/source_monitor/setup/dependency_checker.rb +2 -0
- data/lib/source_monitor/setup/detectors.rb +2 -0
- data/lib/source_monitor/setup/gemfile_editor.rb +2 -0
- data/lib/source_monitor/setup/initializer_patcher.rb +2 -0
- data/lib/source_monitor/setup/install_generator.rb +2 -0
- data/lib/source_monitor/setup/migration_installer.rb +2 -0
- data/lib/source_monitor/setup/node_installer.rb +2 -0
- data/lib/source_monitor/setup/prompter.rb +2 -0
- data/lib/source_monitor/setup/requirements.rb +2 -0
- data/lib/source_monitor/setup/shell_runner.rb +2 -0
- data/lib/source_monitor/setup/verification/action_cable_verifier.rb +2 -0
- data/lib/source_monitor/setup/verification/printer.rb +2 -0
- data/lib/source_monitor/setup/verification/result.rb +2 -0
- data/lib/source_monitor/setup/verification/runner.rb +2 -0
- data/lib/source_monitor/setup/verification/solid_queue_verifier.rb +2 -0
- data/lib/source_monitor/setup/verification/telemetry_logger.rb +2 -0
- data/lib/source_monitor/setup/workflow.rb +2 -0
- data/lib/source_monitor/version.rb +3 -1
- data/lib/source_monitor.rb +140 -58
- data/lib/tasks/source_monitor_assets.rake +2 -0
- data/lib/tasks/source_monitor_setup.rake +2 -0
- data/lib/tasks/source_monitor_tasks.rake +2 -0
- data/source_monitor.gemspec +3 -1
- metadata +144 -4
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tdd-cycle
|
|
3
|
+
description: Guides Test-Driven Development workflow with Red-Green-Refactor cycle using Minitest and fixtures. Use when the user wants to implement a feature using TDD, write tests first, follow test-driven practices, or mentions red-green-refactor.
|
|
4
|
+
allowed-tools: Read, Write, Edit, Bash
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# TDD Cycle — Minitest + Fixtures
|
|
8
|
+
|
|
9
|
+
## Project Conventions
|
|
10
|
+
- **Testing:** Minitest + fixtures (NEVER RSpec or FactoryBot)
|
|
11
|
+
- **Components:** ViewComponents for reusable UI (partials OK for simple one-offs)
|
|
12
|
+
- **Authorization:** Pundit policies (deny by default)
|
|
13
|
+
- **Jobs:** Solid Queue, shallow jobs, `_later`/`_now` naming
|
|
14
|
+
- **Frontend:** Hotwire (Turbo + Stimulus) + Tailwind CSS
|
|
15
|
+
- **State:** State-as-records for business state (booleans only for technical flags)
|
|
16
|
+
- **Architecture:** Rich models first, service objects for multi-model orchestration
|
|
17
|
+
- **Routing:** Everything-is-CRUD (new resource over new action)
|
|
18
|
+
- **Quality:** RuboCop (omakase) + Brakeman
|
|
19
|
+
|
|
20
|
+
## The Cycle
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
1. RED → Write a failing test that describes desired behavior
|
|
24
|
+
2. GREEN → Write the minimum code to pass the test
|
|
25
|
+
3. REFACTOR → Improve code while keeping tests green
|
|
26
|
+
4. REPEAT → Next behavior
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Workflow Checklist
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
TDD Progress:
|
|
33
|
+
- [ ] Step 1: Understand the requirement
|
|
34
|
+
- [ ] Step 2: Choose test type (model/controller/system/component)
|
|
35
|
+
- [ ] Step 3: Write failing test (RED)
|
|
36
|
+
- [ ] Step 4: Verify test fails correctly
|
|
37
|
+
- [ ] Step 5: Implement minimal code (GREEN)
|
|
38
|
+
- [ ] Step 6: Verify test passes
|
|
39
|
+
- [ ] Step 7: Refactor if needed
|
|
40
|
+
- [ ] Step 8: Verify tests still pass
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Step 1: Requirement Analysis
|
|
44
|
+
|
|
45
|
+
Before writing any code, understand:
|
|
46
|
+
- What is the expected input?
|
|
47
|
+
- What is the expected output/behavior?
|
|
48
|
+
- What are the edge cases?
|
|
49
|
+
- What errors should be handled?
|
|
50
|
+
|
|
51
|
+
## Step 2: Choose Test Type
|
|
52
|
+
|
|
53
|
+
| Test Type | Use For | Location |
|
|
54
|
+
|-----------|---------|----------|
|
|
55
|
+
| Model test | Validations, scopes, instance methods | `test/models/` |
|
|
56
|
+
| Controller test | HTTP flow, authorization, responses | `test/controllers/` |
|
|
57
|
+
| System test | Full user flows with JavaScript | `test/system/` |
|
|
58
|
+
| Service test | Business logic, complex operations | `test/services/` |
|
|
59
|
+
| Query test | Complex queries, correctness | `test/queries/` |
|
|
60
|
+
| Component test | ViewComponent rendering | `test/components/` |
|
|
61
|
+
| Policy test | Pundit authorization rules | `test/policies/` |
|
|
62
|
+
| Job test | Background job behavior | `test/jobs/` |
|
|
63
|
+
| Mailer test | Email content, recipients | `test/mailers/` |
|
|
64
|
+
|
|
65
|
+
## Step 3: Write Failing Test (RED)
|
|
66
|
+
|
|
67
|
+
### Model Test Template
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
# test/models/post_test.rb
|
|
71
|
+
require "test_helper"
|
|
72
|
+
|
|
73
|
+
class PostTest < ActiveSupport::TestCase
|
|
74
|
+
setup do
|
|
75
|
+
@post = posts(:published)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
test "requires title" do
|
|
79
|
+
@post.title = nil
|
|
80
|
+
assert_not @post.valid?
|
|
81
|
+
assert_includes @post.errors[:title], "can't be blank"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
test ".recent returns posts in descending order" do
|
|
85
|
+
recent = posts(:recent)
|
|
86
|
+
old = posts(:old)
|
|
87
|
+
assert_equal [recent, old], Post.recent.to_a
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
test "#publish! creates a publication record" do
|
|
91
|
+
post = posts(:draft)
|
|
92
|
+
assert_difference "Publication.count", 1 do
|
|
93
|
+
post.publish!(user: users(:admin))
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Controller (Integration) Test Template
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
# test/controllers/posts_controller_test.rb
|
|
103
|
+
require "test_helper"
|
|
104
|
+
|
|
105
|
+
class PostsControllerTest < ActionDispatch::IntegrationTest
|
|
106
|
+
setup do
|
|
107
|
+
@user = users(:one)
|
|
108
|
+
@post = posts(:one)
|
|
109
|
+
sign_in_as @user
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
test "should get index" do
|
|
113
|
+
get posts_url
|
|
114
|
+
assert_response :success
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
test "should create post" do
|
|
118
|
+
assert_difference("Post.count") do
|
|
119
|
+
post posts_url, params: { post: { title: "New Post", body: "Content" } }
|
|
120
|
+
end
|
|
121
|
+
assert_redirected_to post_url(Post.last)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
test "should not create post with invalid params" do
|
|
125
|
+
assert_no_difference("Post.count") do
|
|
126
|
+
post posts_url, params: { post: { title: "", body: "" } }
|
|
127
|
+
end
|
|
128
|
+
assert_response :unprocessable_entity
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
test "requires authentication" do
|
|
132
|
+
sign_out
|
|
133
|
+
get posts_url
|
|
134
|
+
assert_redirected_to new_session_url
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Service Test Template
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
# test/services/orders/create_service_test.rb
|
|
143
|
+
require "test_helper"
|
|
144
|
+
|
|
145
|
+
class Orders::CreateServiceTest < ActiveSupport::TestCase
|
|
146
|
+
setup do
|
|
147
|
+
@user = users(:one)
|
|
148
|
+
@product = products(:widget)
|
|
149
|
+
@service = Orders::CreateService.new
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
test "creates order with valid params" do
|
|
153
|
+
result = @service.call(user: @user, items: [{ product_id: @product.id, quantity: 2 }])
|
|
154
|
+
|
|
155
|
+
assert result.success?
|
|
156
|
+
assert_kind_of Order, result.data
|
|
157
|
+
assert_equal @user, result.data.user
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
test "returns failure with empty items" do
|
|
161
|
+
result = @service.call(user: @user, items: [])
|
|
162
|
+
|
|
163
|
+
assert result.failure?
|
|
164
|
+
assert_equal :empty_cart, result.code
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
test "wraps in transaction" do
|
|
168
|
+
assert_no_difference "Order.count" do
|
|
169
|
+
@service.call(user: @user, items: [{ product_id: 0, quantity: 1 }])
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### System Test Template
|
|
176
|
+
|
|
177
|
+
```ruby
|
|
178
|
+
# test/system/posts_test.rb
|
|
179
|
+
require "application_system_test_case"
|
|
180
|
+
|
|
181
|
+
class PostsTest < ApplicationSystemTestCase
|
|
182
|
+
setup do
|
|
183
|
+
@user = users(:one)
|
|
184
|
+
sign_in_as @user
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
test "creating a post" do
|
|
188
|
+
visit new_post_url
|
|
189
|
+
|
|
190
|
+
fill_in "Title", with: "My Post"
|
|
191
|
+
fill_in "Body", with: "Post content here"
|
|
192
|
+
click_button "Create Post"
|
|
193
|
+
|
|
194
|
+
assert_text "Post created successfully"
|
|
195
|
+
assert_text "My Post"
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
test "shows validation errors" do
|
|
199
|
+
visit new_post_url
|
|
200
|
+
click_button "Create Post"
|
|
201
|
+
|
|
202
|
+
assert_text "can't be blank"
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### ViewComponent Test Template
|
|
208
|
+
|
|
209
|
+
```ruby
|
|
210
|
+
# test/components/status_badge_component_test.rb
|
|
211
|
+
require "test_helper"
|
|
212
|
+
|
|
213
|
+
class StatusBadgeComponentTest < ViewComponent::TestCase
|
|
214
|
+
test "renders published badge" do
|
|
215
|
+
render_inline(StatusBadgeComponent.new(status: :published))
|
|
216
|
+
|
|
217
|
+
assert_selector ".badge", text: "Published"
|
|
218
|
+
assert_selector ".bg-green-100"
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
test "renders draft badge" do
|
|
222
|
+
render_inline(StatusBadgeComponent.new(status: :draft))
|
|
223
|
+
|
|
224
|
+
assert_selector ".badge", text: "Draft"
|
|
225
|
+
assert_selector ".bg-gray-100"
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Policy Test Template
|
|
231
|
+
|
|
232
|
+
```ruby
|
|
233
|
+
# test/policies/post_policy_test.rb
|
|
234
|
+
require "test_helper"
|
|
235
|
+
|
|
236
|
+
class PostPolicyTest < ActiveSupport::TestCase
|
|
237
|
+
setup do
|
|
238
|
+
@owner = users(:one)
|
|
239
|
+
@other = users(:two)
|
|
240
|
+
@post = posts(:one) # belongs to @owner
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
test "owner can update" do
|
|
244
|
+
assert PostPolicy.new(@owner, @post).update?
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
test "non-owner cannot update" do
|
|
248
|
+
assert_not PostPolicy.new(@other, @post).update?
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
test "scope returns only authorized records" do
|
|
252
|
+
scope = PostPolicy::Scope.new(@owner, Post).resolve
|
|
253
|
+
assert_includes scope, @post
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## Step 4: Verify Failure
|
|
259
|
+
|
|
260
|
+
Run the test:
|
|
261
|
+
```bash
|
|
262
|
+
bin/rails test test/models/post_test.rb --verbose
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
The test MUST fail with a clear error. If it passes immediately, either:
|
|
266
|
+
- The behavior already exists
|
|
267
|
+
- The test isn't testing what you think
|
|
268
|
+
|
|
269
|
+
## Step 5: Implement (GREEN)
|
|
270
|
+
|
|
271
|
+
Write the MINIMUM code to pass:
|
|
272
|
+
- No optimization
|
|
273
|
+
- No edge case handling beyond what's tested
|
|
274
|
+
- No refactoring
|
|
275
|
+
- Just make it work
|
|
276
|
+
|
|
277
|
+
## Step 6: Verify Pass
|
|
278
|
+
|
|
279
|
+
```bash
|
|
280
|
+
bin/rails test test/models/post_test.rb --verbose
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
## Step 7: Refactor
|
|
284
|
+
|
|
285
|
+
Improve code while tests stay green:
|
|
286
|
+
- Extract methods for clarity
|
|
287
|
+
- Improve naming
|
|
288
|
+
- Remove duplication
|
|
289
|
+
- Simplify logic
|
|
290
|
+
|
|
291
|
+
**Rule:** Make ONE change at a time, run tests after EACH change.
|
|
292
|
+
|
|
293
|
+
## Step 8: Final Verification
|
|
294
|
+
|
|
295
|
+
Run all related tests:
|
|
296
|
+
```bash
|
|
297
|
+
bin/rails test
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## Fixtures Best Practices
|
|
301
|
+
|
|
302
|
+
```yaml
|
|
303
|
+
# test/fixtures/posts.yml
|
|
304
|
+
published:
|
|
305
|
+
title: Published Post
|
|
306
|
+
body: This is published content
|
|
307
|
+
user: one
|
|
308
|
+
created_at: <%= 1.day.ago %>
|
|
309
|
+
|
|
310
|
+
draft:
|
|
311
|
+
title: Draft Post
|
|
312
|
+
body: This is draft content
|
|
313
|
+
user: one
|
|
314
|
+
|
|
315
|
+
recent:
|
|
316
|
+
title: Recent Post
|
|
317
|
+
body: Recent content
|
|
318
|
+
user: one
|
|
319
|
+
created_at: <%= 1.hour.ago %>
|
|
320
|
+
|
|
321
|
+
old:
|
|
322
|
+
title: Old Post
|
|
323
|
+
body: Old content
|
|
324
|
+
user: two
|
|
325
|
+
created_at: <%= 1.year.ago %>
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
**Fixture naming tips:**
|
|
329
|
+
- Use descriptive names: `published`, `draft`, `admin_post`
|
|
330
|
+
- Reference other fixtures by name: `user: one`
|
|
331
|
+
- Use ERB for dynamic values: `<%= Time.current %>`
|
|
332
|
+
|
|
333
|
+
## Test Helper Patterns
|
|
334
|
+
|
|
335
|
+
```ruby
|
|
336
|
+
# test/test_helper.rb
|
|
337
|
+
class ActiveSupport::TestCase
|
|
338
|
+
# Use fixtures for all tests
|
|
339
|
+
fixtures :all
|
|
340
|
+
|
|
341
|
+
# Authentication helper
|
|
342
|
+
def sign_in_as(user)
|
|
343
|
+
post session_url, params: { email: user.email, password: "password" }
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def sign_out
|
|
347
|
+
delete session_url
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
## Anti-Patterns to Avoid
|
|
353
|
+
|
|
354
|
+
1. **Testing implementation, not behavior** — Test what it does, not how
|
|
355
|
+
2. **Too many assertions per test** — One concept per test
|
|
356
|
+
3. **Brittle tests** — Don't assert exact timestamps or error messages
|
|
357
|
+
4. **Slow tests** — Prefer model tests over system tests when possible
|
|
358
|
+
5. **Skipping the RED step** — Always see it fail first
|
|
359
|
+
6. **Over-mocking** — Use real objects with fixtures when possible
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: viewcomponent-patterns
|
|
3
|
+
description: Creates ViewComponents for reusable UI elements with TDD. Use when building reusable UI components, extracting complex partials, creating cards/tables/badges/modals, or when user mentions ViewComponent, components, or reusable UI.
|
|
4
|
+
allowed-tools: Read, Write, Edit, Bash, Glob, Grep
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# ViewComponent Patterns for Rails 8
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
ViewComponents are Ruby objects for building reusable, testable view components:
|
|
12
|
+
- Faster than partials (no partial lookup)
|
|
13
|
+
- Unit testable without full request cycle
|
|
14
|
+
- Encapsulate view logic with Ruby
|
|
15
|
+
- Type-safe with explicit interfaces
|
|
16
|
+
|
|
17
|
+
## TDD Workflow
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
ViewComponent Progress:
|
|
21
|
+
- [ ] Step 1: Write component test (RED)
|
|
22
|
+
- [ ] Step 2: Run test (fails - no component)
|
|
23
|
+
- [ ] Step 3: Generate component skeleton
|
|
24
|
+
- [ ] Step 4: Implement component
|
|
25
|
+
- [ ] Step 5: Run test (GREEN)
|
|
26
|
+
- [ ] Step 6: Add variants/slots if needed
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Step 1: Component Test (RED)
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
# test/components/card_component_test.rb
|
|
33
|
+
require "test_helper"
|
|
34
|
+
|
|
35
|
+
class CardComponentTest < ViewComponent::TestCase
|
|
36
|
+
test "renders the title" do
|
|
37
|
+
render_inline(CardComponent.new(title: "Test Title"))
|
|
38
|
+
assert_selector "h3", text: "Test Title"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
test "renders content block" do
|
|
42
|
+
render_inline(CardComponent.new(title: "Title")) { "Card content" }
|
|
43
|
+
assert_text "Card content"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
test "renders subtitle when provided" do
|
|
47
|
+
render_inline(CardComponent.new(title: "Title", subtitle: "Subtitle"))
|
|
48
|
+
assert_selector "p", text: "Subtitle"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
test "does not render subtitle element when not provided" do
|
|
52
|
+
render_inline(CardComponent.new(title: "Title"))
|
|
53
|
+
assert_no_selector ".subtitle"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Step 2-4: Implement Component
|
|
59
|
+
|
|
60
|
+
### Base Component
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
# app/components/application_component.rb
|
|
64
|
+
class ApplicationComponent < ViewComponent::Base
|
|
65
|
+
include ActionView::Helpers::TagHelper
|
|
66
|
+
include ActionView::Helpers::NumberHelper
|
|
67
|
+
|
|
68
|
+
def not_specified_span
|
|
69
|
+
tag.span(I18n.t("components.common.not_specified"), class: "text-slate-400 italic")
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Basic Component
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
# app/components/card_component.rb
|
|
78
|
+
class CardComponent < ApplicationComponent
|
|
79
|
+
def initialize(title:, subtitle: nil)
|
|
80
|
+
@title = title
|
|
81
|
+
@subtitle = subtitle
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
attr_reader :title, :subtitle
|
|
85
|
+
|
|
86
|
+
def subtitle?
|
|
87
|
+
subtitle.present?
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
```erb
|
|
93
|
+
<%# app/components/card_component.html.erb %>
|
|
94
|
+
<div class="bg-white rounded-lg shadow p-6">
|
|
95
|
+
<h3 class="text-lg font-semibold text-slate-900"><%= title %></h3>
|
|
96
|
+
<% if subtitle? %>
|
|
97
|
+
<p class="subtitle text-sm text-slate-500"><%= subtitle %></p>
|
|
98
|
+
<% end %>
|
|
99
|
+
<div class="mt-4">
|
|
100
|
+
<%= content %>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Common Patterns
|
|
106
|
+
|
|
107
|
+
### Pattern 1: Status Badge
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
# app/components/badge_component.rb
|
|
111
|
+
class BadgeComponent < ApplicationComponent
|
|
112
|
+
VARIANTS = {
|
|
113
|
+
success: "bg-green-100 text-green-800",
|
|
114
|
+
warning: "bg-yellow-100 text-yellow-800",
|
|
115
|
+
error: "bg-red-100 text-red-800",
|
|
116
|
+
info: "bg-blue-100 text-blue-800",
|
|
117
|
+
neutral: "bg-slate-100 text-slate-800"
|
|
118
|
+
}.freeze
|
|
119
|
+
|
|
120
|
+
def initialize(text:, variant: :neutral)
|
|
121
|
+
@text = text
|
|
122
|
+
@variant = variant.to_sym
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def call
|
|
126
|
+
tag.span(
|
|
127
|
+
@text,
|
|
128
|
+
class: "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium #{variant_classes}"
|
|
129
|
+
)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
private
|
|
133
|
+
|
|
134
|
+
def variant_classes
|
|
135
|
+
VARIANTS.fetch(@variant, VARIANTS[:neutral])
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Testing:
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
# test/components/badge_component_test.rb
|
|
144
|
+
require "test_helper"
|
|
145
|
+
|
|
146
|
+
class BadgeComponentTest < ViewComponent::TestCase
|
|
147
|
+
test "renders success variant" do
|
|
148
|
+
render_inline(BadgeComponent.new(text: "Active", variant: :success))
|
|
149
|
+
assert_selector ".bg-green-100"
|
|
150
|
+
assert_text "Active"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
test "renders error variant" do
|
|
154
|
+
render_inline(BadgeComponent.new(text: "Failed", variant: :error))
|
|
155
|
+
assert_selector ".bg-red-100"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
test "defaults to neutral variant" do
|
|
159
|
+
render_inline(BadgeComponent.new(text: "Unknown"))
|
|
160
|
+
assert_selector ".bg-slate-100"
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Pattern 2: Component with Slots
|
|
166
|
+
|
|
167
|
+
```ruby
|
|
168
|
+
# app/components/card_component.rb
|
|
169
|
+
class CardComponent < ApplicationComponent
|
|
170
|
+
renders_one :header
|
|
171
|
+
renders_one :footer
|
|
172
|
+
renders_many :actions
|
|
173
|
+
|
|
174
|
+
def initialize(title: nil)
|
|
175
|
+
@title = title
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Testing slots:
|
|
181
|
+
|
|
182
|
+
```ruby
|
|
183
|
+
# test/components/card_component_test.rb
|
|
184
|
+
class CardComponentTest < ViewComponent::TestCase
|
|
185
|
+
test "renders header slot" do
|
|
186
|
+
render_inline(CardComponent.new) do |card|
|
|
187
|
+
card.with_header { "Custom Header" }
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
assert_text "Custom Header"
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
test "renders multiple action slots" do
|
|
194
|
+
render_inline(CardComponent.new) do |card|
|
|
195
|
+
card.with_action { "Action 1" }
|
|
196
|
+
card.with_action { "Action 2" }
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
assert_text "Action 1"
|
|
200
|
+
assert_text "Action 2"
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Pattern 3: Collection Component
|
|
206
|
+
|
|
207
|
+
```ruby
|
|
208
|
+
# app/components/event_card_component.rb
|
|
209
|
+
class EventCardComponent < ApplicationComponent
|
|
210
|
+
with_collection_parameter :event
|
|
211
|
+
|
|
212
|
+
def initialize(event:)
|
|
213
|
+
@event = event
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
delegate :name, :event_date, :status, to: :@event
|
|
217
|
+
|
|
218
|
+
def formatted_date
|
|
219
|
+
return not_specified_span if event_date.nil?
|
|
220
|
+
I18n.l(event_date, format: :long)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def status_badge
|
|
224
|
+
render BadgeComponent.new(text: status.humanize, variant: status_variant)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
private
|
|
228
|
+
|
|
229
|
+
def status_variant
|
|
230
|
+
case status.to_sym
|
|
231
|
+
when :confirmed then :success
|
|
232
|
+
when :cancelled then :error
|
|
233
|
+
when :pending then :warning
|
|
234
|
+
else :neutral
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Testing collections:
|
|
241
|
+
|
|
242
|
+
```ruby
|
|
243
|
+
# test/components/event_card_component_test.rb
|
|
244
|
+
class EventCardComponentTest < ViewComponent::TestCase
|
|
245
|
+
test "renders single event" do
|
|
246
|
+
event = events(:one)
|
|
247
|
+
render_inline(EventCardComponent.new(event: event))
|
|
248
|
+
assert_text event.name
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
test "renders collection" do
|
|
252
|
+
events_list = [events(:one), events(:two)]
|
|
253
|
+
render_inline(EventCardComponent.with_collection(events_list))
|
|
254
|
+
assert_selector ".event-card", count: 2
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Pattern 4: Modal Component
|
|
260
|
+
|
|
261
|
+
```ruby
|
|
262
|
+
# app/components/modal_component.rb
|
|
263
|
+
class ModalComponent < ApplicationComponent
|
|
264
|
+
renders_one :trigger
|
|
265
|
+
renders_one :title
|
|
266
|
+
renders_one :footer
|
|
267
|
+
|
|
268
|
+
def initialize(id:, size: :medium)
|
|
269
|
+
@id = id
|
|
270
|
+
@size = size
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def size_classes
|
|
274
|
+
case @size
|
|
275
|
+
when :small then "max-w-md"
|
|
276
|
+
when :medium then "max-w-lg"
|
|
277
|
+
when :large then "max-w-2xl"
|
|
278
|
+
when :full then "max-w-full mx-4"
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## Usage in Views
|
|
285
|
+
|
|
286
|
+
```erb
|
|
287
|
+
<%# Simple component %>
|
|
288
|
+
<%= render BadgeComponent.new(text: "Active", variant: :success) %>
|
|
289
|
+
|
|
290
|
+
<%# Component with block %>
|
|
291
|
+
<%= render CardComponent.new(title: "Stats") do %>
|
|
292
|
+
<p>Content here</p>
|
|
293
|
+
<% end %>
|
|
294
|
+
|
|
295
|
+
<%# Component with slots %>
|
|
296
|
+
<%= render CardComponent.new do |card| %>
|
|
297
|
+
<% card.with_header do %>
|
|
298
|
+
<h2>Header</h2>
|
|
299
|
+
<% end %>
|
|
300
|
+
Content
|
|
301
|
+
<% end %>
|
|
302
|
+
|
|
303
|
+
<%# Collection %>
|
|
304
|
+
<%= render EventCardComponent.with_collection(@events) %>
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## Previews (Development)
|
|
308
|
+
|
|
309
|
+
```ruby
|
|
310
|
+
# test/components/previews/badge_component_preview.rb
|
|
311
|
+
class BadgeComponentPreview < ViewComponent::Preview
|
|
312
|
+
def success
|
|
313
|
+
render BadgeComponent.new(text: "Active", variant: :success)
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def error
|
|
317
|
+
render BadgeComponent.new(text: "Failed", variant: :error)
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
Access at: `http://localhost:3000/rails/view_components`
|
|
323
|
+
|
|
324
|
+
## Checklist
|
|
325
|
+
|
|
326
|
+
- [ ] Test written first (RED)
|
|
327
|
+
- [ ] Extends `ApplicationComponent`
|
|
328
|
+
- [ ] Uses slots for flexible content
|
|
329
|
+
- [ ] Variants use constants (Open/Closed)
|
|
330
|
+
- [ ] Tested with different inputs
|
|
331
|
+
- [ ] Collection rendering tested
|
|
332
|
+
- [ ] Preview created for development
|
|
333
|
+
- [ ] All tests GREEN
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
4.0.1
|