inline_forms 7.2.11 → 7.9.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 (93) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +286 -0
  3. data/README.rdoc +14 -2
  4. data/app/assets/javascripts/inline_forms/inline_forms.js +26 -10
  5. data/app/assets/stylesheets/inline_forms/inline_forms.scss +33 -16
  6. data/app/controllers/concerns/versions_concern.rb +2 -3
  7. data/app/controllers/inline_forms_application_controller.rb +5 -1
  8. data/app/controllers/inline_forms_controller.rb +185 -34
  9. data/app/helpers/form_elements/ckeditor.rb +4 -30
  10. data/app/helpers/form_elements/plain_text.rb +23 -0
  11. data/app/helpers/form_elements/plain_text_area.rb +7 -3
  12. data/app/helpers/form_elements/text_area.rb +4 -44
  13. data/app/helpers/form_elements/text_area_without_ckeditor.rb +5 -4
  14. data/app/helpers/form_elements/text_field.rb +2 -2
  15. data/app/helpers/inline_forms_helper.rb +144 -74
  16. data/app/views/devise/sessions/_form.html.erb +4 -1
  17. data/app/views/inline_forms/_close.html.erb +9 -5
  18. data/app/views/inline_forms/_edit.html.erb +8 -41
  19. data/app/views/inline_forms/_list.html.erb +53 -55
  20. data/app/views/inline_forms/_new.html.erb +22 -12
  21. data/app/views/inline_forms/_show.html.erb +13 -37
  22. data/app/views/inline_forms/_versions.html.erb +5 -4
  23. data/app/views/inline_forms/_versions_list.html.erb +8 -12
  24. data/app/views/inline_forms/create_list_frame.html.erb +3 -0
  25. data/app/views/inline_forms/field_edit.html.erb +3 -0
  26. data/app/views/inline_forms/field_show.html.erb +3 -0
  27. data/app/views/inline_forms/new_record.html.erb +3 -0
  28. data/app/views/inline_forms/row_close.html.erb +13 -0
  29. data/app/views/inline_forms/row_destroyed.html.erb +9 -0
  30. data/app/views/inline_forms/row_show.html.erb +3 -0
  31. data/app/views/inline_forms/versions_list_panel.html.erb +3 -0
  32. data/app/views/inline_forms/versions_panel.html.erb +6 -0
  33. data/app/views/layouts/application.html.erb +2 -5
  34. data/app/views/layouts/inline_forms.html.erb +12 -6
  35. data/archived/README.md +47 -0
  36. data/archived/form_elements/README.md +27 -0
  37. data/archived/form_elements/chicas/README.md +31 -0
  38. data/archived/form_elements/geo_code_curacao/README.md +62 -0
  39. data/{app → archived/form_elements/geo_code_curacao/app}/helpers/form_elements/geo_code_curacao.rb +0 -1
  40. data/archived/form_elements/geo_code_curacao/app/views/geo_code_curacao/list_streets.html.erb +1 -0
  41. data/archived/form_elements/geo_code_curacao/app/views/geo_code_curacao/list_streets.js.erb +1 -0
  42. data/archived/form_elements/kansen_slider/README.md +31 -0
  43. data/archived/form_elements/tree/README.md +47 -0
  44. data/archived/form_elements/tree/app/views/inline_forms/_show_tree.html.erb +30 -0
  45. data/{app → archived/form_elements/tree/app}/views/inline_forms/_tree.html.erb +18 -5
  46. data/bin/inline_forms +22 -1
  47. data/bin/inline_forms_installer_core.rb +108 -8
  48. data/docs/ujs-to-turbo.md +192 -0
  49. data/lib/generators/USAGE +2 -2
  50. data/lib/generators/assets/stylesheets/inline_forms.scss +33 -16
  51. data/lib/inline_forms/archived_form_elements.rb +70 -0
  52. data/lib/inline_forms/version.rb +1 -1
  53. data/lib/inline_forms.rb +60 -2
  54. data/lib/installer_templates/example_app_tests/test/integration/example_app_apartment_field_turbo_test.rb +73 -0
  55. data/lib/installer_templates/example_app_tests/test/integration/example_app_apartment_name_list_test.rb +73 -0
  56. data/lib/installer_templates/example_app_tests/test/integration/example_app_apartment_name_required_test.rb +21 -0
  57. data/lib/installer_templates/example_app_tests/test/integration/example_app_apartment_photos_pagination_test.rb +227 -15
  58. data/lib/installer_templates/example_app_tests/test/integration/example_app_apartment_row_turbo_test.rb +103 -0
  59. data/lib/installer_templates/example_app_tests/test/integration/example_app_apartment_top_level_new_test.rb +70 -0
  60. data/lib/installer_templates/example_app_tests/test/integration/example_app_apartment_top_level_pagination_test.rb +40 -0
  61. data/lib/installer_templates/example_app_tests/test/integration/example_app_apartment_versions_turbo_test.rb +120 -0
  62. data/lib/installer_templates/example_app_tests/test/integration/example_app_photo_revert_test.rb +94 -0
  63. data/lib/installer_templates/example_app_tests/test/integration/example_app_turbo_layout_test.rb +6 -9
  64. data/lib/installer_templates/example_app_tests/test/models/example_app_apartment_name_validation_test.rb +16 -0
  65. data/lib/installer_templates/example_app_tests/test/models/example_app_plain_text_rich_text_edge_cases_test.rb +46 -0
  66. data/lib/installer_templates/example_app_views/apartments/name_list.html.erb +26 -0
  67. data/lib/installer_templates/example_app_views/inline_forms/_header.html.erb +45 -0
  68. data/test/archived_form_elements_test.rb +41 -0
  69. data/test/form_element_from_callee_test.rb +2 -2
  70. data/test/inline_forms_generator_test.rb +10 -0
  71. data/test/plain_text_configuration_test.rb +90 -0
  72. metadata +45 -24
  73. data/app/views/geo_code_curacao/list_streets.html.erb +0 -1
  74. data/app/views/geo_code_curacao/list_streets.js.erb +0 -1
  75. data/app/views/inline_forms/close.js.erb +0 -4
  76. data/app/views/inline_forms/edit.js.erb +0 -1
  77. data/app/views/inline_forms/list.js.erb +0 -1
  78. data/app/views/inline_forms/new.js.erb +0 -1
  79. data/app/views/inline_forms/record_destroyed.js.erb +0 -1
  80. data/app/views/inline_forms/show.js.erb +0 -1
  81. data/app/views/inline_forms/show_element.js.erb +0 -1
  82. data/app/views/inline_forms/show_undo.js.erb +0 -1
  83. data/app/views/inline_forms/update.js.erb +0 -1
  84. data/app/views/inline_forms/versions.js.erb +0 -4
  85. data/app/views/inline_forms/versions_list.js.erb +0 -1
  86. data/lib/generators/assets/javascripts/ckeditor/config.js +0 -72
  87. /data/{app → archived/form_elements/chicas/app}/helpers/form_elements/chicas_dropdown_with_family_members.rb +0 -0
  88. /data/{app → archived/form_elements/chicas/app}/helpers/form_elements/chicas_family_photo_list.rb +0 -0
  89. /data/{app → archived/form_elements/chicas/app}/helpers/form_elements/chicas_photo_list.rb +0 -0
  90. /data/{app → archived/form_elements/geo_code_curacao/app}/controllers/geo_code_curacao_controller.rb +0 -0
  91. /data/{app → archived/form_elements/geo_code_curacao/app}/models/geo_code_curacao.rb +0 -0
  92. /data/{app → archived/form_elements/kansen_slider/app}/helpers/form_elements/kansen_slider.rb +0 -0
  93. /data/{app → archived/form_elements/tree/app}/helpers/form_elements/move.rb +0 -0
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../example_app/example_integration_test_case"
4
+
5
+ # Stock _show partial: scalar fields are wrapped in <turbo-frame> and use HTML
6
+ # edit/update/cancel (no UJS on field links/forms). Same contract as name_list.
7
+ class ExampleAppApartmentFieldTurboTest < ExampleAppIntegrationTestCase
8
+ setup do
9
+ @apartment = Apartment.find_or_create_by!(name: "Turbo Field Apt") do |a|
10
+ a.title = "Turbo Field Title"
11
+ end
12
+ @frame_id = "apartment_#{@apartment.id}_name"
13
+ @turbo_headers = { "Turbo-Frame" => @frame_id }
14
+ end
15
+
16
+ test "show panel opens as HTML inside row turbo-frame" do
17
+ row_frame = "apartment_#{@apartment.id}"
18
+ get apartment_path(@apartment, update: row_frame),
19
+ headers: { "Turbo-Frame" => row_frame, "Accept" => "text/html" }
20
+
21
+ assert_response :success
22
+ assert_includes @response.body, %(<turbo-frame id="#{row_frame}">)
23
+ assert_includes @response.body, "turbo-frame",
24
+ "row show should embed _show with per-field turbo-frame wrappers"
25
+ assert_includes @response.body, @frame_id
26
+ end
27
+
28
+ test "stock scalar field edit update and cancel via turbo-frame" do
29
+ get edit_apartment_path(
30
+ @apartment,
31
+ attribute: "name",
32
+ form_element: "text_field",
33
+ update: @frame_id
34
+ ), headers: @turbo_headers
35
+ assert_response :success
36
+ assert_includes @response.body, %(<turbo-frame id="#{@frame_id}">)
37
+ refute_includes @response.body, 'data-remote="true"'
38
+
39
+ put apartment_path(
40
+ @apartment,
41
+ attribute: "name",
42
+ form_element: "text_field",
43
+ update: @frame_id
44
+ ), params: { name: "Stock Turbo Name" }, headers: @turbo_headers
45
+ assert_response :success
46
+ assert_includes @response.body, "Stock Turbo Name"
47
+ assert_equal "Stock Turbo Name", @apartment.reload.name
48
+
49
+ get apartment_path(
50
+ @apartment,
51
+ attribute: "name",
52
+ form_element: "text_field",
53
+ update: @frame_id
54
+ ), headers: @turbo_headers
55
+ assert_response :success
56
+ assert_includes @response.body, "Stock Turbo Name"
57
+ refute_includes @response.body, 'name="name"',
58
+ "cancel returns read-only field, not edit form"
59
+ end
60
+
61
+ test "field show cancel responds to html even without Turbo-Frame header" do
62
+ get apartment_path(
63
+ @apartment,
64
+ attribute: "name",
65
+ form_element: "text_field",
66
+ update: @frame_id
67
+ ), headers: { "Accept" => "text/html, application/xhtml+xml" }
68
+
69
+ assert_response :success
70
+ assert_includes @response.body, %(<turbo-frame id="#{@frame_id}">)
71
+ assert_includes @response.body, @apartment.name
72
+ end
73
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../example_app/example_integration_test_case"
4
+
5
+ # Custom page demo: same turbo-field contract as stock _show, without opening the panel.
6
+ class ExampleAppApartmentNameListTest < ExampleAppIntegrationTestCase
7
+ setup do
8
+ @apartments = 10.times.map do |i|
9
+ Apartment.find_or_create_by!(name: "NameList Apt #{i}") do |a|
10
+ a.title = "Title #{i}"
11
+ end
12
+ end
13
+ end
14
+
15
+ test "name list page is reachable and renders turbo-frame inline-edit targets for first 10 apartments" do
16
+ get apartment_name_list_path
17
+ assert_response :success
18
+
19
+ Apartment.order(:id).limit(10).each do |apartment|
20
+ assert_includes @response.body, %(<turbo-frame id="apartment_#{apartment.id}_name">),
21
+ "expected turbo-frame wrapper for apartment #{apartment.id}"
22
+ assert_includes @response.body, apartment.name,
23
+ "expected apartment name on page"
24
+ end
25
+ end
26
+
27
+ test "more menu links to apartment name list" do
28
+ get apartments_path
29
+ assert_response :success
30
+ assert_includes @response.body, apartment_name_list_path,
31
+ "expected More menu link to name list"
32
+ assert_includes @response.body, "Apartment names (first 10)"
33
+ end
34
+
35
+ test "name list field links use turbo-frame navigation not UJS remote" do
36
+ apartment = @apartments.first
37
+ get apartment_name_list_path
38
+ assert_response :success
39
+
40
+ frame_html = @response.body[%r{<turbo-frame id="apartment_#{apartment.id}_name">.*?</turbo-frame>}m]
41
+ assert frame_html, "expected turbo-frame for apartment #{apartment.id}"
42
+ assert_match(
43
+ %r/href="[^"]*\/apartments\/#{apartment.id}\/edit[^"]*update=apartment_#{apartment.id}_name[^"]*"/,
44
+ frame_html,
45
+ "expected edit link with update= matching turbo-frame id"
46
+ )
47
+ refute_includes frame_html, 'data-remote="true"',
48
+ "field edits use Turbo frames, not UJS"
49
+ end
50
+
51
+ test "name list reuses stock turbo field edit update cycle" do
52
+ apartment = @apartments.first
53
+ frame_id = "apartment_#{apartment.id}_name"
54
+ turbo_headers = { "Turbo-Frame" => frame_id }
55
+
56
+ get edit_apartment_path(
57
+ apartment,
58
+ attribute: "name",
59
+ form_element: "text_field",
60
+ update: frame_id
61
+ ), headers: turbo_headers
62
+ assert_response :success
63
+
64
+ put apartment_path(
65
+ apartment,
66
+ attribute: "name",
67
+ form_element: "text_field",
68
+ update: frame_id
69
+ ), params: { name: "Name List Turbo" }, headers: turbo_headers
70
+ assert_response :success
71
+ assert_equal "Name List Turbo", apartment.reload.name
72
+ end
73
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../example_app/example_integration_test_case"
4
+
5
+ class ExampleAppApartmentNameRequiredTest < ExampleAppIntegrationTestCase
6
+ setup do
7
+ @frame = "apartments_list"
8
+ @frame_headers = { "Turbo-Frame" => @frame, "Accept" => "text/html" }
9
+ end
10
+
11
+ test "top-level create without name does not persist" do
12
+ assert_no_difference("Apartment.count") do
13
+ post apartments_path(update: @frame),
14
+ params: { title: "Missing name" },
15
+ headers: @frame_headers
16
+ end
17
+ assert_response :success
18
+ assert_includes @response.body, 'name="name"'
19
+ assert_includes @response.body, 'class="edit_form"'
20
+ end
21
+ end
@@ -197,7 +197,7 @@ class ExampleAppApartmentPhotosPaginationTest < ExampleAppIntegrationTestCase
197
197
  )
