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