compony 0.11.12 → 0.11.13

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 (138) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +2 -0
  3. data/CHANGELOG.md +9 -0
  4. data/Gemfile +3 -0
  5. data/Gemfile.lock +145 -106
  6. data/README.md +12 -1
  7. data/Rakefile +10 -0
  8. data/TODO.md +2 -1
  9. data/VERSION +1 -1
  10. data/compony.gemspec +5 -3
  11. data/doc/ComponentGenerator.html +1 -1
  12. data/doc/Components.html +1 -1
  13. data/doc/ComponentsGenerator.html +1 -1
  14. data/doc/Compony/Component.html +1 -1
  15. data/doc/Compony/ComponentMixins/Default/Labelling.html +1 -1
  16. data/doc/Compony/ComponentMixins/Default/Standalone/ResourcefulVerbDsl.html +1 -1
  17. data/doc/Compony/ComponentMixins/Default/Standalone/StandaloneDsl.html +1 -1
  18. data/doc/Compony/ComponentMixins/Default/Standalone/VerbDsl.html +1 -1
  19. data/doc/Compony/ComponentMixins/Default/Standalone.html +1 -1
  20. data/doc/Compony/ComponentMixins/Default.html +1 -1
  21. data/doc/Compony/ComponentMixins/Resourceful.html +1 -1
  22. data/doc/Compony/ComponentMixins.html +1 -1
  23. data/doc/Compony/Components/Buttons/CssButton.html +1 -1
  24. data/doc/Compony/Components/Buttons/Link.html +1 -1
  25. data/doc/Compony/Components/Buttons.html +1 -1
  26. data/doc/Compony/Components/Destroy.html +1 -1
  27. data/doc/Compony/Components/Edit.html +1 -1
  28. data/doc/Compony/Components/Form.html +1 -1
  29. data/doc/Compony/Components/Index.html +1 -1
  30. data/doc/Compony/Components/List.html +1 -1
  31. data/doc/Compony/Components/New.html +1 -1
  32. data/doc/Compony/Components/Show.html +1 -1
  33. data/doc/Compony/Components/WithForm.html +1 -1
  34. data/doc/Compony/Components.html +1 -1
  35. data/doc/Compony/ControllerMixin.html +1 -1
  36. data/doc/Compony/Engine.html +1 -1
  37. data/doc/Compony/Intent.html +1 -1
  38. data/doc/Compony/ManageIntentsDsl.html +2 -2
  39. data/doc/Compony/MethodAccessibleHash.html +1 -1
  40. data/doc/Compony/ModelFields/Anchormodel.html +1 -1
  41. data/doc/Compony/ModelFields/Association.html +1 -1
  42. data/doc/Compony/ModelFields/Attachment.html +1 -1
  43. data/doc/Compony/ModelFields/Base.html +8 -2
  44. data/doc/Compony/ModelFields/Boolean.html +1 -1
  45. data/doc/Compony/ModelFields/Color.html +1 -1
  46. data/doc/Compony/ModelFields/Currency.html +1 -1
  47. data/doc/Compony/ModelFields/Date.html +1 -1
  48. data/doc/Compony/ModelFields/Datetime.html +1 -1
  49. data/doc/Compony/ModelFields/Decimal.html +1 -1
  50. data/doc/Compony/ModelFields/Email.html +1 -1
  51. data/doc/Compony/ModelFields/Float.html +1 -1
  52. data/doc/Compony/ModelFields/Integer.html +1 -1
  53. data/doc/Compony/ModelFields/Percentage.html +1 -1
  54. data/doc/Compony/ModelFields/Phone.html +1 -1
  55. data/doc/Compony/ModelFields/RichText.html +1 -1
  56. data/doc/Compony/ModelFields/String.html +1 -1
  57. data/doc/Compony/ModelFields/Text.html +1 -1
  58. data/doc/Compony/ModelFields/Time.html +1 -1
  59. data/doc/Compony/ModelFields/Url.html +1 -1
  60. data/doc/Compony/ModelFields.html +1 -1
  61. data/doc/Compony/ModelMixin.html +1 -1
  62. data/doc/Compony/NaturalOrdering.html +1 -1
  63. data/doc/Compony/RequestContext.html +1 -1
  64. data/doc/Compony/Version.html +1 -1
  65. data/doc/Compony/ViewHelpers.html +1 -1
  66. data/doc/Compony/VirtualModel.html +1 -1
  67. data/doc/Compony.html +1 -1
  68. data/doc/ComponyController.html +1 -1
  69. data/doc/_index.html +1 -1
  70. data/doc/file.CHANGELOG.html +84 -77
  71. data/doc/file.README.html +21 -11
  72. data/doc/file.basic_component.html +14 -14
  73. data/doc/file.cookbook.html +27 -27
  74. data/doc/file.destroy.html +2 -2
  75. data/doc/file.dsl_reference.html +199 -199
  76. data/doc/file.edit.html +3 -3
  77. data/doc/file.example.html +13 -13
  78. data/doc/file.example_advanced.html +10 -10
  79. data/doc/file.feasibility.html +2 -2
  80. data/doc/file.form.html +7 -7
  81. data/doc/file.generators.html +3 -3
  82. data/doc/file.glossary.html +44 -44
  83. data/doc/file.gotchas.html +35 -35
  84. data/doc/file.index.html +10 -10
  85. data/doc/file.inheritance.html +5 -5
  86. data/doc/file.installation.html +9 -9
  87. data/doc/file.integrations.html +46 -46
  88. data/doc/file.intents.html +11 -11
  89. data/doc/file.internal_datastructures.html +6 -6
  90. data/doc/file.list.html +35 -35
  91. data/doc/file.maintaining.html +21 -21
  92. data/doc/file.model_fields.html +3 -3
  93. data/doc/file.nesting.html +5 -5
  94. data/doc/file.new.html +3 -3
  95. data/doc/file.ownership.html +2 -2
  96. data/doc/file.patterns.html +50 -50
  97. data/doc/file.pre_built_components.html +2 -2
  98. data/doc/file.resourceful.html +5 -5
  99. data/doc/file.show.html +17 -17
  100. data/doc/file.standalone.html +12 -12
  101. data/doc/file.virtual_models.html +2 -2
  102. data/doc/file.with_form.html +23 -23
  103. data/doc/index.html +21 -11
  104. data/doc/top-level-namespace.html +1 -1
  105. data/lib/compony/model_fields/base.rb +3 -0
  106. data/spec/README.md +22 -0
  107. data/spec/compony/component_spec.rb +80 -0
  108. data/spec/compony/compony_spec.rb +102 -0
  109. data/spec/compony/intent_spec.rb +94 -0
  110. data/spec/compony/method_accessible_hash_spec.rb +32 -0
  111. data/spec/compony/model_fields_spec.rb +70 -0
  112. data/spec/compony/model_mixin_spec.rb +81 -0
  113. data/spec/compony/natural_ordering_spec.rb +61 -0
  114. data/spec/dummy/app/components/statics/home.rb +17 -0
  115. data/spec/dummy/app/components/statics/secret.rb +15 -0
  116. data/spec/dummy/app/components/users/destroy.rb +2 -0
  117. data/spec/dummy/app/components/users/edit.rb +2 -0
  118. data/spec/dummy/app/components/users/form.rb +14 -0
  119. data/spec/dummy/app/components/users/index.rb +29 -0
  120. data/spec/dummy/app/components/users/new.rb +2 -0
  121. data/spec/dummy/app/components/users/show.rb +26 -0
  122. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  123. data/spec/dummy/app/models/ability.rb +7 -0
  124. data/spec/dummy/app/models/application_record.rb +4 -0
  125. data/spec/dummy/app/models/user.rb +22 -0
  126. data/spec/dummy/app/views/layouts/application.html.erb +7 -0
  127. data/spec/dummy/config/application.rb +20 -0
  128. data/spec/dummy/config/database.yml +3 -0
  129. data/spec/dummy/config/environment.rb +3 -0
  130. data/spec/dummy/config/initializers/simple_form.rb +9 -0
  131. data/spec/dummy/config/routes.rb +3 -0
  132. data/spec/dummy/db/schema.rb +14 -0
  133. data/spec/rails_helper.rb +23 -0
  134. data/spec/requests/authorization_spec.rb +16 -0
  135. data/spec/requests/plain_component_spec.rb +9 -0
  136. data/spec/requests/routing_spec.rb +20 -0
  137. data/spec/requests/users_crud_spec.rb +99 -0
  138. metadata +62 -1