198
198
  end
199
199
 
200
- test "row container opts out of Turbo so swapped-in UJS forms (replace photo etc.) keep working" do
200
+ test "nested photo rows are turbo-framed with Turbo presentation links (no data-turbo false on row)" do
201
201
  get photos_path(
202
202
  parent_class: "Apartment",
203
203
  parent_id: @apartment.id,
@@ -206,23 +206,235 @@ class ExampleAppApartmentPhotosPaginationTest < ExampleAppIntegrationTestCase
206
206
  )
207
207
  assert_response :success
208
208
 
209
- # The container-level opt-out is what protects the inline-edit /
210
- # replace-photo flow: when a user replaces a photo, show.js.erb /
211
- # edit.js.erb does $('#<row_id>').html(<form>), so the multipart
212
- # form that submits the upload is a descendant of this row. With
213
- # data-turbo="false" on the row, Turbo doesn't intercept that
214
- # submission -- jquery-ujs + remotipart do, the request goes out
215
- # with Accept: text/javascript, and the controller's format.js
216
- # branch handles it. Without this, Turbo Frames would send
217
- # Accept: text/html and the update action (no format.html for
218
- # not_accessible_through_html? models like Photo) raises
219
- # UnknownFormat AFTER the DB write -- a 406 with a corrupted UI.
220
209
  sample_row_id = "apartment_#{@apartment.id}_photo_#{@apartment.photos.first.id}"
