blanks 1.0.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.
Files changed (92) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +427 -0
  5. data/Rakefile +8 -0
  6. data/examples/advanced_features_example.rb +81 -0
  7. data/examples/assign_from_model_example.rb +54 -0
  8. data/examples/model_name_example.rb +31 -0
  9. data/examples/normalization_example.rb +39 -0
  10. data/examples/post_form_example.rb +52 -0
  11. data/lib/blanks/association_proxy.rb +92 -0
  12. data/lib/blanks/associations.rb +58 -0
  13. data/lib/blanks/base.rb +256 -0
  14. data/lib/blanks/model_naming.rb +24 -0
  15. data/lib/blanks/nested_attributes.rb +131 -0
  16. data/lib/blanks/normalization.rb +38 -0
  17. data/lib/blanks/version.rb +3 -0
  18. data/lib/blanks.rb +18 -0
  19. data/spec/blanks/association_proxy_spec.rb +214 -0
  20. data/spec/blanks/associations_spec.rb +185 -0
  21. data/spec/blanks/attributes_extraction_spec.rb +138 -0
  22. data/spec/blanks/base_spec.rb +361 -0
  23. data/spec/blanks/callbacks_spec.rb +60 -0
  24. data/spec/blanks/custom_primary_key_spec.rb +168 -0
  25. data/spec/blanks/dirty_tracking_spec.rb +61 -0
  26. data/spec/blanks/i18n_spec.rb +33 -0
  27. data/spec/blanks/id_tracking_spec.rb +168 -0
  28. data/spec/blanks/inherit_attributes_from_spec.rb +148 -0
  29. data/spec/blanks/inherit_validations_from_spec.rb +260 -0
  30. data/spec/blanks/model_naming_spec.rb +82 -0
  31. data/spec/blanks/nested_attributes_spec.rb +378 -0
  32. data/spec/blanks/normalization_spec.rb +122 -0
  33. data/spec/dummy/Gemfile +10 -0
  34. data/spec/dummy/Gemfile.lock +242 -0
  35. data/spec/dummy/Rakefile +5 -0
  36. data/spec/dummy/app/controllers/application_controller.rb +4 -0
  37. data/spec/dummy/app/controllers/simple_form/articles_controller.rb +65 -0
  38. data/spec/dummy/app/controllers/simple_form/posts_controller.rb +59 -0
  39. data/spec/dummy/app/controllers/standard/articles_controller.rb +65 -0
  40. data/spec/dummy/app/controllers/standard/posts_controller.rb +59 -0
  41. data/spec/dummy/app/forms/article_form.rb +17 -0
  42. data/spec/dummy/app/forms/cover_image_form.rb +9 -0
  43. data/spec/dummy/app/forms/post_form.rb +10 -0
  44. data/spec/dummy/app/forms/tag_form.rb +12 -0
  45. data/spec/dummy/app/models/application_record.rb +5 -0
  46. data/spec/dummy/app/models/article.rb +11 -0
  47. data/spec/dummy/app/models/cover_image.rb +7 -0
  48. data/spec/dummy/app/models/post.rb +8 -0
  49. data/spec/dummy/app/models/tag.rb +7 -0
  50. data/spec/dummy/app/views/layouts/application.html.erb +43 -0
  51. data/spec/dummy/app/views/simple_form/articles/_form.html.erb +43 -0
  52. data/spec/dummy/app/views/simple_form/articles/edit.html.erb +5 -0
  53. data/spec/dummy/app/views/simple_form/articles/index.html.erb +29 -0
  54. data/spec/dummy/app/views/simple_form/articles/new.html.erb +5 -0
  55. data/spec/dummy/app/views/simple_form/articles/show.html.erb +29 -0
  56. data/spec/dummy/app/views/simple_form/posts/_form.html.erb +19 -0
  57. data/spec/dummy/app/views/simple_form/posts/edit.html.erb +5 -0
  58. data/spec/dummy/app/views/simple_form/posts/index.html.erb +27 -0
  59. data/spec/dummy/app/views/simple_form/posts/new.html.erb +5 -0
  60. data/spec/dummy/app/views/simple_form/posts/show.html.erb +12 -0
  61. data/spec/dummy/app/views/standard/articles/_form.html.erb +61 -0
  62. data/spec/dummy/app/views/standard/articles/edit.html.erb +5 -0
  63. data/spec/dummy/app/views/standard/articles/index.html.erb +29 -0
  64. data/spec/dummy/app/views/standard/articles/new.html.erb +5 -0
  65. data/spec/dummy/app/views/standard/articles/show.html.erb +29 -0
  66. data/spec/dummy/app/views/standard/posts/_form.html.erb +30 -0
  67. data/spec/dummy/app/views/standard/posts/edit.html.erb +5 -0
  68. data/spec/dummy/app/views/standard/posts/index.html.erb +27 -0
  69. data/spec/dummy/app/views/standard/posts/new.html.erb +5 -0
  70. data/spec/dummy/app/views/standard/posts/show.html.erb +12 -0
  71. data/spec/dummy/bin/rails +6 -0
  72. data/spec/dummy/config/application.rb +18 -0
  73. data/spec/dummy/config/boot.rb +5 -0
  74. data/spec/dummy/config/database.yml +12 -0
  75. data/spec/dummy/config/environment.rb +5 -0
  76. data/spec/dummy/config/environments/development.rb +9 -0
  77. data/spec/dummy/config/environments/test.rb +8 -0
  78. data/spec/dummy/config/initializers/simple_form.rb +21 -0
  79. data/spec/dummy/config/routes.rb +15 -0
  80. data/spec/dummy/config/storage.yml +3 -0
  81. data/spec/dummy/config.ru +5 -0
  82. data/spec/dummy/db/migrate/1_create_posts.rb +12 -0
  83. data/spec/dummy/db/migrate/2_create_articles.rb +12 -0
  84. data/spec/dummy/db/migrate/3_create_cover_images.rb +12 -0
  85. data/spec/dummy/db/migrate/4_create_tags.rb +12 -0
  86. data/spec/dummy/db/migrate/5_create_active_storage_tables.rb +36 -0
  87. data/spec/dummy/db/schema.rb +82 -0
  88. data/spec/dummy/spec/examples.txt +145 -0
  89. data/spec/dummy/tmp/local_secret.txt +1 -0
  90. data/spec/examples.txt +157 -0
  91. data/spec/spec_helper.rb +21 -0
  92. metadata +159 -0
