Skip to content

Commit

Permalink
feat(datepicker): 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 475c781 commit bcff8c8
Show file tree
Hide file tree
Showing 6 changed files with 168 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ import nl from 'vanillajs-datepicker/js/i18n/locales/nl'; // eslint-disable-line
import pl from 'vanillajs-datepicker/js/i18n/locales/pl'; // eslint-disable-line import/no-unresolved
// @ts-ignore no existing declaration
import pt from 'vanillajs-datepicker/js/i18n/locales/pt'; // eslint-disable-line import/no-unresolved
import { isDate } 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';
import { type OdsDatepickerDay } from '../../constants/datepicker-day';
import { ODS_DATEPICKER_LOCALE, type OdsDatepickerLocale } from '../../constants/datepicker-locale';
import { formatDate, updateInternals } from '../../controller/ods-datepicker';
import { VALUE_DEFAULT_VALUE, formatDate, getInitialValue, updateInternals } from '../../controller/ods-datepicker';
import { type OdsDatepickerChangeEventDetail } from '../../interfaces/events';

Object.assign(Datepicker.locales, de);
Expand All @@ -30,8 +31,6 @@ Object.assign(Datepicker.locales, nl);
Object.assign(Datepicker.locales, pl);
Object.assign(Datepicker.locales, pt);

const VALUE_DEFAULT_VALUE = null;

