diff --git a/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElement.ts b/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElement.ts index a2b7a615b..0f1dbdae7 100644 --- a/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElement.ts +++ b/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElement.ts @@ -1,6 +1,7 @@ import HTMLElement from '../html-element/HTMLElement'; import IHTMLElement from '../html-element/IHTMLElement'; import IHTMLFormElement from '../html-form-element/IHTMLFormElement'; +import IHTMLSelectElement from '../html-select-element/IHTMLSelectElement'; import IHTMLOptionElement from './IHTMLOptionElement'; /** @@ -58,7 +59,20 @@ export default class HTMLOptionElement extends HTMLElement implements IHTMLOptio * @returns Selected. */ public get selected(): boolean { - return this.getAttributeNS(null, 'selected') !== null; + const parentNode = this.parentNode; + + if (parentNode?.tagName === 'SELECT') { + let index = -1; + for (let i = 0; i < parentNode.options.length; i++) { + if (parentNode.options[i] === this) { + index = i; + break; + } + } + return index !== -1 && parentNode.options.selectedIndex === index; + } + + return false; } /** @@ -67,10 +81,26 @@ export default class HTMLOptionElement extends HTMLElement implements IHTMLOptio * @param selected Selected. */ public set selected(selected: boolean) { - if (!selected) { - this.removeAttributeNS(null, 'selected'); - } else { - this.setAttributeNS(null, 'selected', ''); + const parentNode = this.parentNode; + if (parentNode?.tagName === 'SELECT') { + if (selected) { + let index = -1; + + for (let i = 0; i < parentNode.options.length; i++) { + if (parentNode.options[i] === this) { + index = i; + break; + } + } + + if (index !== -1) { + parentNode.options.selectedIndex = index; + } + } else if (parentNode.options.length) { + parentNode.options.selectedIndex = 0; + } else { + parentNode.options.selectedIndex = -1; + } } } diff --git a/packages/happy-dom/src/nodes/html-option-element/HTMLOptionsCollection.ts b/packages/happy-dom/src/nodes/html-option-element/HTMLOptionsCollection.ts index 9395783e6..2d81159c0 100644 --- a/packages/happy-dom/src/nodes/html-option-element/HTMLOptionsCollection.ts +++ b/packages/happy-dom/src/nodes/html-option-element/HTMLOptionsCollection.ts @@ -2,7 +2,6 @@ import DOMException from '../../exception/DOMException'; import HTMLCollection from '../element/HTMLCollection'; import IHTMLOptGroupElement from '../html-opt-group-element/IHTMLOptGroupElement'; import IHTMLSelectElement from '../html-select-element/IHTMLSelectElement'; -import HTMLOptionElement from './HTMLOptionElement'; import IHTMLOptionElement from './IHTMLOptionElement'; import IHTMLOptionsCollection from './IHTMLOptionsCollection'; @@ -17,6 +16,7 @@ export default class HTMLOptionsCollection implements IHTMLOptionsCollection { private _selectElement: IHTMLSelectElement; + private _selectedIndex = -1; /** * @@ -34,14 +34,7 @@ export default class HTMLOptionsCollection * @returns SelectedIndex. */ public get selectedIndex(): number { - for (let i = 0; i < this.length; i++) { - const item = this[i]; - if (item instanceof HTMLOptionElement && item.selected) { - return i; - } - } - - return -1; + return this._selectedIndex; } /** @@ -50,10 +43,11 @@ export default class HTMLOptionsCollection * @param selectedIndex SelectedIndex. */ public set selectedIndex(selectedIndex: number) { - for (let i = 0; i < this.length; i++) { - const item = this[i]; - if (item instanceof HTMLOptionElement) { - this[i].selected = i === selectedIndex; + if (typeof selectedIndex === 'number' && !isNaN(selectedIndex)) { + if (selectedIndex >= 0 && selectedIndex < this.length) { + this._selectedIndex = selectedIndex; + } else { + this._selectedIndex = -1; } } } diff --git a/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts b/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts index 50ead16f0..b4c0e13a3 100644 --- a/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts +++ b/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts @@ -1,5 +1,3 @@ -import DOMException from '../../exception/DOMException'; -import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum'; import HTMLElement from '../html-element/HTMLElement'; import IHTMLElement from '../html-element/IHTMLElement'; import IHTMLFormElement from '../html-form-element/IHTMLFormElement'; @@ -204,13 +202,6 @@ export default class HTMLSelectElement extends HTMLElement implements IHTMLSelec * @param value Value. */ public set selectedIndex(value: number) { - if (value > this.options.length - 1 || value < -1) { - throw new DOMException( - 'Select elements selected index must be valid', - DOMExceptionNameEnum.indexSizeError - ); - } - this.options.selectedIndex = value; } @@ -296,6 +287,10 @@ export default class HTMLSelectElement extends HTMLElement implements IHTMLSelec if (element.tagName === 'OPTION' || element.tagName === 'OPTGROUP') { this.options.push(element); + + if (this.options.length === 1) { + this.options.selectedIndex = 0; + } } this._updateIndexProperties(previousLength, this.options.length); @@ -337,6 +332,10 @@ export default class HTMLSelectElement extends HTMLElement implements IHTMLSelec } else { this.options.push(newElement); } + + if (this.options.length === 1) { + this.options.selectedIndex = 0; + } } this._updateIndexProperties(previousLength, this.options.length); @@ -355,9 +354,18 @@ export default class HTMLSelectElement extends HTMLElement implements IHTMLSelec if (element.tagName === 'OPTION' || element.tagName === 'OPTION') { const index = this.options.indexOf(node); + if (index !== -1) { this.options.splice(index, 1); } + + if (this.options.selectedIndex >= this.options.length) { + this.options.selectedIndex = this.options.length - 1; + } + + if (!this.options.length) { + this.options.selectedIndex = -1; + } } this._updateIndexProperties(previousLength, this.options.length); diff --git a/packages/happy-dom/test/nodes/html-option-element/HTMLOptionElement.test.ts b/packages/happy-dom/test/nodes/html-option-element/HTMLOptionElement.test.ts index 8db4f0778..dc8769636 100644 --- a/packages/happy-dom/test/nodes/html-option-element/HTMLOptionElement.test.ts +++ b/packages/happy-dom/test/nodes/html-option-element/HTMLOptionElement.test.ts @@ -1,16 +1,17 @@ import Window from '../../../src/window/Window'; import Document from '../../../src/nodes/document/Document'; -import HTMLOptionElement from '../../../src/nodes/html-option-element/HTMLOptionElement'; +import IHTMLOptionElement from '../../../src/nodes/html-option-element/IHTMLOptionElement'; +import IHTMLSelectElement from '../../../src/nodes/html-select-element/IHTMLSelectElement'; describe('HTMLOptionElement', () => { let window: Window; let document: Document; - let element: HTMLOptionElement; + let element: IHTMLOptionElement; beforeEach(() => { window = new Window(); document = window.document; - element = document.createElement('option'); + element = document.createElement('option'); }); describe('Object.prototype.toString', () => { @@ -33,20 +34,78 @@ describe('HTMLOptionElement', () => { }); }); - for (const property of ['disabled', 'selected']) { - describe(`get ${property}()`, () => { - it('Returns attribute value.', () => { - expect(element[property]).toBe(false); - element.setAttribute(property, ''); - expect(element[property]).toBe(true); - }); + describe('get disabled()', () => { + it('Returns the attribute "disabled".', () => { + element.setAttribute('disabled', ''); + expect(element.disabled).toBe(true); }); + }); - describe(`set ${property}()`, () => { - it('Sets attribute value.', () => { - element[property] = true; - expect(element.getAttribute(property)).toBe(''); - }); + describe('set disabled()', () => { + it('Sets the attribute "disabled".', () => { + element.disabled = true; + expect(element.getAttribute('disabled')).toBe(''); }); - } + }); + + describe('get selected()', () => { + it('Returns the selected state of the option.', () => { + const select = document.createElement('select'); + const option1 = document.createElement('option'); + const option2 = document.createElement('option'); + + expect(option1.selected).toBe(false); + expect(option2.selected).toBe(false); + + select.appendChild(option1); + select.appendChild(option2); + + expect(option1.selected).toBe(true); + expect(option2.selected).toBe(false); + expect(option1.getAttribute('selected')).toBe(null); + expect(option2.getAttribute('selected')).toBe(null); + + select.options.selectedIndex = 1; + + expect(option1.selected).toBe(false); + expect(option2.selected).toBe(true); + expect(option1.getAttribute('selected')).toBe(null); + expect(option2.getAttribute('selected')).toBe(null); + + select.options.selectedIndex = -1; + + expect(option1.selected).toBe(false); + expect(option2.selected).toBe(false); + }); + }); + + describe('set selected()', () => { + it('Sets the selected state of the option.', () => { + const select = document.createElement('select'); + const option1 = document.createElement('option'); + const option2 = document.createElement('option'); + + expect(option1.selected).toBe(false); + expect(option2.selected).toBe(false); + + option1.selected = true; + + expect(select.selectedIndex).toBe(-1); + + select.appendChild(option1); + select.appendChild(option2); + + option1.selected = true; + + expect(select.selectedIndex).toBe(0); + + option2.selected = true; + + expect(select.selectedIndex).toBe(1); + + option2.selected = false; + + expect(select.selectedIndex).toBe(0); + }); + }); }); diff --git a/packages/happy-dom/test/nodes/html-option-element/HTMLOptionsCollection.test.ts b/packages/happy-dom/test/nodes/html-option-element/HTMLOptionsCollection.test.ts index f9ea7f3a0..9c63dcdbd 100644 --- a/packages/happy-dom/test/nodes/html-option-element/HTMLOptionsCollection.test.ts +++ b/packages/happy-dom/test/nodes/html-option-element/HTMLOptionsCollection.test.ts @@ -3,7 +3,7 @@ import IWindow from '../../../src/window/IWindow'; import IDocument from '../../../src/nodes/document/IDocument'; import HTMLSelectElement from '../../../src/nodes/html-select-element/HTMLSelectElement'; import HTMLOptionElement from '../../../src/nodes/html-option-element/HTMLOptionElement'; -import { DOMException } from '../../../src'; +import DOMException from '../../../src/exception/DOMException'; describe('HTMLOptionsCollection', () => { let window: IWindow; @@ -18,57 +18,51 @@ describe('HTMLOptionsCollection', () => { jest.restoreAllMocks(); }); - describe('get selectedindex()', () => { - it('Returns the index of the first option element in the list of options in tree order that has its selectedness set to true.', () => { - const select = document.createElement('select'); - const option1 = document.createElement('option'); - const option2 = document.createElement('option'); - option1.selected = true; - option1.value = 'option1'; - option2.value = 'option2'; - select.appendChild(option1); - select.appendChild(option2); - - expect(select.options.selectedIndex).toBe(0); - }); - + describe('get selectedIndex()', () => { it('Returns -1 if there are no options.', () => { const select = document.createElement('select'); expect(select.options.selectedIndex).toBe(-1); }); - it('Returns -1 if no option is selected.', () => { + it('Returns 0 by default.', () => { const select = document.createElement('select'); const option1 = document.createElement('option'); const option2 = document.createElement('option'); + option1.value = 'option1'; option2.value = 'option2'; + select.appendChild(option1); select.appendChild(option2); - expect(select.options.selectedIndex).toBe(-1); + expect(select.options.selectedIndex).toBe(0); }); }); - describe('set selectedindex()', () => { + describe('set selectedIndex()', () => { it('Updates option.selected', () => { const select = document.createElement('select'); - select.appendChild(document.createElement('option')); - select.appendChild(document.createElement('option')); - document.body.appendChild(select); + const option1 = document.createElement('option'); + const option2 = document.createElement('option'); + + expect(option1.selected).toBe(false); + expect(option2.selected).toBe(false); + + select.appendChild(option1); + select.appendChild(option2); - expect((select.options[0]).selected).toBe(false); - expect((select.options[1]).selected).toBe(false); + expect(option1.selected).toBe(true); + expect(option2.selected).toBe(false); select.options.selectedIndex = 1; - expect((select.options[0]).selected).toBe(false); - expect((select.options[1]).selected).toBe(true); + expect(option1.selected).toBe(false); + expect(option2.selected).toBe(true); select.options.selectedIndex = -1; - expect((select.options[0]).selected).toBe(false); - expect((select.options[1]).selected).toBe(false); + expect(option1.selected).toBe(false); + expect(option2.selected).toBe(false); }); }); @@ -134,16 +128,21 @@ describe('HTMLOptionsCollection', () => { const select = document.createElement('select'); const option = document.createElement('option'); const option2 = document.createElement('option'); + + expect(select.options.selectedIndex).toBe(-1); + select.appendChild(option); select.appendChild(option2); - document.body.appendChild(select); - expect(select.options.selectedIndex).toBe(-1); + + expect(select.options.selectedIndex).toBe(0); select.options.selectedIndex = 1; expect(select.options.selectedIndex).toBe(1); - // No option is selected after removing the selected option select.options.remove(1); + expect(select.options.selectedIndex).toBe(0); + + select.options.remove(0); expect(select.options.selectedIndex).toBe(-1); }); }); diff --git a/packages/happy-dom/test/nodes/html-select-element/HTMLSelectElement.test.ts b/packages/happy-dom/test/nodes/html-select-element/HTMLSelectElement.test.ts index c2f6f0272..f7705efdd 100644 --- a/packages/happy-dom/test/nodes/html-select-element/HTMLSelectElement.test.ts +++ b/packages/happy-dom/test/nodes/html-select-element/HTMLSelectElement.test.ts @@ -70,6 +70,10 @@ describe('HTMLSelectElement', () => { element.appendChild(option1); element.appendChild(option2); + expect(element.value).toBe('option1'); + + element.selectedIndex = -1; + expect(element.value).toBe(''); }); }); @@ -87,8 +91,6 @@ describe('HTMLSelectElement', () => { expect(element.options.selectedIndex).toBe(0); }); - - it('Trims and removes new lines.', () => {}); }); for (const property of ['disabled', 'autofocus', 'required', 'multiple']) { @@ -149,6 +151,19 @@ describe('HTMLSelectElement', () => { element.selectedIndex = 1; expect(element.options.selectedIndex).toBe(1); }); + + it('Ignores invalid values gracefully.', () => { + element.appendChild(document.createElement('option')); + element.appendChild(document.createElement('option')); + + expect(element.options.selectedIndex).toBe(0); + + element.selectedIndex = undefined; + expect(element.options.selectedIndex).toBe(0); + + element.selectedIndex = 1000; + expect(element.options.selectedIndex).toBe(-1); + }); }); describe(`add()`, () => {