practical 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 (119) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +37 -0
  3. data/Rakefile +10 -0
  4. data/app/components/practical/views/base_component.rb +6 -0
  5. data/app/components/practical/views/button_component.rb +27 -0
  6. data/app/components/practical/views/datatable/filter_applied.rb +25 -0
  7. data/app/components/practical/views/datatable/filter_section_component.html.erb +9 -0
  8. data/app/components/practical/views/datatable/filter_section_component.rb +19 -0
  9. data/app/components/practical/views/datatable/sort_link_component.rb +48 -0
  10. data/app/components/practical/views/datatable.rb +36 -0
  11. data/app/components/practical/views/flash_messages_component.rb +65 -0
  12. data/app/components/practical/views/form/error_list_component.rb +15 -0
  13. data/app/components/practical/views/form/error_list_item_component.rb +20 -0
  14. data/app/components/practical/views/form/error_list_item_template_component.rb +9 -0
  15. data/app/components/practical/views/form/fallback_errors_section_component.html.erb +7 -0
  16. data/app/components/practical/views/form/fallback_errors_section_component.rb +21 -0
  17. data/app/components/practical/views/form/field_errors_component.rb +28 -0
  18. data/app/components/practical/views/form/field_title_component.rb +23 -0
  19. data/app/components/practical/views/form/fieldset_title_component.rb +20 -0
  20. data/app/components/practical/views/form/input_component.html.erb +7 -0
  21. data/app/components/practical/views/form/input_component.rb +22 -0
  22. data/app/components/practical/views/form/option_label_component.rb +21 -0
  23. data/app/components/practical/views/form/practical_editor_component.rb +26 -0
  24. data/app/components/practical/views/form/required_radio_collection_wrapper_component.rb +23 -0
  25. data/app/components/practical/views/form_wrapper.rb +21 -0
  26. data/app/components/practical/views/icon_component.rb +36 -0
  27. data/app/components/practical/views/icon_for_file_extension_component.rb +53 -0
  28. data/app/components/practical/views/modal_dialog_component.html.erb +10 -0
  29. data/app/components/practical/views/modal_dialog_component.rb +16 -0
  30. data/app/components/practical/views/navigation/breadcrumb_item_component.rb +20 -0
  31. data/app/components/practical/views/navigation/breadcrumbs_component.html.erb +31 -0
  32. data/app/components/practical/views/navigation/breadcrumbs_component.rb +41 -0
  33. data/app/components/practical/views/navigation/navigation_link_component.rb +39 -0
  34. data/app/components/practical/views/navigation/pagination/goto_form_component.html.erb +31 -0
  35. data/app/components/practical/views/navigation/pagination/goto_form_component.rb +34 -0
  36. data/app/components/practical/views/navigation/pagination_component.html.erb +11 -0
  37. data/app/components/practical/views/navigation/pagination_component.rb +98 -0
  38. data/app/components/practical/views/open_dialog_button_component.rb +16 -0
  39. data/app/components/practical/views/page_component.html.erb +53 -0
  40. data/app/components/practical/views/page_component.rb +12 -0
  41. data/app/components/practical/views/relative_time_component.rb +13 -0
  42. data/app/components/practical/views/tiptap_document_component.rb +311 -0
  43. data/app/components/practical/views/toast_component.html.erb +26 -0
  44. data/app/components/practical/views/toast_component.rb +19 -0
  45. data/app/controllers/concerns/practical/auth/passkeys/emergency_registrations.rb +57 -0
  46. data/app/controllers/concerns/practical/auth/passkeys/web_authn_debug_context.rb +13 -0
  47. data/app/controllers/concerns/practical/views/flash_helpers.rb +37 -0
  48. data/app/controllers/concerns/practical/views/json_redirection.rb +7 -0
  49. data/app/lib/practical/defaults/shrine.rb +48 -0
  50. data/app/lib/practical/test/helpers/administrator/test_helpers.rb +7 -0
  51. data/app/lib/practical/test/helpers/extra_assertions.rb +7 -0
  52. data/app/lib/practical/test/helpers/flash_assertions.rb +8 -0
  53. data/app/lib/practical/test/helpers/integration/assertions.rb +23 -0
  54. data/app/lib/practical/test/helpers/passkey/system/base.rb +52 -0
  55. data/app/lib/practical/test/helpers/passkey/system/rack_test.rb +45 -0
  56. data/app/lib/practical/test/helpers/passkey/system/selenium.rb +107 -0
  57. data/app/lib/practical/test/helpers/passkey/test_helper.rb +128 -0
  58. data/app/lib/practical/test/helpers/postmark.rb +11 -0
  59. data/app/lib/practical/test/helpers/relation_builder_assertions.rb +18 -0
  60. data/app/lib/practical/test/helpers/setup/debug.rb +8 -0
  61. data/app/lib/practical/test/helpers/setup/faker_seed_pinning.rb +8 -0
  62. data/app/lib/practical/test/helpers/setup/simplecov.rb +17 -0
  63. data/app/lib/practical/test/helpers/shrine/test_data.rb +101 -0
  64. data/app/lib/practical/test/helpers/spy_assertions.rb +7 -0
  65. data/app/lib/practical/test/helpers/system/assertions.rb +33 -0
  66. data/app/lib/practical/test/helpers/system/capybara_prep.rb +10 -0
  67. data/app/lib/practical/test/shared/auth/passkeys/controllers/emergency_registration/base.rb +372 -0
  68. data/app/lib/practical/test/shared/auth/passkeys/controllers/emergency_registration/self_service.rb +66 -0
  69. data/app/lib/practical/test/shared/auth/passkeys/controllers/reauthentication/base.rb +119 -0
  70. data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/no_self_destroy.rb +13 -0
  71. data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/no_self_signup.rb +22 -0
  72. data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/self_destroy.rb +134 -0
  73. data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/self_signup.rb +221 -0
  74. data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/update.rb +220 -0
  75. data/app/lib/practical/test/shared/auth/passkeys/controllers/sessions/base.rb +108 -0
  76. data/app/lib/practical/test/shared/auth/passkeys/forms/emergency_registration.rb +82 -0
  77. data/app/lib/practical/test/shared/auth/passkeys/models/emergency_registration/base.rb +89 -0
  78. data/app/lib/practical/test/shared/auth/passkeys/models/emergency_registration/use_for_and_notify.rb +48 -0
  79. data/app/lib/practical/test/shared/auth/passkeys/models/passkey.rb +101 -0
  80. data/app/lib/practical/test/shared/auth/passkeys/models/resource_with_passkeys.rb +57 -0
  81. data/app/lib/practical/test/shared/auth/passkeys/policies/passkey.rb +18 -0
  82. data/app/lib/practical/test/shared/auth/passkeys/services/send_emergency_registration.rb +41 -0
  83. data/app/lib/practical/test/shared/models/normalized_email.rb +23 -0
  84. data/app/lib/practical/test/shared/models/user.rb +27 -0
  85. data/app/lib/practical/test/shared/models/utility/ip_address.rb +42 -0
  86. data/app/lib/practical/test/shared/models/utility/user_agent.rb +43 -0
  87. data/app/lib/practical/views/button/styling.rb +23 -0
  88. data/app/lib/practical/views/error_handling.rb +33 -0
  89. data/app/lib/practical/views/form_builders/base.rb +152 -0
  90. data/app/lib/practical/views/icon_set.rb +156 -0
  91. data/app/lib/practical/views/web_awesome/style_utility/appearance_variant.rb +19 -0
  92. data/app/lib/practical/views/web_awesome/style_utility/base.rb +21 -0
  93. data/app/lib/practical/views/web_awesome/style_utility/color_variant.rb +17 -0
  94. data/app/lib/practical/views/web_awesome/style_utility/size.rb +31 -0
  95. data/config/locales/auth.en.yml +38 -0
  96. data/config/locales/devise.passkeys.en.yml +18 -0
  97. data/config/locales/practical_framework.en.yml +9 -0
  98. data/config/routes.rb +4 -0
  99. data/db/seeds/setup.rb +13 -0
  100. data/db/seeds/users/default.rb +34 -0
  101. data/lib/generators/practical/test/helper/USAGE +8 -0
  102. data/lib/generators/practical/test/helper/helper_generator.rb +9 -0
  103. data/lib/generators/practical/test/helper/templates/helper.rb.tt +4 -0
  104. data/lib/generators/practical/test/shared_test/USAGE +9 -0
  105. data/lib/generators/practical/test/shared_test/shared_test_generator.rb +7 -0
  106. data/lib/generators/practical/test/shared_test/templates/shared_test.rb.tt +9 -0
  107. data/lib/generators/practical/views/component/USAGE +9 -0
  108. data/lib/generators/practical/views/component/component_generator.rb +20 -0
  109. data/lib/practical/framework/engine.rb +35 -0
  110. data/lib/practical/helpers/form_with_helper.rb +10 -0
  111. data/lib/practical/helpers/icon_helper.rb +18 -0
  112. data/lib/practical/helpers/text_helper.rb +20 -0
  113. data/lib/practical/helpers/translation_helper.rb +25 -0
  114. data/lib/practical/version.rb +5 -0
  115. data/lib/practical/views/element_helper.rb +48 -0
  116. data/lib/practical.rb +21 -0
  117. data/lib/tasks/practical/coverage.rake +19 -0
  118. data/lib/tasks/practical/framework_tasks.rake +6 -0
  119. metadata +303 -0
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webauthn/fake_client"
4
+
5
+ module Practical::Test::Helpers::Passkey::System::Selenium
6
+ extend ActiveSupport::Concern
7
+ include Practical::Test::Helpers::Passkey::System::Base
8
+
9
+ included do
10
+ teardown do
11
+ if @virtual_authenticator.present? && @virtual_authenticator.valid?
12
+ @virtual_authenticator.remove!
13
+ end
14
+ rescue Selenium::WebDriver::Error::InvalidArgumentError
15
+ end
16
+
17
+ def self.selenium_driver_key
18
+ if ENV.has_key?("HEADLESS_TESTS")
19
+ return :headless_chrome
20
+ else
21
+ return :chrome
22
+ end
23
+ end
24
+
25
+ driven_by :selenium, using: selenium_driver_key, screen_size: [1400, 1400] do |options|
26
+ options.accept_insecure_certs = true
27
+ options.args << "--auto-open-devtools-for-tabs"
28
+ end
29
+ end
30
+
31
+ def create_passkey_for_user_and_return_webauthn_credential(user:)
32
+ webauthn_credential = create_webauthn_credential_from_scratch(webauthn_client: user_webauthn_client,
33
+ rp_id: user_relying_party_id,
34
+ relying_party: user_relying_party
35
+ )
36
+ # rubocop:disable Layout/LineLength
37
+ keypair = fake_authenticator.instance_variable_get("@credentials")[user_relying_party_id][webauthn_credential.id][:credential_key]
38
+ # rubocop:enable Layout/LineLength
39
+
40
+ resident_credential = create_credential(
41
+ rp_id: user_relying_party_id,
42
+ id: webauthn_credential.id.bytes,
43
+ keypair: keypair
44
+ )
45
+
46
+ authenticator = add_virtual_authenticator
47
+ authenticator.add_credential(resident_credential)
48
+
49
+ user.passkeys.create!(
50
+ label: SecureRandom.hex,
51
+ external_id: Base64.strict_encode64(webauthn_credential.id),
52
+ public_key: Base64.strict_encode64(webauthn_credential.public_key),
53
+ sign_count: 0
54
+ )
55
+
56
+ return webauthn_credential
57
+ end
58
+
59
+ def authenticator_options(options: {})
60
+ options = options.reverse_merge({
61
+ protocol: :ctap2,
62
+ resident_key: true,
63
+ user_verification: true,
64
+ user_verified: true
65
+ })
66
+ Selenium::WebDriver::VirtualAuthenticatorOptions.new(**options)
67
+ end
68
+
69
+ def add_virtual_authenticator(options: authenticator_options)
70
+ @virtual_authenticator ||= Capybara.current_session.driver.browser.add_virtual_authenticator(options)
71
+ end
72
+
73
+ def default_authenticator
74
+ @virtual_authenticator
75
+ end
76
+
77
+ def create_credential(rp_id:, user_handle: generate_user_handle, id:, keypair:)
78
+ private_key = Base64.strict_encode64(keypair.private_to_der)
79
+ decoded_private_key = Base64.strict_decode64(private_key).bytes
80
+
81
+ return Selenium::WebDriver::Credential.resident(
82
+ id: id,
83
+ private_key: decoded_private_key,
84
+ rp_id: rp_id,
85
+ user_handle: user_handle
86
+ )
87
+
88
+ Selenium::WebDriver::Credential.resident(
89
+ id: generate_credential_id,
90
+ private_key: decoded_private_key,
91
+ rp_id: "localhost",
92
+ user_handle: generate_user_handle
93
+ )
94
+
95
+ keypair.public_key.to_bn.to_s(2)
96
+ private_key = keypair.private_key.to_bn.to_s(2)
97
+ return Selenium::WebDriver::Credential.resident(id: id.bytes,
98
+ private_key: private_key.bytes,
99
+ rp_id: rp_id,
100
+ user_handle: user_handle.bytes
101
+ )
102
+ end
103
+
104
+ def generate_user_handle
105
+ SecureRandom.uuid.bytes
106
+ end
107
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webauthn/fake_client"
4
+
5
+ module Practical::Test::Helpers::Passkey::TestHelper
6
+ def assert_passkey_registration_challenge(
7
+ data:,
8
+ stored_challenge:,
9
+ relying_party_data:,
10
+ user_data:,
11
+ credentials_to_exclude: []
12
+ )
13
+ authentication_selection = {"residentKey"=>"required", "userVerification"=>"required"}
14
+
15
+ assert_equal stored_challenge, data["challenge"]
16
+ assert_equal relying_party_data, data["rp"]
17
+ assert_equal user_data, data["user"]
18
+ assert_equal authentication_selection, data["authenticatorSelection"]
19
+
20
+ assert_equal credentials_to_exclude, data["excludeCredentials"]
21
+ end
22
+
23
+ def assert_passkey_authentication_challenge(
24
+ data:,
25
+ stored_challenge:,
26
+ credentials_to_allow: []
27
+ )
28
+ assert_equal stored_challenge, data["challenge"]
29
+ assert_equal "required", data["userVerification"]
30
+
31
+ assert_equal credential_id_for_challenge(passkeys: credentials_to_allow), data["allowCredentials"]
32
+ end
33
+
34
+ def credential_id_for_challenge(passkeys:)
35
+ Array.wrap(passkeys).map{|x| {"type" => "public-key", "id" => x.external_id} }
36
+ end
37
+
38
+ def administrator_user_data(administrator:)
39
+ {"name"=>administrator.email, "id"=>administrator.webauthn_id, "displayName"=>administrator.email}
40
+ end
41
+
42
+ def user_data(user:)
43
+ {"name"=>user.email, "id"=>user.webauthn_id, "displayName"=>user.name}
44
+ end
45
+
46
+ def credential_data_for_passkey(passkey:)
47
+ {"type"=>"public-key", "id"=> passkey.external_id}
48
+ end
49
+
50
+ def relying_party_origin
51
+ AppSettings.relying_party_origin
52
+ end
53
+
54
+ def admin_relying_party_origin
55
+ AppSettings.admin_relying_party_origin
56
+ end
57
+
58
+ def admin_relying_party_id
59
+ URI.parse(admin_relying_party_origin).host
60
+ end
61
+
62
+ def user_relying_party_id
63
+ URI.parse(relying_party_origin).host
64
+ end
65
+
66
+ def admin_relying_party(options: {})
67
+ return WebAuthn::RelyingParty.new(**{
68
+ allowed_origins: admin_relying_party_origin,
69
+ name: I18n.translate("administrator.app_title.text")
70
+ }.merge(options))
71
+ end
72
+
73
+ def relying_party(options: {})
74
+ return WebAuthn::RelyingParty.new(**{
75
+ allowed_origins: relying_party_origin,
76
+ name: I18n.translate("app_title.text")
77
+ }.merge(options))
78
+ end
79
+
80
+ def fake_authenticator
81
+ return WebAuthn::FakeAuthenticator.new
82
+ end
83
+
84
+ def fake_client(origin: "https://example.test", authenticator: WebAuthn::FakeAuthenticator.new)
85
+ return WebAuthn::FakeClient.new(origin, authenticator: authenticator)
86
+ end
87
+
88
+ def encode_challenge(raw_challenge:)
89
+ Base64.strict_encode64(raw_challenge)
90
+ end
91
+
92
+ def assertion_from_client(client:, challenge:, user_verified: true)
93
+ client.get(challenge: challenge, user_verified: user_verified)
94
+ end
95
+
96
+ def hydrate_response_from_raw_credential(client:, relying_party:, raw_credential:)
97
+ attestation_object = if client.encoding
98
+ relying_party.encoder.decode(raw_credential["response"]["attestationObject"])
99
+ else
100
+ raw_credential["response"]["attestationObject"]
101
+ end
102
+
103
+ client_data_json =
104
+ if client.encoding
105
+ relying_party.encoder.decode(raw_credential["response"]["clientDataJSON"])
106
+ else
107
+ raw_credential["response"]["clientDataJSON"]
108
+ end
109
+
110
+ return WebAuthn::AuthenticatorAttestationResponse.new(
111
+ attestation_object: attestation_object,
112
+ client_data_json: client_data_json,
113
+ relying_party: relying_party
114
+ )
115
+ end
116
+
117
+ def create_credential_and_return_payload_from_challenge(client:, challenge:, user_verified: true)
118
+ return client.create(challenge: challenge, user_verified: user_verified)
119
+ end
120
+
121
+ def get_credential_payload_from_challenge(client:, challenge:, user_verified: true)
122
+ client.get(challenge: challenge, user_verified: user_verified)
123
+ end
124
+
125
+ def encoded_public_key(credential:, relying_party:)
126
+ relying_party.encoder.encode(credential.public_key)
127
+ end
128
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Test::Helpers::Postmark
4
+ def extract_template_model(mail:)
5
+ scanner = StringScanner.new(mail.body.encoded)
6
+ scanner.skip_until(/Model:\r\n/)
7
+
8
+ result = scanner.scan_until(/^Use the #prerender/)
9
+ return JSON.parse(result.gsub("Use the #prerender", ''))
10
+ end
11
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Test::Helpers::RelationBuilderAssertions
4
+ def assert_relation_ordering_matches(expected:, relation:, instance:)
5
+ assert_not_equal expected, relation
6
+ assert_sql_transformation(expected: expected, actual: instance.apply_ordering(scope: relation))
7
+ end
8
+
9
+ def assert_relation_filtering_matches(expected:, relation:, instance:)
10
+ assert_not_equal expected, relation
11
+ assert_sql_transformation(expected: expected, actual: instance.apply_filtering(scope: relation))
12
+ end
13
+
14
+ def assert_sql_transformation(expected:, actual:)
15
+ message = {expected_sql: expected.to_sql, actual_sql: actual.to_sql}
16
+ assert_equal expected, actual, message
17
+ end
18
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Test::Helpers::Setup::Debug
4
+ # rubocop:disable Rails/Output
5
+ puts "MINITEST_PARALLEL_EXECUTOR_SIZE: #{Minitest.parallel_executor.size}"
6
+ puts "PARALLEL_WORKERS: #{ENV["PARALLEL_WORKERS"]}"
7
+ # rubocop:enable Rails/Output
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Test::Helpers::Setup::FakerSeedPinning
4
+ def before_setup
5
+ super
6
+ Faker::Config.random = Random.new(Minitest.seed)
7
+ end
8
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Test::Helpers::Setup::Simplecov
4
+ extend ActiveSupport::Concern
5
+
6
+ if ENV['COVERAGE']
7
+ included do
8
+ parallelize_setup do |worker|
9
+ SimpleCov.command_name "#{SimpleCov.command_name}-#{worker}"
10
+ end
11
+
12
+ parallelize_teardown do |worker|
13
+ SimpleCov.result
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "shrine/storage/memory"
4
+
5
+ Shrine.storages = {
6
+ cache: Shrine::Storage::Memory.new,
7
+ store: Shrine::Storage::Memory.new
8
+ }
9
+
10
+ module Practical::Test::Helpers::Shrine::TestData
11
+ def image_data
12
+ attacher = Shrine::Attacher.new
13
+ attacher.set(uploaded_image)
14
+
15
+ attacher.data
16
+ end
17
+
18
+ def uploaded_image
19
+ file = image_file
20
+ uploaded_file = Shrine.upload(file, :store, metadata: false)
21
+ uploaded_file.metadata.merge!(
22
+ "size" => File.size(file.path),
23
+ "mime_type" => "image/jpeg",
24
+ "filenae" => "test.jpeg"
25
+ )
26
+
27
+ uploaded_file
28
+ end
29
+
30
+ def image_filepath
31
+ file_fixture("dog.jpeg")
32
+ end
33
+
34
+ def image_file
35
+ File.open(image_filepath)
36
+ end
37
+
38
+ def heic_filepath
39
+ file_fixture("island.heic")
40
+ end
41
+
42
+ def heic_file
43
+ File.open(heic_filepath)
44
+ end
45
+
46
+ def csv_filepath
47
+ file_fixture("example.csv")
48
+ end
49
+
50
+ def csv_file
51
+ File.open(csv_filepath)
52
+ end
53
+
54
+ def excel_filepath
55
+ file_fixture("example.xlsx")
56
+ end
57
+
58
+ def excel_file
59
+ File.open(excel_filepath)
60
+ end
61
+
62
+ def word_filepath
63
+ file_fixture("example.docx")
64
+ end
65
+
66
+ def pdf_file
67
+ File.open(pdf_filepath)
68
+ end
69
+
70
+ def pdf_filepath
71
+ file_fixture("example.pdf")
72
+ end
73
+
74
+ def word_file
75
+ File.open(word_filepath)
76
+ end
77
+
78
+ def numbers_filepath
79
+ file_fixture("numbers-example.numbers")
80
+ end
81
+
82
+ def numbers_file
83
+ File.open(numbers_filepath)
84
+ end
85
+
86
+ def plaintext_filepath
87
+ file_fixture("example.txt")
88
+ end
89
+
90
+ def plaintext_file
91
+ File.open(plaintext_filepath)
92
+ end
93
+
94
+ def rtf_filepath
95
+ file_fixture("example.rtf")
96
+ end
97
+
98
+ def rtf_file
99
+ File.open(rtf_filepath)
100
+ end
101
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Test::Helpers::SpyAssertions
4
+ def assert_times_called(spy:, times:)
5
+ assert_equal times, spy.calls.count
6
+ end
7
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Test::Helpers::System::Assertions
4
+ def assert_toast_message(text:)
5
+ within(".notification-messages") do
6
+ assert_selector("dialog", text: text)
7
+ end
8
+ end
9
+
10
+ def assert_sign_in_user(user:)
11
+ create_passkey_for_user_and_return_webauthn_credential(
12
+ user: user
13
+ )
14
+
15
+ if Capybara.current_driver == :rack_test
16
+ page.driver.browser.post(new_user_session_challenge_url, as: :json)
17
+ challenge_data = JSON.parse(page.driver.response.body)
18
+ end
19
+
20
+ visit new_user_session_url
21
+
22
+ fill_in "Email", with: user.email
23
+
24
+ if Capybara.current_driver == :rack_test
25
+ credential_data = user_webauthn_client.get(challenge: challenge_data["challenge"], user_verified: true)
26
+ find("input[type=hidden]#user_passkey_credential", visible: false).set(JSON.generate(credential_data))
27
+ end
28
+
29
+ click_on "Sign in"
30
+
31
+ assert_toast_message(text: I18n.translate("devise.sessions.signed_in"))
32
+ end
33
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Test::Helpers::System::CapybaraPrep
4
+ extend ActiveSupport::Concern
5
+ included do
6
+ Capybara.save_path = Rails.root.join("tmp", "capybara").to_s
7
+ Capybara.server_host = AppSettings.default_host
8
+ Capybara.server = :puma, { Host: "ssl://#{Capybara.server_host}"}
9
+ end
10
+ end