Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions app/components/webauthn_input_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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' }
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I probably should have clarified how this is working: We have .js and .no-js class styles which hide the content in JavaScript disabled / enabled environments respectively (applied and toggled in the base layout). So by adding js as a class here, it's how we hide the content for browsers with JavaScript disabled.

end
end
end
76 changes: 24 additions & 52 deletions app/javascript/packages/webauthn/webauthn-input-element.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,73 +17,45 @@ describe('WebauthnInputElement', () => {
quibble.reset();
});

context('input for non-platform authenticator', () => {
beforeEach(() => {
document.body.innerHTML = `<lg-webauthn-input hidden></lg-webauthn-input>`;
});
context('device does not support passkey', () => {
context('unsupported passkey not shown', () => {
beforeEach(() => {
isWebauthnPasskeySupported.returns(false);
document.body.innerHTML = `<lg-webauthn-input hidden></lg-webauthn-input>`;
});

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 = `<lg-webauthn-input platform hidden></lg-webauthn-input>`;
isWebauthnPasskeySupported.returns(false);
document.body.innerHTML = `<lg-webauthn-input show-unsupported-passkey hidden></lg-webauthn-input>`;
});

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 = `<lg-webauthn-input platform passkey-supported-only hidden></lg-webauthn-input>`;
});

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 = `<lg-webauthn-input platform passkey-supported-only show-unsupported-passkey hidden></lg-webauthn-input>`;
});

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 = `<lg-webauthn-input platform passkey-supported-only hidden></lg-webauthn-input>`;
});
context('device supports passkey', () => {
beforeEach(() => {
isWebauthnPasskeySupported.returns(true);
document.body.innerHTML = `<lg-webauthn-input hidden></lg-webauthn-input>`;
});

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();
});
});
});
16 changes: 6 additions & 10 deletions app/javascript/packages/webauthn/webauthn-input-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
110 changes: 51 additions & 59 deletions spec/components/webauthn_input_component_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
12 changes: 8 additions & 4 deletions spec/features/webauthn/hidden_spec.rb
Original file line number Diff line number Diff line change
@@ -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' }

Expand Down Expand Up @@ -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