@@ -0,0 +1,81 @@
1
+ RSpec.describe Compony::ModelMixin do
2
+ describe '.field' do
3
+ it 'registers fields with the correct types' do
4
+ expect(User.fields[:name]).to be_a(Compony::ModelFields::String)
5
+ expect(User.fields[:age]).to be_a(Compony::ModelFields::Integer)
6
+ expect(User.fields[:comment]).to be_a(Compony::ModelFields::Text)
7
+ expect(User.fields[:email]).to be_a(Compony::ModelFields::Email)
8
+ expect(User.fields[:admin]).to be_a(Compony::ModelFields::Boolean)
9
+ expect(User.fields[:born_on]).to be_a(Compony::ModelFields::Date)
10
+ expect(User.fields[:created_at]).to be_a(Compony::ModelFields::Datetime)
11
+ end
12
+
13
+ it 'keeps the field name and model class' do
14
+ field = User.fields[:name]
15
+ expect(field.name).to eq(:name)
16
+ expect(field.model_class).to eq(User)
17
+ end
18
+
19
+ it 'rebinds fields to the subclass on inheritance' do
20
+ subclass = Class.new(User)
21
+ expect(subclass.fields[:name].model_class).to eq(subclass)
22
+ expect(User.fields[:name].model_class).to eq(User) # parent untouched
23
+ end
24
+
25
+ it 'does not leak fields defined on a subclass into the parent' do
26
+ subclass = Class.new(User) do
27
+ def self.name = 'SpecialUser'
28
+ field :extra, :string
29
+ end
30
+ expect(subclass.fields).to have_key(:extra)
31
+ expect(User.fields).not_to have_key(:extra)
32
+ end
33
+ end
34
+
35
+ describe 'feasibility' do
36
+ it 'is feasible when no prevention triggers' do
37
+ user = User.create!(name: 'Alice', admin: false)
38
+ expect(user.feasible?(:destroy)).to be(true)
39
+ expect(user.feasibility_messages(:destroy)).to eq([])
40
+ expect(user.full_feasibility_messages(:destroy)).to eq('')
41
+ end
42
+
43
+ it 'is infeasible when a prevention triggers' do
44
+ admin = User.create!(name: 'Root', admin: true)
45
+ expect(admin.feasible?(:destroy)).to be(false)
46
+ expect(admin.feasibility_messages(:destroy)).to eq(['Cannot destroy admins'])
47
+ expect(admin.full_feasibility_messages(:destroy)).to eq('Cannot destroy admins.')
48
+ end
49
+
50
+ it 'caches the result until recompute is requested' do
51
+ user = User.create!(name: 'Alice', admin: false)
52
+ expect(user.feasible?(:destroy)).to be(true)
53
+ user.admin = true
54
+ expect(user.feasible?(:destroy)).to be(true) # cached
55
+ expect(user.feasible?(:destroy, recompute: true)).to be(false)
56
+ end
57
+
58
+ it 'returns true for unknown actions' do
59
+ user = User.create!(name: 'Alice')
60
+ expect(user.feasible?(:frobnicate)).to be(true)
61
+ end
62
+ end
63
+
64
+ describe '.owned_by' do
65
+ it 'stores the owner attribute' do
66
+ klass = Class.new(User) do
67
+ def self.name = 'OwnedUser'
68
+ owned_by :parent
69
+ end
70
+ expect(klass.owner_model_attr).to eq(:parent)
71
+ expect(User.owner_model_attr).to be_nil
72
+ end
73
+ end
74
+
75
+ describe '#field' do
76
+ it 'returns the formatted value for a field' do
77
+ user = User.create!(name: 'Alice', age: 30)
78
+ expect(user.field(:name, nil)).to eq('Alice')
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,61 @@
1
+ RSpec.describe Compony::NaturalOrdering do
2
+ subject(:collection) { described_class.new }
3
+
4
+ it 'appends elements in order' do
5
+ collection.natural_push(:a, 1)
6
+ collection.natural_push(:b, 2)
7
+ expect(collection.map(&:name)).to eq(%i[a b])
8
+ expect(collection.map(&:payload)).to eq([1, 2])
9
+ end
10
+
11
+ it 'inserts before a given element' do
12
+ collection.natural_push(:a, 1)
13
+ collection.natural_push(:c, 3)
14
+ collection.natural_push(:b, 2, before: :c)
15
+ expect(collection.map(&:name)).to eq(%i[a b c])
16
+ end
17
+
18
+ it 'overwrites an existing element in place' do
19
+ collection.natural_push(:a, 1)
20
+ collection.natural_push(:b, 2)
21
+ collection.natural_push(:a, 99)
22
+ expect(collection.map(&:name)).to eq(%i[a b])
23
+ expect(collection.first.payload).to eq(99)
24
+ end
25
+
26
+ it 'keeps old kwargs when overwriting without specifying them' do
27
+ collection.natural_push(:a, 1, hidden: true)
28
+ collection.natural_push(:a, 2)
29
+ expect(collection.first.hidden).to be(true)
30
+ expect(collection.first.payload).to eq(2)
31
+ end
32
+
33
+ it 'allows omitting the payload when overwriting' do
34
+ collection.natural_push(:a, 1)
35
+ collection.natural_push(:a, hidden: true)
36
+ expect(collection.first.payload).to eq(1)
37
+ expect(collection.first.hidden).to be(true)
38
+ end
39
+
40
+ it 'moves an existing element when overwriting with before:' do
41
+ collection.natural_push(:a, 1)
42
+ collection.natural_push(:b, 2)
43
+ collection.natural_push(:c, 3)
44
+ collection.natural_push(:c, 3, before: :a)
45
+ expect(collection.map(&:name)).to eq(%i[c a b])
46
+ end
47
+
48
+ it 'fails when inserting a new element without payload' do
49
+ expect { collection.natural_push(:a) }.to raise_error(RuntimeError, /without a payload/)
50
+ end
51
+
52
+ it 'fails when before: refers to a missing element' do
53
+ collection.natural_push(:a, 1)
54
+ expect { collection.natural_push(:b, 2, before: :nope) }.to raise_error(RuntimeError, /not found/)
55
+ end
56
+
57
+ it 'stores extra kwargs on the element' do
58
+ collection.natural_push(:a, 1, hidden: true)
59
+ expect(collection.select(&:hidden).map(&:name)).to eq([:a])
60
+ end
61
+ end
@@ -0,0 +1,17 @@
1
+ class Components::Statics::Home < Compony::Component
2
+ setup do
3
+ label(:all) { 'Home' }
4
+
5
+ standalone path: 'home' do
6
+ skip_authentication!
7
+ verb :get do
8
+ authorize { true }
9
+ end
10
+ end
11
+
12
+ content do
13
+ h1 'Welcome home'
14
+ para 'This is a plain component.'
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,15 @@
1
+ class Components::Statics::Secret < Compony::Component
2
+ setup do
3
+ label(:all) { 'Secret' }
4
+
5
+ standalone path: 'secret' do
6
+ verb :get do
7
+ authorize { false }
8
+ end
9
+ end
10
+
11
+ content do
12
+ h1 'You should never see this'
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,2 @@
1
+ class Components::Users::Destroy < Compony::Components::Destroy
2
+ end
@@ -0,0 +1,2 @@
1
+ class Components::Users::Edit < Compony::Components::Edit
2
+ end
@@ -0,0 +1,14 @@
1
+ class Components::Users::Form < Compony::Components::Form
2
+ setup do
3
+ form_fields do
4
+ concat field(:name)
5
+ concat field(:age)
6
+ concat field(:comment)
7
+ concat field(:email)
8
+ concat field(:admin)
9
+ concat field(:born_on)
10
+ end
11
+
12
+ schema_fields :name, :age, :comment, :email, :admin, :born_on
13
+ end
14
+ end
@@ -0,0 +1,29 @@
1
+ class Components::Users::Index < Compony::Component
2
+ include Compony::ComponentMixins::Resourceful
3
+
4
+ setup do
5
+ label(:all) { 'Users' }
6
+ standalone path: 'users' do
7
+ verb :get do
8
+ authorize { true }
9
+ end
10
+ end
11
+
12
+ load_data { @data = User.order(:id) }
13
+
14
+ content do
15
+ h4 'Users:'
16
+ concat render_intent(:new, :users)
17
+ div class: 'users' do
18
+ @data.each do |user|
19
+ div class: 'user' do
20
+ span user.name
21
+ concat render_intent(:show, user, button: { label: { format: :short } })
22
+ concat render_intent(:edit, user, button: { label: { format: :short } })
23
+ concat render_intent(:destroy, user, button: { label: { format: :short } })
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,2 @@
1
+ class Components::Users::New < Compony::Components::New
2
+ end
@@ -0,0 +1,26 @@
1
+ class Components::Users::Show < Compony::Component
2
+ include Compony::ComponentMixins::Resourceful
3
+
4
+ setup do
5
+ label(:short) { |_u| 'Show' }
6
+ label(:long) { |u| "Show user #{u.label}" }
7
+
8
+ standalone path: 'users/show/:id' do
9
+ verb :get do
10
+ authorize { true }
11
+ end
12
+ end
13
+
14
+ content do
15
+ h3 @data.label
16
+ table do
17
+ tr do
18
+ @data.fields.each_value { |field| th field.label }
19
+ end
20
+ tr do
21
+ @data.fields.each_value { |field| td field.value_for(@data, controller:) }
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,3 @@
1
+ class ApplicationController < ActionController::Base
2
+ attr_accessor :current_user
3
+ end
@@ -0,0 +1,7 @@
1
+ class Ability
2
+ include CanCan::Ability
3
+
4
+ def initialize(_user)
5
+ can :manage, :all
6
+ end
7
+ end
@@ -0,0 +1,4 @@
1
+ class ApplicationRecord < ActiveRecord::Base
2
+ primary_abstract_class
3
+ include Compony::ModelMixin
4
+ end
@@ -0,0 +1,22 @@
1
+ class User < ApplicationRecord
2
+ field :name, :string
3
+ field :age, :integer
4
+ field :comment, :text
5
+ field :email, :email
6
+ field :admin, :boolean
7
+ field :born_on, :date
8
+ field :margin_relative, :percentage
9
+ field :salary, :currency
10
+ field :homepage, :url
11
+ field :created_at, :datetime
12
+
13
+ validates :name, presence: true
14
+
15
+ prevent :destroy, 'Cannot destroy admins' do
16
+ admin?
17
+ end
18
+
19
+ def label
20
+ name
21
+ end
22
+ end
@@ -0,0 +1,7 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head><title>Dummy</title></head>
4
+ <body data-layout="application">
5
+ <%= yield %>
6
+ </body>
7
+ </html>
@@ -0,0 +1,20 @@
1
+ require 'rails'
2
+ require 'active_record/railtie'
3
+ require 'action_controller/railtie'
4
+ require 'action_view/railtie'
5
+
6
+ require 'compony'
7
+
8
+ module Dummy
9
+ class Application < Rails::Application
10
+ config.root = File.expand_path('..', __dir__)
11
+ config.load_defaults Rails::VERSION::STRING.to_f
12
+ config.eager_load = false
13
+ config.hosts.clear
14
+ config.secret_key_base = 'dummy-secret-key-base-for-tests-only'
15
+ config.logger = Logger.new(nil)
16
+ config.active_support.deprecation = :stderr
17
+ config.action_dispatch.show_exceptions = :none
18
+ config.action_controller.allow_forgery_protection = false
19
+ end
20
+ end
@@ -0,0 +1,3 @@
1
+ test:
2
+ adapter: sqlite3
3
+ database: ':memory:'
@@ -0,0 +1,3 @@
1
+ require_relative 'application'
2
+
3
+ Rails.application.initialize!
@@ -0,0 +1,9 @@
1
+ SimpleForm.setup do |config|
2
+ config.wrappers :default, class: :input do |b|
3
+ b.use :html5
4
+ b.use :label_input
5
+ b.use :error, wrap_with: { tag: :span, class: :error }
6
+ end
7
+ config.default_wrapper = :default
8
+ config.button_class = 'btn'
9
+ end
@@ -0,0 +1,3 @@
1
+ # Compony draws its routes via the gem's own config/routes.rb (loaded by the engine).
2
+ Rails.application.routes.draw do # rubocop:disable Lint/EmptyBlock
3
+ end
@@ -0,0 +1,14 @@
1
+ ActiveRecord::Schema.define do
2
+ create_table :users, force: true do |t|
3
+ t.string :name
4
+ t.integer :age
5
+ t.text :comment
6
+ t.string :email
7
+ t.boolean :admin, default: false, null: false
8
+ t.date :born_on
9
+ t.decimal :margin_relative
10
+ t.decimal :salary
11
+ t.string :homepage
12
+ t.timestamps
13
+ end
14
+ end
@@ -0,0 +1,23 @@
1
+ ENV['RAILS_ENV'] = 'test'
2
+
3
+ require_relative 'dummy/config/environment'
4
+ require 'rspec/rails'
5
+
6
+ ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
7
+ load File.expand_path('dummy/db/schema.rb', __dir__)
8
+
9
+ RSpec.configure do |config|
10
+ config.use_transactional_fixtures = true
11
+ config.infer_spec_type_from_file_location!
12
+
13
+ config.expect_with :rspec do |expectations|
14
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
15
+ end
16
+
17
+ config.mock_with :rspec do |mocks|
18
+ mocks.verify_partial_doubles = true
19
+ end
20
+
21
+ config.order = :random
22
+ Kernel.srand config.seed
23
+ end
@@ -0,0 +1,16 @@
1
+ RSpec.describe 'Authorization', type: :request do
2
+ it 'denies access when the authorize block returns false' do
3
+ expect { get '/secret' }.to raise_error(CanCan::AccessDenied)
4
+ end
5
+
6
+ it 'grants access when the authorize block returns true' do
7
+ get '/home'
8
+ expect(response).to have_http_status(:ok)
9
+ end
10
+
11
+ it 'hides buttons pointing to unauthorized components' do
12
+ comp = Components::Statics::Secret.new
13
+ controller = ApplicationController.new
14
+ expect(comp.standalone_access_permitted_for?(controller, verb: :get)).to be(false)
15
+ end
16
+ end
@@ -0,0 +1,9 @@
1
+ RSpec.describe 'Plain standalone component', type: :request do
2
+ it 'renders content within the application layout' do
3
+ get '/home'
4
+ expect(response).to have_http_status(:ok)
5
+ expect(response.body).to include('data-layout="application"')
6
+ expect(response.body).to include('<h1>Welcome home</h1>')
7
+ expect(response.body).to include('<p>This is a plain component.</p>')
8
+ end
9
+ end
@@ -0,0 +1,20 @@
1
+ RSpec.describe 'Compony routing', type: :request do
2
+ it 'generates a route per standalone component' do
3
+ routes = Rails.application.routes.routes.map { |r| r.path.spec.to_s }
4
+ expect(routes).to include('/users(.:format)')
5
+ expect(routes).to include('/users/show/:id(.:format)')
6
+ expect(routes).to include('/home(.:format)')
7
+ end
8
+
9
+ it 'creates named path helpers' do
10
+ expect(Rails.application.routes.url_helpers.index_users_comp_path).to eq('/users')
11
+ rescue NoMethodError
12
+ # Fallback: assert helper name from standalone config
13
+ helper = Components::Users::Index.new.standalone_configs.values.first.path_helper_name
14
+ expect(Rails.application.routes.url_helpers.send(:"#{helper}_path")).to eq('/users')
15
+ end
16
+
17
+ it 'routes only the configured verbs' do
18
+ expect { post '/users/show/1' }.to raise_error(ActionController::RoutingError)
19
+ end
20
+ end
@@ -0,0 +1,99 @@
1
+ RSpec.describe 'Users CRUD via pre-built components', type: :request do
2
+ describe 'index' do
3
+ it 'lists users with buttons' do
4
+ alice = User.create!(name: 'Alice')
5
+ get '/users'
6
+ expect(response).to have_http_status(:ok)
7
+ expect(response.body).to include('Alice')
8
+ expect(response.body).to include("/users/show/#{alice.id}")
9
+ end
10
+
11
+ it 'disables the destroy button for admins (feasibility)' do
12
+ User.create!(name: 'Root', admin: true)
13
+ get '/users'
14
+ expect(response.body).to include('Cannot destroy admins.')
15
+ expect(response.body).to include('disabled')
16
+ end
17
+ end
18
+
19
+ describe 'show' do
20
+ it 'renders the fields of the user' do
21
+ user = User.create!(name: 'Alice', age: 30, comment: 'Hi there')
22
+ get "/users/show/#{user.id}"
23
+ expect(response).to have_http_status(:ok)
24
+ expect(response.body).to include('<h3>Alice</h3>')
25
+ expect(response.body).to include('Hi there')
26
+ expect(response.body).to include('30')
27
+ end
28
+
29
+ it 'raises RecordNotFound for a missing id' do
30
+ expect { get '/users/show/999999' }.to raise_error(ActiveRecord::RecordNotFound)
31
+ end
32
+ end
33
+
34
+ describe 'new/create' do
35
+ it 'renders the form' do
36
+ get '/users/new'
37
+ expect(response).to have_http_status(:ok)
38
+ expect(response.body).to include('form')
39
+ expect(response.body).to include('user[name]')
40
+ expect(response.body).to include('user[age]')
41
+ end
42
+
43
+ it 'creates a user and redirects' do
44
+ expect do
45
+ post '/users/new', params: { user: { name: 'Bob', age: '25', comment: 'New guy', email: 'bob@example.com', admin: '0', born_on: '2000-01-01' } }
46
+ end.to change(User, :count).by(1)
47
+ user = User.order(:id).last
48
+ expect(user.name).to eq('Bob')
49
+ expect(user.age).to eq(25)
50
+ expect(user.born_on).to eq(Date.new(2000, 1, 1))
51
+ expect(response).to have_http_status(:found)
52
+ end
53
+
54
+ it 're-renders the form on validation errors' do
55
+ expect do
56
+ post '/users/new', params: { user: { name: '' } }
57
+ end.not_to change(User, :count)
58
+ expect(response.body).to include('form')
59
+ end
60
+ end
61
+
62
+ describe 'edit/update' do
63
+ let(:user) { User.create!(name: 'Alice', age: 30) }
64
+
65
+ it 'renders the form with existing values' do
66
+ get "/users/#{user.id}/edit"
67
+ expect(response).to have_http_status(:ok)
68
+ expect(response.body).to include('Alice')
69
+ end
70
+
71
+ it 'updates the user and redirects' do
72
+ patch "/users/#{user.id}/edit", params: { user: { name: 'Alicia' } }
73
+ expect(user.reload.name).to eq('Alicia')
74
+ expect(response).to have_http_status(:found)
75
+ end
76
+
77
+ it 'does not update on validation errors' do
78
+ patch "/users/#{user.id}/edit", params: { user: { name: '' } }
79
+ expect(user.reload.name).to eq('Alice')
80
+ end
81
+ end
82
+
83
+ describe 'destroy' do
84
+ it 'asks for confirmation on GET' do
85
+ user = User.create!(name: 'Alice')
86
+ get "/users/#{user.id}/destroy"
87
+ expect(response).to have_http_status(:ok)
88
+ expect(User.exists?(user.id)).to be(true)
89
+ end
90
+
91
+ it 'destroys on DELETE and redirects' do
92
+ user = User.create!(name: 'Alice')
93
+ expect do
94
+ delete "/users/#{user.id}/destroy"
95
+ end.to change(User, :count).by(-1)
96
+ expect(response).to have_http_status(:see_other) # 303 forces a GET, required for Turbo
97
+ end
98
+ end
99
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: compony
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.12
4
+ version: 0.11.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sandro Kalbermatter
@@ -52,6 +52,34 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: 2.18.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec-rails
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '6.1'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '6.1'
69
+ - !ruby/object:Gem::Dependency
70
+ name: sqlite3
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '1.7'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '1.7'
55
83
  - !ruby/object:Gem::Dependency
