shoelace-rails 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/main.yml +85 -0
  3. data/.gitignore +20 -0
  4. data/Appraisals +25 -0
  5. data/CHANGELOG.md +3 -0
  6. data/CODE_OF_CONDUCT.md +84 -0
  7. data/Gemfile +11 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +178 -0
  10. data/Rakefile +27 -0
  11. data/app/helpers/shoelace/form_helper.rb +451 -0
  12. data/bin/console +15 -0
  13. data/bin/setup +8 -0
  14. data/dist/.keep +0 -0
  15. data/dist/types/.keep +0 -0
  16. data/gemfiles/rails_50.gemfile +12 -0
  17. data/gemfiles/rails_51.gemfile +11 -0
  18. data/gemfiles/rails_52.gemfile +11 -0
  19. data/gemfiles/rails_60.gemfile +11 -0
  20. data/gemfiles/rails_61.gemfile +11 -0
  21. data/gemfiles/rails_70.gemfile +11 -0
  22. data/gemfiles/rails_edge.gemfile +14 -0
  23. data/lib/shoelace/engine.rb +8 -0
  24. data/lib/shoelace/rails/version.rb +7 -0
  25. data/lib/shoelace/rails.rb +10 -0
  26. data/lib/shoelace/testing.rb +40 -0
  27. data/package.json +50 -0
  28. data/rollup.config.js +49 -0
  29. data/shoelace-rails.gemspec +35 -0
  30. data/src/index.ts +2 -0
  31. data/src/turbo/index.ts +6 -0
  32. data/src/turbo/polyfills/formdata-event.js +27 -0
  33. data/src/turbo/sl-turbo-form.ts +110 -0
  34. data/src/turbolinks/features/confirm.ts +42 -0
  35. data/src/turbolinks/features/disable.ts +94 -0
  36. data/src/turbolinks/features/remote.ts +107 -0
  37. data/src/turbolinks/index.ts +6 -0
  38. data/src/turbolinks/selectors.ts +38 -0
  39. data/src/turbolinks/start.ts +38 -0
  40. data/src/turbolinks/turbolinks.ts +78 -0
  41. data/src/turbolinks/utils/ajax.ts +146 -0
  42. data/src/turbolinks/utils/csp.ts +20 -0
  43. data/src/turbolinks/utils/csrf.ts +33 -0
  44. data/src/turbolinks/utils/dom.ts +40 -0
  45. data/src/turbolinks/utils/event.ts +57 -0
  46. data/src/turbolinks/utils/form.ts +58 -0
  47. data/test/dummy_app/Gemfile +19 -0
  48. data/test/dummy_app/Rakefile +6 -0
  49. data/test/dummy_app/app/controllers/hotwire_forms_controller.rb +46 -0
  50. data/test/dummy_app/app/controllers/turbolinks_forms_controller.rb +37 -0
  51. data/test/dummy_app/app/models/user.rb +16 -0
  52. data/test/dummy_app/app/packs/entrypoints/hotwire.js +1 -0
  53. data/test/dummy_app/app/packs/entrypoints/turbolinks.js +5 -0
  54. data/test/dummy_app/app/views/hotwire_forms/form.html.erb +45 -0
  55. data/test/dummy_app/app/views/hotwire_forms/show.html.erb +5 -0
  56. data/test/dummy_app/app/views/layouts/application.html.erb +39 -0
  57. data/test/dummy_app/app/views/turbolinks_forms/form.html.erb +44 -0
  58. data/test/dummy_app/app/views/turbolinks_forms/show.html.erb +5 -0
  59. data/test/dummy_app/bin/rails +5 -0
  60. data/test/dummy_app/bin/webpack +18 -0
  61. data/test/dummy_app/bin/yarn +18 -0
  62. data/test/dummy_app/config/application.rb +16 -0
  63. data/test/dummy_app/config/boot.rb +4 -0
  64. data/test/dummy_app/config/environment.rb +2 -0
  65. data/test/dummy_app/config/environments/development.rb +10 -0
  66. data/test/dummy_app/config/environments/test.rb +18 -0
  67. data/test/dummy_app/config/routes.rb +4 -0
  68. data/test/dummy_app/config/webpack/development.js +5 -0
  69. data/test/dummy_app/config/webpack/production.js +1 -0
  70. data/test/dummy_app/config/webpack/test.js +5 -0
  71. data/test/dummy_app/config/webpacker.yml +33 -0
  72. data/test/dummy_app/config.ru +6 -0
  73. data/test/dummy_app/package.json +24 -0
  74. data/test/dummy_app/test/system/hotwire_form_test.rb +65 -0
  75. data/test/dummy_app/test/system/turbolinks_form_test.rb +39 -0
  76. data/test/dummy_app/test/test_helper.rb +68 -0
  77. data/test/helpers/form_helper_test.rb +397 -0
  78. data/test/test_helper.rb +18 -0
  79. data/tsconfig.json +19 -0
  80. data/yarn.lock +249 -0
  81. metadata +196 -0
