Skip to content

Commit

Permalink
feat(quantity): add tests about odsChange emit on first render
Browse files Browse the repository at this point in the history
  • Loading branch information
dpellier committed Nov 28, 2024
1 parent 93085eb commit 475c781
Show file tree
Hide file tree
Showing 7 changed files with 236 additions and 18 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { AttachInternals, Component, Element, Event, type EventEmitter, type FunctionalComponent, Host, Listen, Method, Prop, State, Watch, h } from '@stencil/core';
import { submitFormOnEnter } from '../../../../../utils/dom';
import { isNumeric } from '../../../../../utils/type';
import { ODS_BUTTON_COLOR, ODS_BUTTON_SIZE, ODS_BUTTON_VARIANT } from '../../../../button/src';
import { ODS_ICON_NAME } from '../../../../icon/src';
import { ODS_SPINNER_COLOR } from '../../../../spinner/src';
Expand Down Expand Up @@ -250,16 +251,12 @@ export class OdsInput {

this.odsChange.emit({
name: this.name,
previousValue: typeof this.value === 'number' && this.isNumeric(previousValue) ? Number(previousValue) : previousValue,
previousValue: typeof this.value === 'number' && isNumeric(previousValue) ? Number(previousValue) : previousValue,
validity: this.internals.validity,
value: this.value,
});
}

private isNumeric(n?: string | number | null): boolean {
return !!n && !isNaN(parseFloat(n.toString())) && isFinite(n as number);
}

