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.
- 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
|