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()`, () => {