futurism 1.2.0.pre7 → 1.2.0.pre10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/Appraisals +24 -0
  3. data/Appraisals~ +24 -0
  4. data/CHANGELOG.md +370 -0
  5. data/Gemfile +15 -0
  6. data/Gemfile.lock +215 -0
  7. data/Gemfile~ +17 -0
  8. data/README.md +59 -10
  9. data/README.md~ +405 -0
  10. data/app/assets/javascripts/futurism.js +191 -0
  11. data/app/assets/javascripts/futurism.min.js +2 -0
  12. data/app/assets/javascripts/futurism.min.js.map +1 -0
  13. data/app/assets/javascripts/futurism.umd.js +188 -0
  14. data/app/assets/javascripts/futurism.umd.min.js +2 -0
  15. data/app/assets/javascripts/futurism.umd.min.js.map +1 -0
  16. data/{lib → app/channels}/futurism/channel.rb +1 -1
  17. data/bin/rails +25 -0
  18. data/bin/standardize +5 -0
  19. data/bin/test +5 -0
  20. data/futurism.gemspec +38 -0
  21. data/futurism.gemspec~ +30 -0
  22. data/lib/futurism/configuration.rb +21 -0
  23. data/lib/futurism/engine.rb +19 -0
  24. data/lib/futurism/helpers.rb +5 -2
  25. data/lib/futurism/importmap.rb +2 -0
  26. data/lib/futurism/version.rb +1 -1
  27. data/lib/futurism.rb +2 -1
  28. data/package.json +47 -0
  29. data/package.json~ +51 -0
  30. data/rollup.config.js +77 -0
  31. data/rollup.config.js~ +73 -0
  32. data/test/cable/channel_test.rb +319 -0
  33. data/test/dummy/app/channels/application_cable/channel.rb +4 -0
  34. data/test/dummy/app/channels/application_cable/connection.rb +4 -0
  35. data/test/dummy/app/controllers/application_controller.rb +2 -0
  36. data/test/dummy/app/controllers/home_controller.rb +6 -0
  37. data/test/dummy/app/controllers/posts_controller.rb +59 -0
  38. data/test/dummy/app/helpers/application_helper.rb +2 -0
  39. data/test/dummy/app/helpers/posts_helper.rb +2 -0
  40. data/test/dummy/app/jobs/application_job.rb +7 -0
  41. data/test/dummy/app/models/action_item.rb +2 -0
  42. data/test/dummy/app/models/application_record.rb +3 -0
  43. data/test/dummy/app/models/post.rb +2 -0
  44. data/test/dummy/config/application.rb +29 -0
  45. data/test/dummy/config/boot.rb +5 -0
  46. data/test/dummy/config/environment.rb +5 -0
  47. data/test/dummy/config/environments/development.rb +40 -0
  48. data/test/dummy/config/environments/production.rb +94 -0
  49. data/test/dummy/config/environments/test.rb +39 -0
  50. data/test/dummy/config/initializers/application_controller_renderer.rb +8 -0
  51. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  52. data/test/dummy/config/initializers/content_security_policy.rb +28 -0
  53. data/test/dummy/config/initializers/cookies_serializer.rb +5 -0
  54. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  55. data/test/dummy/config/initializers/inflections.rb +16 -0
  56. data/test/dummy/config/initializers/mime_types.rb +4 -0
  57. data/test/dummy/config/initializers/wrap_parameters.rb +9 -0
  58. data/test/dummy/config/puma.rb +38 -0
  59. data/test/dummy/config/routes.rb +12 -0
  60. data/test/dummy/config/spring.rb +6 -0
  61. data/test/dummy/db/migrate/20200711122838_create_posts.rb +9 -0
  62. data/test/dummy/db/migrate/2021042923813_create_action_items.rb +9 -0
  63. data/test/dummy/db/schema.rb +27 -0
  64. data/test/futurism_test.rb +28 -0
  65. data/test/helper/helper_test.rb +206 -0
  66. data/test/integration/navigation_test.rb +7 -0
  67. data/test/resolver/controller/renderer_test.rb +120 -0
  68. data/test/resolver/controller_test.rb +26 -0
  69. data/test/test_helper.rb +14 -0
  70. data/yarn.lock +3263 -0
  71. metadata +122 -18
  72. data/config/routes.rb +0 -2
  73. data/lib/futurism/channel.rb~ +0 -31
  74. data/lib/futurism/helpers.rb~ +0 -118
  75. data/lib/futurism/options_transformer.rb~ +0 -0
  76. data/lib/futurism/resolver/controller/renderer.rb~ +0 -76
  77. data/lib/futurism/resolver/controller.rb~ +0 -17
  78. data/lib/futurism/resolver/resources.rb~ +0 -101
  79. data/lib/futurism/version.rb~ +0 -3
  80. data/lib/futurism.rb~ +0 -30
  81. data/lib/tasks/futurism_tasks.rake +0 -39
  82. data/lib/tasks/futurism_tasks.rake~ +0 -39