render(): FunctionalComponent {
const hasClearableIcon = this.isClearable && !this.isLoading && !!this.value;
const hasToggleMaskIcon = this.isPassword && !this.isLoading;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { AttachInternals, Component, Event, type EventEmitter, type FunctionalComponent, Host, Listen, Method, Prop, State, h } from '@stencil/core';
import { submitFormOnEnter } from '../../../../../utils/dom';
import { isNumeric } from '../../../../../utils/type';
import { ODS_BUTTON_COLOR, ODS_BUTTON_SIZE, ODS_BUTTON_VARIANT } from '../../../../button/src';
import { ODS_ICON_NAME } from '../../../../icon/src';
import { ODS_INPUT_TYPE, type OdsInput, type OdsInputChangeEvent } from '../../../../input/src';
import { isMinusButtonDisabled, isPlusButtonDisabled, updateInternals } from '../../controller/ods-quantity';
import { VALUE_DEFAULT_VALUE, getInitialValue, isMinusButtonDisabled, isPlusButtonDisabled, updateInternals } from '../../controller/ods-quantity';
import { type OdsQuantityChangeEventDetail } from '../../interfaces/events';

const VALUE_DEFAULT_VALUE = null;

@Component({
formAssociated: true,
shadow: {
Expand Down Expand Up @@ -99,9 +98,7 @@ export class OdsQuantity {
}

componentWillLoad(): void {
if (!this.value && this.value !== 0 && (this.value !== VALUE_DEFAULT_VALUE || this.defaultValue !== undefined)) {
this.value = this.defaultValue ?? null;
}
this.value = getInitialValue(this.value, this.defaultValue);
}

async componentDidLoad(): Promise<void> {
Expand Down Expand Up @@ -182,7 +179,7 @@ export class OdsQuantity {
ariaLabel={ this.ariaLabel }
ariaLabelledby= { this.ariaLabelledby }
class="ods-quantity__input"
defaultValue={ this.defaultValue }
defaultValue={ isNumeric(this.defaultValue) ? this.defaultValue : undefined }
exportparts="input"
hasError={ this.getHasError() }
isDisabled={ this.isDisabled }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
import { setInternalsValidityFromOdsComponent } from '../../../../utils/dom';
import { isNumeric } from '../../../../utils/type';
import { type OdsInput } from '../../../input/src';

const VALUE_DEFAULT_VALUE = null;

function getInitialValue(value: number | null, defaultValue?: number): number | null {
if (defaultValue !== undefined && value === VALUE_DEFAULT_VALUE) {
return isNumeric(defaultValue) ? defaultValue : VALUE_DEFAULT_VALUE;
}

return isNumeric(value) ? value : VALUE_DEFAULT_VALUE;
}

function isMinusButtonDisabled(isDisabled: boolean, isReadonly: boolean, value: number | null, min?: number): boolean {
return isDisabled || isReadonly || (min !== undefined && value !== null && min >= value);
}
Expand All @@ -9,8 +20,7 @@ function isPlusButtonDisabled(isDisabled: boolean, isReadonly: boolean, value: n
return isDisabled || isReadonly || (max !== undefined && value !== null && max <= value);
}

//function updateInternals(internals: ElementInternals, value: number | string | null, inputEl?: HTMLInputElement): void {
async function updateInternals(internals: ElementInternals, value: number | string | null, inputEl?: HTMLElement & OdsInput): Promise<void> {
async function updateInternals(internals: ElementInternals, value: number | null, inputEl?: HTMLElement & OdsInput): Promise<void> {
internals.setFormValue(value?.toString() ?? '');

if (inputEl) {
Expand All @@ -19,7 +29,9 @@ async function updateInternals(internals: ElementInternals, value: number | stri
}

export {
getInitialValue,
isMinusButtonDisabled,
isPlusButtonDisabled,
updateInternals,
VALUE_DEFAULT_VALUE,
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { E2EElement, E2EPage } from '@stencil/core/testing';
import { newE2EPage } from '@stencil/core/testing';
import { type E2EElement, type E2EPage, newE2EPage } from '@stencil/core/testing';
import { type OdsQuantityChangeEventDetail } from '../../src';

describe('ods-quantity behaviour', () => {
let el: E2EElement;
Expand All @@ -22,6 +22,105 @@ describe('ods-quantity behaviour', () => {

beforeEach(jest.clearAllMocks);

describe('initialization', () => {
let odsChangeEventCount = 0;
let odsChangeEventDetail = {};

async function setupWithSpy(content: string): Promise<void> {
odsChangeEventCount = 0;
odsChangeEventDetail = {};
page = await newE2EPage();

// page.spyOnEvent doesn't seems to work to observe event emitted on first render, before any action happens
// so we spy manually
await page.exposeFunction('onOdsChangeEvent', (detail: OdsQuantityChangeEventDetail) => {
odsChangeEventCount++;
odsChangeEventDetail = detail;
});

await page.evaluateOnNewDocument(() => {
window.addEventListener('odsChange', (event: Event) => {
// @ts-ignore function is exposed manually
window.onOdsChangeEvent((event as CustomEvent<OdsQuantityChangeEventDetail>).detail);
});
});

await page.setContent(content);
}

describe('with no value attribute defined', () => {
it('should trigger a uniq odsChange event', async() => {
await setupWithSpy('<ods-quantity></ods-quantity>');

expect(odsChangeEventCount).toBe(1);
expect(odsChangeEventDetail).toEqual({
validity: {},
value: null,
});
});
});

describe('with non number value', () => {
it('should trigger a uniq odsChange event', async() => {
await setupWithSpy('<ods-quantity value=""></ods-quantity>');

expect(odsChangeEventCount).toBe(1);
expect(odsChangeEventDetail).toEqual({
validity: {},
value: null,
});
});
});

describe('with no value but non number default-value', () => {
it('should trigger a uniq odsChange event', async() => {
await setupWithSpy('<ods-quantity default-value=""></ods-quantity>');

expect(odsChangeEventCount).toBe(1);
expect(odsChangeEventDetail).toEqual({
validity: {},
value: null,
});
});
});

describe('with no value but default-value defined', () => {
it('should trigger a uniq odsChange event', async() => {
await setupWithSpy('<ods-quantity default-value="33"></ods-quantity>');

expect(odsChangeEventCount).toBe(1);
expect(odsChangeEventDetail).toEqual({
validity: {},
value: 33,
});
});
});

describe('with defined value', () => {
it('should trigger a uniq odsChange event', async() => {
await setupWithSpy('<ods-quantity value="42"></ods-quantity>');

expect(odsChangeEventCount).toBe(1);
expect(odsChangeEventDetail).toEqual({
validity: {},
value: 42,
});
});
});

describe('with defined value and default value', () => {
it('should trigger a uniq odsChange event', async() => {
await setupWithSpy('<ods-quantity default-value="33" value="42"></ods-quantity>');

expect(odsChangeEventCount).toBe(1);
expect(odsChangeEventDetail).toEqual({
validity: {},
value: 42,
});
});
});
});

describe('methods', () => {
describe('clear', () => {
it('should receive odsClear event', async() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,49 @@
import { isMinusButtonDisabled, isPlusButtonDisabled } from '../../src/controller/ods-quantity';
jest.mock('../../../../utils/dom');

describe('ods-input controller', () => {
import { setInternalsValidityFromOdsComponent } from '../../../../utils/dom';
import { type OdsInput } from '../../../input/src';
import { getInitialValue, isMinusButtonDisabled, isPlusButtonDisabled, updateInternals } from '../../src/controller/ods-quantity';

describe('ods-input controller', () => {
beforeEach(jest.clearAllMocks);

describe('getInitialValue', () => {
it('should return null if value is null and no default value', () => {
expect(getInitialValue(null)).toBe(null);
});

it('should return null if value is set but not a number', () => {
// @ts-ignore for test purpose
expect(getInitialValue('str')).toBe(null);
// @ts-ignore for test purpose
expect(getInitialValue(false)).toBe(null);
// @ts-ignore for test purpose
expect(getInitialValue({ dummy: 'obj' })).toBe(null);
});

it('should return null if value is not set and default value is not a number', () => {
// @ts-ignore for test purpose
expect(getInitialValue(null, 'str')).toBe(null);
// @ts-ignore for test purpose
expect(getInitialValue(null, false)).toBe(null);
// @ts-ignore for test purpose
expect(getInitialValue(null, { dummy: 'obj' })).toBe(null);
});

it('should return string or number if value is set regarding of default value', () => {
expect(getInitialValue(0)).toBe(0);
expect(getInitialValue(42)).toBe(42);
expect(getInitialValue(42, 33)).toBe(42);
// @ts-ignore for test purpose
expect(getInitialValue(42, null)).toBe(42);
});

it('should return default value if value is null', () => {
expect(getInitialValue(null, 0)).toBe(0);
expect(getInitialValue(null, 33)).toBe(33);
});
});

describe('isPlusButtonDisabled', () => {
it('should return true', async() => {
// disabled
Expand Down Expand Up @@ -51,4 +91,44 @@ describe('ods-input controller', () => {
expect(isMinusButtonDisabled(false, false, 0, undefined)).toBe(false);
});
});

describe('updateInternals', () => {
const dummyInput = { dummy: 'input' };
const dummyInternal = {
setFormValue: jest.fn(),
} as unknown as ElementInternals;

it('should set internal value with empty string', async() => {
// @ts-ignore for test purpose
await updateInternals(dummyInternal);
expect(dummyInternal.setFormValue).toHaveBeenCalledWith('');

// @ts-ignore for test purpose
await updateInternals(dummyInternal, undefined, {} as HTMLElement & OdsInput);
expect(dummyInternal.setFormValue).toHaveBeenCalledWith('');

await updateInternals(dummyInternal, null, {} as HTMLElement & OdsInput);
expect(dummyInternal.setFormValue).toHaveBeenCalledWith('');
});

it('should set internal value with string value', async() => {
const dummyValue = 33;

await updateInternals(dummyInternal, dummyValue, {} as HTMLElement & OdsInput);

expect(dummyInternal.setFormValue).toHaveBeenCalledWith(`${dummyValue}`);
});

it('should not set internal validity if no input element is defined', async() => {
await updateInternals(dummyInternal, 33);

expect(setInternalsValidityFromOdsComponent).not.toHaveBeenCalled();
});

it('should set internal validity if input element is defined', async() => {
await updateInternals(dummyInternal, 33, dummyInput as unknown as HTMLElement & OdsInput);

expect(setInternalsValidityFromOdsComponent).toHaveBeenCalledWith(dummyInput, dummyInternal);
});
});
});
7 changes: 7 additions & 0 deletions packages/ods/src/utils/type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
function isNumeric(n?: string | number | null): boolean {
return n === 0 || !!n && !isNaN(parseFloat(n.toString())) && isFinite(n as number);
}

export {
isNumeric,
};
26 changes: 26 additions & 0 deletions packages/ods/tests/utils/type.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { isNumeric } from '../../src/utils/type';

describe('utils type', () => {
describe('isNumeric', () => {
it('should return false if the value is not a number', () => {
expect(isNumeric()).toBe(false);
expect(isNumeric(null)).toBe(false);
expect(isNumeric(NaN)).toBe(false);
expect(isNumeric('str')).toBe(false);
// @ts-ignore for test purpose
expect(isNumeric(false)).toBe(false);
// @ts-ignore for test purpose
expect(isNumeric(true)).toBe(false);
// @ts-ignore for test purpose
expect(isNumeric({ dummy: 'object' })).toBe(false);
});

it('should return true if the value is a number', () => {
expect(isNumeric(0)).toBe(true);
expect(isNumeric(42)).toBe(true);
expect(isNumeric(-33)).toBe(true);
expect(isNumeric(123.45)).toBe(true);
expect(isNumeric(-98.76)).toBe(true);
});
});
});

0 comments on commit 475c781

Please sign in to comment.