56
84
  name: rails
57
85
  requirement: !ruby/object:Gem::Requirement
@@ -169,6 +197,7 @@ extensions: []
169
197
  extra_rdoc_files: []
170
198
  files:
171
199
  - ".gitignore"
200
+ - ".rspec"
172
201
  - ".ruby-version"
173
202
  - ".yardopts"
174
203
  - CHANGELOG.md
@@ -396,6 +425,38 @@ files:
396
425
  - lib/generators/components/USAGE
397
426
  - lib/generators/components/components_generator.rb
398
427
  - logo.svg
428
+ - spec/README.md
429
+ - spec/compony/component_spec.rb
430
+ - spec/compony/compony_spec.rb
431
+ - spec/compony/intent_spec.rb
432
+ - spec/compony/method_accessible_hash_spec.rb
433
+ - spec/compony/model_fields_spec.rb
434
+ - spec/compony/model_mixin_spec.rb
435
+ - spec/compony/natural_ordering_spec.rb
436
+ - spec/dummy/app/components/statics/home.rb
437
+ - spec/dummy/app/components/statics/secret.rb
438
+ - spec/dummy/app/components/users/destroy.rb
439
+ - spec/dummy/app/components/users/edit.rb
440
+ - spec/dummy/app/components/users/form.rb
441
+ - spec/dummy/app/components/users/index.rb
442
+ - spec/dummy/app/components/users/new.rb
443
+ - spec/dummy/app/components/users/show.rb
444
+ - spec/dummy/app/controllers/application_controller.rb
445
+ - spec/dummy/app/models/ability.rb
446
+ - spec/dummy/app/models/application_record.rb
447
+ - spec/dummy/app/models/user.rb
448
+ - spec/dummy/app/views/layouts/application.html.erb
449
+ - spec/dummy/config/application.rb
450
+ - spec/dummy/config/database.yml
451
+ - spec/dummy/config/environment.rb
452
+ - spec/dummy/config/initializers/simple_form.rb
453
+ - spec/dummy/config/routes.rb
454
+ - spec/dummy/db/schema.rb
455
+ - spec/rails_helper.rb
456
+ - spec/requests/authorization_spec.rb
457
+ - spec/requests/plain_component_spec.rb
458
+ - spec/requests/routing_spec.rb
459
+ - spec/requests/users_crud_spec.rb
399
460
  licenses: []
400
461
  metadata: {}
401
462
  rdoc_options: []