diff --git a/app/components/webauthn_input_component.rb b/app/components/webauthn_input_component.rb
index 069f539ee9e..60d4dc22cf5 100644
--- a/app/components/webauthn_input_component.rb
+++ b/app/components/webauthn_input_component.rb
@@ -22,10 +22,16 @@ def call
:'lg-webauthn-input',
content,
**tag_options,
- hidden: true,
- platform: platform?.presence,
- 'passkey-supported-only': passkey_supported_only?.presence,
+ **initial_hidden_tag_options,
'show-unsupported-passkey': show_unsupported_passkey?.presence,
)
end
+
+ def initial_hidden_tag_options
+ if platform? && passkey_supported_only?
+ { hidden: true }
+ else
+ { class: 'js' }
+ end
+ end
end
diff --git a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts
index 18fb3e36093..7c6ee1a0ffb 100644
--- a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts
+++ b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts
@@ -17,73 +17,45 @@ describe('WebauthnInputElement', () => {
quibble.reset();
});
- context('input for non-platform authenticator', () => {
- beforeEach(() => {
- document.body.innerHTML = ``;
- });
+ context('device does not support passkey', () => {
+ context('unsupported passkey not shown', () => {
+ beforeEach(() => {
+ isWebauthnPasskeySupported.returns(false);
+ document.body.innerHTML = ``;
+ });
- it('becomes visible', () => {
- const element = document.querySelector('lg-webauthn-input')!;
+ it('stays hidden', () => {
+ const element = document.querySelector('lg-webauthn-input')!;
- expect(element.hidden).to.be.false();
+ expect(element.hidden).to.be.true();
+ });
});
- });
- context('input for platform authenticator', () => {
- context('no passkey only restriction', () => {
+ context('unsupported passkey shown', () => {
beforeEach(() => {
- document.body.innerHTML = ``;
+ isWebauthnPasskeySupported.returns(false);
+ document.body.innerHTML = ``;
});
- it('becomes visible', () => {
+ it('becomes visible, with modifier class', () => {
const element = document.querySelector('lg-webauthn-input')!;
expect(element.hidden).to.be.false();
+ expect(element.classList.contains('webauthn-input--unsupported-passkey')).to.be.true();
});
});
+ });
- context('passkey supported only', () => {
- context('device does not support passkey', () => {
- context('unsupported passkey not shown', () => {
- beforeEach(() => {
- isWebauthnPasskeySupported.returns(false);
- document.body.innerHTML = ``;
- });
-
- it('stays hidden', () => {
- const element = document.querySelector('lg-webauthn-input')!;
-
- expect(element.hidden).to.be.true();
- });
- });
-
- context('unsupported passkey shown', () => {
- beforeEach(() => {
- isWebauthnPasskeySupported.returns(false);
- document.body.innerHTML = ``;
- });
-
- it('becomes visible, with modifier class', () => {
- const element = document.querySelector('lg-webauthn-input')!;
-
- expect(element.hidden).to.be.false();
- expect(element.classList.contains('webauthn-input--unsupported-passkey')).to.be.true();
- });
- });
- });
-
- context('device supports passkey', () => {
- beforeEach(() => {
- isWebauthnPasskeySupported.returns(true);
- document.body.innerHTML = ``;
- });
+ context('device supports passkey', () => {
+ beforeEach(() => {
+ isWebauthnPasskeySupported.returns(true);
+ document.body.innerHTML = ``;
+ });
- it('becomes visible', () => {
- const element = document.querySelector('lg-webauthn-input')!;
+ it('becomes visible', () => {
+ const element = document.querySelector('lg-webauthn-input')!;
- expect(element.hidden).to.be.false();
- });
- });
+ expect(element.hidden).to.be.false();
});
});
});
diff --git a/app/javascript/packages/webauthn/webauthn-input-element.ts b/app/javascript/packages/webauthn/webauthn-input-element.ts
index 1d2fd218fb6..11938ab51a6 100644
--- a/app/javascript/packages/webauthn/webauthn-input-element.ts
+++ b/app/javascript/packages/webauthn/webauthn-input-element.ts
@@ -2,27 +2,23 @@ import isWebauthnPasskeySupported from './is-webauthn-passkey-supported';
export class WebauthnInputElement extends HTMLElement {
connectedCallback() {
- this.toggleVisibleIfSupported();
+ this.toggleVisibleIfPasskeySupported();
}
get isPlatform(): boolean {
return this.hasAttribute('platform');
}
- get isOnlyPasskeySupported(): boolean {
- return this.hasAttribute('passkey-supported-only');
- }
-
get showUnsupportedPasskey(): boolean {
return this.hasAttribute('show-unsupported-passkey');
}
- isSupported(): boolean {
- return !this.isPlatform || !this.isOnlyPasskeySupported || isWebauthnPasskeySupported();
- }
+ toggleVisibleIfPasskeySupported() {
+ if (!this.hasAttribute('hidden')) {
+ return;
+ }
- toggleVisibleIfSupported() {
- if (this.isSupported()) {
+ if (isWebauthnPasskeySupported()) {
this.hidden = false;
} else if (this.showUnsupportedPasskey) {
this.hidden = false;
diff --git a/spec/components/webauthn_input_component_spec.rb b/spec/components/webauthn_input_component_spec.rb
index 766ddfd71e5..2e9f99d9931 100644
--- a/spec/components/webauthn_input_component_spec.rb
+++ b/spec/components/webauthn_input_component_spec.rb
@@ -6,12 +6,7 @@
subject(:rendered) { render_inline component }
it 'renders element with expected attributes' do
- element = rendered.css('lg-webauthn-input').first
-
- expect(element.attr('hidden')).to be_present
- expect(element.attr('platform')).to be_nil
- expect(element.attr('passkey-supported-only')).to be_nil
- expect(element.attr('show-unsupported-passkey')).to be_nil
+ expect(rendered).to have_css('lg-webauthn-input.js:not([show-unsupported-passkey])')
end
it 'exposes boolean alias for platform option' do
@@ -28,66 +23,63 @@
context 'with platform option' do
context 'with platform option false' do
- let(:options) { { platform: false } }
+ let(:options) { super().merge(platform: false) }
- it 'renders without platform attribute' do
- expect(rendered).to have_css('lg-webauthn-input[hidden]:not([platform])', visible: false)
+ it 'renders as visible for js-enabled browsers' do
+ expect(rendered).to have_css('lg-webauthn-input.js:not([show-unsupported-passkey])')
end
end
context 'with platform option true' do
- let(:options) { { platform: true } }
+ let(:options) { super().merge(platform: true) }
- it 'renders with platform attribute' do
- expect(rendered).to have_css('lg-webauthn-input[hidden][platform]', visible: false)
+ it 'renders as visible for js-enabled browsers' do
+ expect(rendered).to have_css('lg-webauthn-input.js:not([show-unsupported-passkey])')
end
- end
- end
-
- context 'with passkey_supported_only option' do
- context 'with passkey_supported_only option false' do
- let(:options) { { passkey_supported_only: false } }
-
- it 'renders without passkey-supported-only attribute' do
- expect(rendered).to have_css(
- 'lg-webauthn-input[hidden]:not([passkey-supported-only])',
- visible: false,
- )
- end
- end
-
- context 'with passkey_supported_only option true' do
- let(:options) { { passkey_supported_only: true } }
-
- it 'renders with passkey-supported-only attribute' do
- expect(rendered).to have_css(
- 'lg-webauthn-input[hidden][passkey-supported-only]',
- visible: false,
- )
- end
- end
- end
-
- context 'with show_unsupported_passkey option' do
- context 'with show_unsupported_passkey option false' do
- let(:options) { { show_unsupported_passkey: false } }
-
- it 'renders without show-unsupported-passkey attribute' do
- expect(rendered).to have_css(
- 'lg-webauthn-input[hidden]:not([show-unsupported-passkey])',
- visible: false,
- )
- end
- end
-
- context 'with show_unsupported_passkey option true' do
- let(:options) { { show_unsupported_passkey: true } }
- it 'renders with show-unsupported-passkey attribute' do
- expect(rendered).to have_css(
- 'lg-webauthn-input[hidden][show-unsupported-passkey]',
- visible: false,
- )
+ context 'with passkey_supported_only option' do
+ context 'with passkey_supported_only option false' do
+ let(:options) { super().merge(passkey_supported_only: false) }
+
+ it 'renders as visible for js-enabled browsers' do
+ expect(rendered).to have_css('lg-webauthn-input.js:not([show-unsupported-passkey])')
+ end
+ end
+
+ context 'with passkey_supported_only option true' do
+ let(:options) { super().merge(passkey_supported_only: true) }
+
+ it 'renders as hidden' do
+ expect(rendered).to have_css(
+ 'lg-webauthn-input[hidden]:not([show-unsupported-passkey])',
+ visible: false,
+ )
+ end
+
+ context 'with show_unsupported_passkey option' do
+ context 'with show_unsupported_passkey option false' do
+ let(:options) { super().merge(show_unsupported_passkey: false) }
+
+ it 'renders as hidden' do
+ expect(rendered).to have_css(
+ 'lg-webauthn-input[hidden]:not([show-unsupported-passkey])',
+ visible: false,
+ )
+ end
+ end
+
+ context 'with show_unsupported_passkey option true' do
+ let(:options) { super().merge(show_unsupported_passkey: true) }
+
+ it 'renders with show-unsupported-passkey attribute' do
+ expect(rendered).to have_css(
+ 'lg-webauthn-input[hidden][show-unsupported-passkey]',
+ visible: false,
+ )
+ end
+ end
+ end
+ end
end
end
end
@@ -96,7 +88,7 @@
let(:options) { super().merge(data: { foo: 'bar' }) }
it 'renders with additional attributes' do
- expect(rendered).to have_css('lg-webauthn-input[hidden][data-foo="bar"]', visible: false)
+ expect(rendered).to have_css('lg-webauthn-input[data-foo="bar"]')
end
end
end
diff --git a/spec/features/webauthn/hidden_spec.rb b/spec/features/webauthn/hidden_spec.rb
index 5ce217fb300..73045e44b40 100644
--- a/spec/features/webauthn/hidden_spec.rb
+++ b/spec/features/webauthn/hidden_spec.rb
@@ -1,6 +1,8 @@
require 'rails_helper'
RSpec.describe 'webauthn hide' do
+ include JavascriptDriverHelper
+
describe 'security key' do
let(:option_id) { 'two_factor_options_form_selection_webauthn' }
@@ -102,9 +104,11 @@
end
def webauthn_option_hidden?
- page.find("label[for=#{option_id}]")
- false
- rescue Capybara::ElementNotFound
- true
+ label = page.find("label[for=#{option_id}]", visible: :all)
+ if javascript_enabled?
+ !label.visible?
+ else
+ label.ancestor('.js,[hidden]', visible: :all).present?
+ end
end
end