From 932f0f08795929b01e005a2434017413ef5c7d82 Mon Sep 17 00:00:00 2001 From: malko Date: Sun, 4 Jun 2023 23:06:04 +0200 Subject: [PATCH] #945@major: Add support for set valueAsNumber. --- .../html-input-element/HTMLInputElement.ts | 79 +++++++++++++++++++ .../HTMLInputElement.test.ts | 79 +++++++++++++++++++ 2 files changed, 158 insertions(+) diff --git a/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts b/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts index 0902d0977..207c49066 100644 --- a/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts +++ b/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts @@ -23,6 +23,24 @@ import IShadowRoot from '../shadow-root/IShadowRoot'; import NodeList from '../node/NodeList'; import EventPhaseEnum from '../../event/EventPhaseEnum'; +/** + * Return iso-week number from given date + * + * @param date Date|number. + * @returns Iso-week string. + */ +export const dateIsoWeek = (date: Date | number): string => { + date = new Date(date); + const day = (date.getDay() + 6) % 7; + date.setDate(date.getDate() - day + 3); + const firstThursday = date.getTime(); + date.setMonth(0, 1); + if (date.getDay() !== 4) { + date.setMonth(0, 1 + ((4 - date.getDay() + 7) % 7)); + } + return String(1 + Math.ceil((firstThursday - date.getTime()) / 604800000)).padStart(2, '0'); +}; + /** * HTML Input Element. * @@ -841,6 +859,67 @@ export default class HTMLInputElement extends HTMLElement implements IHTMLInputE } } + /** + * Sets value from a number. + * + * @param value number. + */ + public set valueAsNumber(value: number) { + // Specs at https://html.spec.whatwg.org/multipage/input.html + switch (this.type) { + case 'number': + case 'range': + // We Rely on HTMLInputElementValueSanitizer + this.value = Number(value).toString(); + break; + case 'date': + case 'datetime-local': { + const d = new Date(Number(value)); + if (isNaN(d.getTime())) { + // Reset to default value + this.value = ''; + break; + } + if (this.type == 'date') { + this.value = d.toISOString().slice(0, 10); + } else { + this.value = d.toISOString().slice(0, -1); + } + break; + } + case 'month': + if (!Number.isInteger(value) || value < 0) { + this.value = ''; + } else { + this.value = new Date(Date.UTC(1970, Number(value))).toISOString().slice(0, 7); + } + break; + case 'time': + if (!Number.isInteger(value) || value < 0) { + this.value = ''; + } else { + this.value = new Date(Number(value)).toISOString().slice(11, -1); + } + break; + case 'week': + case 'week': { + const d = new Date(Number(value)); + if (isNaN(d.getTime())) { + this.value = ''; + } else { + d.setTime(d.getTime() + d.getTimezoneOffset() * 60000); + this.value = d.toISOString().split('T')[0].slice(0, 5) + 'W' + dateIsoWeek(d); + } + break; + } + default: + throw new DOMException( + "Failed to set the 'valueAsNumber' property on 'HTMLInputElement': This input element does not support Number values.", + DOMExceptionNameEnum.invalidStateError + ); + } + } + /** * Returns the associated label elements. * diff --git a/packages/happy-dom/test/nodes/html-input-element/HTMLInputElement.test.ts b/packages/happy-dom/test/nodes/html-input-element/HTMLInputElement.test.ts index fb38b56e5..268c846af 100644 --- a/packages/happy-dom/test/nodes/html-input-element/HTMLInputElement.test.ts +++ b/packages/happy-dom/test/nodes/html-input-element/HTMLInputElement.test.ts @@ -9,6 +9,7 @@ import HTMLInputElementSelectionModeEnum from '../../../src/nodes/html-input-ele import HTMLInputElementSelectionDirectionEnum from '../../../src/nodes/html-input-element/HTMLInputElementSelectionDirectionEnum'; import ValidityState from '../../../src/validity-state/ValidityState'; import { IHTMLFormElement } from '../../../src'; +import DOMExceptionNameEnum from '../../../src/exception/DOMExceptionNameEnum'; describe('HTMLInputElement', () => { let window: IWindow; @@ -275,6 +276,84 @@ describe('HTMLInputElement', () => { }); }); }); + describe('set valueAsNumber()', () => { + describe('Should throw exception for non-numeric input', () => { + it.each([ + 'button', + 'checkbox', + 'color', + 'email', + 'file', + 'hidden', + 'image', + 'password', + 'radio', + 'reset', + 'search', + 'submit', + 'tel', + 'text', + 'url' + ])('Of type %s.', (type) => { + element.type = type; + expect(() => (element.valueAsNumber = 0)).toThrowError( + new DOMException( + "Failed to set the 'valueAsNumber' property on 'HTMLInputElement': This input element does not support Number values.", + DOMExceptionNameEnum.invalidStateError + ) + ); + }); + }); + + describe('With invalid value for', () => { + it.each(['number', 'date', 'datetime-local', 'month', 'time', 'week'])( + 'Type "%s" should set default empty value.', + (type) => { + element.type = type; + expect(() => { + // @ts-ignore + element.valueAsNumber = 'x'; + }).not.toThrow(); + expect(element.value).toBe(''); + } + ); + it(`Type "range" should set default middle range value.`, () => { + element.type = 'range'; + expect(() => { + // @ts-ignore + element.valueAsNumber = 'x'; + }).not.toThrow(); + expect(element.value).toBe('50'); + }); + }); + + describe('With valid value for', () => { + const testCases = [ + { type: 'number', value: 123, want: '123' }, + { type: 'number', value: 1.23, want: '1.23' }, + { type: 'range', value: 75, want: '75' }, + { type: 'range', value: 12.5, want: '12.5' }, + { type: 'date', value: new Date('2019-01-01').getTime(), want: '2019-01-01' }, + { type: 'datetime-local', value: 1546300800000, want: '2019-01-01T00:00' }, + { type: 'month', value: 588, want: '2019-01' }, + { type: 'time', value: 0, want: '00:00' }, + { type: 'time', value: 43200000, want: '12:00' }, + { type: 'time', value: 68100000, want: '18:55' }, + { type: 'time', value: 83709010, want: '23:15:09.01' }, + { type: 'week', value: 1685318400000, want: '2023-W22' }, + { type: 'week', value: 1672531200000, want: '2022-W52' } + ]; + it.each(testCases)( + `Type "$type" should set a corresponding value`, + ({ type, value, want }) => { + element.type = type; + element.valueAsNumber = value; + expect(element.value).toEqual(want); + } + ); + }); + }); + describe('get selectionStart()', () => { it('Returns the length of the attribute "value" if value has not been set using the property.', () => { element.setAttribute('value', 'TEST_VALUE');