@@ -0,0 +1,361 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Blanks::Base do
6
+ describe ".attribute" do
7
+ it "defines an attribute with a type" do
8
+ form_class = Class.new(described_class) do
9
+ attribute :title, :string
10
+ end
11
+
12
+ form = form_class.new(title: "hello")
13
+
14
+ expect(form.title).to eq("hello")
15
+ end
16
+
17
+ it "supports default values" do
18
+ form_class = Class.new(described_class) do
19
+ attribute :status, :string, default: "draft"
20
+ end
21
+
22
+ form = form_class.new
23
+
24
+ expect(form.status).to eq("draft")
25
+ end
26
+
27
+ it "supports default values from proc" do
28
+ form_class = Class.new(described_class) do
29
+ attribute :timestamp, :datetime, default: -> { Time.new(2025, 1, 1) }
30
+ end
31
+
32
+ form = form_class.new
33
+
34
+ expect(form.timestamp).to eq(Time.new(2025, 1, 1))
35
+ end
36
+ end
37
+
38
+ describe "#initialize" do
39
+ it "accepts a hash of attributes" do
40
+ form_class = Class.new(described_class) do
41
+ attribute :title, :string
42
+ attribute :count, :integer
43
+ end
44
+
45
+ form = form_class.new(title: "test", count: 5)
46
+
47
+ expect(form.title).to eq("test")
48
+ end
49
+
50
+ it "accepts an empty hash" do
51
+ form_class = Class.new(described_class)
52
+
53
+ form = form_class.new({})
54
+
55
+ expect(form).to be_a(described_class)
56
+ end
57
+ end
58
+
59
+ describe "#assign_attributes" do
60
+ it "assigns multiple attributes" do
61
+ form_class = Class.new(described_class) do
62
+ attribute :title, :string
63
+ attribute :content, :string
64
+ end
65
+
66
+ form = form_class.new
67
+ form.assign_attributes(title: "new title", content: "new content")
68
+
69
+ expect(form.title).to eq("new title")
70
+ end
71
+
72
+ it "handles nested attributes with _attributes suffix" do
73
+ nested_class = Class.new(described_class) do
74
+ attribute :url, :string
75
+ end
76
+ stub_const("PhotoForm", nested_class)
77
+
78
+ form_class = Class.new(described_class) do
79
+ has_one :photo
80
+ attribute :title, :string
81
+ end
82
+
83
+ form = form_class.new
84
+ form.assign_attributes(photo_attributes: { url: "https://example.com" })
85
+
86
+ expect(form.photo.url).to eq("https://example.com")
87
+ end
88
+ end
89
+
90
+ describe ".from_model" do
91
+ it "creates new instance from model" do
92
+ model = Struct.new(:title, :content).new("model title", "model content")
93
+
94
+ form_class = Class.new(described_class) do
95
+ attribute :title, :string
96
+ attribute :content, :string
97
+ end
98
+
99
+ form = form_class.from_model(model)
100
+
101
+ expect(form.title).to eq("model title")
102
+ end
103
+ end
104
+
105
+ describe "#from_model" do
106
+ it "copies matching attributes from model" do
107
+ model = Struct.new(:title, :content).new("model title", "model content")
108
+
109
+ form_class = Class.new(described_class) do
110
+ attribute :title, :string
111
+ attribute :content, :string
112
+ end
113
+
114
+ form = form_class.new
115
+ form.from_model(model)
116
+
117
+ expect(form.title).to eq("model title")
118
+ end
119
+
120
+ it "returns self for chaining" do
121
+ model = Struct.new(:title).new("title")
122
+
123
+ form_class = Class.new(described_class) do
124
+ attribute :title, :string
125
+ end
126
+
127
+ form = form_class.new
128
+ result = form.from_model(model)
129
+
130
+ expect(result).to eq(form)
131
+ end
132
+
133
+ it "ignores attributes not defined on form" do
134
+ model = Struct.new(:title, :extra).new("title", "extra")
135
+
136
+ form_class = Class.new(described_class) do
137
+ attribute :title, :string
138
+ end
139
+
140
+ form = form_class.new
141
+ form.from_model(model)
142
+
143
+ expect(form.title).to eq("title")
144
+ end
145
+
146
+ it "handles nil model" do
147
+ form_class = Class.new(described_class) do
148
+ attribute :title, :string
149
+ end
150
+
151
+ form = form_class.new
152
+ form.from_model(nil)
153
+
154
+ expect(form.title).to be_nil
155
+ end
156
+
157
+ it "assigns has_one associations from model" do
158
+ model_photo = Struct.new(:url).new("https://example.com/photo.jpg")
159
+ model = Struct.new(:title, :photo).new("test", model_photo)
160
+
161
+ photo_form_class = Class.new(described_class) do
162
+ attribute :url, :string
163
+ end
164
+ stub_const("PhotoForm", photo_form_class)
165
+
166
+ form_class = Class.new(described_class) do
167
+ has_one :photo
168
+ attribute :title, :string
169
+ end
170
+
171
+ form = form_class.new
172
+ form.from_model(model)
173
+
174
+ expect(form.photo.url).to eq("https://example.com/photo.jpg")
175
+ end
176
+
177
+ it "assigns has_many associations from model" do
178
+ model_image1 = Struct.new(:url).new("https://example.com/1.jpg")
179
+ model_image2 = Struct.new(:url).new("https://example.com/2.jpg")
180
+ model = Struct.new(:title, :images).new("test", [model_image1, model_image2])
181
+
182
+ image_form_class = Class.new(described_class) do
183
+ attribute :url, :string
184
+ end
185
+ stub_const("ImageForm", image_form_class)
186
+
187
+ form_class = Class.new(described_class) do
188
+ has_many :images
189
+ attribute :title, :string
190
+ end
191
+
192
+ form = form_class.new
193
+ form.from_model(model)
194
+
195
+ expect(form.images.count).to eq(2)
196
+ end
197
+ end
198
+
199
+ describe "#persisted?" do
200
+ it "returns false when id is not present" do
201
+ form_class = Class.new(described_class) do
202
+ attribute :id, :integer
203
+ end
204
+
205
+ form = form_class.new
206
+
207
+ expect(form.persisted?).to be(false)
208
+ end
209
+
210
+ it "returns true when id is present" do
211
+ form_class = Class.new(described_class) do
212
+ attribute :id, :integer
213
+ end
214
+
215
+ form = form_class.new(id: 123)
216
+
217
+ expect(form.persisted?).to be(true)
218
+ end
219
+
220
+ it "returns false when id attribute is not defined" do
221
+ form_class = Class.new(described_class)
222
+
223
+ form = form_class.new
224
+
225
+ expect(form.persisted?).to be(false)
226
+ end
227
+ end
228
+
229
+ describe "#to_key" do
230
+ it "returns nil when not persisted" do
231
+ form_class = Class.new(described_class) do
232
+ attribute :id, :integer
233
+ end
234
+
235
+ form = form_class.new
236
+
237
+ expect(form.to_key).to be_nil
238
+ end
239
+
240
+ it "returns array with id when persisted" do
241
+ form_class = Class.new(described_class) do
242
+ attribute :id, :integer
243
+ end
244
+
245
+ form = form_class.new(id: 456)
246
+
247
+ expect(form.to_key).to eq([456])
248
+ end
249
+ end
250
+
251
+ describe "#to_param" do
252
+ it "returns nil when not persisted" do
253
+ form_class = Class.new(described_class) do
254
+ attribute :id, :integer
255
+ end
256
+
257
+ form = form_class.new
258
+
259
+ expect(form.to_param).to be_nil
260
+ end
261
+
262
+ it "returns id as string when persisted" do
263
+ form_class = Class.new(described_class) do
264
+ attribute :id, :integer
265
+ end
266
+
267
+ form = form_class.new(id: 789)
268
+
269
+ expect(form.to_param).to eq("789")
270
+ end
271
+ end
272
+
273
+ describe "#to_model" do
274
+ it "returns self" do
275
+ form_class = Class.new(described_class)
276
+
277
+ form = form_class.new
278
+
279
+ expect(form.to_model).to eq(form)
280
+ end
281
+ end
282
+
283
+ describe "#valid?" do
284
+ it "returns true when all validations pass" do
285
+ form_class = Class.new(described_class) do
286
+ attribute :title, :string
287
+ validates :title, presence: true
288
+ end
289
+
290
+ form = form_class.new(title: "present")
291
+
292
+ expect(form.valid?).to be(true)
293
+ end
294
+
295
+ it "returns false when validations fail" do
296
+ form_class = Class.new(described_class) do
297
+ attribute :title, :string
298
+ validates :title, presence: true
299
+ end
300
+
301
+ form = form_class.new
302
+
303
+ expect(form.valid?).to be(false)
304
+ end
305
+
306
+ it "validates nested has_one forms" do
307
+ nested_class = Class.new(described_class) do
308
+ attribute :url, :string
309
+ validates :url, presence: true
310
+ end
311
+ stub_const("PhotoForm", nested_class)
312
+
313
+ form_class = Class.new(described_class) do
314
+ has_one :photo
315
+ attribute :title, :string
316
+ end
317
+
318
+ form = form_class.new(title: "test")
319
+ form.build_photo
320
+
321
+ expect(form.valid?).to be(false)
322
+ end
323
+
324
+ it "validates nested has_many forms" do
325
+ nested_class = Class.new(described_class) do
326
+ attribute :url, :string
327
+ validates :url, presence: true
328
+ end
329
+ stub_const("ImageForm", nested_class)
330
+
331
+ form_class = Class.new(described_class) do
332
+ has_many :images
333
+ attribute :title, :string
334
+ end
335
+
336
+ form = form_class.new(title: "test")
337
+ form.images.new
338
+
339
+ expect(form.valid?).to be(false)
340
+ end
341
+
342
+ it "includes nested form errors in parent errors" do
343
+ nested_class = Class.new(described_class) do
344
+ attribute :url, :string
345
+ validates :url, presence: true
346
+ end
347
+ stub_const("PhotoForm", nested_class)
348
+
349
+ form_class = Class.new(described_class) do
350
+ has_one :photo
351
+ attribute :title, :string
352
+ end
353
+
354
+ form = form_class.new(title: "test")
355
+ form.build_photo
356
+ form.valid?
357
+
358
+ expect(form.errors.messages.keys).to include(:"photo.url")
359
+ end
360
+ end
361
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "Callbacks" do
6
+ it "runs before_validation callback" do
7
+ form_class = Class.new(Blanks::Base) do
8
+ attribute :title, :string
9
+ before_validation :normalize_title
10
+
11
+ def normalize_title
12
+ self.title = title&.strip&.downcase
13
+ end
14
+ end
15
+
16
+ form = form_class.new(title: " HELLO ")
17
+ form.valid?
18
+
19
+ expect(form.title).to eq("hello")
20
+ end
21
+
22
+ it "runs after_validation callback" do
23
+ callback_ran = false
24
+
25
+ form_class = Class.new(Blanks::Base) do
26
+ attribute :title, :string
27
+ validates :title, presence: true
28
+
29
+ after_validation do
30
+ callback_ran = true
31
+ end
32
+
33
+ define_method(:callback_ran=) { |value| callback_ran = value }
34
+ end
35
+
36
+ form = form_class.new(title: "test")
37
+ form.valid?
38
+
39
+ expect(callback_ran).to be(true)
40
+ end
41
+
42
+ it "runs around_validation callback" do
43
+ form_class = Class.new(Blanks::Base) do
44
+ attribute :title, :string
45
+ attribute :validated, :boolean, default: false
46
+
47
+ around_validation :mark_validated
48
+
49
+ def mark_validated
50
+ self.validated = true
51
+ yield
52
+ end
53
+ end
54
+
55
+ form = form_class.new(title: "test")
56
+ form.valid?
57
+
58
+ expect(form.validated).to be(true)
59
+ end
60
+ end
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "Custom primary key for nested forms" do
6
+ before(:all) do
7
+ class CustomPrimaryKeyImageForm < Blanks::Base
8
+ attribute :uuid, :string
9
+ attribute :url, :string
10
+ end
11
+
12
+ class CustomPrimaryKeyPostForm < Blanks::Base
13
+ attribute :title, :string
14
+ has_many :images, class_name: "CustomPrimaryKeyImageForm", primary_key: :uuid
15
+ end
16
+ end
17
+
18
+ after(:all) do
19
+ Object.send(:remove_const, :CustomPrimaryKeyImageForm)
20
+ Object.send(:remove_const, :CustomPrimaryKeyPostForm)
21
+ end
22
+
23
+ describe "has_many with custom primary_key" do
24
+ it "uses custom primary_key to find existing records" do
25
+ form = CustomPrimaryKeyPostForm.new
26
+ form.images.new(uuid: "abc-123", url: "original.jpg")
27
+
28
+ form.images_attributes = [
29
+ { uuid: "abc-123", url: "updated.jpg" }
30
+ ]
31
+
32
+ expect(form.images.count).to eq(1)
33
+ expect(form.images.first.url).to eq("updated.jpg")
34
+ expect(form.images.first.uuid).to eq("abc-123")
35
+ end
36
+
37
+ it "creates new record when primary_key value not found" do
38
+ form = CustomPrimaryKeyPostForm.new
39
+ form.images.new(uuid: "abc-123", url: "original.jpg")
40
+
41
+ form.images_attributes = [
42
+ { uuid: "def-456", url: "new.jpg" }
43
+ ]
44
+
45
+ expect(form.images.count).to eq(2)
46
+ expect(form.images.map(&:uuid)).to contain_exactly("abc-123", "def-456")
47
+ end
48
+
49
+ it "excludes primary_key from updated attributes" do
50
+ form = CustomPrimaryKeyPostForm.new
51
+ form.images.new(uuid: "abc-123", url: "original.jpg")
52
+
53
+ form.images_attributes = [
54
+ { uuid: "abc-123", url: "updated.jpg" }
55
+ ]
56
+
57
+ image = form.images.first
58
+ expect(image.uuid).to eq("abc-123")
59
+ expect(image.url).to eq("updated.jpg")
60
+ end
61
+ end
62
+
63
+ describe "has_many with custom primary_key and allow_destroy" do
64
+ before(:all) do
65
+ class DestroyableImageForm < Blanks::Base
66
+ attribute :uuid, :string
67
+ attribute :url, :string
68
+ end
69
+
70
+ class DestroyablePostForm < Blanks::Base
71
+ attribute :title, :string
72
+ has_many :images, class_name: "DestroyableImageForm", primary_key: :uuid, allow_destroy: true
73
+ end
74
+ end
75
+
76
+ after(:all) do
77
+ Object.send(:remove_const, :DestroyableImageForm)
78
+ Object.send(:remove_const, :DestroyablePostForm)
79
+ end
80
+
81
+ it "marks record for destruction using custom primary_key" do
82
+ form = DestroyablePostForm.new
83
+ form.images.new(uuid: "abc-123", url: "original.jpg")
84
+
85
+ form.images_attributes = [
86
+ { uuid: "abc-123", _destroy: true }
87
+ ]
88
+
89
+ expect(form.images.first.marked_for_destruction?).to be true
90
+ end
91
+ end
92
+
93
+ describe "has_many defaults to :id when no primary_key specified" do
94
+ before(:all) do
95
+ class DefaultIdImageForm < Blanks::Base
96
+ attribute :id, :integer
97
+ attribute :url, :string
98
+ end
99
+
100
+ class DefaultIdPostForm < Blanks::Base
101
+ attribute :title, :string
102
+ has_many :images, class_name: "DefaultIdImageForm"
103
+ end
104
+ end
105
+
106
+ after(:all) do
107
+ Object.send(:remove_const, :DefaultIdImageForm)
108
+ Object.send(:remove_const, :DefaultIdPostForm)
109
+ end
110
+
111
+ it "uses id as default primary_key" do
112
+ form = DefaultIdPostForm.new
113
+ form.images.new(id: 1, url: "original.jpg")
114
+
115
+ form.images_attributes = [
116
+ { id: 1, url: "updated.jpg" }
117
+ ]
118
+
119
+ expect(form.images.count).to eq(1)
120
+ expect(form.images.first.url).to eq("updated.jpg")
121
+ expect(form.images.first.id).to eq(1)
122
+ end
123
+ end
124
+
125
+ describe "AssociationProxy#find_by" do
126
+ before(:all) do
127
+ class ProxyTestForm < Blanks::Base
128
+ attribute :id, :integer
129
+ attribute :uuid, :string
130
+ attribute :url, :string
131
+ end
132
+ end
133
+
134
+ after(:all) do
135
+ Object.send(:remove_const, :ProxyTestForm)
136
+ end
137
+
138
+ it "finds record by custom attribute" do
139
+ proxy = Blanks::AssociationProxy.new("ProxyTestForm")
140
+ image = ProxyTestForm.new(uuid: "abc-123", url: "test.jpg")
141
+ proxy.push(image)
142
+
143
+ found = proxy.find_by(:uuid, "abc-123")
144
+
145
+ expect(found).to eq(image)
146
+ end
147
+
148
+ it "returns nil when attribute value not found" do
149
+ proxy = Blanks::AssociationProxy.new("ProxyTestForm")
150
+ image = ProxyTestForm.new(uuid: "abc-123", url: "test.jpg")
151
+ proxy.push(image)
152
+
153
+ found = proxy.find_by(:uuid, "not-found")
154
+
155
+ expect(found).to be_nil
156
+ end
157
+
158
+ it "uses id cache when finding by id" do
159
+ proxy = Blanks::AssociationProxy.new("ProxyTestForm")
160
+ image = ProxyTestForm.new(id: 1, url: "test.jpg")
161
+ proxy.push(image)
162
+
163
+ found = proxy.find_by(:id, 1)
164
+
165
+ expect(found).to eq(image)
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "Dirty tracking" do
6
+ it "tracks attribute changes" do
7
+ form_class = Class.new(Blanks::Base) do
8
+ attribute :title, :string
9
+ end
10
+
11
+ form = form_class.new(title: "original")
12
+ form.title = "changed"
13
+
14
+ expect(form.title_changed?).to be(true)
15
+ end
16
+
17
+ it "tracks previous value" do
18
+ form_class = Class.new(Blanks::Base) do
19
+ attribute :title, :string
20
+ end
21
+
22
+ form = form_class.new(title: "original")
23
+ form.title = "changed"
24
+
25
+ expect(form.title_was).to eq("original")
26
+ end
27
+
28
+ it "provides changes hash" do
29
+ form_class = Class.new(Blanks::Base) do
30
+ attribute :title, :string
31
+ attribute :content, :string
32
+ end
33
+
34
+ form = form_class.new(title: "original", content: "original content")
35
+ form.title = "changed"
36
+
37
+ expect(form.changes).to eq("title" => ["original", "changed"])
38
+ end
39
+
40
+ it "clears changes after assign_attributes" do
41
+ form_class = Class.new(Blanks::Base) do
42
+ attribute :title, :string
43
+ end
44
+
45
+ form = form_class.new(title: "original")
46
+ form.title = "changed"
47
+ form.assign_attributes(title: "new")
48
+
49
+ expect(form.changed?).to be(false)
50
+ end
51
+
52
+ it "detects if any attributes changed" do
53
+ form_class = Class.new(Blanks::Base) do
54
+ attribute :title, :string
55
+ end
56
+
57
+ form = form_class.new(title: "original")
58
+
59
+ expect(form.changed?).to be(false)
60
+ end
61
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "I18n support" do
6
+ it "provides human_attribute_name" do
7
+ form_class = Class.new(Blanks::Base) do
8
+ attribute :title, :string
9
+ end
10
+ stub_const("PostForm", form_class)
11
+
12
+ expect(PostForm.human_attribute_name(:title)).to eq("Title")
13
+ end
14
+
15
+ it "uses model_name for translation lookup" do
16
+ form_class = Class.new(Blanks::Base) do
17
+ attribute :title, :string
18
+ end
19
+ stub_const("PostForm", form_class)
20
+
21
+ expect(PostForm.model_name.i18n_key).to eq(:post)
22
+ end
23
+
24
+ it "supports custom translations via model_name_for" do
25
+ form_class = Class.new(Blanks::Base) do
26
+ model_name_for :article
27
+ attribute :title, :string
28
+ end
29
+ stub_const("AdminPostForm", form_class)
30
+
31
+ expect(AdminPostForm.model_name.i18n_key).to eq(:article)
32
+ end
33
+ end