data/lib/futurism.rb CHANGED
@@ -1,13 +1,14 @@
1
1
  require "rails"
2
+
2
3
  require "action_cable"
3
4
  require "cable_ready"
5
+ require "futurism/configuration"
4
6
  require "futurism/engine"
5
7
  require "futurism/message_verifier"
6
8
  require "futurism/options_transformer"
7
9
  require "futurism/resolver/resources"
8
10
  require "futurism/resolver/controller"
9
11
  require "futurism/resolver/controller/renderer"
10
- require "futurism/channel"
11
12
  require "futurism/helpers"
12
13
 
13
14
  module Futurism
data/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@stimulus_reflex/futurism",
3
+ "version": "1.2.0-pre10",
4
+ "description": "Lazy-load Rails partials via CableReady",
5
+ "main": "./dist/futurism.umd.min.js",
6
+ "module": "./dist/futurism.min.js",
7
+ "files": [
8
+ "dist/*",
9
+ "javascript/*"
10
+ ],
11
+ "scripts": {
12
+ "test": "yarn run mocha",
13
+ "lint": "yarn run prettier-standard:check",
14
+ "format": "yarn run prettier-standard:format",
15
+ "prettier-standard:check": "yarn run prettier-standard --check ./javascript/**/*.js rollup.config.js",
16
+ "prettier-standard:format": "yarn run prettier-standard ./javascript/**/*.js rollup.config.js",
17
+ "build": "yarn rollup -c",
18
+ "watch": "yarn rollup -wc"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/stimulusreflex/futurism.git"
23
+ },
24
+ "keywords": [
25
+ "cable_ready",
26
+ "lazy",
27
+ "loading"
28
+ ],
29
+ "author": "Julian Rubisch <julian.rubisch@hey.com>",
30
+ "license": "MIT",
31
+ "bugs": {
32
+ "url": "https://github.com/stimulusreflex/futurism/issues"
33
+ },
34
+ "homepage": "https://github.com/stimulusreflex/futurism#readme",
35
+ "dependencies": {
36
+ "cable_ready": "^5.0.0-pre9"
37
+ },
38
+ "devDependencies": {
39
+ "@rollup/plugin-commonjs": "^21.0.3",
40
+ "@rollup/plugin-json": "^4.1.0",
41
+ "@rollup/plugin-node-resolve": "^13.1.3",
42
+ "mocha": "^8.0.1",
43
+ "prettier-standard": "^16.4.1",
44
+ "rollup": "^2.70.1",
45
+ "rollup-plugin-terser": "^7.0.2"
46
+ }
47
+ }
data/package.json~ ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@stimulus_reflex/futurism",
3
+ "version": "1.2.0-pre10",
4
+ "description": "Lazy-load Rails partials via CableReady",
5
+ "main": "./dist/futurism.umd.min.js",
6
+ "module": "./dist/futurism.min.js",
7
+ "files": [
8
+ "dist/*",
9
+ "javascript/*"
10
+ ],
11
+ "scripts": {
12
+ "test": "yarn run mocha",
13
+ "lint": "yarn run prettier-standard:check",
14
+ "format": "yarn run prettier-standard:format",
15
+ "prettier-standard:check": "yarn run prettier-standard --check ./javascript/**/*.js rollup.config.js",
16
+ "prettier-standard:format": "yarn run prettier-standard ./javascript/**/*.js rollup.config.js",
17
+ "build": "yarn rollup -c",
18
+ "watch": "yarn rollup -wc"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/stimulusreflex/futurism.git"
23
+ },
24
+ "keywords": [
25
+ "cable_ready",
26
+ "lazy",
27
+ "loading"
28
+ ],
29
+ "author": "Julian Rubisch <julian.rubisch@hey.com>",
30
+ "license": "MIT",
31
+ "bugs": {
32
+ "url": "https://github.com/stimulusreflex/futurism/issues"
33
+ },
34
+ "homepage": "https://github.com/stimulusreflex/futurism#readme",
35
+ "dependencies": {
36
+ <<<<<<< Updated upstream
37
+ "cable_ready": "^5.0.0-pre9"
38
+ =======
39
+ "cable_ready": "5.0.0-pre9"
40
+ >>>>>>> Stashed changes
41
+ },
42
+ "devDependencies": {
43
+ "@rollup/plugin-commonjs": "^21.0.3",
44
+ "@rollup/plugin-json": "^4.1.0",
45
+ "@rollup/plugin-node-resolve": "^13.1.3",
46
+ "mocha": "^8.0.1",
47
+ "prettier-standard": "^16.4.1",
48
+ "rollup": "^2.70.1",
49
+ "rollup-plugin-terser": "^7.0.2"
50
+ }
51
+ }
data/rollup.config.js ADDED
@@ -0,0 +1,77 @@
1
+ import resolve from '@rollup/plugin-node-resolve'
2
+ import commonjs from '@rollup/plugin-commonjs'
3
+ import json from '@rollup/plugin-json'
4
+ import { terser } from 'rollup-plugin-terser'
5
+
6
+ const pretty = () => {
7
+ return terser({
8
+ mangle: false,
9
+ compress: false,
10
+ format: {
11
+ beautify: true,
12
+ indent_level: 2
13
+ }
14
+ })
15
+ }
16
+
17
+ const minify = () => {
18
+ return terser({
19
+ mangle: true,
20
+ compress: true
21
+ })
22
+ }
23
+
24
+ const esConfig = {
25
+ format: 'es',
26
+ inlineDynamicImports: true
27
+ }
28
+
29
+ const umdConfig = {
30
+ name: 'Futurism',
31
+ format: 'umd',
32
+ exports: 'named',
33
+ globals: {
34
+ cable_ready: 'CableReady'
35
+ }
36
+ }
37
+
38
+ const distFolders = ['dist/', 'app/assets/javascripts/']
39
+
40
+ const output = distFolders
41
+ .map(distFolder => [
42
+ {
43
+ ...esConfig,
44
+ file: `${distFolder}/futurism.js`,
45
+ plugins: [pretty()]
46
+ },
47
+ {
48
+ ...esConfig,
49
+ file: `${distFolder}/futurism.min.js`,
50
+ sourcemap: true,
51
+ plugins: [minify()]
52
+ },
53
+ {
54
+ ...umdConfig,
55
+ file: `${distFolder}/futurism.umd.js`,
56
+ plugins: [pretty()]
57
+ },
58
+ {
59
+ ...umdConfig,
60
+ file: `${distFolder}/futurism.umd.min.js`,
61
+ sourcemap: true,
62
+ plugins: [minify()]
63
+ }
64
+ ])
65
+ .flat()
66
+
67
+ export default [
68
+ {
69
+ external: ['cable_ready'],
70
+ input: 'javascript/index.js',
71
+ output,
72
+ plugins: [commonjs(), resolve(), json()],
73
+ watch: {
74
+ include: 'javascript/**'
75
+ }
76
+ }
77
+ ]
data/rollup.config.js~ ADDED
@@ -0,0 +1,73 @@
1
+ import resolve from '@rollup/plugin-node-resolve'
2
+ import commonjs from '@rollup/plugin-commonjs'
3
+ import json from '@rollup/plugin-json'
4
+ import { terser } from 'rollup-plugin-terser'
5
+
6
+ const pretty = () => {
7
+ return terser({
8
+ mangle: false,
9
+ compress: false,
10
+ format: {
11
+ beautify: true,
12
+ indent_level: 2
13
+ }
14
+ })
15
+ }
16
+
17
+ const minify = () => {
18
+ return terser({
19
+ mangle: true,
20
+ compress: true
21
+ })
22
+ }
23
+
24
+ const esConfig = {
25
+ format: 'es',
26
+ inlineDynamicImports: true
27
+ }
28
+
29
+ const umdConfig = {
30
+ name: 'Futurism',
31
+ format: 'umd',
32
+ exports: 'named'
33
+ }
34
+
35
+ const distFolders = ['dist/', 'app/assets/javascripts/']
36
+
37
+ const output = distFolders
38
+ .map(distFolder => [
39
+ {
40
+ ...esConfig,
41
+ file: `${distFolder}/futurism.js`,
42
+ plugins: [pretty()]
43
+ },
44
+ {
45
+ ...esConfig,
46
+ file: `${distFolder}/futurism.min.js`,
47
+ sourcemap: true,
48
+ plugins: [minify()]
49
+ },
50
+ {
51
+ ...umdConfig,
52
+ file: `${distFolder}/futurism.umd.js`,
53
+ plugins: [pretty()]
54
+ },
55
+ {
56
+ ...umdConfig,
57
+ file: `${distFolder}/futurism.umd.min.js`,
58
+ sourcemap: true,
59
+ plugins: [minify()]
60
+ }
61
+ ])
62
+ .flat()
63
+
64
+ export default [
65
+ {
66
+ input: 'javascript/index.js',
67
+ output,
68
+ plugins: [commonjs(), resolve(), json()],
69
+ watch: {
70
+ include: 'javascript/**'
71
+ }
72
+ }
73
+ ]
@@ -0,0 +1,319 @@
1
+ require "test_helper"
2
+
3
+ def with_mocked_renderer
4
+ renderer = Minitest::Mock.new
5
+
6
+ Futurism::Resolver::Controller::Renderer.stub(:for, renderer) do
7
+ yield(renderer)
8
+ end
9
+ end
10
+
11
+ def with_mocked_cable_ready
12
+ cable_ready_mock = MiniTest::Mock.new
13
+ cable_ready_channel = MiniTest::Mock.new
14
+ cable_ready_channel.expect(:outer_html, nil, [Hash])
15
+ cable_ready_channel.expect(:outer_html, nil, [Hash])
16
+
17
+ cable_ready_mock.expect(:[], cable_ready_channel, ["1"])
18
+ cable_ready_mock.expect(:[], cable_ready_channel, ["1"])
19
+ cable_ready_mock.expect(:broadcast, nil)
20
+ cable_ready_mock.expect(:broadcast, nil)
21
+ cable_ready_mock.expect(:broadcast, nil)
22
+
23
+ CableReady::Broadcaster.alias_method(:orig_cable_ready, :cable_ready)
24
+
25
+ CableReady::Broadcaster.define_method(:cable_ready) do
26
+ cable_ready_mock
27
+ end
28
+
29
+ yield cable_ready_mock
30
+
31
+ CableReady::Broadcaster.undef_method(:cable_ready)
32
+
33
+ CableReady::Broadcaster.alias_method(:cable_ready, :orig_cable_ready)
34
+ end
35
+
36
+ class Futurism::ChannelTest < ActionCable::Channel::TestCase
37
+ include Futurism::Helpers
38
+ include ActionView::Helpers
39
+ include ActionView::Context
40
+ include CableReady::Broadcaster
41
+
42
+ setup do
43
+ stub_connection(env: {"SCRIPT_NAME" => "/cable"}, identifiers: [:current_user], current_user: Struct.new(:id)[1])
44
+ end
45
+
46
+ test "subscribed" do
47
+ subscribe(channel: "Futurism::Channel")
48
+
49
+ assert subscription.confirmed?
50
+
51
+ assert_has_stream "Futurism::Channel:1"
52
+ end
53
+
54
+ test "broadcasts a rendered model after receiving signed params" do
55
+ with_mocked_renderer do |mock_renderer|
56
+ post = Post.create title: "Lorem"
57
+ fragment = Nokogiri::HTML.fragment(futurize(post) {})
58
+ signed_params_array = fragment.children.map { |element| element["data-signed-params"] }
59
+ sgids = fragment.children.map { |element| element["data-sgid"] }
60
+ subscribe
61
+
62
+ mock_renderer.expect :render, "<tag></tag>", [post]
63
+
64
+ perform :receive, {"signed_params" => signed_params_array, "sgids" => sgids}
65
+
66
+ assert_mock mock_renderer
67
+ end
68
+ end
69
+
70
+ test "broadcasts an ActiveRecord::Relation" do
71
+ with_mocked_renderer do |mock_renderer|
72
+ post1 = Post.create(title: "Lorem")
73
+ post2 = Post.create(title: "Ipsum")
74
+
75
+ fragment = Nokogiri::HTML.fragment(futurize(Post.all) {})
76
+ signed_params_array = fragment.children.map { |element| element["data-signed-params"] }
77
+ sgids = fragment.children.map { |element| element["data-sgid"] }
78
+ subscribe
79
+
80
+ mock_renderer
81
+ .expect(:render, "<tag></tag>", [post1])
82
+ .expect :render, "<tag></tag>", [post2]
83
+
84
+ perform :receive, {"signed_params" => signed_params_array, "sgids" => sgids}
85
+
86
+ assert_mock mock_renderer
87
+ end
88
+ end
89
+
90
+ test "broadcasts a rendered partial after receiving signed params" do
91
+ with_mocked_renderer do |mock_renderer|
92
+ post = Post.create title: "Lorem"
93
+ fragment = Nokogiri::HTML.fragment(futurize(partial: "posts/card", locals: {post: post}) {})
94
+ signed_params = fragment.children.first["data-signed-params"]
95
+ subscribe
96
+
97
+ mock_renderer
98
+ .expect(:render, "<tag></tag>", [partial: "posts/card", locals: {post: post}])
99
+
100
+ perform :receive, {"signed_params" => [signed_params]}
101
+
102
+ assert_mock mock_renderer
103
+ end
104
+ end
105
+
106
+ test "broadcasts a rendered partial after receiving the shorthand syntax" do
107
+ with_mocked_renderer do |mock_renderer|
108
+ post = Post.create title: "Lorem"
109
+ fragment = Nokogiri::HTML.fragment(futurize("posts/card", post: post) {})
110
+ signed_params = fragment.children.first["data-signed-params"]
111
+ subscribe
112
+
113
+ mock_renderer.expect(:render, "<tag></tag>", [partial: "posts/card", locals: {post: post}])
114
+ perform :receive, {"signed_params" => [signed_params]}
115
+
116
+ assert_mock mock_renderer
117
+ end
118
+ end
119
+
120
+ test "broadcasts a rendered partial after receiving the shorthand syntax with html options" do
121
+ with_mocked_renderer do |mock_renderer|
122
+ post = Post.create title: "Lorem"
123
+ fragment = Nokogiri::HTML.fragment(futurize("posts/card", post: post, html_options: {style: "color: green"}) {})
124
+ signed_params = fragment.children.first["data-signed-params"]
125
+ subscribe
126
+
127
+ mock_renderer.expect(:render, "<tag></tag>", [partial: "posts/card", locals: {post: post}])
128
+
129
+ perform :receive, {"signed_params" => [signed_params]}
130
+
131
+ assert_mock mock_renderer
132
+ end
133
+ end
134
+
135
+ test "broadcasts a collection" do
136
+ with_mocked_renderer do |mock_renderer|
137
+ Post.create title: "Lorem"
138
+ Post.create title: "Ipsum"
139
+ fragment = Nokogiri::HTML.fragment(futurize(partial: "posts/card", collection: Post.all, locals: {important_local: "needed to render"}) {})
140
+ subscribe
141
+
142
+ mock_renderer
143
+ .expect(:render, "<tag></tag>", [partial: "posts/card", locals: {post: Post.first, important_local: "needed to render", post_counter: 0}])
144
+ .expect(:render, "<tag></tag>", [partial: "posts/card", locals: {post: Post.last, important_local: "needed to render", post_counter: 1}])
145
+
146
+ signed_params = fragment.children.first["data-signed-params"]
147
+ perform :receive, {"signed_params" => [signed_params]}
148
+
149
+ signed_params = fragment.children.last["data-signed-params"]
150
+ perform :receive, {"signed_params" => [signed_params]}
151
+
152
+ assert_mock mock_renderer
153
+ end
154
+ end
155
+
156
+ test "broadcasts a collection (with multi-word class)" do
157
+ with_mocked_renderer do |mock_renderer|
158
+ ActionItem.create description: "Do this"
159
+ ActionItem.create description: "Do that"
160
+ fragment = Nokogiri::HTML.fragment(futurize(partial: "posts/card", collection: ActionItem.all, locals: {important_local: "needed to render"}) {})
161
+
162
+ subscribe
163
+
164
+ mock_renderer
165
+ .expect(:render, "<tag></tag>", [partial: "posts/card", locals: {action_item: ActionItem.first, important_local: "needed to render", action_item_counter: 0}])
166
+ .expect(:render, "<tag></tag>", [partial: "posts/card", locals: {action_item: ActionItem.last, important_local: "needed to render", action_item_counter: 1}])
167
+
168
+ signed_params = fragment.children.first["data-signed-params"]
169
+ perform :receive, {"signed_params" => [signed_params]}
170
+
171
+ signed_params = fragment.children.last["data-signed-params"]
172
+ perform :receive, {"signed_params" => [signed_params]}
173
+
174
+ assert_mock mock_renderer
175
+ end
176
+ end
177
+
178
+ test "broadcasts a collection with :as" do
179
+ with_mocked_renderer do |mock_renderer|
180
+ Post.create title: "Lorem"
181
+ Post.create title: "Ipsum"
182
+ fragment = Nokogiri::HTML.fragment(futurize(partial: "posts/card", collection: Post.all, as: :post_item) {})
183
+ subscribe
184
+
185
+ mock_renderer.expect(:render, "<tag></tag>", [partial: "posts/card", locals: {post_item: Post.first, post_item_counter: 0}])
186
+ signed_params = fragment.children.first["data-signed-params"]
187
+ perform :receive, {"signed_params" => [signed_params]}
188
+
189
+ mock_renderer.expect(:render, "<tag></tag>", [partial: "posts/card", locals: {post_item: Post.last, post_item_counter: 1}])
190
+ signed_params = fragment.children.last["data-signed-params"]
191
+ perform :receive, {"signed_params" => [signed_params]}
192
+
193
+ assert_mock mock_renderer
194
+ end
195
+ end
196
+
197
+ test "broadcasts elements of a collection immediately" do
198
+ with_mocked_cable_ready do |cable_ready_mock|
199
+ Post.create title: "Lorem"
200
+ Post.create title: "Ipsum"
201
+ fragment = Nokogiri::HTML.fragment(futurize(partial: "posts/card", collection: Post.all, broadcast_each: true, locals: {important_local: "needed to render"}) {})
202
+ subscribe
203
+
204
+ signed_params_1 = fragment.children.first["data-signed-params"]
205
+ broadcast_each_1 = fragment.children.first["data-broadcast-each"]
206
+ signed_params_2 = fragment.children.last["data-signed-params"]
207
+ broadcast_each_2 = fragment.children.last["data-broadcast-each"]
208
+ perform :receive, {"signed_params" => [signed_params_1, signed_params_2], "broadcast_each" => [broadcast_each_1, broadcast_each_2]}
209
+
210
+ assert_mock cable_ready_mock
211
+ end
212
+ end
213
+
214
+ test "broadcasts an inline rendered text" do
215
+ fragment = Nokogiri::HTML.fragment(futurize(inline: "<%= 1 + 2 %>") {})
216
+ signed_params = fragment.children.first["data-signed-params"]
217
+ subscribe(channel: "Futurism::Channel")
218
+
219
+ assert_cable_ready_operation_on("Futurism::Channel:1", operation: "outerHtml", selector: "[data-signed-params='#{signed_params}']", html: "3") do
220
+ perform :receive, {"signed_params" => [signed_params]}
221
+ end
222
+ end
223
+
224
+ test "broadcasts a correctly formed path" do
225
+ post = Post.create title: "Lorem"
226
+ fragment = Nokogiri::HTML.fragment(futurize(partial: "posts/card", locals: {post: post}) {})
227
+ signed_params = fragment.children.first["data-signed-params"]
228
+ subscribe(channel: "Futurism::Channel")
229
+
230
+ assert_cable_ready_operation_on("Futurism::Channel:1", operation: "outerHtml",
231
+ selector: "[data-signed-params='#{signed_params}']",
232
+ html: "<div class=\"card\">\n Lorem\n <a href=\"/posts/1/edit\">Edit</a>\n</div>\n") do
233
+ perform :receive, {"signed_params" => [signed_params]}
234
+ end
235
+ end
236
+
237
+ test "passes parsed params to controller render" do
238
+ with_mocked_renderer do |mock_renderer|
239
+ post = Post.create title: "Lorem"
240
+ fragment = Nokogiri::HTML.fragment(futurize(post) {})
241
+ signed_params_array = fragment.children.map { |element| element["data-signed-params"] }
242
+ sgids = fragment.children.map { |element| element["data-sgid"] }
243
+ urls = Array.new(fragment.children.length, "http://www.example.org/route?param1=true&param2=1234")
244
+ subscribe
245
+
246
+ mock_renderer.expect(:render, "<tag></tag>", [post])
247
+
248
+ perform :receive, {"signed_params" => signed_params_array, "sgids" => sgids, "urls" => urls}
249
+
250
+ assert_mock mock_renderer
251
+ end
252
+ end
253
+
254
+ test "renders error message when rendering invalid partial error" do
255
+ fragment = Nokogiri::HTML.fragment(futurize(partial: "INVALID/PARTIAL") {})
256
+ signed_params = fragment.children.first["data-signed-params"]
257
+ subscribe(channel: "Futurism::Channel")
258
+
259
+ assert_cable_ready_operation_on("Futurism::Channel:1", operation: "outerHtml",
260
+ selector: "[data-signed-params='#{signed_params}']",
261
+ html: /Missing partial INVALID\/_PARTIAL/) do
262
+ perform :receive, {"signed_params" => [signed_params]}
263
+ end
264
+ end
265
+
266
+ test "renders error message when wrong variable name" do
267
+ Post.create title: "Lorem"
268
+ fragment = Nokogiri::HTML.fragment(futurize(partial: "posts/card", collection: Post.all, as: :wrong_variable_name) {})
269
+ signed_params = fragment.children.first["data-signed-params"]
270
+ subscribe(channel: "Futurism::Channel")
271
+
272
+ assert_cable_ready_operation_on("Futurism::Channel:1", operation: "outerHtml",
273
+ selector: "[data-signed-params='#{signed_params}']",
274
+ html: /undefined local variable or method/) do
275
+ perform :receive, {"signed_params" => [signed_params]}
276
+ end
277
+ end
278
+
279
+ def assert_cable_ready_operation_on(stream, operation:, selector:, html:, &block)
280
+ data = {
281
+ "cableReady" => true,
282
+ "operations" => [{
283
+ "selector" => selector,
284
+ "html" => html,
285
+ "operation" => operation
286
+ }]
287
+ }
288
+
289
+ old_messages = broadcasts(stream)
290
+ clear_messages(stream)
291
+
292
+ assert_nothing_raised(&block)
293
+
294
+ new_messages = broadcasts(stream)
295
+ clear_messages(stream)
296
+
297
+ # Restore all sent messages
298
+ (old_messages + new_messages).each { |m| pubsub_adapter.broadcast(stream, m) }
299
+
300
+ message = new_messages.find { |msg| cable_ready_match?(ActiveSupport::JSON.decode(msg), data) }
301
+
302
+ assert message, "No messages sent with #{data} to #{stream}"
303
+ end
304
+
305
+ def cable_ready_match?(message, matcher)
306
+ return true if message == matcher
307
+
308
+ first_matching_operation = ["operations", 0]
309
+
310
+ matcher_operation = matcher.dig(*first_matching_operation)
311
+ message_operation = message.dig(*first_matching_operation)
312
+
313
+ message.dig("cableReady") == true &&
314
+ (matcher_operation.dig("selector") === message_operation.dig("selector") ||
315
+ matcher_operation.dig("selector").match(message_operation.dig("selector"))) &&
316
+ (matcher_operation.dig("html") === message_operation.dig("html") ||
317
+ matcher_operation.dig("html").match(message_operation.dig("html")))
318
+ end
319
+ end
@@ -0,0 +1,4 @@
1
+ module ApplicationCable
2
+ class Channel < ActionCable::Channel::Base
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module ApplicationCable
2
+ class Connection < ActionCable::Connection::Base
3
+ end
4
+ end
@@ -0,0 +1,2 @@
1
+ class ApplicationController < ActionController::Base
2
+ end
@@ -0,0 +1,6 @@
1
+ class HomeController < ApplicationController
2
+ # GET /home
3
+ def index
4
+ head :no_content
5
+ end
6
+ end
@@ -0,0 +1,59 @@
1
+ class PostsController < ApplicationController
2
+ before_action :set_post, only: [:show, :edit, :update, :destroy]
3
+
4
+ # GET /posts
5
+ def index
6
+ @posts = Post.all
7
+ end
8
+
9
+ # GET /posts/1
10
+ def show
11
+ end
12
+
13
+ # GET /posts/new
14
+ def new
15
+ @post = Post.new
16
+ end
17
+
18
+ # GET /posts/1/edit
19
+ def edit
20
+ end
21
+
22
+ # POST /posts
23
+ def create
24
+ @post = Post.new(post_params)
25
+
26
+ if @post.save
27
+ redirect_to @post, notice: "Post was successfully created."
28
+ else
29
+ render :new
30
+ end
31
+ end
32
+
33
+ # PATCH/PUT /posts/1
34
+ def update
35
+ if @post.update(post_params)
36
+ redirect_to @post, notice: "Post was successfully updated."
37
+ else
38
+ render :edit
39
+ end
40
+ end
41
+
42
+ # DELETE /posts/1
43
+ def destroy
44
+ @post.destroy
45
+ redirect_to posts_url, notice: "Post was successfully destroyed."
46
+ end
47
+
48
+ private
49
+
50
+ # Use callbacks to share common setup or constraints between actions.
51
+ def set_post
52
+ @post = Post.find(params[:id])
53
+ end
54
+
55
+ # Only allow a trusted parameter "white list" through.
56
+ def post_params
57
+ params.require(:post).permit(:title)
58
+ end
59
+ end
@@ -0,0 +1,2 @@
1
+ module ApplicationHelper
2
+ end
@@ -0,0 +1,2 @@
1
+ module PostsHelper
2
+ end
@@ -0,0 +1,7 @@
1
+ class ApplicationJob < ActiveJob::Base
2
+ # Automatically retry jobs that encountered a deadlock
3
+ # retry_on ActiveRecord::Deadlocked
4
+
5
+ # Most jobs are safe to ignore if the underlying records are no longer available
6
+ # discard_on ActiveJob::DeserializationError
7
+ end
@@ -0,0 +1,2 @@
1
+ class ActionItem < ApplicationRecord
2
+ end