shoelace-rails 0.1.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 (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