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
3 changes: 2 additions & 1 deletion app/assets/stylesheets/components/_step-indicator.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ $step-indicator-current-step-border-width: 3px;
$step-indicator-line-height: 4px;
$step-indicator-pending-color: #a8b6c6;

.step-indicator {
lg-step-indicator {
display: block;
border-bottom: 1px solid color('primary-light');
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.1);
margin-bottom: units(4);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import StepIndicator from '@18f/identity-step-indicator';
import sinon from 'sinon';
import userEvent from '@testing-library/user-event';
import { useSandbox } from '../../support/sinon';
import useDefineProperty from '../../support/define-property';
import { useDefineProperty } from '@18f/identity-test-helpers';
import StepIndicator from './index';

describe('StepIndicator', () => {
const sandbox = useSandbox();
const sandbox = sinon.createSandbox();
const defineProperty = useDefineProperty();

before(() => {
if (!customElements.get('lg-step-indicator')) {
customElements.define('lg-step-indicator', StepIndicator);
}
});

function initialize({ currentStepIndex = 0 } = {}) {
document.body.innerHTML = `
<div role="region" aria-label="Step progress" class="step-indicator">
<lg-step-indicator role="region" aria-label="Step progress">
<ol class="step-indicator__scroller">
${Array.from(Array(5))
.map(
Expand All @@ -25,12 +31,20 @@ describe('StepIndicator', () => {
)
.join('')}
</ol>
</div>`;
const stepIndicator = new StepIndicator(document.body.firstElementChild);
stepIndicator.bind();
return stepIndicator;
</lg-step-indicator>`;
return document.querySelector('lg-step-indicator') as StepIndicator;
}

it('cleans up event listeners', () => {
window.resizeTo(1024, 768);
initialize();
sandbox.spy(HTMLElement.prototype, 'setAttribute');
document.body.innerHTML = '';
window.resizeTo(340, 600);

expect(HTMLElement.prototype.setAttribute).not.to.have.been.called();
});

context('small viewport', () => {
beforeEach(() => {
window.resizeTo(340, 600);
Expand All @@ -43,9 +57,12 @@ describe('StepIndicator', () => {
});

it('scrolls to current item', () => {
sandbox.stub(window, 'getComputedStyle').callsFake((element) => ({
paddingLeft: element.classList.contains('step-indicator__scroller') ? '24px' : '0',
}));
sandbox.stub(window, 'getComputedStyle').callsFake(
(element) =>
({
paddingLeft: element.classList.contains('step-indicator__scroller') ? '24px' : '0',
} as CSSStyleDeclaration),
);
defineProperty(window.Element.prototype, 'scrollWidth', {
get() {
return this.classList.contains('step-indicator__scroller') ? 593 : 0;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,36 +1,42 @@
const SMALL_VIEWPORT_MEDIA_QUERY = '(max-width: 639px)';

class StepIndicator {
/**
* @param {HTMLElement} wrapper
*/
constructor(wrapper) {
this.elements = {
wrapper,
scroller: /** @type {HTMLElement} */ (wrapper.querySelector('.step-indicator__scroller')),
currentStep: /** @type {HTMLElement?} */ (
wrapper.querySelector('.step-indicator__step--current')
),
};
}
interface StepIndicatorElements {
scroller: HTMLElement;

currentStep: HTMLElement | null;
}

class StepIndicator extends HTMLElement {
elements: StepIndicatorElements;

mediaQueryList: MediaQueryList | null;

get isSmallViewport() {
return this.mediaQueryList ? this.mediaQueryList.matches : false;
}

bind() {
connectedCallback() {
this.elements = {
scroller: this.querySelector('.step-indicator__scroller')!,
currentStep: this.querySelector('.step-indicator__step--current'),
};

this.mediaQueryList = window.matchMedia(SMALL_VIEWPORT_MEDIA_QUERY);
this.mediaQueryList.addListener(() => this.onBreakpointMatchChange());
this.mediaQueryList.addListener(this.onBreakpointMatchChange);
this.onBreakpointMatchChange();
if (this.isSmallViewport) {
this.setScrollOffset();
}
}

onBreakpointMatchChange() {
this.toggleWrapperFocusable();
disconnectedCallback() {
this.mediaQueryList?.removeListener(this.onBreakpointMatchChange);
}

onBreakpointMatchChange = () => {
this.toggleWrapperFocusable();
};

setScrollOffset() {
const { currentStep, scroller } = this.elements;
if (currentStep) {
Expand Down
3 changes: 3 additions & 0 deletions app/javascript/packages/test-helpers/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# `@18f/identity-test-helpers`

Convenient utilities and Mocha lifecycle helpers for common test implementations.
1 change: 1 addition & 0 deletions app/javascript/packages/test-helpers/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { default as useDefineProperty } from './use-define-property';
export { default as usePropertyValue } from './use-property-value';
19 changes: 19 additions & 0 deletions app/javascript/packages/test-helpers/use-define-property.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import useDefineProperty from './use-define-property';

describe('useDefineProperty', () => {
const defineProperty = useDefineProperty();

before(() => {
(global as any).useDefineProperty = 10;
defineProperty(global, 'useDefineProperty', { get: () => 20 });
});

after(() => {
expect((global as any).useDefineProperty).to.equal(10);
delete (global as any).useDefineProperty;
});

it('has property descriptor during spec', () => {
expect((global as any).useDefineProperty).to.equal(20);
});
});
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
type RedefinedProperty = [any, PropertyKey, PropertyDescriptor | undefined];

/**
* A proxy to Object.defineProperty to use in redefining an existing object and reverting that
* definition to its original value after the test has completed.
*
* @return {ObjectConstructor['defineProperty']}
*/
export default function useDefineProperty() {
let redefined = [];
function useDefineProperty(): ObjectConstructor['defineProperty'] {
let redefined: Array<RedefinedProperty> = [];

afterEach(() => {
redefined.forEach(([object, property, originalDescriptor]) => {
Expand All @@ -18,9 +18,15 @@ export default function useDefineProperty() {
redefined = [];
});

return function defineProperty(object, property, descriptor) {
return function defineProperty<O>(
object: O,
property: PropertyKey,
descriptor: PropertyDescriptor,
) {
const originalDescriptor = Object.getOwnPropertyDescriptor(object, property);
redefined.push([object, property, originalDescriptor]);
Object.defineProperty(object, property, descriptor);
return Object.defineProperty(object, property, descriptor);
};
}

export default useDefineProperty;
10 changes: 5 additions & 5 deletions app/javascript/packages/test-helpers/use-property-value.spec.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import usePropertyValue from './use-property-value';

describe('usePropertyValue', () => {
(global as any).foo = 10;
usePropertyValue(global as any, 'foo', 20);
(global as any).usePropertyValue = 10;
usePropertyValue(global as any, 'usePropertyValue', 20);

after(() => {
expect((global as any).foo).to.equal(10);
delete (global as any).foo;
expect((global as any).usePropertyValue).to.equal(10);
delete (global as any).usePropertyValue;
});

it('has value during spec', () => {
expect((global as any).foo).to.equal(20);
expect((global as any).usePropertyValue).to.equal(20);
});
});
3 changes: 1 addition & 2 deletions app/javascript/packs/step-indicator.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import StepIndicator from '@18f/identity-step-indicator';

const wrappers = Array.from(document.querySelectorAll('.step-indicator'));
wrappers.forEach((wrapper) => new StepIndicator(/** @type {HTMLElement} */ (wrapper)).bind());
customElements.define('lg-step-indicator', StepIndicator);
9 changes: 4 additions & 5 deletions app/views/shared/_step_indicator.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ locals:
* locale_scope: Scope under which to find title translations for steps given as names.
* current_step: Current step.
%>
<% classes = ['step-indicator']
classes << local_assigns[:class] if local_assigns[:class]
normalized_steps = steps.map do |step|
<% normalized_steps = steps.map do |step|
if local_assigns[:locale_scope]
{ title: t(step[:name], scope: [:step_indicator, :flows, locale_scope]) }.merge(step)
else
Expand All @@ -16,10 +14,11 @@ locals:
end
current_step_index = normalized_steps.index { |step| step[:name] == local_assigns[:current_step] } %>

<%= tag.div(
<%= content_tag(
:'lg-step-indicator',
role: 'region',
aria: { label: t('step_indicator.accessible_label') },
class: classes,
class: local_assigns[:class],
) do %>
<ol class="step-indicator__scroller">
<% normalized_steps.each_with_index do |step, index| %>
Expand Down
2 changes: 1 addition & 1 deletion spec/javascripts/packs/form-steps-wait-spec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { fireEvent, findByRole } from '@testing-library/dom';
import { useDefineProperty } from '@18f/identity-test-helpers';
import { useSandbox } from '../support/sinon';
import useDefineProperty from '../support/define-property';
import {
FormStepsWait,
getDOMFromHTML,
Expand Down
2 changes: 1 addition & 1 deletion spec/javascripts/packs/webauthn-setup-spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useDefineProperty } from '@18f/identity-test-helpers';
import { useSandbox } from '../support/sinon';
import useDefineProperty from '../support/define-property';
import { reloadWithError } from '../../../app/javascript/packs/webauthn-setup';

describe('webauthn-setup', () => {
Expand Down
2 changes: 1 addition & 1 deletion spec/javascripts/packs/webauthn-unhide-spec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { screen } from '@testing-library/dom';
import { useDefineProperty } from '@18f/identity-test-helpers';
import { useSandbox } from '../support/sinon';
import useDefineProperty from '../support/define-property';
import { unhideWebauthn } from '../../../app/javascript/packs/webauthn-unhide';

describe('webauthn-unhide', () => {
Expand Down
6 changes: 3 additions & 3 deletions spec/views/shared/_step_indicator.html.erb_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,16 @@
context 'without custom classes given' do
let(:classes) { nil }

it 'renders with default classes' do
expect(rendered).to have_selector('.step-indicator')
it 'renders with default tag' do
expect(rendered).to have_selector('lg-step-indicator')
end
end

context 'with custom classes' do
let(:classes) { 'my-custom-class' }

it 'renders with additional custom classes' do
expect(rendered).to have_selector('.step-indicator.my-custom-class')
expect(rendered).to have_selector('lg-step-indicator.my-custom-class')
end
end
end
Expand Down