shoelace-rails 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/main.yml +85 -0
- data/.gitignore +20 -0
- data/Appraisals +25 -0
- data/CHANGELOG.md +3 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +11 -0
- data/LICENSE.txt +21 -0
- data/README.md +178 -0
- data/Rakefile +27 -0
- data/app/helpers/shoelace/form_helper.rb +451 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/dist/.keep +0 -0
- data/dist/types/.keep +0 -0
- data/gemfiles/rails_50.gemfile +12 -0
- data/gemfiles/rails_51.gemfile +11 -0
- data/gemfiles/rails_52.gemfile +11 -0
- data/gemfiles/rails_60.gemfile +11 -0
- data/gemfiles/rails_61.gemfile +11 -0
- data/gemfiles/rails_70.gemfile +11 -0
- data/gemfiles/rails_edge.gemfile +14 -0
- data/lib/shoelace/engine.rb +8 -0
- data/lib/shoelace/rails/version.rb +7 -0
- data/lib/shoelace/rails.rb +10 -0
- data/lib/shoelace/testing.rb +40 -0
- data/package.json +50 -0
- data/rollup.config.js +49 -0
- data/shoelace-rails.gemspec +35 -0
- data/src/index.ts +2 -0
- data/src/turbo/index.ts +6 -0
- data/src/turbo/polyfills/formdata-event.js +27 -0
- data/src/turbo/sl-turbo-form.ts +110 -0
- data/src/turbolinks/features/confirm.ts +42 -0
- data/src/turbolinks/features/disable.ts +94 -0
- data/src/turbolinks/features/remote.ts +107 -0
- data/src/turbolinks/index.ts +6 -0
- data/src/turbolinks/selectors.ts +38 -0
- data/src/turbolinks/start.ts +38 -0
- data/src/turbolinks/turbolinks.ts +78 -0
- data/src/turbolinks/utils/ajax.ts +146 -0
- data/src/turbolinks/utils/csp.ts +20 -0
- data/src/turbolinks/utils/csrf.ts +33 -0
- data/src/turbolinks/utils/dom.ts +40 -0
- data/src/turbolinks/utils/event.ts +57 -0
- data/src/turbolinks/utils/form.ts +58 -0
- data/test/dummy_app/Gemfile +19 -0
- data/test/dummy_app/Rakefile +6 -0
- data/test/dummy_app/app/controllers/hotwire_forms_controller.rb +46 -0
- data/test/dummy_app/app/controllers/turbolinks_forms_controller.rb +37 -0
- data/test/dummy_app/app/models/user.rb +16 -0
- data/test/dummy_app/app/packs/entrypoints/hotwire.js +1 -0
- data/test/dummy_app/app/packs/entrypoints/turbolinks.js +5 -0
- data/test/dummy_app/app/views/hotwire_forms/form.html.erb +45 -0
- data/test/dummy_app/app/views/hotwire_forms/show.html.erb +5 -0
- data/test/dummy_app/app/views/layouts/application.html.erb +39 -0
- data/test/dummy_app/app/views/turbolinks_forms/form.html.erb +44 -0
- data/test/dummy_app/app/views/turbolinks_forms/show.html.erb +5 -0
- data/test/dummy_app/bin/rails +5 -0
- data/test/dummy_app/bin/webpack +18 -0
- data/test/dummy_app/bin/yarn +18 -0
- data/test/dummy_app/config/application.rb +16 -0
- data/test/dummy_app/config/boot.rb +4 -0
- data/test/dummy_app/config/environment.rb +2 -0
- data/test/dummy_app/config/environments/development.rb +10 -0
- data/test/dummy_app/config/environments/test.rb +18 -0
- data/test/dummy_app/config/routes.rb +4 -0
- data/test/dummy_app/config/webpack/development.js +5 -0
- data/test/dummy_app/config/webpack/production.js +1 -0
- data/test/dummy_app/config/webpack/test.js +5 -0
- data/test/dummy_app/config/webpacker.yml +33 -0
- data/test/dummy_app/config.ru +6 -0
- data/test/dummy_app/package.json +24 -0
- data/test/dummy_app/test/system/hotwire_form_test.rb +65 -0
- data/test/dummy_app/test/system/turbolinks_form_test.rb +39 -0
- data/test/dummy_app/test/test_helper.rb +68 -0
- data/test/helpers/form_helper_test.rb +397 -0
- data/test/test_helper.rb +18 -0
- data/tsconfig.json +19 -0
- data/yarn.lock +249 -0
- 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,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,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 @@
|
|
1
|
+
test.js
|
@@ -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,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
|