@@ -0,0 +1,44 @@
1
+ <% locations = { tokyo: "Tokyo", new_york: "New York", london: "London" } %>
2
+ <%= sl_form_for(@user, url: turbolinks_forms_path) do |form| %>
3
+ <div>
4
+ <%= form.text_field :name do %>
5
+ <span slot="help-text" style="color: rgb(var(--sl-color-danger-600));">
6
+ <%= @user.errors.full_messages_for(:name).first %>
7
+ </span>
8
+ <% end %>
9
+ </div>
10
+
11
+ <div>
12
+ <%= form.color_field :color %>
13
+ </div>
14
+
15
+ <div>
16
+ <%= form.range_field :score, min: 0, max: 100, step: 1 %>
17
+ </div>
18
+
19
+ <div>
20
+ <%= form.collection_radio_buttons :current_city, locations, :first, :last %>
21
+ </div>
22
+
23
+ <div>
24
+ <%= form.collection_select :previous_city, locations, :first, :last, {}, { placeholder: "Select one" } %>
25
+ </div>
26
+
27
+ <div>
28
+ <%= form.collection_select :past_cities, locations, :first, :last, {}, { placeholder: "Select two or more", multiple: true, clearable: true } %>
29
+ </div>
30
+
31
+ <div>
32
+ <%= form.check_box :remember_me %>
33
+ </div>
34
+
35
+ <div>
36
+ <%= form.switch_field :subscribe_to_emails, value: "1" %>
37
+ </div>
38
+
39
+ <div>
40
+ <%= form.text_area :description %>
41
+ </div>
42
+
43
+ <%= form.submit %>
44
+ <% end %>
@@ -0,0 +1,5 @@
1
+ <% @user.attributes.each do |attribute_name, value| %>
2
+ <div>
3
+ <%= attribute_name.titleize %>: <%= value %>
4
+ </div>
5
+ <% end %>
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ APP_PATH = File.expand_path('../config/application', __dir__)
4
+ require_relative "../config/boot"
5
+ require "rails/commands"
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development"
4
+ ENV["NODE_ENV"] ||= "development"
5
+
6
+ require "pathname"
7
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
8
+ Pathname.new(__FILE__).realpath)
9
+
10
+ require "bundler/setup"
11
+
12
+ require "webpacker"
13
+ require "webpacker/webpack_runner"
14
+
15
+ APP_ROOT = File.expand_path("..", __dir__)
16
+ Dir.chdir(APP_ROOT) do
17
+ Webpacker::WebpackRunner.run(ARGV)
18
+ end
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ APP_ROOT = File.expand_path("..", __dir__)
4
+ Dir.chdir(APP_ROOT) do
5
+ yarn = ENV["PATH"].split(File::PATH_SEPARATOR).
6
+ select { |dir| File.expand_path(dir) != __dir__ }.
7
+ product(["yarn", "yarnpkg", "yarn.cmd", "yarn.ps1"]).
8
+ map { |dir, file| File.expand_path(file, dir) }.
9
+ find { |file| File.executable?(file) }
10
+
11
+ if yarn
12
+ exec yarn, *ARGV
13
+ else
14
+ $stderr.puts "Yarn executable was not detected in the system."
15
+ $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install"
16
+ exit 1
17
+ end
18
+ end
@@ -0,0 +1,16 @@
1
+ require_relative "boot"
2
+
3
+ require "rails"
4
+ require "active_model/railtie"
5
+ require "action_controller/railtie"
6
+ require "action_view/railtie"
7
+ require "rails/test_unit/railtie"
8
+
9
+ Bundler.require(*Rails.groups)
10
+
11
+ module ShoelaceTest
12
+ class Application < Rails::Application
13
+ config.load_defaults 6.1
14
+ Rails.backtrace_cleaner.remove_silencers! if ENV["BACKTRACE"]
15
+ end
16
+ end
@@ -0,0 +1,4 @@
1
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
2
+
3
+ require "bundler/setup"
4
+ require "bootsnap/setup"
@@ -0,0 +1,2 @@
1
+ require_relative "application"
2
+ Rails.application.initialize!
@@ -0,0 +1,10 @@
1
+ Rails.application.configure do
2
+ config.cache_classes = false
3
+ config.eager_load = false
4
+ config.consider_all_requests_local = true
5
+ config.action_controller.perform_caching = false
6
+ config.cache_store = :null_store
7
+ config.active_support.deprecation = :log
8
+ config.active_support.disallowed_deprecation = :raise
9
+ config.active_support.disallowed_deprecation_warnings = []
10
+ end
@@ -0,0 +1,18 @@
1
+ require "active_support/core_ext/integer/time"
2
+
3
+ Rails.application.configure do
4
+ config.cache_classes = true
5
+ config.eager_load = false
6
+ config.public_file_server.enabled = true
7
+ config.public_file_server.headers = {
8
+ 'Cache-Control' => "public, max-age=#{1.hour.to_i}"
9
+ }
10
+ config.consider_all_requests_local = true
11
+ config.action_controller.perform_caching = false
12
+ config.cache_store = :null_store
13
+ config.action_dispatch.show_exceptions = false
14
+ config.action_controller.allow_forgery_protection = false
15
+ config.active_support.deprecation = :stderr
16
+ config.active_support.disallowed_deprecation = :raise
17
+ config.active_support.disallowed_deprecation_warnings = []
18
+ end
@@ -0,0 +1,4 @@
1
+ Rails.application.routes.draw do
2
+ resources :hotwire_forms
3
+ resources :turbolinks_forms
4
+ end
@@ -0,0 +1,5 @@
1
+ process.env.NODE_ENV = process.env.NODE_ENV || 'development'
2
+
3
+ const { webpackConfig } = require('@rails/webpacker')
4
+
5
+ module.exports = webpackConfig
@@ -0,0 +1 @@
1
+ test.js
@@ -0,0 +1,5 @@
1
+ process.env.NODE_ENV = process.env.NODE_ENV || 'development'
2
+
3
+ const { webpackConfig } = require('@rails/webpacker')
4
+
5
+ module.exports = webpackConfig
@@ -0,0 +1,33 @@
1
+ default: &default
2
+ source_path: app/packs
3
+ source_entry_path: entrypoints
4
+ public_root_path: public
5
+ public_output_path: packs
6
+ cache_path: tmp/webpacker
7
+ webpack_compile_output: true
8
+ additional_paths: []
9
+ cache_manifest: false
10
+
11
+ development:
12
+ <<: *default
13
+ compile: true
14
+ dev_server:
15
+ https: false
16
+ host: localhost
17
+ port: 3035
18
+ hmr: false
19
+ client:
20
+ overlay: true
21
+ compress: true
22
+ allowed_hosts: "all"
23
+ pretty: true
24
+ headers:
25
+ 'Access-Control-Allow-Origin': '*'
26
+ static:
27
+ watch:
28
+ ignored: '**/node_modules/**'
29
+
30
+ test:
31
+ <<: *default
32
+ compile: false
33
+ public_output_path: packs-test
@@ -0,0 +1,6 @@
1
+ # This file is used by Rack-based servers to start the application.
2
+
3
+ require_relative "config/environment"
4
+
5
+ run Rails.application
6
+ Rails.application.load_server
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "app",
3
+ "private": true,
4
+ "babel": {
5
+ "presets": [
6
+ "./node_modules/@rails/webpacker/package/babel/preset.js"
7
+ ]
8
+ },
9
+ "browserslist": [
10
+ "defaults"
11
+ ],
12
+ "dependencies": {
13
+ "@hotwired/turbo-rails": "^7.0.1",
14
+ "@rails/ujs": "^7.0.2",
15
+ "@rails/webpacker": "^6.0.0-rc.5",
16
+ "@yuki24/shoelace-rails": "file:./../../../shoelace-rails",
17
+ "turbolinks": "^5.2.0",
18
+ "webpack": "^5.51.1",
19
+ "webpack-cli": "^4.8.0"
20
+ },
21
+ "devDependencies": {
22
+ "@webpack-cli/serve": "^1.5.2"
23
+ }
24
+ }
@@ -0,0 +1,65 @@
1
+ require "test_helper"
2
+
3
+ class HotwireFormTest < ApplicationSystemTestCase
4
+ setup do
5
+ visit new_hotwire_form_path
6
+ end
7
+
8
+ test "It can submit a form with a POST method" do
9
+ shadow_fill_in 'sl-input[label="Name"]', with: "Yuki Nishijima"
10
+ shadow_fill_in 'sl-range[name="user[score]"]', with: "50"
11
+
12
+ find('sl-radio', text: "New York").click # Selecting a radio button does not work...
13
+
14
+ sl_select "Tokyo", from: "Select one"
15
+ sl_multi_select "Tokyo", "New York", from: "Select two or more"
16
+ sl_check "Remember me"
17
+ sl_toggle "Subscribe to emails"
18
+ shadow_fill_in 'sl-textarea[name="user[description]"]', "textarea", with: "I am a human."
19
+
20
+ find("sl-button", text: "Create User").click
21
+
22
+ assert_current_path hotwire_form_path(1)
23
+ assert_text "Name: Yuki Nishijima"
24
+ assert_text "Description: I am a human."
25
+ assert_text "Color: #ffffff"
26
+ assert_text "Score: 50"
27
+ # assert_text "Current City:"
28
+ assert_text "Previous City: tokyo"
29
+ assert_text 'Past Cities: ["tokyo", "new_york"]'
30
+ assert_text "Remember Me: 1"
31
+ assert_text "Subscribe To Emails: 1"
32
+ end
33
+
34
+ test "It can submit a form with a POST method without asynchronous submission" do
35
+ shadow_fill_in 'sl-input[label="Name"]', with: "Yuki Nishijima"
36
+ shadow_fill_in 'sl-range[name="user[score]"]', with: "50"
37
+
38
+ find('sl-radio', text: "New York").click # Selecting a radio button does not work...
39
+
40
+ sl_select "Tokyo", from: "Select one"
41
+ sl_multi_select "Tokyo", "New York", from: "Select two or more"
42
+ sl_check "Remember me"
43
+ sl_toggle "Subscribe to emails"
44
+ shadow_fill_in 'sl-textarea[name="user[description]"]', "textarea", with: "I am a human."
45
+
46
+ find("sl-button", text: "Submit without Turbo").click
47
+
48
+ assert_current_path hotwire_form_path(1)
49
+ assert_text "Name: Yuki Nishijima"
50
+ assert_text "Description: I am a human."
51
+ assert_text "Color: #ffffff"
52
+ assert_text "Score: 50"
53
+ # assert_text "Current City:"
54
+ assert_text "Previous City: tokyo"
55
+ assert_text 'Past Cities: ["tokyo", "new_york"]'
56
+ assert_text "Remember Me: 1"
57
+ assert_text "Subscribe To Emails: 1"
58
+ end
59
+
60
+ test "It can handle an error form submission" do
61
+ find("sl-button", text: "Create User").click
62
+
63
+ assert_text "Name can't be blank"
64
+ end
65
+ end
@@ -0,0 +1,39 @@
1
+ require "test_helper"
2
+
3
+ class TurbolinksFormTest < ApplicationSystemTestCase
4
+ setup do
5
+ visit new_turbolinks_form_path
6
+ end
7
+
8
+ test "It can submit a form with a POST method" do
9
+ shadow_fill_in 'sl-input[label="Name"]', with: "Yuki Nishijima"
10
+ shadow_fill_in 'sl-range[name="user[score]"]', with: "50"
11
+
12
+ find('sl-radio', text: "New York").click # Selecting a radio button does not work...
13
+
14
+ sl_select "Tokyo", from: "Select one"
15
+ sl_multi_select "Tokyo", "New York", from: "Select two or more"
16
+ sl_check "Remember me"
17
+ sl_toggle "Subscribe to emails"
18
+ shadow_fill_in 'sl-textarea[name="user[description]"]', "textarea", with: "I am a human."
19
+
20
+ find("sl-button", text: "Create User").click
21
+
22
+ assert_current_path turbolinks_forms_path
23
+ assert_text "Name: Yuki Nishijima"
24
+ assert_text "Description: I am a human."
25
+ assert_text "Color: #ffffff"
26
+ assert_text "Score: 50"
27
+ # assert_text "Current City:"
28
+ assert_text "Previous City: tokyo"
29
+ assert_text 'Past Cities: ["tokyo", "new_york"]'
30
+ assert_text "Remember Me: 1"
31
+ assert_text "Subscribe To Emails: 1"
32
+ end
33
+
34
+ test "It can handle an error form submission" do
35
+ find("sl-button", text: "Create User").click
36
+
37
+ assert_text "Name can't be blank"
38
+ end
39
+ end
@@ -0,0 +1,68 @@
1
+ ENV['RAILS_ENV'] ||= 'test'
2
+
3
+ require_relative "../config/environment"
4
+ require "rails/test_help"
5
+ require "action_dispatch/system_testing/server"
6
+ require "shoelace/testing"
7
+
8
+ Capybara.server = :webrick
9
+
10
+ class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
11
+ include Shoelace::Testing
12
+
13
+ if ENV['BROWSERSTACK_URL'].present?
14
+ browserstack_url = URI(ENV['BROWSERSTACK_URL'])
15
+ browserstack_url.user = ENV['BROWSERSTACK_USERNAME'] if ENV['BROWSERSTACK_USERNAME']
16
+ browserstack_url.password = ENV['BROWSERSTACK_ACCESS_KEY'] if ENV['BROWSERSTACK_ACCESS_KEY']
17
+
18
+ os, os_version, browser, browser_version =
19
+ ENV.fetch('TARGET_BROWSER', 'Windows, 10, Edge, latest').split(", ")
20
+
21
+ caps = Selenium::WebDriver::Remote::Capabilities.new(
22
+ name: "Shoelace Rails",
23
+ server: browserstack_url.host,
24
+ user: browserstack_url.user,
25
+ key: browserstack_url.password,
26
+ os: os,
27
+ os_version: os_version,
28
+ browser: browser,
29
+ browser_version: browser_version,
30
+ "browserstack.console": "errors",
31
+ # "browserstack.debug": true,
32
+ "browserstack.local": true,
33
+ "browserstack.networkLogs": true,
34
+ )
35
+
36
+ # Running multiple sessions with browserstack-local is not stable, so setting it to 1 for now.
37
+ parallelize workers: 1
38
+
39
+ # Safari has some limitations due to their security models so we have to stick with localhost:3000.
40
+ if browser.downcase == 'safari'
41
+ Capybara.app_host = "http://localhost"
42
+ Capybara.server_port = 3000
43
+ end
44
+
45
+ driven_by :selenium, using: :remote, options: { url: browserstack_url.to_s, capabilities: caps }
46
+ else
47
+ parallelize workers: :number_of_processors
48
+
49
+ driven_by :selenium, using: (ENV["JS_DRIVER"] || :headless_chrome).downcase.to_sym, screen_size: [1400, 1400]
50
+ end
51
+
52
+ def shadow_fill_in(shadow_host, *locators, with:, currently_with: nil, fill_options: {}, **find_options)
53
+ shadow_host = shadow_host.respond_to?(:to_capybara_node) ? shadow_host.to_capybara_node : find(shadow_host)
54
+
55
+ locators = ['input'] if locators.empty?
56
+ locators
57
+ .reduce(shadow_host.shadow_root) { |node, locator| node.find(locator).shadow_root || node.find(locator) }
58
+ .set(with, **fill_options)
59
+ end
60
+
61
+ def dispatch_event(query_selector: , event: , detail: {}.to_json)
62
+ execute_script(<<~JAVASCRIPT)
63
+ document
64
+ .querySelector('#{query_selector}')
65
+ .dispatchEvent(new CustomEvent('#{event}', { detail: #{detail} }))
66
+ JAVASCRIPT
67
+ end
68
+ end