@Component({
formAssociated: true,
shadow: {
Expand Down Expand Up @@ -185,7 +184,9 @@ export class OdsDatepicker {

@Watch('value')
onValueChangeFromJs(): void {
this.value && this.datepickerInstance?.setDate(new Date(this.value.toDateString()));
if (this.value && isDate(this.value)) {
this.datepickerInstance?.setDate(new Date(this.value.toDateString()));
}
}

componentWillLoad(): void {
Expand All @@ -196,9 +197,7 @@ export class OdsDatepicker {

// We set the value before the observer starts to avoid calling the mutation callback twice
// as it will be called on componentDidLoad (when native element validity is up-to-date)
if (!this.value && this.value !== 0 && (this.value !== VALUE_DEFAULT_VALUE || this.defaultValue)) {
this.value = this.defaultValue ? new Date(Datepicker.parseDate(this.defaultValue, this.format)) : null;
}
this.value = getInitialValue(this.value, this.format, this.defaultValue);

this.observer = new MutationObserver((mutations: MutationRecord[]) => {
for (const mutation of mutations) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { Datepicker } from 'vanillajs-datepicker';
import { setInternalsValidityFromHtmlElement } from '../../../../utils/dom';
import { isDate } from '../../../../utils/type';

const VALUE_DEFAULT_VALUE = null;

function formatDate(date: Date | null, format: string): string {
if (date && isDate(date)) {
Expand All @@ -8,9 +11,12 @@ function formatDate(date: Date | null, format: string): string {
return '';
}

function isDate(date: Date): boolean {
// Needed as values from runtime are not TS problem anymore
return date instanceof Date && !isNaN(date.valueOf());
function getInitialValue(value: Date | null, format: string, defaultValue?: string): Date | null {
if (defaultValue !== undefined && value === VALUE_DEFAULT_VALUE) {
return new Date(Datepicker.parseDate(defaultValue, format));
}

return value;
}

function updateInternals(internals: ElementInternals, value: number | string | null, inputEl?: HTMLInputElement): void {
Expand All @@ -23,5 +29,7 @@ function updateInternals(internals: ElementInternals, value: number | string | n

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

describe('ods-datepicker behaviour', () => {
let el: E2EElement;
Expand All @@ -14,6 +14,7 @@ describe('ods-datepicker behaviour', () => {
async function setup(content: string): Promise<void> {
page = await newE2EPage();

await page.emulateTimezone('Atlantic/Reykjavik'); // UTC+0
await page.setContent(content);
await page.evaluate(() => document.body.style.setProperty('margin', '0px'));

Expand All @@ -24,6 +25,88 @@ describe('ods-datepicker behaviour', () => {

beforeEach(jest.clearAllMocks);

describe('initialization', () => {
const dummyDefaultValue = '2000-01-02';
const dummyFormat = 'yyyy-mm-dd';
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: OdsDatepickerChangeEventDetail) => {
odsChangeEventCount++;
odsChangeEventDetail = detail;
});

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

await page.emulateTimezone('Atlantic/Reykjavik'); // UTC+0
await page.setContent(content);
}

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

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

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

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

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

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

describe('with no value but default-value defined', () => {
it('should trigger a uniq odsChange event', async() => {
await setupWithSpy(`<ods-datepicker default-value="${dummyDefaultValue}" format="${dummyFormat}"></ods-datepicker>`);

expect(odsChangeEventCount).toBe(1);
expect(odsChangeEventDetail).toEqual({
formattedValue: dummyDefaultValue,
validity: {},
value: new Date(dummyDefaultValue).toISOString(),
});
});
});
});

describe('methods', () => {
describe('clear', () => {
it('should emit an odsClear event', async() => {
Expand Down Expand Up @@ -115,7 +198,6 @@ describe('ods-datepicker behaviour', () => {
it('should emit an odsChange event', async() => {
const value = new Date('10 May 2024');
await setup('<ods-datepicker name="ods-datepicker"></ods-datepicker>');
await page.emulateTimezone('Europe/Madrid');
await page.evaluate((value) => {
document.querySelector<OdsDatepicker & HTMLElement>('ods-datepicker')!.value = value;
}, value);
Expand All @@ -132,7 +214,7 @@ describe('ods-datepicker behaviour', () => {
name: 'ods-datepicker',
previousValue: value.toISOString(),
validity: {},
value: '2024-05-10T22:00:00.000Z',
value: '2024-05-11T00:00:00.000Z',
});
});
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,36 @@
jest.mock('vanillajs-datepicker', () => ({
Datepicker: {
formatDate: jest.fn().mockReturnValue('formatted date'),
parseDate: jest.fn().mockReturnValue('2000-01-02'),
},
}));
jest.mock('../../../../utils/dom');

import { Datepicker } from 'vanillajs-datepicker';
import { formatDate, updateInternals } from '../../src/controller/ods-datepicker';
import { setInternalsValidityFromHtmlElement } from '../../../../utils/dom';
import { formatDate, getInitialValue, updateInternals } from '../../src/controller/ods-datepicker';

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

describe('getInitialValue', () => {
const dummyFormat = 'dummy format';

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

it('should return date if value is set regarding of default value', () => {
expect(getInitialValue(new Date(0), dummyFormat)).toEqual(new Date(0));
expect(getInitialValue(new Date(0), dummyFormat, '2000-01-02')).toEqual(new Date(0));
});

it('should return default value if value is null', () => {
expect(getInitialValue(null, dummyFormat, 'default')).toEqual(new Date('2000-01-02'));
expect(Datepicker.parseDate).toHaveBeenCalledWith('default', dummyFormat);
});
});

describe('formatDate', () => {
const dummyFormat = 'dd/mm/yyyy';

Expand Down Expand Up @@ -40,6 +61,7 @@ describe('ods-datepicker controller', () => {
});

describe('updateInternals', () => {
const dummyInput = { dummy: 'input' };
const dummyInternal = {
setFormValue: jest.fn(),
} as unknown as ElementInternals;
Expand All @@ -64,5 +86,17 @@ describe('ods-datepicker controller', () => {

expect(dummyInternal.setFormValue).toHaveBeenCalledWith(dummyValue);
});

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

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

it('should set internal validity if input element is defined', () => {
updateInternals(dummyInternal, 'dummyValue', dummyInput as unknown as HTMLInputElement);

expect(setInternalsValidityFromHtmlElement).toHaveBeenCalledWith(dummyInput, dummyInternal);
});
});
});
6 changes: 6 additions & 0 deletions packages/ods/src/utils/type.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
function isDate(date: Date): boolean {
// Needed as values from runtime are not TS problem anymore
return date instanceof Date && !isNaN(date.valueOf());
}

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

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

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

it('should return true if the value is a date', () => {
expect(isDate(new Date())).toBe(true);
expect(isDate(new Date(0))).toBe(true);
});
});

describe('isNumeric', () => {
it('should return false if the value is not a number', () => {
expect(isNumeric()).toBe(false);
Expand Down

0 comments on commit bcff8c8

Please sign in to comment.