221
210
  assert_match(
222
- %r{<div[^>]+id="#{Regexp.escape(sample_row_id)}"[^>]*data-turbo="false"|<div[^>]+data-turbo="false"[^>]*id="#{Regexp.escape(sample_row_id)}"},
211
+ %r{<turbo-frame[^>]*\bid="#{Regexp.escape(sample_row_id)}"},
223
212
  @response.body,
224
- "expected the per-row container (id=\"#{sample_row_id}\") to carry data-turbo=\"false\" " \
225
- "so swapped-in UJS forms inherit the Turbo opt-out"
213
+ "expected each nested photo row to be a <turbo-frame id=\"#{sample_row_id}\">"
226
214
  )
215
+ refute_match(
216
+ %r{<turbo-frame[^>]*id="#{Regexp.escape(sample_row_id)}"[^>]*data-turbo="false"},
217
+ @response.body,
218
+ "nested turbo-frame rows must not opt out of Turbo (field cancel / pagination live inside frames)"
219
+ )
220
+ assert_select %(turbo-frame##{sample_row_id} a[data-turbo='true'][data-turbo-frame='#{sample_row_id}']), minimum: 1
221
+ end
222
+
223
+ test "nested Photo versions restore targets nested row turbo-frame not bare photo id" do
224
+ photo = @apartment.photos.first!
225
+ original_name = photo.name
226
+ photo.update!(name: "#{original_name}-changed")
227
+ row_id = "apartment_#{@apartment.id}_photo_#{photo.id}"
228
+ versions_frame = "photo_#{photo.id}_versions"
229
+ version = photo.versions.where(event: "update").order(:id).last
230
+ assert version, "expected an update version to revert"
231
+
232
+ get list_versions_photo_path(photo, update: versions_frame),
233
+ headers: { "Turbo-Frame" => versions_frame, "Accept" => "text/html" }
234
+ assert_response :success
235
+ assert_includes @response.body, "data-turbo-frame=\"#{row_id}\"",
236
+ "restore must target the nested row frame (#{row_id}), not photo_#{photo.id}"
237
+
238
+ # 7.9.0: revert always responds with turbo-stream (the legacy
239
+ # `format.html` fallback was dropped).
240
+ post revert_photo_path(version.id, update: row_id),
241
+ headers: {
242
+ "Turbo-Frame" => versions_frame,
243
+ "Accept" => "text/vnd.turbo-stream.html"
244
+ }
245
+ assert_response :success
246
+ assert_includes @response.body, %(action="replace")
247
+ assert_includes @response.body, %(target="#{row_id}")
248
+ assert_equal original_name, photo.reload.name
249
+ end
250
+
251
+ test "nested Photo row opens and closes via Turbo HTML (not_accessible_through_html model)" do
252
+ photo = @apartment.photos.first!
253
+ row_id = "apartment_#{@apartment.id}_photo_#{photo.id}"
254
+ row_headers = { "Turbo-Frame" => row_id, "Accept" => "text/html" }
255
+
256
+ get photo_path(photo, update: row_id), headers: row_headers
257
+ assert_response :success
258
+ assert_includes @response.body, %(<turbo-frame id="#{row_id}">)
259
+ assert_includes @response.body, "object_presentation"
260
+
261
+ get photo_path(photo, update: row_id, close: true), headers: row_headers
262
+ assert_response :success
263
+ assert_includes @response.body, %(<turbo-frame id="#{row_id}">)
264
+ refute_includes @response.body, "object_presentation"
265
+ end
266
+
267
+ test "nested Photo name field cancel returns field show HTML (Turbo)" do
268
+ photo = @apartment.photos.first!
269
+ frame_id = "apartment_#{@apartment.id}_photo_#{photo.id}_name"
270
+ turbo_headers = { "Turbo-Frame" => frame_id, "Accept" => "text/html" }
271
+
272
+ get edit_photo_path(
273
+ photo,
274
+ attribute: "name",
275
+ form_element: "text_field",
276
+ update: frame_id
277
+ ), headers: turbo_headers
278
+ assert_response :success
279
+
280
+ get photo_path(
281
+ photo,
282
+ attribute: "name",
283
+ form_element: "text_field",
284
+ update: frame_id
285
+ ), headers: turbo_headers
286
+ assert_response :success
287
+ assert_includes @response.body, %(<turbo-frame id="#{frame_id}">)
288
+ refute_includes @response.body, 'name="name"',
289
+ "cancel must return read-only field, not the edit form"
290
+ end
291
+
292
+ # Step 3 (ujs-to-turbo.md): nested `not_accessible_through_html?` Photo + CarrierWave
293
+ # `image` must accept Turbo-driven multipart PUT inside the field `<turbo-frame>`
294
+ # (no `UnknownFormat` / 406 after DB write — regression class from 7.2.0).
295
+ test "nested Photo image field updates via Turbo multipart PUT inside field frame" do
296
+ photo = @apartment.photos.first!
297
+ frame_id = "apartment_#{@apartment.id}_photo_#{photo.id}_image"
298
+ turbo_headers = { "Turbo-Frame" => frame_id, "Accept" => "text/html" }
299
+
300
+ get edit_photo_path(
301
+ photo,
302
+ attribute: "image",
303
+ form_element: "image_field",
304
+ update: frame_id
305
+ ), headers: turbo_headers
306
+ assert_response :success
307
+ assert_includes @response.body, %(enctype="multipart/form-data"),
308
+ "image edit form must stay multipart when Turbo omits remote: true"
309
+ assert_includes @response.body, %(<turbo-frame id="#{frame_id}">)
310
+
311
+ seed_dir = Rails.root.join("db", "seed_images")
312
+ jpgs = Dir.glob(seed_dir.join("*.{jpg,jpeg}"), File::FNM_CASEFOLD).sort
313
+ assert_operator jpgs.size, :>=, 2,
314
+ "need at least two seed jpgs so replacement can differ from current mount"
315
+
316
+ replacement = jpgs.find { |abs| File.basename(abs) != photo.name } || jpgs.last
317
+ uploaded = Rack::Test::UploadedFile.new(replacement, "image/jpeg")
318
+
319
+ put photo_path(
320
+ photo,
321
+ attribute: "image",
322
+ form_element: "image_field",
323
+ update: frame_id
324
+ ),
325
+ params: { image: uploaded },
326
+ headers: turbo_headers
327
+
328
+ assert_response :success,
329
+ "multipart image update must respond with HTML (not 406 UnknownFormat)"
330
+ assert_includes @response.body, %(<turbo-frame id="#{frame_id}">)
331
+ refute_match(/UnknownFormat|406/, @response.body)
332
+
333
+ photo.reload
334
+ assert photo.image.present?, "expected CarrierWave mount after Turbo multipart PUT"
335
+ end
336
+
337
+ # 7.5.2 regression: after cancel / update on a field, the swapped
338
+ # `<turbo-frame id="…">` must contain a Turbo link
339
+ # (`data-turbo="true" data-turbo-frame="_self"`) so the user can re-open
340
+ # the editor. 7.5.1 emitted `data-remote="true"`, which jquery_ujs
341
+ # intercepts as a JS request the controller does not register, so the
342
+ # second click silently fails (no swap, no edit form).
343
+ test "nested Photo image field show after update has Turbo (not data-remote) link" do
344
+ photo = @apartment.photos.first!
345
+ frame_id = "apartment_#{@apartment.id}_photo_#{photo.id}_image"
346
+ turbo_headers = { "Turbo-Frame" => frame_id, "Accept" => "text/html" }
347
+
348
+ seed_dir = Rails.root.join("db", "seed_images")
349
+ jpgs = Dir.glob(seed_dir.join("*.{jpg,jpeg}"), File::FNM_CASEFOLD).sort
350
+ replacement = jpgs.find { |abs| File.basename(abs) != photo.name } || jpgs.last
351
+ uploaded = Rack::Test::UploadedFile.new(replacement, "image/jpeg")
352
+
353
+ put photo_path(
354
+ photo,
355
+ attribute: "image",
356
+ form_element: "image_field",
357
+ update: frame_id
358
+ ),
359
+ params: { image: uploaded },
360
+ headers: turbo_headers
361
+ assert_response :success
362
+ refute_match(
363
+ /data-remote="true"/,
364
+ @response.body,
365
+ "field_show after Turbo update must use Turbo data attributes; " \
366
+ "data-remote=\"true\" hits jquery_ujs (no JS responder) and the " \
367
+ "second click silently fails."
368
+ )
369
+ assert_match(
370
+ /data-turbo="true"/,
371
+ @response.body,
372
+ "expected Turbo data attribute on the inline-edit link inside the swapped field frame"
373
+ )
374
+ end
375
+
376
+ # Same regression on the cancel path (no DB write): clicking the field
377
+ # cancel returns the read-only field; the link inside must be a Turbo link
378
+ # so the user can re-open the editor.
379
+ test "nested Photo image field show after cancel has Turbo (not data-remote) link" do
380
+ photo = @apartment.photos.first!
381
+ frame_id = "apartment_#{@apartment.id}_photo_#{photo.id}_image"
382
+ turbo_headers = { "Turbo-Frame" => frame_id, "Accept" => "text/html" }
383
+
384
+ get photo_path(
385
+ photo,
386
+ attribute: "image",
387
+ form_element: "image_field",
388
+ update: frame_id
389
+ ), headers: turbo_headers
390
+ assert_response :success
391
+ refute_match(/data-remote="true"/, @response.body,
392
+ "cancel-side field_show must not regress to UJS data-remote=\"true\"")
393
+ assert_match(/data-turbo="true"/, @response.body)
394
+ end
395
+
396
+ test "nested Photo new cancel and create via Turbo inside associated list frame" do
397
+ frame = "apartment_#{@apartment.id}_photos"
398
+ headers = { "Turbo-Frame" => frame, "Accept" => "text/html" }
399
+
400
+ get new_photo_path(update: frame, parent_class: "Apartment", parent_id: @apartment.id),
401
+ headers: headers
402
+ assert_response :success
403
+ assert_includes @response.body, %(<turbo-frame id="#{frame}">)
404
+ assert_includes @response.body, "stylesheet", "new form must use inline_forms layout (styled)"
405
+ assert_includes @response.body, %(enctype="multipart/form-data")
406
+ assert_includes @response.body, 'class="edit_form"'
407
+ assert_includes @response.body, 'name="name"'
408
+
409
+ get photos_path(
410
+ parent_class: "Apartment",
411
+ parent_id: @apartment.id,
412
+ update: frame,
413
+ ul_needed: true
414
+ ), headers: headers
415
+ assert_response :success
416
+ assert_match %r{<turbo-frame id="#{frame}"}, @response.body
417
+ assert_match %r{<turbo-frame id="#{@update_span}"}, @response.body
418
+
419
+ seed = Rails.root.join("db/seed_images/dsc00099.jpg")
420
+ uploaded = Rack::Test::UploadedFile.new(seed, "image/jpeg")
421
+
422
+ assert_difference("Photo.count", 1) do
423
+ post photos_path(
424
+ update: frame,
425
+ parent_class: "Apartment",
426
+ parent_id: @apartment.id
427
+ ),
428
+ params: {
429
+ name: "curl_new_photo.jpg",
430
+ caption: "from turbo test",
431
+ image: uploaded
432
+ },
433
+ headers: headers
434
+ end
435
+ assert_response :success
436
+ assert_match %r{<turbo-frame id="#{frame}"}, @response.body
437
+ assert_match %r{<turbo-frame id="#{@update_span}"}, @response.body
438
+ assert_includes @response.body, "curl_new_photo.jpg"
227
439
  end
228
440
  end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../example_app/example_integration_test_case"
4
+
5
+ # Stock `/apartments` index: each row is `<turbo-frame id="apartment_<id>">`;
6
+ # opening/closing the inline panel uses Turbo GET `show` + `close` (HTML), not UJS.
7
+ class ExampleAppApartmentRowTurboTest < ExampleAppIntegrationTestCase
8
+ setup do
9
+ @apartment = Apartment.first || Apartment.create!(name: "Row Turbo Apt", title: "T")
10
+ @row_frame = "apartment_#{@apartment.id}"
11
+ @row_headers = { "Turbo-Frame" => @row_frame, "Accept" => "text/html" }
12
+ end
13
+
14
+ test "apartments index wraps each row in a turbo-frame" do
15
+ get apartments_path
16
+ assert_response :success
17
+ assert_includes @response.body, %(<turbo-frame id="#{@row_frame}">),
18
+ "expected top-level list row inside turbo-frame"
19
+ end
20
+
21
+ test "row presentation link uses turbo navigation not UJS remote" do
22
+ get apartments_path
23
+ assert_response :success
24
+ assert_select "turbo-frame##{@row_frame} div.small-11.column > a", count: 1 do |elements|
25
+ el = elements.first
26
+ assert_equal "true", el["data-turbo"]
27
+ assert_equal @row_frame, el["data-turbo-frame"]
28
+ assert_nil el["data-remote"]
29
+ end
30
+ end
31
+
32
+ test "row open show returns full panel inside matching turbo-frame" do
33
+ get apartment_path(@apartment, update: @row_frame), headers: @row_headers
34
+ assert_response :success
35
+ assert_includes @response.body, %(<turbo-frame id="#{@row_frame}">)
36
+ assert_includes @response.body, "object_presentation",
37
+ "expected expanded _show panel"
38
+ end
39
+
40
+ test "row close returns collapsed row inside matching turbo-frame" do
41
+ get apartment_path(@apartment, update: @row_frame, close: true), headers: @row_headers
42
+ assert_response :success
43
+ assert_includes @response.body, %(<turbo-frame id="#{@row_frame}">)
44
+ refute_includes @response.body, "object_presentation",
45
+ "expected _close row, not full panel"
46
+ end
47
+
48
+ test "row close responds to html without Turbo-Frame header" do
49
+ get apartment_path(
50
+ @apartment,
51
+ update: @row_frame,
52
+ close: true
53
+ ), headers: { "Accept" => "text/html, application/xhtml+xml" }
54
+
55
+ assert_response :success
56
+ assert_includes @response.body, %(<turbo-frame id="#{@row_frame}">)
57
+ end
58
+
59
+ test "row toolbar trash and destroy links use Turbo not UJS remote" do
60
+ get apartments_path
61
+ assert_response :success
62
+ assert_select "turbo-frame##{@row_frame} a[data-turbo='true'][data-turbo-frame='#{@row_frame}']", minimum: 1
63
+ refute_select "turbo-frame##{@row_frame} a[data-remote='true']"
64
+ end
65
+
66
+ test "destroy via Turbo DELETE returns undo inside matching turbo-frame" do
67
+ doomed = Apartment.create!(name: "Turbo Destroy Me", title: "X")
68
+ frame = "apartment_#{doomed.id}"
69
+ headers = { "Turbo-Frame" => frame, "Accept" => "text/html" }
70
+
71
+ delete apartment_path(doomed, update: frame), headers: headers
72
+ assert_response :success
73
+ assert_includes @response.body, %(<turbo-frame id="#{frame}">)
74
+ assert_includes @response.body, "undo"
75
+ assert_not Apartment.exists?(doomed.id)
76
+ end
77
+
78
+ test "revert via Turbo POST restores row as collapsed turbo-frame" do
79
+ doomed = Apartment.create!(name: "Turbo Revert Me", title: "Y")
80
+ apt_id = doomed.id
81
+ frame = "apartment_#{apt_id}"
82
+ delete_headers = { "Turbo-Frame" => frame, "Accept" => "text/html" }
83
+
84
+ delete apartment_path(doomed, update: frame), headers: delete_headers
85
+ assert_response :success
86
+
87
+ # 7.9.0 dropped the `format.html` fallback in `revert`; restore links
88
+ # always request a turbo-stream now (the response replaces both the
89
+ # row and the versions panel in one stream).
90
+ versions_frame = "#{frame}_versions"
91
+ destroy_version = PaperTrail::Version.where(item_type: "Apartment", item_id: apt_id).order(:id).last
92
+ post revert_apartment_path(destroy_version.id, update: frame),
93
+ headers: {
94
+ "Turbo-Frame" => versions_frame,
95
+ "Accept" => "text/vnd.turbo-stream.html"
96
+ }
97
+ assert_response :success
98
+ assert Apartment.where(name: "Turbo Revert Me").exists?
99
+ assert_includes @response.body, %(action="replace")
100
+ assert_includes @response.body, %(target="#{frame}")
101
+ assert_includes @response.body, %(target="#{versions_frame}")
102
+ end
103
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../example_app/example_integration_test_case"
4
+
5
+ # Step 4: top-level `/apartments` list, +new, cancel, and create use Turbo Frames
6
+ # (`<turbo-frame id="apartments_list">`) instead of UJS + new.js.erb / list.js.erb.
7
+ class ExampleAppApartmentTopLevelNewTest < ExampleAppIntegrationTestCase
8
+ setup do
9
+ @frame = "apartments_list"
10
+ @frame_headers = { "Turbo-Frame" => @frame, "Accept" => "text/html" }
11
+ @apartment = Apartment.find_or_create_by!(name: "Top Level List Apt") do |a|
12
+ a.title = "Top level list seed"
13
+ end
14
+ end
15
+
16
+ test "top-level apartments index wraps list in turbo-frame" do
17
+ get apartments_path
18
+ assert_response :success
19
+ assert_match(
20
+ %r{<turbo-frame[^>]+id="#{Regexp.escape(@frame)}"[^>]*class="list_container"},
21
+ @response.body
22
+ )
23
+ refute_match(
24
+ %r{<div[^>]+id="#{Regexp.escape(@frame)}"[^>]*class="list_container"},
25
+ @response.body,
26
+ "list root must be turbo-frame, not legacy div"
27
+ )
28
+ end
29
+
30
+ test "top-level + new link uses Turbo not UJS remote" do
31
+ get apartments_path
32
+ assert_response :success
33
+ assert_match(
34
+ %r{<a [^>]*class="button new_button"[^>]*data-turbo="true"[^>]*data-turbo-frame="#{Regexp.escape(@frame)}"[^>]*href="/apartments/new\?update=apartments_list"},
35
+ @response.body
36
+ )
37
+ refute_match(
38
+ %r{<a [^>]*class="button new_button"[^>]*data-remote="true"},
39
+ @response.body
40
+ )
41
+ end
42
+
43
+ test "top-level new returns new form inside matching turbo-frame" do
44
+ get new_apartment_path(update: @frame), headers: @frame_headers
45
+ assert_response :success
46
+ assert_includes @response.body, %(<turbo-frame id="#{@frame}">)
47
+ assert_includes @response.body, 'name="name"'
48
+ assert_includes @response.body, 'class="edit_form"'
49
+ refute_includes @response.content_type.to_s, "javascript"
50
+ end
51
+
52
+ test "top-level cancel returns list inside matching turbo-frame" do
53
+ get apartments_path(update: @frame, ul_needed: true), headers: @frame_headers
54
+ assert_response :success
55
+ assert_includes @response.body, %(<turbo-frame id="#{@frame}")
56
+ assert_includes @response.body, "<turbo-frame id=\"apartment_"
57
+ end
58
+
59
+ test "top-level create via Turbo persists and returns the list frame" do
60
+ name = "TopLevelNewApt-#{SecureRandom.hex(4)}"
61
+ assert_difference("Apartment.count", 1) do
62
+ post apartments_path(update: @frame),
63
+ params: { name: name, title: "Top level new test" },
64
+ headers: @frame_headers
65
+ end
66
+ assert_response :success
67
+ assert_includes @response.body, %(<turbo-frame id="#{@frame}">)
68
+ assert_includes @response.body, name
69
+ end
70
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../example_app/example_integration_test_case"
4
+
5
+ # Step 4: top-level index pagination swaps `<turbo-frame id="apartments_list">`.
6
+ class ExampleAppApartmentTopLevelPaginationTest < ExampleAppIntegrationTestCase
7
+ setup do
8
+ @frame = "apartments_list"
9
+ @frame_headers = { "Turbo-Frame" => @frame, "Accept" => "text/html" }
10
+ @original_per_page = Apartment.per_page
11
+ Apartment.per_page = 3
12
+ 6.times do |i|
13
+ Apartment.find_or_create_by!(name: "Paginate Apt #{i}") do |a|
14
+ a.title = "Pagination seed #{i}"
15
+ end
16
+ end
17
+ end
18
+
19
+ teardown do
20
+ Apartment.per_page = @original_per_page
21
+ end
22
+
23
+ test "top-level pagination links target apartments_list frame id" do
24
+ get apartments_path
25
+ assert_response :success
26
+ assert_match %r{class="pagination"}, @response.body
27
+ assert_match(
28
+ /update=apartments_list/,
29
+ @response.body,
30
+ "pagination must pass update=apartments_list for Turbo frame swap"
31
+ )
32
+ end
33
+
34
+ test "top-level page 2 returns matching turbo-frame with Turbo-Frame header" do
35
+ get apartments_path(page: 2, update: @frame, ul_needed: true), headers: @frame_headers
36
+ assert_response :success
37
+ assert_includes @response.body, %(<turbo-frame id="#{@frame}")
38
+ refute_match(/id="outer_container"/, @response.body)
39
+ end
40
+ end