import type { GetFunction, SetFunction } from 'ripple';
import {
	RippleArray,
	RippleObject,
	bindBorderBoxSize,
	bindChecked,
	bindClientHeight,
	bindClientWidth,
	bindContentBoxSize,
	bindContentRect,
	bindDevicePixelContentBoxSize,
	bindFiles,
	bindGroup,
	bindIndeterminate,
	bindInnerHTML,
	bindInnerText,
	bindNode,
	bindOffsetHeight,
	bindOffsetWidth,
	bindTextContent,
	bindValue,
	effect,
	flushSync,
	track,
	untrack,
} from 'ripple';

// Mock ResizeObserver for testing
const resizeObserverCallbacks = new Map<any, ResizeObserverCallback>();
const observedElements = new Map();

function createMockResizeObserver(callback: ResizeObserverCallback) {
	const instance = {
		observe(element: Element, options?: ResizeObserverOptions) {
			observedElements.set(element, options);
		},
		unobserve(element: Element) {
			observedElements.delete(element);
		},
		disconnect() {
			observedElements.clear();
		},
	};

	resizeObserverCallbacks.set(instance, callback);
	return instance;
}

function triggerResize(element: Element, entry: Partial<ResizeObserverEntry>) {
	const defaultEntry: ResizeObserverEntry = {
		target: element,
		contentRect: entry.contentRect || new DOMRectReadOnly(0, 0, 100, 100),
		borderBoxSize: entry.borderBoxSize || [],
		contentBoxSize: entry.contentBoxSize || [],
		devicePixelContentBoxSize: entry.devicePixelContentBoxSize || [],
		...entry,
	} as ResizeObserverEntry;

	// Trigger all callbacks for this element
	for (const [instance, callback] of resizeObserverCallbacks) {
		callback([defaultEntry], instance as any);
	}
}

// Mock DataTransfer for testing file inputs
class MockDataTransfer {
	items: MockDataTransferItemList;

	files: FileList;

	constructor() {
		this.items = new MockDataTransferItemList();
		this.files = this.items.files;
	}
}

class MockDataTransferItemList {
	_files: File[] = [];

	get files(): FileList {
		return this._files as any as FileList;
	}

	add(file: File): void {
		this._files.push(file);
	}

	get length(): number {
		return this._files.length;
	}
}

// Setup ResizeObserver mock
beforeAll(() => {
	(global as any).ResizeObserver = createMockResizeObserver;
	(global as any).DataTransfer = MockDataTransfer;
});

afterAll(() => {
	resizeObserverCallbacks.clear();
	observedElements.clear();
});

describe('use value()', () => {
	it('should update value on input', () => {
		const logs: string[] = [];

		function App() {
			return <>
				const text = track('');
				effect(() => {
					logs.push('text changed', text.value);
				});
				<input type="text" ref={bindValue(text)} />
			</>;
		}
		render(App);
		flushSync();

		const input = container.querySelector('input') as HTMLInputElement;
		input.value = 'Hello';
		input.dispatchEvent(new Event('input', { bubbles: true }));
		flushSync();
		expect(input.value).toBe('Hello');
		expect(logs).toEqual(['text changed', '', 'text changed', 'Hello']);
	});

	it('should update value on input with getter and setter', () => {
		const logs: string[] = [];

		function App() {
			return <>
				const text = new RippleObject({ value: '' });
				effect(() => {
					logs.push('text changed', text.value);
				});
				<input type="text" ref={bindValue(() => text.value, (v) => (text.value = v))} />
			</>;
		}
		render(App);
		flushSync();

		const input = container.querySelector('input') as HTMLInputElement;
		input.value = 'Hello';
		input.dispatchEvent(new Event('input', { bubbles: true }));
		flushSync();
		expect(input.value).toBe('Hello');
		expect(logs).toEqual(['text changed', '', 'text changed', 'Hello']);
	});

	it('should update value on input with a predefined value', () => {
		const logs: string[] = [];

		function App() {
			return <>
				const text = track('foo');
				effect(() => {
					logs.push('text changed', text.value);
				});
				<input type="text" ref={bindValue(text)} />
			</>;
		}
		render(App);
		flushSync();

		const input = container.querySelector('input') as HTMLInputElement;
		expect(input.value).toBe('foo');
		input.value = 'Hello';
		input.dispatchEvent(new Event('input', { bubbles: true }));
		flushSync();
		expect(input.value).toBe('Hello');
		expect(logs).toEqual(['text changed', 'foo', 'text changed', 'Hello']);
	});

	it('should update value on input with a predefined value and with a getter and setter', () => {
		const logs: string[] = [];

		function App() {
			return <>
				const text = new RippleObject({ value: 'foo' });
				effect(() => {
					logs.push('text changed', text.value);
				});
				<input type="text" ref={bindValue(() => text.value, (v) => (text.value = v))} />
			</>;
		}
		render(App);
		flushSync();

		const input = container.querySelector('input') as HTMLInputElement;
		expect(input.value).toBe('foo');
		input.value = 'Hello';
		input.dispatchEvent(new Event('input', { bubbles: true }));
		flushSync();
		expect(input.value).toBe('Hello');
		expect(logs).toEqual(['text changed', 'foo', 'text changed', 'Hello']);
	});

	it('should update text input element when tracked value changes', () => {
		function App() {
			return <>
				const text = track('initial');
				<div>
					<input type="text" ref={bindValue(text)} />
					<button onClick={() => (text.value = 'updated')}>{'Update'}</button>
				</div>
			</>;
		}
		render(App);
		flushSync();

		const input = container.querySelector('input') as HTMLInputElement;
		const button = container.querySelector('button') as HTMLButtonElement;

		expect(input.value).toBe('initial');

		button.click();
		flushSync();

		expect(input.value).toBe('updated');
	});

	it('should update text input element when tracked value changes with a getter and setter', () => {
		function App() {
			return <>
				const text = new RippleObject({ value: 'initial' });
				<div>
					<input type="text" ref={bindValue(() => text.value, (v) => (text.value = v))} />
					<button onClick={() => (text.value = 'updated')}>{'Update'}</button>
				</div>
			</>;
		}
		render(App);
		flushSync();

		const input = container.querySelector('input') as HTMLInputElement;
		const button = container.querySelector('button') as HTMLButtonElement;

		expect(input.value).toBe('initial');

		button.click();
		flushSync();

		expect(input.value).toBe('updated');
	});

	it('should update checked on input', () => {
		const logs: (string | boolean)[] = [];

		function App() {
			return <>
				const value = track(false);
				effect(() => {
					logs.push('checked changed', value.value);
				});
				<input type="checkbox" ref={bindChecked(value)} />
			</>;
		}
		render(App);
		flushSync();

		const input = container.querySelector('input') as HTMLInputElement;
		input.checked = true;
		input.dispatchEvent(new Event('change', { bubbles: true }));
		flushSync();

		expect(input.checked).toBe(true);
		expect(logs).toEqual(['checked changed', false, 'checked changed', true]);
	});

	it('should update checked on input with a getter and setter', () => {
		const logs: (string | boolean)[] = [];

		function App() {
			return <>
				const obj = new RippleObject({ value: false });
				effect(() => {
					logs.push('checked changed', obj.value);
				});
				<input
					type="checkbox"
					ref={bindChecked(() => obj.value, (v: boolean) => (obj.value = v))}
				/>
			</>;
		}
		render(App);
		flushSync();

		const input = container.querySelector('input') as HTMLInputElement;
		input.checked = true;
		input.dispatchEvent(new Event('change', { bubbles: true }));
		flushSync();

		expect(input.checked).toBe(true);
		expect(logs).toEqual(['checked changed', false, 'checked changed', true]);
	});

	it('should update checkbox element when tracked value changes', () => {
		function App() {
			return <>
				const value = track(false);
				<div>
					<input type="checkbox" ref={bindChecked(value)} />
					<button onClick={() => (value.value = true)}>{'Check'}</button>
				</div>
			</>;
		}
		render(App);
		flushSync();

		const input = container.querySelector('input') as HTMLInputElement;
		const button = container.querySelector('button') as HTMLButtonElement;

		expect(input.checked).toBe(false);

		button.click();
		flushSync();

		expect(input.checked).toBe(true);
	});

	it('should update checkbox element when tracked value changes with a getter and setter', () => {
		function App() {
			return <>
				const obj = new RippleObject({ value: false });
				<div>
					<input
						type="checkbox"
						ref={bindChecked(() => obj.value, (v: boolean) => (obj.value = v))}
					/>
					<button onClick={() => (obj.value = true)}>{'Check'}</button>
				</div>
			</>;
		}
		render(App);
		flushSync();

		const input = container.querySelector('input') as HTMLInputElement;
		const button = container.querySelector('button') as HTMLButtonElement;

		expect(input.checked).toBe(false);

		button.click();
		flushSync();

		expect(input.checked).toBe(true);
	});

	it('should update indeterminate on input', () => {
		const logs: (string | boolean)[] = [];

		function App() {
			return <>
				const value = track(false);
				effect(() => {
					logs.push('indeterminate changed', value.value);
				});
				<input type="checkbox" ref={bindIndeterminate(value)} />
			</>;
		}
		render(App);
		flushSync();

		const input = container.querySelector('input') as HTMLInputElement;
		expect(input.indeterminate).toBe(false);

		input.indeterminate = true;
		input.dispatchEvent(new Event('change', { bubbles: true }));
		flushSync();

		expect(input.indeterminate).toBe(true);
		expect(logs).toEqual(['indeterminate changed', false, 'indeterminate changed', true]);
	});

	it('should update indeterminate on input with a getter and setter', () => {
		const logs: (string | boolean)[] = [];

		function App() {
			return <>
				const obj = new RippleObject({ value: false });
				effect(() => {
					logs.push('indeterminate changed', obj.value);
				});
				<input
					type="checkbox"
					ref={bindIndeterminate(() => obj.value, (v: boolean) => (obj.value = v))}
				/>
			</>;
		}
		render(App);
		flushSync();

		const input = container.querySelector('input') as HTMLInputElement;
		expect(input.indeterminate).toBe(false);

		input.indeterminate = true;
		input.dispatchEvent(new Event('change', { bubbles: true }));
		flushSync();

		expect(input.indeterminate).toBe(true);
		expect(logs).toEqual(['indeterminate changed', false, 'indeterminate changed', true]);
	});

	it('should update checkbox indeterminate element when tracked value changes', () => {
		function App() {
			return <>
				const value = track(false);
				<div>
					<input type="checkbox" ref={bindIndeterminate(value)} />
					<button onClick={() => (value.value = true)}>{'Set Indeterminate'}</button>
				</div>
			</>;
		}
		render(App);
		flushSync();

		const input = container.querySelector('input') as HTMLInputElement;
		const button = container.querySelector('button') as HTMLButtonElement;

		expect(input.indeterminate).toBe(false);

		button.click();
		flushSync();

		expect(input.indeterminate).toBe(true);
	});

	it(
		'should update checkbox indeterminate element when tracked value changes with a getter and setter',
		() => {
			function App() {
				return <>
					const obj = new RippleObject({ value: false });
					<div>
						<input
							type="checkbox"
							ref={bindIndeterminate(() => obj.value, (v: boolean) => (obj.value = v))}
						/>
						<button onClick={() => (obj.value = true)}>{'Set Indeterminate'}</button>
					</div>
				</>;
			}
			render(App);
			flushSync();

			const input = container.querySelector('input') as HTMLInputElement;
			const button = container.querySelector('button') as HTMLButtonElement;

			expect(input.indeterminate).toBe(false);

			button.click();
			flushSync();

			expect(input.indeterminate).toBe(true);
		},
	);

	it('should update select value on change', () => {
		const logs: string[] = [];

		function App() {
			return <>
				const select = track('2');
				effect(() => {
					logs.push('select changed', select.value);
				});
				<select ref={bindValue(select)}>
					<option value="1">{'One'}</option>
					<option value="2">{'Two'}</option>
					<option value="3">{'Three'}</option>
				</select>
			</>;
		}

		render(App);
		flushSync();

		const select = container.querySelector('select') as HTMLSelectElement;
		select.value = '3';
		select.dispatchEvent(new Event('change', { bubbles: true }));
		flushSync();

		expect(select.value).toBe('3');
		expect(logs).toEqual(['select changed', '2', 'select changed', '3']);
	});

	it('should update select value on change with a getter and setter', () => {
		const logs: string[] = [];

		function App() {
			return <>
				const select = new RippleObject({ value: '2' });
				effect(() => {
					logs.push('select changed', select.value);
				});
				<select ref={bindValue(() => select.value, (v) => (select.value = v))}>
					<option value="1">{'One'}</option>
					<option value="2">{'Two'}</option>
					<option value="3">{'Three'}</option>
				</select>
			</>;
		}

		render(App);
		flushSync();

		const select = container.querySelector('select') as HTMLSelectElement;
		select.value = '3';
		select.dispatchEvent(new Event('change', { bubbles: true }));
		flushSync();

		expect(select.value).toBe('3');
		expect(logs).toEqual(['select changed', '2', 'select changed', '3']);
	});

	it('should update select element when tracked value changes', () => {
		function App() {
			return <>
				const select = track('1');
				<div>
					<select ref={bindValue(select)}>
						<option value="1">{'One'}</option>
						<option value="2">{'Two'}</option>
						<option value="3">{'Three'}</option>
					</select>
					<button onClick={() => (select.value = '3')}>{'Update'}</button>
				</div>
			</>;
		}

		render(App);
		flushSync();

		const selectEl = container.querySelector('select') as HTMLSelectElement;
		const button = container.querySelector('button') as HTMLButtonElement;

		expect(selectEl.value).toBe('1');

		button.click();
		flushSync();

		expect(selectEl.value).toBe('3');
	});

	it('should update select element when tracked value changes with a getter and setter', () => {
		function App() {
			return <>
				const select = new RippleObject({ value: '1' });
				<div>
					<select ref={bindValue(() => select.value, (v) => (select.value = v))}>
						<option value="1">{'One'}</option>
						<option value="2">{'Two'}</option>
						<option value="3">{'Three'}</option>
					</select>
					<button onClick={() => (select.value = '3')}>{'Update'}</button>
				</div>
			</>;
		}

		render(App);
		flushSync();

		const selectEl = container.querySelector('select') as HTMLSelectElement;
		const button = container.querySelector('button') as HTMLButtonElement;

		expect(selectEl.value).toBe('1');

		button.click();
		flushSync();

		expect(selectEl.value).toBe('3');
	});

	it('should bind checkbox group', () => {
		const logs: string[] = [];

		function App() {
			return <>
				const selected = track(['b']);
				effect(() => {
					logs.push('selected changed', JSON.stringify(selected.value));
				});
				<div>
					<input type="checkbox" value="a" ref={bindGroup(selected)} />
					<input type="checkbox" value="b" ref={bindGroup(selected)} />
					<input type="checkbox" value="c" ref={bindGroup(selected)} />
				</div>
			</>;
		}

		render(App);
		flushSync();

		const inputs = container.querySelectorAll('input') as NodeListOf<HTMLInputElement>;
		expect(inputs[0].checked).toBe(false);
		expect(inputs[1].checked).toBe(true);
		expect(inputs[2].checked).toBe(false);

		// Check first checkbox
		inputs[0].checked = true;
		inputs[0].dispatchEvent(new Event('change', { bubbles: true }));
		flushSync();

		expect(logs).toContain('selected changed');
		expect(logs[logs.length - 1]).toBe(JSON.stringify(['b', 'a']));

		// Uncheck second checkbox
		inputs[1].checked = false;
		inputs[1].dispatchEvent(new Event('change', { bubbles: true }));
		flushSync();

		expect(logs[logs.length - 1]).toBe(JSON.stringify(['a']));

		// Check third checkbox
		inputs[2].checked = true;
		inputs[2].dispatchEvent(new Event('change', { bubbles: true }));
		flushSync();

		expect(logs[logs.length - 1]).toBe(JSON.stringify(['a', 'c']));
	});

	it('should bind checkbox group with a getter and setter', () => {
		const logs: string[] = [];

		function App() {
			return <>
				const obj = new RippleObject({ selected: ['b'] });
				effect(() => {
					logs.push('selected changed', JSON.stringify(obj.selected));
				});
				<div>
					<input
						type="checkbox"
						value="a"
						ref={bindGroup(() => obj.selected, (v: string[]) => (obj.selected = v))}
					/>
					<input
						type="checkbox"
						value="b"
						ref={bindGroup(() => obj.selected, (v: string[]) => (obj.selected = v))}
					/>
					<input
						type="checkbox"
						value="c"
						ref={bindGroup(() => obj.selected, (v: string[]) => (obj.selected = v))}
					/>
				</div>
			</>;
		}

		render(App);
		flushSync();

		const inputs = container.querySelectorAll('input') as NodeListOf<HTMLInputElement>;
		expect(inputs[0].checked).toBe(false);
		expect(inputs[1].checked).toBe(true);
		expect(inputs[2].checked).toBe(false);

		// Check first checkbox
		inputs[0].checked = true;
		inputs[0].dispatchEvent(new Event('change', { bubbles: true }));
		flushSync();

		expect(logs).toContain('selected changed');
		expect(logs[logs.length - 1]).toBe(JSON.stringify(['b', 'a']));

		// Uncheck second checkbox
		inputs[1].checked = false;
		inputs[1].dispatchEvent(new Event('change', { bubbles: true }));
		flushSync();

		expect(logs[logs.length - 1]).toBe(JSON.stringify(['a']));

		// Check third checkbox
		inputs[2].checked = true;
		inputs[2].dispatchEvent(new Event('change', { bubbles: true }));
		flushSync();

		expect(logs[logs.length - 1]).toBe(JSON.stringify(['a', 'c']));
	});

	it('should bind radio group', () => {
		const logs: string[] = [];

		function App() {
			return <>
				const selected = track('b');
				effect(() => {
					logs.push('selected changed', selected.value);
				});
				<div>
					<input type="radio" name="test" value="a" ref={bindGroup(selected)} />
					<input type="radio" name="test" value="b" ref={bindGroup(selected)} />
					<input type="radio" name="test" value="c" ref={bindGroup(selected)} />
				</div>
			</>;
		}

		render(App);
		flushSync();

		const inputs = container.querySelectorAll('input') as NodeListOf<HTMLInputElement>;
		expect(inputs[0].checked).toBe(false);
		expect(inputs[1].checked).toBe(true);
		expect(inputs[2].checked).toBe(false);

		// Select first radio
		inputs[0].checked = true;
		inputs[0].dispatchEvent(new Event('change', { bubbles: true }));
		flushSync();

		expect(logs).toContain('selected changed');
		expect(logs[logs.length - 1]).toBe('a');
		expect(inputs[0].checked).toBe(true);
		expect(inputs[1].checked).toBe(false);
		expect(inputs[2].checked).toBe(false);

		// Select third radio
		inputs[2].checked = true;
		inputs[2].dispatchEvent(new Event('change', { bubbles: true }));
		flushSync();

		expect(logs[logs.length - 1]).toBe('c');
		expect(inputs[0].checked).toBe(false);
		expect(inputs[1].checked).toBe(false);
		expect(inputs[2].checked).toBe(true);
	});

	it('should bind radio group with a getter and setter', () => {
		const logs: string[] = [];

		function App() {
			return <>
				const selected = new RippleObject({ value: 'b' });
				effect(() => {
					logs.push('selected changed', selected.value);
				});
				<div>
					<input
						type="radio"
						name="test"
						value="a"
						ref={bindGroup(() => selected.value, (v: string) => (selected.value = v))}
					/>
					<input
						type="radio"
						name="test"
						value="b"
						ref={bindGroup(() => selected.value, (v: string) => (selected.value = v))}
					/>
					<input
						type="radio"
						name="test"
						value="c"
						ref={bindGroup(() => selected.value, (v: string) => (selected.value = v))}
					/>
				</div>
			</>;
		}

		render(App);
		flushSync();

		const inputs = container.querySelectorAll('input') as NodeListOf<HTMLInputElement>;
		expect(inputs[0].checked).toBe(false);
		expect(inputs[1].checked).toBe(true);
		expect(inputs[2].checked).toBe(false);

		// Select first radio
		inputs[0].checked = true;
		inputs[0].dispatchEvent(new Event('change', { bubbles: true }));
		flushSync();

		expect(logs).toContain('selected changed');
		expect(logs[logs.length - 1]).toBe('a');
		expect(inputs[0].checked).toBe(true);
		expect(inputs[1].checked).toBe(false);
		expect(inputs[2].checked).toBe(false);

		// Select third radio
		inputs[2].checked = true;
		inputs[2].dispatchEvent(new Event('change', { bubbles: true }));
		flushSync();

		expect(logs[logs.length - 1]).toBe('c');
		expect(inputs[0].checked).toBe(false);
		expect(inputs[1].checked).toBe(false);
		expect(inputs[2].checked).toBe(true);
	});

	it('should update checkbox group from tracked value change', () => {
		function App() {
			return <>
				const selected = track(['a']);
				<div>
					<input type="checkbox" value="a" ref={bindGroup(selected)} />
					<input type="checkbox" value="b" ref={bindGroup(selected)} />
					<input type="checkbox" value="c" ref={bindGroup(selected)} />
					<button onClick={() => (selected.value = ['b', 'c'])}>{'Update'}</button>
				</div>
			</>;
		}

		render(App);
		flushSync();

		const inputs = container.querySelectorAll('input') as NodeListOf<HTMLInputElement>;
		const button = container.querySelector('button') as HTMLButtonElement;

		expect(inputs[0].checked).toBe(true);
		expect(inputs[1].checked).toBe(false);
		expect(inputs[2].checked).toBe(false);

		button.click();
		flushSync();

		expect(inputs[0].checked).toBe(false);
		expect(inputs[1].checked).toBe(true);
		expect(inputs[2].checked).toBe(true);
	});

	it('should update checkbox group from tracked value change with a getter and setter', () => {
		function App() {
			return <>
				const selected = new RippleObject({ value: ['a'] });
				<div>
					<input
						type="checkbox"
						value="a"
						ref={bindGroup(() => selected.value, (v: string[]) => (selected.value = v))}
					/>
					<input
						type="checkbox"
						value="b"
						ref={bindGroup(() => selected.value, (v: string[]) => (selected.value = v))}
					/>
					<input
						type="checkbox"
						value="c"
						ref={bindGroup(() => selected.value, (v: string[]) => (selected.value = v))}
					/>
					<button onClick={() => (selected.value = ['b', 'c'])}>{'Update'}</button>
				</div>
			</>;
		}

		render(App);
		flushSync();

		const inputs = container.querySelectorAll('input') as NodeListOf<HTMLInputElement>;
		const button = container.querySelector('button') as HTMLButtonElement;

		expect(inputs[0].checked).toBe(true);
		expect(inputs[1].checked).toBe(false);
		expect(inputs[2].checked).toBe(false);

		button.click();
		flushSync();

		expect(inputs[0].checked).toBe(false);
		expect(inputs[1].checked).toBe(true);
		expect(inputs[2].checked).toBe(true);
	});

	it('should update radio group from tracked value change', () => {
		function App() {
			return <>
				const selected = track('a');
				<div>
					<input type="radio" name="test" value="a" ref={bindGroup(selected)} />
					<input type="radio" name="test" value="b" ref={bindGroup(selected)} />
					<input type="radio" name="test" value="c" ref={bindGroup(selected)} />
					<button onClick={() => (selected.value = 'c')}>{'Update'}</button>
				</div>
			</>;
		}

		render(App);
		flushSync();

		const inputs = container.querySelectorAll('input') as NodeListOf<HTMLInputElement>;
		const button = container.querySelector('button') as HTMLButtonElement;

		expect(inputs[0].checked).toBe(true);
		expect(inputs[1].checked).toBe(false);
		expect(inputs[2].checked).toBe(false);

		button.click();
		flushSync();

		expect(inputs[0].checked).toBe(false);
		expect(inputs[1].checked).toBe(false);
		expect(inputs[2].checked).toBe(true);
	});

	it('should update radio group from tracked value change with a getter and setter', () => {
		function App() {
			return <>
				const selected = new RippleObject({ value: 'a' });
				<div>
					<input
						type="radio"
						name="test"
						value="a"
						ref={bindGroup(() => selected.value, (v: string) => (selected.value = v))}
					/>
					<input
						type="radio"
						name="test"
						value="b"
						ref={bindGroup(() => selected.value, (v: string) => (selected.value = v))}
					/>
					<input
						type="radio"
						name="test"
						value="c"
						ref={bindGroup(() => selected.value, (v: string) => (selected.value = v))}
					/>
					<button onClick={() => (selected.value = 'c')}>{'Update'}</button>
				</div>
			</>;
		}

		render(App);
		flushSync();

		const inputs = container.querySelectorAll('input') as NodeListOf<HTMLInputElement>;
		const button = container.querySelector('button') as HTMLButtonElement;

		expect(inputs[0].checked).toBe(true);
		expect(inputs[1].checked).toBe(false);
		expect(inputs[2].checked).toBe(false);

		button.click();
		flushSync();

		expect(inputs[0].checked).toBe(false);
		expect(inputs[1].checked).toBe(false);
		expect(inputs[2].checked).toBe(true);
	});

	it('should handle checkbox group with initial empty array', () => {
		function App() {
			return <>
				const selected = track([]);
				<div>
					<input type="checkbox" value="a" ref={bindGroup(selected)} />
					<input type="checkbox" value="b" ref={bindGroup(selected)} />
				</div>
			</>;
		}

		render(App);
		flushSync();

		const inputs = container.querySelectorAll('input') as NodeListOf<HTMLInputElement>;

		expect(inputs[0].checked).toBe(false);
		expect(inputs[1].checked).toBe(false);

		inputs[0].checked = true;
		inputs[0].dispatchEvent(new Event('change', { bubbles: true }));
		flushSync();

		expect(inputs[0].checked).toBe(true);
		expect(inputs[1].checked).toBe(false);
	});

	it('should handle checkbox group with initial empty array with a getter and setter', () => {
		function App() {
			return <>
				const selected: RippleObject<{ value: string[] }> = new RippleObject({ value: [] });
				<div>
					<input
						type="checkbox"
						value="a"
						ref={bindGroup(() => selected.value, (v: string[]) => (selected.value = v))}
					/>
					<input
						type="checkbox"
						value="b"
						ref={bindGroup(() => selected.value, (v: string[]) => (selected.value = v))}
					/>
				</div>
			</>;
		}

		render(App);
		flushSync();

		const inputs = container.querySelectorAll('input') as NodeListOf<HTMLInputElement>;

		expect(inputs[0].checked).toBe(false);
		expect(inputs[1].checked).toBe(false);

		inputs[0].checked = true;
		inputs[0].dispatchEvent(new Event('change', { bubbles: true }));
		flushSync();

		expect(inputs[0].checked).toBe(true);
		expect(inputs[1].checked).toBe(false);
	});

	it('should handle number input type', () => {
		function App() {
			return <>
				const value = track(42);
				<input type="number" ref={bindValue(value)} />
			</>;
		}

		render(App);
		flushSync();

		const input = container.querySelector('input') as HTMLInputElement;
		expect(input.value).toBe('42');

		input.value = '100';
		input.dispatchEvent(new Event('input', { bubbles: true }));
		flushSync();

		expect(input.value).toBe('100');
	});

	it('should handle number input type with a getter and setter', () => {
		function App() {
			return <>
				const obj = new RippleObject({ value: 42 });
				<input type="number" ref={bindValue(() => obj.value, (v) => (obj.value = v))} />
			</>;
		}

		render(App);
		flushSync();

		const input = container.querySelector('input') as HTMLInputElement;
		expect(input.value).toBe('42');

		input.value = '100';
		input.dispatchEvent(new Event('input', { bubbles: true }));
		flushSync();

		expect(input.value).toBe('100');
	});

	it('should update number input element when tracked value changes', () => {
		function App() {
			return <>
				const value = track(10);
				<div>
					<input type="number" ref={bindValue(value)} />
					<button onClick={() => (value.value = 99)}>{'Update'}</button>
				</div>
			</>;
		}

		render(App);
		flushSync();

		const input = container.querySelector('input') as HTMLInputElement;
		const button = container.querySelector('button') as HTMLButtonElement;

		expect(input.value).toBe('10');

		button.click();
		flushSync();

		expect(input.value).toBe('99');
	});

	it(
		'should update number input element when tracked value changes with a getter and setter',
		() => {
			function App() {
				return <>
					const obj = new RippleObject({ value: 10 });
					<div>
						<input type="number" ref={bindValue(() => obj.value, (v) => (obj.value = v))} />
						<button onClick={() => (obj.value = 99)}>{'Update'}</button>
					</div>
				</>;
			}

			render(App);
			flushSync();

			const input = container.querySelector('input') as HTMLInputElement;
			const button = container.querySelector('button') as HTMLButtonElement;

			expect(input.value).toBe('10');

			button.click();
			flushSync();

			expect(input.value).toBe('99');
		},
	);

	it('should handle range input type', () => {
		function App() {
			return <>
				const value = track(50);
				<input type="range" min="0" max="100" ref={bindValue(value)} />
			</>;
		}

		render(App);
		flushSync();

		const input = container.querySelector('input') as HTMLInputElement;
		expect(input.value).toBe('50');

		input.value = '75';
		input.dispatchEvent(new Event('input', { bubbles: true }));
		flushSync();

		expect(input.value).toBe('75');
	});

	it('should handle range input type with a getter and setter', () => {
		function App() {
			return <>
				const obj = new RippleObject({ value: 50 });
				<input
					type="range"
					min="0"
					max="100"
					ref={bindValue(() => obj.value, (v) => (obj.value = v))}
				/>
			</>;
		}

		render(App);
		flushSync();

		const input = container.querySelector('input') as HTMLInputElement;
		expect(input.value).toBe('50');

		input.value = '75';
		input.dispatchEvent(new Event('input', { bubbles: true }));
		flushSync();

		expect(input.value).toBe('75');
	});

	it('should update range input element when tracked value changes', () => {
		function App() {
			return <>
				const value = track(25);
				<div>
					<input type="range" min="0" max="100" ref={bindValue(value)} />
					<button onClick={() => (value.value = 80)}>{'Update'}</button>
				</div>
			</>;
		}

		render(App);
		flushSync();

		const input = container.querySelector('input') as HTMLInputElement;
		const button = container.querySelector('button') as HTMLButtonElement;

		expect(input.value).toBe('25');

		button.click();
		flushSync();

		expect(input.value).toBe('80');
	});

	it(
		'should update range input element when tracked value changes with a getter and setter',
		() => {
			function App() {
				return <>
					const obj = new RippleObject({ value: 25 });
					<div>
						<input
							type="range"
							min="0"
							max="100"
							ref={bindValue(() => obj.value, (v) => (obj.value = v))}
						/>
						<button onClick={() => (obj.value = 80)}>{'Update'}</button>
					</div>
				</>;
			}

			render(App);
			flushSync();

			const input = container.querySelector('input') as HTMLInputElement;
			const button = container.querySelector('button') as HTMLButtonElement;

			expect(input.value).toBe('25');

			button.click();
			flushSync();

			expect(input.value).toBe('80');
		},
	);

	it('should handle empty number input as null', () => {
		function App() {
			return <>
				const value = track(null);
				<input type="number" ref={bindValue(value)} />
			</>;
		}

		render(App);
		flushSync();

		const input = container.querySelector('input') as HTMLInputElement;
		expect(input.value).toBe('');

		input.value = '';
		input.dispatchEvent(new Event('input', { bubbles: true }));
		flushSync();

		expect(input.value).toBe('');
	});

	it('should handle empty number input as null with a getter and setter', () => {
		function App() {
			return <>
				const obj = new RippleObject({ value: null });
				<input type="number" ref={bindValue(() => obj.value, (v) => (obj.value = v))} />
			</>;
		}

		render(App);
		flushSync();

		const input = container.querySelector('input') as HTMLInputElement;
		expect(input.value).toBe('');

		input.value = '';
		input.dispatchEvent(new Event('input', { bubbles: true }));
		flushSync();

		expect(input.value).toBe('');
	});

	it('should handle date input type', () => {
		function App() {
			return <>
				const value = track('2025-11-14');
				<input type="date" ref={bindValue(value)} />
			</>;
		}

		render(App);
		flushSync();

		const input = container.querySelector('input') as HTMLInputElement;
		expect(input.value).toBe('2025-11-14');

		input.value = '2025-12-25';
		input.dispatchEvent(new Event('input', { bubbles: true }));
		flushSync();

		expect(input.value).toBe('2025-12-25');
	});

	it('should handle date input type with a getter and setter', () => {
		function App() {
			return <>
				const obj = new RippleObject({ value: '2025-11-14' });
				<input type="date" ref={bindValue(() => obj.value, (v) => (obj.value = v))} />
			</>;
		}

		render(App);
		flushSync();

		const input = container.querySelector('input') as HTMLInputElement;
		expect(input.value).toBe('2025-11-14');

		input.value = '2025-12-25';
		input.dispatchEvent(new Event('input', { bubbles: true }));
		flushSync();

		expect(input.value).toBe('2025-12-25');
	});

	it('should update date input element when tracked value changes', () => {
		function App() {
			return <>
				const value = track('2025-01-01');
				<div>
					<input type="date" ref={bindValue(value)} />
					<button onClick={() => (value.value = '2025-12-31')}>{'Update'}</button>
				</div>
			</>;
		}

		render(App);
		flushSync();

		const input = container.querySelector('input') as HTMLInputElement;
		const button = container.querySelector('button') as HTMLButtonElement;

		expect(input.value).toBe('2025-01-01');

		button.click();
		flushSync();

		expect(input.value).toBe('2025-12-31');
	});

	it('should update date input element when tracked value changes with a getter and setter', () => {
		function App() {
			return <>
				const obj = new RippleObject({ value: '2025-01-01' });
				<div>
					<input type="date" ref={bindValue(() => obj.value, (v) => (obj.value = v))} />
					<button onClick={() => (obj.value = '2025-12-31')}>{'Update'}</button>
				</div>
			</>;
		}

		render(App);
		flushSync();

		const input = container.querySelector('input') as HTMLInputElement;
		const button = container.querySelector('button') as HTMLButtonElement;

		expect(input.value).toBe('2025-01-01');

		button.click();
		flushSync();

		expect(input.value).toBe('2025-12-31');
	});

	it('should handle select with multiple attribute', () => {
		function App() {
			return <>
				const selected = track(['2', '3']);
				<select multiple ref={bindValue(selected)}>
					<option value="1">{'One'}</option>
					<option value="2">{'Two'}</option>
					<option value="3">{'Three'}</option>
					<option value="4">{'Four'}</option>
				</select>
			</>;
		}

		render(App);
		flushSync();

		const select = container.querySelector('select') as HTMLSelectElement;
		const options = select.options;

		expect(options[0].selected).toBe(false);
		expect(options[1].selected).toBe(true);
		expect(options[2].selected).toBe(true);
		expect(options[3].selected).toBe(false);

		// Change selection
		options[0].selected = true;
		options[1].selected = false;
		select.dispatchEvent(new Event('change', { bubbles: true }));
		flushSync();

		expect(options[0].selected).toBe(true);
		expect(options[1].selected).toBe(false);
		expect(options[2].selected).toBe(true);
	});

	it('should handle select with multiple attribute with a getter and setter', () => {
		function App() {
			return <>
				const selected = track(['2', '3']);
				<select multiple ref={bindValue(() => selected.value, (v) => (selected.value = v))}>
					<option value="1">{'One'}</option>
					<option value="2">{'Two'}</option>
					<option value="3">{'Three'}</option>
					<option value="4">{'Four'}</option>
				</select>
			</>;
		}

		render(App);
		flushSync();

		const select = container.querySelector('select') as HTMLSelectElement;
		const options = select.options;

		expect(options[0].selected).toBe(false);
		expect(options[1].selected).toBe(true);
		expect(options[2].selected).toBe(true);
		expect(options[3].selected).toBe(false);

		// Change selection
		options[0].selected = true;
		options[1].selected = false;
		select.dispatchEvent(new Event('change', { bubbles: true }));
		flushSync();

		expect(options[0].selected).toBe(true);
		expect(options[1].selected).toBe(false);
		expect(options[2].selected).toBe(true);
	});

	it('should update multiple select element when tracked value changes', () => {
		function App() {
			return <>
				const selected = track(['1']);
				<div>
					<select multiple ref={bindValue(selected)}>
						<option value="1">{'One'}</option>
						<option value="2">{'Two'}</option>
						<option value="3">{'Three'}</option>
						<option value="4">{'Four'}</option>
					</select>
					<button onClick={() => (selected.value = ['2', '4'])}>{'Update'}</button>
				</div>
			</>;
		}

		render(App);
		flushSync();

		const select = container.querySelector('select') as HTMLSelectElement;
		const button = container.querySelector('button') as HTMLButtonElement;
		const options = select.options;

		expect(options[0].selected).toBe(true);
		expect(options[1].selected).toBe(false);
		expect(options[2].selected).toBe(false);
		expect(options[3].selected).toBe(false);

		button.click();
		flushSync();

		expect(options[0].selected).toBe(false);
		expect(options[1].selected).toBe(true);
		expect(options[2].selected).toBe(false);
		expect(options[3].selected).toBe(true);
	});

	it(
		'should update multiple select element when tracked value changes with a getter and setter',
		() => {
			function App() {
				return <>
					const selected = track(['1']);
					<div>
						<select multiple ref={bindValue(() => selected.value, (v) => (selected.value = v))}>
							<option value="1">{'One'}</option>
							<option value="2">{'Two'}</option>
							<option value="3">{'Three'}</option>
							<option value="4">{'Four'}</option>
						</select>
						<button onClick={() => (selected.value = ['2', '4'])}>{'Update'}</button>
					</div>
				</>;
			}

			render(App);
			flushSync();

			const select = container.querySelector('select') as HTMLSelectElement;
			const button = container.querySelector('button') as HTMLButtonElement;
			const options = select.options;

			expect(options[0].selected).toBe(true);
			expect(options[1].selected).toBe(false);
			expect(options[2].selected).toBe(false);
			expect(options[3].selected).toBe(false);

			button.click();
			flushSync();

			expect(options[0].selected).toBe(false);
			expect(options[1].selected).toBe(true);
			expect(options[2].selected).toBe(false);
			expect(options[3].selected).toBe(true);
		},
	);

	it('should handle select without initial value and fall back to first option', () => {
		function App() {
			return <>
				const selected = track();
				<select ref={bindValue(selected)}>
					<option value="1">{'One'}</option>
					<option value="2">{'Two'}</option>
				</select>
			</>;
		}

		render(App);
		flushSync();

		const select = container.querySelector('select') as HTMLSelectElement;
		// Should pick up first option when undefined
		expect(select.selectedIndex).toBeGreaterThanOrEqual(0);
	});

	it(
		'should handle select without initial value and fall back to first option with a getter and setter',
		() => {
			function App() {
				return <>
					const selected = track();
					<select ref={bindValue(() => selected.value, (v) => (selected.value = v))}>
						<option value="1">{'One'}</option>
						<option value="2">{'Two'}</option>
					</select>
				</>;
			}

			render(App);
			flushSync();

			const select = container.querySelector('select') as HTMLSelectElement;
			// Should pick up first option when undefined
			expect(select.selectedIndex).toBeGreaterThanOrEqual(0);
		},
	);

	it('should handle select with disabled options', () => {
		function App() {
			return <>
				const selected = track();
				<select ref={bindValue(selected)}>
					<option value="1" disabled>{'One'}</option>
					<option value="2">{'Two'}</option>
				</select>
			</>;
		}

		render(App);
		flushSync();

		const select = container.querySelector('select') as HTMLSelectElement;
		// Should fall back to first non-disabled option
		expect(select.options[1].selected).toBe(true);
	});

	it('should handle select with disabled options with a getter and setter', () => {
		function App() {
			return <>
				const selected = track();
				<select ref={bindValue(() => selected.value, (v) => (selected.value = v))}>
					<option value="1" disabled>{'One'}</option>
					<option value="2">{'Two'}</option>
				</select>
			</>;
		}

		render(App);
		flushSync();

		const select = container.querySelector('select') as HTMLSelectElement;
		// Should fall back to first non-disabled option
		expect(select.options[1].selected).toBe(true);
	});

	it('should preselect numeric option values in select bindValue', () => {
		function App() {
			return <>
				const selected = track(2);
				<select ref={bindValue(selected)}>
					<option value={1}>{'One'}</option>
					<option value={2}>{'Two'}</option>
					<option value={3}>{'Three'}</option>
				</select>
			</>;
		}

		render(App);
		flushSync();

		const select = container.querySelector('select') as HTMLSelectElement;

		expect(select.value).toBe('2');
		expect(select.options[0].selected).toBe(false);
		expect(select.options[1].selected).toBe(true);
		expect(select.options[2].selected).toBe(false);
	});

	it('should preselect numeric option values in select bindValue with a getter and setter', () => {
		function App() {
			return <>
				const selected = track(2);
				<select ref={bindValue(() => selected.value, (v) => (selected.value = v))}>
					<option value={1}>{'One'}</option>
					<option value={2}>{'Two'}</option>
					<option value={3}>{'Three'}</option>
				</select>
			</>;
		}

		render(App);
		flushSync();

		const select = container.querySelector('select') as HTMLSelectElement;

		expect(select.value).toBe('2');
		expect(select.options[0].selected).toBe(false);
		expect(select.options[1].selected).toBe(true);
		expect(select.options[2].selected).toBe(false);
	});

	it('should preserve numeric select values on change', () => {
		const logs: number[] = [];

		function App() {
			return <>
				const selected = track(2);
				effect(() => {
					logs.push(selected.value);
				});
				<select ref={bindValue(selected)}>
					<option value={1}>{'One'}</option>
					<option value={2}>{'Two'}</option>
					<option value={3}>{'Three'}</option>
				</select>
			</>;
		}

		render(App);
		flushSync();

		const select = container.querySelector('select') as HTMLSelectElement;
		select.options[2].selected = true;
		select.dispatchEvent(new Event('change', { bubbles: true }));
		flushSync();

		expect(select.value).toBe('3');
		expect(logs).toEqual([2, 3]);
	});

	it('should reselect tracked value when matching option is added later', async () => {
		function App() {
			return <>
				const selected = track(5);
				const options = RippleArray([
					{ id: 1, text: 'One' },
					{ id: 2, text: 'Two' },
				]);
				<div>
					<select ref={bindValue(selected)}>
						for (const option of options) {
							<option value={option.id}>{option.text}</option>
						}
					</select>
					<button onClick={() => options.push({ id: 5, text: 'Five' })}>{'Add'}</button>
				</div>
			</>;
		}

		render(App);
		flushSync();

		const select = container.querySelector('select') as HTMLSelectElement;
		const button = container.querySelector('button') as HTMLButtonElement;

		expect(select.value).toBe('');

		button.click();
		flushSync();
		await new Promise((resolve) => setTimeout(resolve, 0));
		flushSync();

		expect(select.value).toBe('5');
		expect(select.selectedOptions[0].value).toBe('5');
	});

	it('should reselect tracked value when option lists are replaced later', async () => {
		function App() {
			return <>
				const selected = track(22);
				const options = RippleArray([
					{ id: 1, text: 'One' },
				]);
				effect(() => {
					const timeout = setTimeout(() => {
						options.splice(0, options.length, { id: 21, text: 'Tokyo' }, { id: 22, text: 'Osaka' });
					}, 0);

					return () => clearTimeout(timeout);
				});
				<select ref={bindValue(selected)}>
					for (const option of options) {
						<option value={option.id}>{option.text}</option>
					}
				</select>
			</>;
		}

		render(App);
		flushSync();
		await new Promise((resolve) => setTimeout(resolve, 0));
		flushSync();

		const select = container.querySelector('select') as HTMLSelectElement;

		expect(select.value).toBe('22');
		expect(select.options[1].selected).toBe(true);
	});

	it('should accurately reflect values mutated through a tracked setter', () => {
		function App() {
			return <>
				let value = track(
					'',
					(val) => {
						return val;
					},
					(next) => {
						if (next.includes('c')) {
							next = next.replace(/c/g, '');
						}
						return next;
					},
				);
				<input type="text" ref={bindValue(value)} />
				<div>{value.value}</div>
			</>;
		}

		render(App);
		flushSync();

		const input = container.querySelector('input') as HTMLInputElement;
		const div = container.querySelector('div') as HTMLDivElement;

		expect(input.value).toBe('');
		expect(div.textContent).toBe('');

		input.value = 'abc';
		input.dispatchEvent(new Event('input', { bubbles: true }));
		flushSync();

		expect(input.value).toBe('ab');
		expect(div.textContent).toBe('ab');
	});

	it('should accurately reflect values when a getter modifies value', () => {
		function App() {
			return <>
				let value = track(
					'',
					(val) => {
						if (val.includes('c')) {
							val = val.replace(/c/g, '');
						}
						return val;
					},
					(next) => {
						return next;
					},
				);
				<input type="text" ref={bindValue(value)} />
				<div>{value.value}</div>
			</>;
		}

		render(App);
		flushSync();

		const input = container.querySelector('input') as HTMLInputElement;
		const div = container.querySelector('div') as HTMLDivElement;

		expect(input.value).toBe('');
		expect(div.textContent).toBe('');

		input.value = 'abc';
		input.dispatchEvent(new Event('input', { bubbles: true }));
		flushSync();

		expect(input.value).toBe('ab');
		expect(div.textContent).toBe('ab');
	});

	it('should always prefer what getter returns even if setter mutates next', () => {
		function App() {
			return <>
				let value = track(
					'',
					(val) => {
						return val.replace(/[c,b]+/g, '');
					},
					(next) => {
						if (next.includes('c')) {
							next = next.replace(/c/g, '');
						}
						return next;
					},
				);
				<input type="text" ref={bindValue(value)} />
				<div>{value.value}</div>
			</>;
		}

		render(App);
		flushSync();

		const input = container.querySelector('input') as HTMLInputElement;
		const div = container.querySelector('div') as HTMLDivElement;

		expect(input.value).toBe('');
		expect(div.textContent).toBe('');

		input.value = 'abc';
		input.dispatchEvent(new Event('input', { bubbles: true }));
		flushSync();

		expect(input.value).toBe('a');
		expect(div.textContent).toBe('a');
	});

	it(
		'should accurately reflect values mutated through an effect even after a setter mutation',
		() => {
			function App() {
				return <>
					let value = track(
						'',
						(val) => {
							return val;
						},
						(next) => {
							if (next.includes('c')) {
								next = next.replace(/c/g, '');
							}
							return next;
						},
					);
					effect(() => {
						value.value;

						untrack(() => {
							if (value.value.includes('a')) {
								value.value = value.value.replace(/a/g, '');
							}
						});
					});
					<input type="text" ref={bindValue(value)} />
					<div>{value.value}</div>
				</>;
			}

			render(App);
			flushSync();

			const input = container.querySelector('input') as HTMLInputElement;
			const div = container.querySelector('div') as HTMLDivElement;

			expect(input.value).toBe('');
			expect(div.textContent).toBe('');

			input.value = 'abc';
			input.dispatchEvent(new Event('input', { bubbles: true }));
			flushSync();

			expect(input.value).toBe('b');
			expect(div.textContent).toBe('b');
		},
	);

	it('should accurately reflect values mutated through a tracked setter via bind accessors', () => {
		function App() {
			return <>
				let value = track('');
				const value_accessors: [GetFunction<string>, SetFunction<string>] = [
					() => {
						return value.value;
					},
					(v: string) => {
						if (v.includes('c')) {
							v = v.replace(/c/g, '');
						}
						value.value = v;
					},
				];
				<input type="text" ref={bindValue(...value_accessors)} />
			</>;
		}

		render(App);
		flushSync();

		const input = container.querySelector('input') as HTMLInputElement;

		expect(input.value).toBe('');

		input.value = 'abc';
		input.dispatchEvent(new Event('input', { bubbles: true }));
		flushSync();

		expect(input.value).toBe('ab');
	});

	it('should prefer what getter returns via bind accessors', () => {
		function App() {
			return <>
				let value = track('');
				const value_accessors: [GetFunction<string>, SetFunction<string>] = [
					() => {
						if (value.value.includes('c')) {
							return value.value.replace(/c/g, '');
						}
						return value.value;
					},
					(v: string) => {
						value.value = v;
					},
				];
				<input type="text" ref={bindValue(...value_accessors)} />
			</>;
		}

		render(App);
		flushSync();

		const input = container.querySelector('input') as HTMLInputElement;

		expect(input.value).toBe('');

		input.value = 'abc';
		input.dispatchEvent(new Event('input', { bubbles: true }));
		flushSync();

		expect(input.value).toBe('ab');
	});

	it(
		'should always prefer what getter returns even if setter mutates next via bind accessors',
		() => {
			function App() {
				return <>
					let value = track('');
					const value_accessors: [GetFunction<string>, SetFunction<string>] = [
						() => {
							return value.value.replace(/[c,b]+/g, '');
						},
						(v: string) => {
							if (v.includes('c')) {
								v = v.replace(/c/g, '');
							}
							value.value = v;
						},
					];
					<input type="text" ref={bindValue(...value_accessors)} />
				</>;
			}

			render(App);
			flushSync();

			const input = container.querySelector('input') as HTMLInputElement;

			expect(input.value).toBe('');

			input.value = 'abc';
			input.dispatchEvent(new Event('input', { bubbles: true }));
			flushSync();

			expect(input.value).toBe('a');
		},
	);

	it(
		'should accurately reflect values mutated through an effect even after a setter mutation via bind accessors',
		() => {
			function App() {
				return <>
					let value = track('');
					const value_accessors: [GetFunction<string>, SetFunction<string>] = [
						() => {
							return value.value;
						},
						(v: string) => {
							if (v.includes('c')) {
								v = v.replace(/c/g, '');
							}
							value.value = v;
						},
					];
					effect(() => {
						value.value;

						untrack(() => {
							if (value.value.includes('a')) {
								value.value = value.value.replace(/a/g, '');
							}
						});
					});
					<input type="text" ref={bindValue(...value_accessors)} />
				</>;
			}

			render(App);
			flushSync();

			const input = container.querySelector('input') as HTMLInputElement;

			expect(input.value).toBe('');

			input.value = 'abc';
			input.dispatchEvent(new Event('input', { bubbles: true }));
			flushSync();

			expect(input.value).toBe('b');
		},
	);

	it(
		'should keep the input.value unchanged and synced with the tracked when the setter blocks updates to the tracked via bind accessors',
		() => {
			function App() {
				return <>
					let value = track('');
					const value_accessors: [GetFunction<string>, SetFunction<string>] = [
						() => {
							return value.value;
						},
						(v: string) => {
							// no update
						},
					];
					<input type="text" ref={bindValue(...value_accessors)} />
					<div>{value.value}</div>
				</>;
			}

			render(App);
			flushSync();

			const input = container.querySelector('input') as HTMLInputElement;
			const div = container.querySelector('div') as HTMLDivElement;

			expect(input.value).toBe('');

			input.value = 'abc';
			input.dispatchEvent(new Event('input', { bubbles: true }));
			flushSync();

			expect(input.value).toBe('');
			expect(div.textContent).toBe('');
		},
	);

	it(
		'should keep the input.value unchanged and synced with the tracked when the setter blocks updates to the tracked via track get / set',
		() => {
			function App() {
				return <>
					let value = track(
						'',
						(v) => {
							return v;
						},
						() => {
							return '';
						},
					);
					<input type="text" ref={bindValue(value)} />
					<div>{value.value}</div>
				</>;
			}

			render(App);
			flushSync();

			const input = container.querySelector('input') as HTMLInputElement;
			const div = container.querySelector('div') as HTMLDivElement;

			expect(input.value).toBe('');

			input.value = 'abc';
			input.dispatchEvent(new Event('input', { bubbles: true }));
			flushSync();

			expect(input.value).toBe('');
			expect(div.textContent).toBe('');
		},
	);
});

describe('bindClientWidth and bindClientHeight', () => {
	it('should bind element clientWidth', () => {
		const logs: number[] = [];

		function App() {
			return <>
				const width = track(0);
				effect(() => {
					logs.push(width.value);
				});
				<div ref={bindClientWidth(width)} />
			</>;
		}

		render(App);
		flushSync();

		const div = container.querySelector('div') as HTMLDivElement;

		Object.defineProperty(div, 'clientWidth', {
			configurable: true,
			get: () => 200,
		});

		triggerResize(div, {
			contentRect: new DOMRectReadOnly(0, 0, 200, 100),
		});
		flushSync();

		expect(logs[logs.length - 1]).toBe(200);
	});

	it('should bind element clientWidth with a getter and setter', () => {
		const logs: number[] = [];

		function App() {
			return <>
				const width = new RippleObject({ value: 0 });
				effect(() => {
					logs.push(width.value);
				});
				<div ref={bindClientWidth(() => width.value, (v: number) => (width.value = v))} />
			</>;
		}

		render(App);
		flushSync();

		const div = container.querySelector('div') as HTMLDivElement;

		Object.defineProperty(div, 'clientWidth', {
			configurable: true,
			get: () => 200,
		});

		triggerResize(div, {
			contentRect: new DOMRectReadOnly(0, 0, 200, 100),
		});
		flushSync();

		expect(logs[logs.length - 1]).toBe(200);
	});

	it('should bind element clientHeight', () => {
		const logs: number[] = [];

		function App() {
			return <>
				const height = track(0);
				effect(() => {
					logs.push(height.value);
				});
				<div ref={bindClientHeight(height)} />
			</>;
		}

		render(App);
		flushSync();

		const div = container.querySelector('div') as HTMLDivElement;

		Object.defineProperty(div, 'clientHeight', {
			configurable: true,
			get: () => 150,
		});

		triggerResize(div, {
			contentRect: new DOMRectReadOnly(0, 0, 100, 150),
		});
		flushSync();

		expect(logs[logs.length - 1]).toBe(150);
	});

	it('should bind element clientHeight with a getter and setter', () => {
		const logs: number[] = [];

		function App() {
			return <>
				const height = new RippleObject({ value: 0 });
				effect(() => {
					logs.push(height.value);
				});
				<div ref={bindClientHeight(() => height.value, (v: number) => (height.value = v))} />
			</>;
		}

		render(App);
		flushSync();

		const div = container.querySelector('div') as HTMLDivElement;

		Object.defineProperty(div, 'clientHeight', {
			configurable: true,
			get: () => 150,
		});

		triggerResize(div, {
			contentRect: new DOMRectReadOnly(0, 0, 100, 150),
		});
		flushSync();

		expect(logs[logs.length - 1]).toBe(150);
	});
});

describe('bindOffsetWidth and bindOffsetHeight', () => {
	it('should bind element offsetWidth', () => {
		const logs: number[] = [];

		function App() {
			return <>
				const width = track(0);
				effect(() => {
					logs.push(width.value);
				});
				<div ref={bindOffsetWidth(width)} />
			</>;
		}

		render(App);
		flushSync();

		const div = container.querySelector('div') as HTMLDivElement;

		Object.defineProperty(div, 'offsetWidth', {
			configurable: true,
			get: () => 250,
		});

		triggerResize(div, {
			contentRect: new DOMRectReadOnly(0, 0, 250, 100),
		});
		flushSync();

		expect(logs[logs.length - 1]).toBe(250);
	});

	it('should bind element offsetWidth with a getter and setter', () => {
		const logs: number[] = [];

		function App() {
			return <>
				const width = new RippleObject({ value: 0 });
				effect(() => {
					logs.push(width.value);
				});
				<div ref={bindOffsetWidth(() => width.value, (v: number) => (width.value = v))} />
			</>;
		}

		render(App);
		flushSync();

		const div = container.querySelector('div') as HTMLDivElement;

		Object.defineProperty(div, 'offsetWidth', {
			configurable: true,
			get: () => 250,
		});

		triggerResize(div, {
			contentRect: new DOMRectReadOnly(0, 0, 250, 100),
		});
		flushSync();

		expect(logs[logs.length - 1]).toBe(250);
	});

	it('should bind element offsetHeight', () => {
		const logs: number[] = [];

		function App() {
			return <>
				const height = track(0);
				effect(() => {
					logs.push(height.value);
				});
				<div ref={bindOffsetHeight(height)} />
			</>;
		}

		render(App);
		flushSync();

		const div = container.querySelector('div') as HTMLDivElement;

		Object.defineProperty(div, 'offsetHeight', {
			configurable: true,
			get: () => 175,
		});

		triggerResize(div, {
			contentRect: new DOMRectReadOnly(0, 0, 100, 175),
		});
		flushSync();

		expect(logs[logs.length - 1]).toBe(175);
	});

	it('should bind element offsetHeight with a getter and setter', () => {
		const logs: number[] = [];

		function App() {
			return <>
				const height = new RippleObject({ value: 0 });
				effect(() => {
					logs.push(height.value);
				});
				<div ref={bindOffsetHeight(() => height.value, (v: number) => (height.value = v))} />
			</>;
		}

		render(App);
		flushSync();

		const div = container.querySelector('div') as HTMLDivElement;

		Object.defineProperty(div, 'offsetHeight', {
			configurable: true,
			get: () => 175,
		});

		triggerResize(div, {
			contentRect: new DOMRectReadOnly(0, 0, 100, 175),
		});
		flushSync();

		expect(logs[logs.length - 1]).toBe(175);
	});
});

describe('bindContentRect', () => {
	it('should bind element contentRect', () => {
		const logs: DOMRectReadOnly[] = [];

		function App() {
			return <>
				const rect = track(null);
				effect(() => {
					if (rect.value) logs.push(rect.value);
				});
				<div ref={bindContentRect(rect)} />
			</>;
		}

		render(App);
		flushSync();

		const div = container.querySelector('div') as HTMLDivElement;

		const mockRect = new DOMRectReadOnly(10, 20, 300, 200);
		triggerResize(div, {
			contentRect: mockRect,
		});
		flushSync();

		expect(logs.length).toBeGreaterThan(0);
		const lastRect = logs[logs.length - 1];
		expect(lastRect.width).toBe(300);
		expect(lastRect.height).toBe(200);
	});

	it('should bind element contentRect with a getter and setter', () => {
		const logs: DOMRectReadOnly[] = [];

		function App() {
			return <>
				const rect: RippleObject<{ value: DOMRectReadOnly | null }> = new RippleObject({
					value: null,
				});
				effect(() => {
					if (rect.value) logs.push(rect.value);
				});
				<div
					ref={bindContentRect<null | DOMRectReadOnly>(
						() => rect.value,
						(v: DOMRectReadOnly) => (rect.value = v),
					)}
				/>
			</>;
		}

		render(App);
		flushSync();

		const div = container.querySelector('div') as HTMLDivElement;

		const mockRect = new DOMRectReadOnly(10, 20, 300, 200);
		triggerResize(div, {
			contentRect: mockRect,
		});
		flushSync();

		expect(logs.length).toBeGreaterThan(0);
		const lastRect = logs[logs.length - 1];
		expect(lastRect.width).toBe(300);
		expect(lastRect.height).toBe(200);
	});
});

describe('bindContentBoxSize', () => {
	it('should bind element contentBoxSize', () => {
		const logs: any[] = [];

		function App() {
			return <>
				const boxSize = track(null);
				effect(() => {
					if (boxSize.value) logs.push(boxSize.value);
				});
				<div ref={bindContentBoxSize(boxSize)} />
			</>;
		}

		render(App);
		flushSync();

		const div = container.querySelector('div') as HTMLDivElement;

		const mockBoxSize = [
			{ blockSize: 200, inlineSize: 300 },
		];
		triggerResize(div, {
			contentBoxSize: mockBoxSize as any,
		});
		flushSync();

		expect(logs.length).toBeGreaterThan(0);
		expect(logs[logs.length - 1]).toBe(mockBoxSize);
	});

	it('should bind element contentBoxSize with a getter and setter', () => {
		const logs: any[] = [];
		const mockBoxSize = [
			{ blockSize: 200, inlineSize: 300 },
		];

		function App() {
			return <>
				const boxSize: RippleObject<{ value: typeof mockBoxSize | null }> = new RippleObject({
					value: null,
				});
				effect(() => {
					if (boxSize.value) logs.push(boxSize.value);
				});
				<div
					ref={bindContentBoxSize<null | typeof mockBoxSize>(
						() => boxSize.value,
						(v: typeof mockBoxSize) => (boxSize.value = v),
					)}
				/>
			</>;
		}

		render(App);
		flushSync();

		const div = container.querySelector('div') as HTMLDivElement;

		triggerResize(div, {
			contentBoxSize: mockBoxSize as any,
		});
		flushSync();

		expect(logs.length).toBeGreaterThan(0);
		expect(logs[logs.length - 1]).toBe(mockBoxSize);
	});
});

describe('bindBorderBoxSize', () => {
	it('should bind element borderBoxSize', () => {
		const logs: any[] = [];

		function App() {
			return <>
				const boxSize = track(null);
				effect(() => {
					if (boxSize.value) logs.push(boxSize.value);
				});
				<div ref={bindBorderBoxSize(boxSize)} />
			</>;
		}

		render(App);
		flushSync();

		const div = container.querySelector('div') as HTMLDivElement;

		const mockBoxSize = [
			{ blockSize: 220, inlineSize: 320 },
		];
		triggerResize(div, {
			borderBoxSize: mockBoxSize as any,
		});
		flushSync();

		expect(logs.length).toBeGreaterThan(0);
		expect(logs[logs.length - 1]).toBe(mockBoxSize);
	});

	it('should bind element borderBoxSize with a getter and setter', () => {
		const logs: any[] = [];
		const mockBoxSize = [
			{ blockSize: 220, inlineSize: 320 },
		];

		function App() {
			return <>
				const boxSize: RippleObject<{ value: typeof mockBoxSize | null }> = new RippleObject({
					value: null,
				});
				effect(() => {
					if (boxSize.value) logs.push(boxSize.value);
				});
				<div
					ref={bindBorderBoxSize<null | typeof mockBoxSize>(
						() => boxSize.value,
						(v: typeof mockBoxSize) => (boxSize.value = v),
					)}
				/>
			</>;
		}

		render(App);
		flushSync();

		const div = container.querySelector('div') as HTMLDivElement;

		triggerResize(div, {
			borderBoxSize: mockBoxSize as any,
		});
		flushSync();

		expect(logs.length).toBeGreaterThan(0);
		expect(logs[logs.length - 1]).toBe(mockBoxSize);
	});
});

describe('bindDevicePixelContentBoxSize', () => {
	it('should bind element devicePixelContentBoxSize', () => {
		const logs: any[] = [];

		function App() {
			return <>
				const boxSize = track(null);
				effect(() => {
					if (boxSize.value) logs.push(boxSize.value);
				});
				<div ref={bindDevicePixelContentBoxSize(boxSize)} />
			</>;
		}

		render(App);
		flushSync();

		const div = container.querySelector('div') as HTMLDivElement;

		const mockBoxSize = [
			{ blockSize: 400, inlineSize: 600 },
		];
		triggerResize(div, {
			devicePixelContentBoxSize: mockBoxSize as any,
		});
		flushSync();

		expect(logs.length).toBeGreaterThan(0);
		expect(logs[logs.length - 1]).toBe(mockBoxSize);
	});

	it('should bind element devicePixelContentBoxSize with a getter and setter', () => {
		const logs: any[] = [];
		const mockBoxSize = [
			{ blockSize: 400, inlineSize: 600 },
		];

		function App() {
			return <>
				const boxSize: RippleObject<{ value: typeof mockBoxSize | null }> = new RippleObject({
					value: null,
				});
				effect(() => {
					if (boxSize.value) logs.push(boxSize.value);
				});
				<div
					ref={bindDevicePixelContentBoxSize<null | typeof mockBoxSize>(
						() => boxSize.value,
						(v: typeof mockBoxSize) => (boxSize.value = v),
					)}
				/>
			</>;
		}

		render(App);
		flushSync();

		const div = container.querySelector('div') as HTMLDivElement;

		triggerResize(div, {
			devicePixelContentBoxSize: mockBoxSize as any,
		});
		flushSync();

		expect(logs.length).toBeGreaterThan(0);
		expect(logs[logs.length - 1]).toBe(mockBoxSize);
	});
});

describe('bindInnerHTML', () => {
	it('should bind element innerHTML', () => {
		const logs: string[] = [];

		function App() {
			return <>
				const html = track('<strong>Hello</strong>');
				effect(() => {
					logs.push(html.value);
				});
				<div contenteditable="true" ref={bindInnerHTML(html)} />
			</>;
		}

		render(App);
		flushSync();

		const div = container.querySelector('div') as HTMLDivElement;
		expect(div.innerHTML).toBe('<strong>Hello</strong>');

		div.innerHTML = '<em>World</em>';
		div.dispatchEvent(new Event('input', { bubbles: true }));
		flushSync();

		expect(logs[logs.length - 1]).toBe('<em>World</em>');
	});

	it('should bind element innerHTML', () => {
		const logs: string[] = [];

		function App() {
			return <>
				const html = new RippleObject({ value: '<strong>Hello</strong>' });
				effect(() => {
					logs.push(html.value);
				});
				<div
					contenteditable="true"
					ref={bindInnerHTML(() => html.value, (v: string) => (html.value = v))}
				/>
			</>;
		}

		render(App);
		flushSync();

		const div = container.querySelector('div') as HTMLDivElement;
		expect(div.innerHTML).toBe('<strong>Hello</strong>');

		div.innerHTML = '<em>World</em>';
		div.dispatchEvent(new Event('input', { bubbles: true }));
		flushSync();

		expect(logs[logs.length - 1]).toBe('<em>World</em>');
	});

	it('should update innerHTML when tracked value changes', () => {
		function App() {
			return <>
				const html = track('<p>Initial</p>');
				<div>
					<div contenteditable="true" ref={bindInnerHTML(html)} />
					<button onClick={() => (html.value = '<p>Updated</p>')}>{'Update'}</button>
				</div>
			</>;
		}

		render(App);
		flushSync();

		const div = container.querySelector('div[contenteditable]') as HTMLDivElement;
		const button = container.querySelector('button') as HTMLButtonElement;

		expect(div.innerHTML).toBe('<p>Initial</p>');

		button.click();
		flushSync();

		expect(div.innerHTML).toBe('<p>Updated</p>');
	});

	it('should update innerHTML when tracked value changes with a getter and setter', () => {
		function App() {
			return <>
				const html = new RippleObject({ value: '<p>Initial</p>' });
				<div>
					<div
						contenteditable="true"
						ref={bindInnerHTML(() => html.value, (v: string) => (html.value = v))}
					/>
					<button onClick={() => (html.value = '<p>Updated</p>')}>{'Update'}</button>
				</div>
			</>;
		}

		render(App);
		flushSync();

		const div = container.querySelector('div[contenteditable]') as HTMLDivElement;
		const button = container.querySelector('button') as HTMLButtonElement;

		expect(div.innerHTML).toBe('<p>Initial</p>');

		button.click();
		flushSync();

		expect(div.innerHTML).toBe('<p>Updated</p>');
	});

	it('should handle null innerHTML value', () => {
		function App() {
			return <>
				const html = track(null);
				<div contenteditable="true" ref={bindInnerHTML(html)} />
			</>;
		}

		render(App);
		flushSync();

		const div = container.querySelector('div') as HTMLDivElement;
		// Should set to current innerHTML when null
		expect(div.innerHTML).toBeDefined();
	});

	it('should handle null innerHTML value with a getter and setter', () => {
		function App() {
			return <>
				const html: RippleObject<{ value: null | string }> = new RippleObject({ value: null });
				<div
					contenteditable="true"
					ref={bindInnerHTML(() => html.value, (v: string | null) => (html.value = v))}
				/>
			</>;
		}

		render(App);
		flushSync();

		const div = container.querySelector('div') as HTMLDivElement;
		// Should set to current innerHTML when null
		expect(div.innerHTML).toBeDefined();
	});
});

describe('bindInnerText', () => {
	it('should bind element innerText', () => {
		const logs: string[] = [];

		function App() {
			return <>
				const text = track('Hello World');
				effect(() => {
					logs.push(text.value);
				});
				<div contenteditable="true" ref={bindInnerText(text)} />
			</>;
		}

		render(App);
		flushSync();

		const div = container.querySelector('div') as HTMLDivElement;
		expect(div.innerText).toBe('Hello World');

		div.innerText = 'Goodbye World';
		div.dispatchEvent(new Event('input', { bubbles: true }));
		flushSync();

		expect(logs[logs.length - 1]).toBe('Goodbye World');
	});

	it('should bind element innerText with a getter and setter', () => {
		const logs: string[] = [];

		function App() {
			return <>
				const text = new RippleObject({ value: 'Hello World' });
				effect(() => {
					logs.push(text.value);
				});
				<div
					contenteditable="true"
					ref={bindInnerText(() => text.value, (v: string) => (text.value = v))}
				/>
			</>;
		}

		render(App);
		flushSync();

		const div = container.querySelector('div') as HTMLDivElement;
		expect(div.innerText).toBe('Hello World');

		div.innerText = 'Goodbye World';
		div.dispatchEvent(new Event('input', { bubbles: true }));
		flushSync();

		expect(logs[logs.length - 1]).toBe('Goodbye World');
	});

	it('should update innerText when tracked value changes', () => {
		function App() {
			return <>
				const text = track('Before');
				<div>
					<div contenteditable="true" ref={bindInnerText(text)} />
					<button onClick={() => (text.value = 'After')}>{'Update'}</button>
				</div>
			</>;
		}

		render(App);
		flushSync();

		const div = container.querySelector('div[contenteditable]') as HTMLDivElement;
		const button = container.querySelector('button') as HTMLButtonElement;

		expect(div.innerText).toBe('Before');

		button.click();
		flushSync();

		expect(div.innerText).toBe('After');
	});

	it('should update innerText when tracked value changes with a getter and setter', () => {
		function App() {
			return <>
				const text = new RippleObject({ value: 'Before' });
				<div>
					<div
						contenteditable="true"
						ref={bindInnerText(() => text.value, (v: string) => (text.value = v))}
					/>
					<button onClick={() => (text.value = 'After')}>{'Update'}</button>
				</div>
			</>;
		}

		render(App);
		flushSync();

		const div = container.querySelector('div[contenteditable]') as HTMLDivElement;
		const button = container.querySelector('button') as HTMLButtonElement;

		expect(div.innerText).toBe('Before');

		button.click();
		flushSync();

		expect(div.innerText).toBe('After');
	});
});

describe('bindTextContent', () => {
	it('should bind element textContent', () => {
		const logs: string[] = [];

		function App() {
			return <>
				const text = track('Sample text');
				effect(() => {
					logs.push(text.value);
				});
				<div contenteditable="true" ref={bindTextContent(text)} />
			</>;
		}

		render(App);
		flushSync();

		const div = container.querySelector('div') as HTMLDivElement;
		expect(div.textContent).toBe('Sample text');

		div.textContent = 'Modified text';
		div.dispatchEvent(new Event('input', { bubbles: true }));
		flushSync();

		expect(logs[logs.length - 1]).toBe('Modified text');
	});

	it('should bind element textContent with a getter and setter', () => {
		const logs: string[] = [];

		function App() {
			return <>
				const text = new RippleObject({ value: 'Sample text' });
				effect(() => {
					logs.push(text.value);
				});
				<div
					contenteditable="true"
					ref={bindTextContent(() => text.value, (v: string) => (text.value = v))}
				/>
			</>;
		}

		render(App);
		flushSync();

		const div = container.querySelector('div') as HTMLDivElement;
		expect(div.textContent).toBe('Sample text');

		div.textContent = 'Modified text';
		div.dispatchEvent(new Event('input', { bubbles: true }));
		flushSync();

		expect(logs[logs.length - 1]).toBe('Modified text');
	});

	it('should update textContent when tracked value changes', () => {
		function App() {
			return <>
				const text = track('Start');
				<div>
					<div contenteditable="true" ref={bindTextContent(text)} />
					<button onClick={() => (text.value = 'End')}>{'Update'}</button>
				</div>
			</>;
		}

		render(App);
		flushSync();

		const div = container.querySelector('div[contenteditable]') as HTMLDivElement;
		const button = container.querySelector('button') as HTMLButtonElement;

		expect(div.textContent).toBe('Start');

		button.click();
		flushSync();

		expect(div.textContent).toBe('End');
	});

	it('should update textContent when tracked value changes with a getter and setter', () => {
		function App() {
			return <>
				const text = new RippleObject({ value: 'Start' });
				<div>
					<div
						contenteditable="true"
						ref={bindTextContent(() => text.value, (v: string) => (text.value = v))}
					/>
					<button onClick={() => (text.value = 'End')}>{'Update'}</button>
				</div>
			</>;
		}

		render(App);
		flushSync();

		const div = container.querySelector('div[contenteditable]') as HTMLDivElement;
		const button = container.querySelector('button') as HTMLButtonElement;

		expect(div.textContent).toBe('Start');

		button.click();
		flushSync();

		expect(div.textContent).toBe('End');
	});

	it('should handle null textContent value', () => {
		function App() {
			return <>
				const text = track(null);
				<div contenteditable="true" ref={bindTextContent(text)} />
			</>;
		}

		render(App);
		flushSync();

		const div = container.querySelector('div') as HTMLDivElement;
		// Should set to current textContent when null
		expect(div.textContent).toBeDefined();
	});

	it('should handle null textContent value with a getter and setter', () => {
		function App() {
			return <>
				const text: RippleObject<{ value: string | null }> = new RippleObject({ value: null });
				<div
					contenteditable="true"
					ref={bindTextContent<string | null>(
						() => text.value,
						(v: string | null) => (text.value = v),
					)}
				/>
			</>;
		}

		render(App);
		flushSync();

		const div = container.querySelector('div') as HTMLDivElement;
		// Should set to current textContent when null
		expect(div.textContent).toBeDefined();
	});
});

describe('bindNode', () => {
	it('should update tracked value with element reference', () => {
		let capturedNode: HTMLElement | null = null;

		function App() {
			return <>
				const nodeRef = track(null);
				effect(() => {
					capturedNode = nodeRef.value;
				});
				<div ref={bindNode(nodeRef)} />
			</>;
		}

		render(App);
		flushSync();

		const div = container.querySelector('div') as HTMLDivElement;
		expect(capturedNode).toBe(div);
	});

	it('should update tracked value with element reference and with a getter and setter', () => {
		let capturedNode: HTMLElement | null = null;

		function App() {
			return <>
				const nodeRef: RippleObject<{ value: HTMLElement | null }> = new RippleObject({
					value: null,
				});
				effect(() => {
					capturedNode = nodeRef.value;
				});
				<div ref={bindNode(() => nodeRef.value, (v: HTMLElement | null) => (nodeRef.value = v))} />
			</>;
		}

		render(App);
		flushSync();

		const div = container.querySelector('div') as HTMLDivElement;
		expect(capturedNode).toBe(div);
	});

	it('should allow access to bound element', () => {
		function App() {
			return <>
				const inputRef = track<HTMLInputElement | null>(null);
				<div>
					<input type="text" ref={bindNode(inputRef)} />
					<button
						onClick={() => {
							if (inputRef.value) {
								inputRef.value.value = 'Set by ref';
							}
						}}
					>
						{'Set Value'}
					</button>
				</div>
			</>;
		}

		render(App);
		flushSync();

		const input = container.querySelector('input') as HTMLInputElement;
		const button = container.querySelector('button') as HTMLButtonElement;

		expect(input.value).toBe('');

		button.click();
		flushSync();

		expect(input.value).toBe('Set by ref');
	});

	it('should allow access to bound element with a getter and setter', () => {
		function App() {
			return <>
				const inputRef: RippleObject<{ node: HTMLInputElement | null }> = new RippleObject({
					node: null,
				});
				<div>
					<input
						type="text"
						ref={bindNode(() => inputRef.node, (v: HTMLInputElement | null) => (inputRef.node = v))}
					/>
					<button
						onClick={() => {
							if (inputRef.node) {
								inputRef.node.value = 'Set by ref';
							}
						}}
					>
						{'Set Value'}
					</button>
				</div>
			</>;
		}

		render(App);
		flushSync();

		const input = container.querySelector('input') as HTMLInputElement;
		const button = container.querySelector('button') as HTMLButtonElement;

		expect(input.value).toBe('');

		button.click();
		flushSync();

		expect(input.value).toBe('Set by ref');
	});
});

describe('bindFiles', () => {
	it('should bind files from file input', () => {
		const logs: FileList[] = [];

		function App() {
			return <>
				const files = track(null);
				effect(() => {
					files.value;
					if (files.value) logs.push(files.value);
				});
				<input type="file" multiple ref={bindFiles(files)} />
			</>;
		}

		render(App);
		flushSync();

		const input = container.querySelector('input') as HTMLInputElement;

		// Create mock FileList using DataTransfer
		const dt = new DataTransfer();
		const file1 = new File(['content1'], 'file1.txt', { type: 'text/plain' });
		const file2 = new File(['content2'], 'file2.txt', { type: 'text/plain' });
		dt.items.add(file1);
		dt.items.add(file2);

		// Simulate file selection
		Object.defineProperty(input, 'files', {
			value: dt.files,
			writable: true,
		});
		input.dispatchEvent(new Event('change', { bubbles: true }));
		flushSync();

		expect(logs.length).toBeGreaterThan(0);
		const lastFiles = logs[logs.length - 1];
		expect(lastFiles.length).toBe(2);
		expect(lastFiles[0].name).toBe('file1.txt');
		expect(lastFiles[1].name).toBe('file2.txt');
	});

	it('should bind files from file input with a getter and setter', () => {
		const logs: FileList[] = [];

		function App() {
			return <>
				const files: RippleObject<{ items: FileList | null }> = new RippleObject({ items: null });
				effect(() => {
					files.items;
					if (files.items) logs.push(files.items);
				});
				<input
					type="file"
					multiple
					ref={bindFiles(() => files.items, (v: FileList | null) => (files.items = v))}
				/>
			</>;
		}

		render(App);
		flushSync();

		const input = container.querySelector('input') as HTMLInputElement;

		// Create mock FileList using DataTransfer
		const dt = new DataTransfer();
		const file1 = new File(['content1'], 'file1.txt', { type: 'text/plain' });
		const file2 = new File(['content2'], 'file2.txt', { type: 'text/plain' });
		dt.items.add(file1);
		dt.items.add(file2);

		// Simulate file selection
		Object.defineProperty(input, 'files', {
			value: dt.files,
			writable: true,
		});
		input.dispatchEvent(new Event('change', { bubbles: true }));
		flushSync();

		expect(logs.length).toBeGreaterThan(0);
		const lastFiles = logs[logs.length - 1];
		expect(lastFiles.length).toBe(2);
		expect(lastFiles[0].name).toBe('file1.txt');
		expect(lastFiles[1].name).toBe('file2.txt');
	});

	it('should update tracked value when files are selected', () => {
		let capturedFiles: FileList | null = null;

		function App() {
			return <>
				const files = track<FileList | null>(null);
				effect(() => {
					capturedFiles = files.value;
				});
				<input type="file" ref={bindFiles(files)} />
			</>;
		}

		render(App);
		flushSync();

		const input = container.querySelector('input') as HTMLInputElement;

		// Create mock file
		const dt = new DataTransfer();
		const file = new File(['test content'], 'test.txt', { type: 'text/plain' });
		dt.items.add(file);

		Object.defineProperty(input, 'files', {
			value: dt.files,
			writable: true,
		});
		input.dispatchEvent(new Event('change', { bubbles: true }));
		flushSync();

		expect(capturedFiles).not.toBeNull();
		expect(capturedFiles!.length).toBe(1);
		expect(capturedFiles![0].name).toBe('test.txt');
	});

	it('should update tracked value when files are selected with a getter and setter', () => {
		let capturedFiles: FileList | null = null;

		function App() {
			return <>
				const files: RippleObject<{ items: FileList | null }> = new RippleObject({ items: null });
				effect(() => {
					capturedFiles = files.items;
				});
				<input
					type="file"
					ref={bindFiles(() => files.items, (v: FileList | null) => (files.items = v))}
				/>
			</>;
		}

		render(App);
		flushSync();

		const input = container.querySelector('input') as HTMLInputElement;

		// Create mock file
		const dt = new DataTransfer();
		const file = new File(['test content'], 'test.txt', { type: 'text/plain' });
		dt.items.add(file);

		Object.defineProperty(input, 'files', {
			value: dt.files,
			writable: true,
		});
		input.dispatchEvent(new Event('change', { bubbles: true }));
		flushSync();

		expect(capturedFiles).not.toBeNull();
		expect(capturedFiles!.length).toBe(1);
		expect(capturedFiles![0].name).toBe('test.txt');
	});

	it('should allow clearing files via input.files', () => {
		let capturedFiles: FileList | null = null;

		function App() {
			return <>
				const files = track<FileList | null>(null);
				const input = track<HTMLInputElement | null>(null);
				effect(() => {
					capturedFiles = files.value;
				});
				<div>
					<input type="file" ref={bindFiles<FileList>(files)} ref={bindNode(input)} />
					<button
						onClick={() => {
							if (input.value) {
								input.value.files = new DataTransfer().files;
								input.value.dispatchEvent(new Event('change', { bubbles: true }));
							}
						}}
					>
						{'Clear'}
					</button>
				</div>
			</>;
		}

		render(App);
		flushSync();

		const input = container.querySelector('input') as HTMLInputElement;
		const button = container.querySelector('button') as HTMLButtonElement;

		// Add a file first
		const dt = new DataTransfer();
		const file = new File(['content'], 'file.txt', { type: 'text/plain' });
		dt.items.add(file);

		Object.defineProperty(input, 'files', {
			value: dt.files,
			writable: true,
		});
		input.dispatchEvent(new Event('change', { bubbles: true }));
		flushSync();

		expect(capturedFiles!.length).toBe(1);

		// Clear via button
		button.click();
		flushSync();

		expect(capturedFiles!.length).toBe(0);
	});

	it('should allow clearing files via input.files with a getter and setter', () => {
		let capturedFiles: FileList | null = null;

		function App() {
			return <>
				const files: RippleObject<{ items: FileList | null }> = new RippleObject({ items: null });
				const input = track<HTMLInputElement | null>(null);
				effect(() => {
					capturedFiles = files.items;
				});
				<div>
					<input
						type="file"
						ref={bindFiles(() => files.items, (v: FileList | null) => (files.items = v))}
						ref={bindNode(input)}
					/>
					<button
						onClick={() => {
							if (input.value) {
								input.value.files = new DataTransfer().files;
								input.value.dispatchEvent(new Event('change', { bubbles: true }));
							}
						}}
					>
						{'Clear'}
					</button>
				</div>
			</>;
		}

		render(App);
		flushSync();

		const input = container.querySelector('input') as HTMLInputElement;
		const button = container.querySelector('button') as HTMLButtonElement;

		// Add a file first
		const dt = new DataTransfer();
		const file = new File(['content'], 'file.txt', { type: 'text/plain' });
		dt.items.add(file);

		Object.defineProperty(input, 'files', {
			value: dt.files,
			writable: true,
		});
		input.dispatchEvent(new Event('change', { bubbles: true }));
		flushSync();

		expect(capturedFiles!.length).toBe(1);

		// Clear via button
		button.click();
		flushSync();

		expect(capturedFiles!.length).toBe(0);
	});

	it('should handle multiple file selections', () => {
		const fileCounts: number[] = [];

		function App() {
			return <>
				const files = track<FileList | null>(null);
				effect(() => {
					files.value;
					if (files.value) {
						fileCounts.push(files.value.length);
					}
				});
				<input type="file" multiple ref={bindFiles(files)} />
			</>;
		}

		render(App);
		flushSync();

		const input = container.querySelector('input') as HTMLInputElement;

		// First selection: 2 files
		const dt1 = new DataTransfer();
		dt1.items.add(new File(['a'], 'a.txt'));
		dt1.items.add(new File(['b'], 'b.txt'));

		Object.defineProperty(input, 'files', {
			value: dt1.files,
			writable: true,
		});
		input.dispatchEvent(new Event('change', { bubbles: true }));
		flushSync();

		// Second selection: 3 files
		const dt2 = new DataTransfer();
		dt2.items.add(new File(['x'], 'x.txt'));
		dt2.items.add(new File(['y'], 'y.txt'));
		dt2.items.add(new File(['z'], 'z.txt'));

		Object.defineProperty(input, 'files', {
			value: dt2.files,
			writable: true,
		});
		input.dispatchEvent(new Event('change', { bubbles: true }));
		flushSync();

		expect(fileCounts).toEqual([2, 3]);
	});

	it('should handle multiple file selections with a getter and setter', () => {
		const fileCounts: number[] = [];

		function App() {
			return <>
				const files: RippleObject<{ items: FileList | null }> = new RippleObject({ items: null });
				effect(() => {
					files.items;
					if (files.items) {
						fileCounts.push(files.items.length);
					}
				});
				<input
					type="file"
					multiple
					ref={bindFiles(() => files.items, (v: FileList | null) => (files.items = v))}
				/>
			</>;
		}

		render(App);
		flushSync();

		const input = container.querySelector('input') as HTMLInputElement;

		// First selection: 2 files
		const dt1 = new DataTransfer();
		dt1.items.add(new File(['a'], 'a.txt'));
		dt1.items.add(new File(['b'], 'b.txt'));

		Object.defineProperty(input, 'files', {
			value: dt1.files,
			writable: true,
		});
		input.dispatchEvent(new Event('change', { bubbles: true }));
		flushSync();

		// Second selection: 3 files
		const dt2 = new DataTransfer();
		dt2.items.add(new File(['x'], 'x.txt'));
		dt2.items.add(new File(['y'], 'y.txt'));
		dt2.items.add(new File(['z'], 'z.txt'));

		Object.defineProperty(input, 'files', {
			value: dt2.files,
			writable: true,
		});
		input.dispatchEvent(new Event('change', { bubbles: true }));
		flushSync();

		expect(fileCounts).toEqual([2, 3]);
	});

	it('should handle file input without multiple attribute', () => {
		let capturedFile: File | null = null;

		function App() {
			return <>
				const files = track<FileList | null>(null);
				effect(() => {
					files.value;
					if (files.value && files.value.length > 0) {
						capturedFile = files.value[0];
					}
				});
				<input type="file" ref={bindFiles(files)} />
			</>;
		}

		render(App);
		flushSync();

		const input = container.querySelector('input') as HTMLInputElement;

		const dt = new DataTransfer();
		const file = new File(['single file content'], 'single.pdf', { type: 'application/pdf' });
		dt.items.add(file);

		Object.defineProperty(input, 'files', {
			value: dt.files,
			writable: true,
		});
		input.dispatchEvent(new Event('change', { bubbles: true }));
		flushSync();

		expect(capturedFile).not.toBeNull();
		expect(capturedFile!.name).toBe('single.pdf');
		expect(capturedFile!.type).toBe('application/pdf');
	});

	it('should handle file input without multiple attribute with a getter and setter', () => {
		let capturedFile: File | null = null;

		function App() {
			return <>
				const files: RippleObject<{ items: FileList | null }> = new RippleObject({ items: null });
				effect(() => {
					files.items;
					if (files.items && files.items.length > 0) {
						capturedFile = files.items[0];
					}
				});
				<input
					type="file"
					ref={bindFiles(() => files.items, (v: FileList | null) => (files.items = v))}
				/>
			</>;
		}

		render(App);
		flushSync();

		const input = container.querySelector('input') as HTMLInputElement;

		const dt = new DataTransfer();
		const file = new File(['single file content'], 'single.pdf', { type: 'application/pdf' });
		dt.items.add(file);

		Object.defineProperty(input, 'files', {
			value: dt.files,
			writable: true,
		});
		input.dispatchEvent(new Event('change', { bubbles: true }));
		flushSync();

		expect(capturedFile).not.toBeNull();
		expect(capturedFile!.name).toBe('single.pdf');
		expect(capturedFile!.type).toBe('application/pdf');
	});

	it('should handle empty file selection', () => {
		const logs: (FileList | null)[] = [];

		function App() {
			return <>
				const files = track(null);
				effect(() => {
					logs.push(files.value);
				});
				<input type="file" ref={bindFiles(files)} />
			</>;
		}

		render(App);
		flushSync();

		const input = container.querySelector('input') as HTMLInputElement;

		// Select a file
		const dt = new DataTransfer();
		dt.items.add(new File(['test'], 'test.txt'));

		Object.defineProperty(input, 'files', {
			value: dt.files,
			writable: true,
		});
		input.dispatchEvent(new Event('change', { bubbles: true }));
		flushSync();

		// Clear selection
		Object.defineProperty(input, 'files', {
			value: new DataTransfer().files,
			writable: true,
		});
		input.dispatchEvent(new Event('change', { bubbles: true }));
		flushSync();

		expect(logs.length).toBeGreaterThan(1);
		const lastFiles = logs[logs.length - 1];
		expect(lastFiles?.length).toBe(0);
	});

	it('should handle empty file selection with a getter and setter', () => {
		const logs: (FileList | null)[] = [];

		function App() {
			return <>
				const files: RippleObject<{ items: FileList | null }> = new RippleObject({ items: null });
				effect(() => {
					logs.push(files.items);
				});
				<input
					type="file"
					ref={bindFiles(() => files.items, (v: FileList | null) => (files.items = v))}
				/>
			</>;
		}

		render(App);
		flushSync();

		const input = container.querySelector('input') as HTMLInputElement;

		// Select a file
		const dt = new DataTransfer();
		dt.items.add(new File(['test'], 'test.txt'));

		Object.defineProperty(input, 'files', {
			value: dt.files,
			writable: true,
		});
		input.dispatchEvent(new Event('change', { bubbles: true }));
		flushSync();

		// Clear selection
		Object.defineProperty(input, 'files', {
			value: new DataTransfer().files,
			writable: true,
		});
		input.dispatchEvent(new Event('change', { bubbles: true }));
		flushSync();

		expect(logs.length).toBeGreaterThan(1);
		const lastFiles = logs[logs.length - 1];
		expect(lastFiles?.length).toBe(0);
	});
});

describe('error handling', () => {
	it('should throw error when bindValue receives non-tracked object', () => {
		expect(() => {
			function App() {
				return <><input type="text" ref={bindValue({ value: 'not tracked' })} /></>;
			}
			render(App);
		}).toThrow('bindValue() argument is not a tracked object');
	});

	it('should throw error when bindChecked receives non-tracked object', () => {
		expect(() => {
			function App() {
				return <><input type="checkbox" ref={bindChecked({ value: false })} /></>;
			}
			render(App);
		}).toThrow('bindChecked() argument is not a tracked object');
	});

	it('should throw error when bindIndeterminate receives non-tracked object', () => {
		expect(() => {
			function App() {
				return <><input type="checkbox" ref={bindIndeterminate({ value: false })} /></>;
			}
			render(App);
		}).toThrow('bindIndeterminate() argument is not a tracked object');
	});

	it('should throw error when bindGroup receives non-tracked object', () => {
		expect(() => {
			function App() {
				return <><input type="checkbox" value="a" ref={bindGroup({ value: [] })} /></>;
			}
			render(App);
		}).toThrow('bindGroup() argument is not a tracked object');
	});

	it('should throw error when bindClientWidth receives non-tracked object', () => {
		expect(() => {
			function App() {
				return <><div ref={bindClientWidth({ value: 0 })} /></>;
			}
			render(App);
		}).toThrow('bindClientWidth() argument is not a tracked object');
	});

	it('should throw error when bindClientHeight receives non-tracked object', () => {
		expect(() => {
			function App() {
				return <><div ref={bindClientHeight({ value: 0 })} /></>;
			}
			render(App);
		}).toThrow('bindClientHeight() argument is not a tracked object');
	});

	it('should throw error when bindOffsetWidth receives non-tracked object', () => {
		expect(() => {
			function App() {
				return <><div ref={bindOffsetWidth({ value: 0 })} /></>;
			}
			render(App);
		}).toThrow('bindOffsetWidth() argument is not a tracked object');
	});

	it('should throw error when bindOffsetHeight receives non-tracked object', () => {
		expect(() => {
			function App() {
				return <><div ref={bindOffsetHeight({ value: 0 })} /></>;
			}
			render(App);
		}).toThrow('bindOffsetHeight() argument is not a tracked object');
	});

	it('should throw error when bindContentRect receives non-tracked object', () => {
		expect(() => {
			function App() {
				return <><div ref={bindContentRect({ value: null })} /></>;
			}
			render(App);
		}).toThrow('bindContentRect() argument is not a tracked object');
	});

	it('should throw error when bindContentBoxSize receives non-tracked object', () => {
		expect(() => {
			function App() {
				return <><div ref={bindContentBoxSize({ value: null })} /></>;
			}
			render(App);
		}).toThrow('bindContentBoxSize() argument is not a tracked object');
	});

	it('should throw error when bindBorderBoxSize receives non-tracked object', () => {
		expect(() => {
			function App() {
				return <><div ref={bindBorderBoxSize({ value: null })} /></>;
			}
			render(App);
		}).toThrow('bindBorderBoxSize() argument is not a tracked object');
	});

	it('should throw error when bindDevicePixelContentBoxSize receives non-tracked object', () => {
		expect(() => {
			function App() {
				return <><div ref={bindDevicePixelContentBoxSize({ value: null })} /></>;
			}
			render(App);
		}).toThrow('bindDevicePixelContentBoxSize() argument is not a tracked object');
	});

	it('should throw error when bindInnerHTML receives non-tracked object', () => {
		expect(() => {
			function App() {
				return <><div ref={bindInnerHTML({ value: '' })} /></>;
			}
			render(App);
		}).toThrow('bindInnerHTML() argument is not a tracked object');
	});

	it('should throw error when bindInnerText receives non-tracked object', () => {
		expect(() => {
			function App() {
				return <><div ref={bindInnerText({ value: '' })} /></>;
			}
			render(App);
		}).toThrow('bindInnerText() argument is not a tracked object');
	});

	it('should throw error when bindTextContent receives non-tracked object', () => {
		expect(() => {
			function App() {
				return <><div ref={bindTextContent({ value: '' })} /></>;
			}
			render(App);
		}).toThrow('bindTextContent() argument is not a tracked object');
	});

	it('should throw error when bindNode receives non-tracked object', () => {
		expect(() => {
			function App() {
				return <><div ref={bindNode({ value: null })} /></>;
			}
			render(App);
		}).toThrow('bindNode() argument is not a tracked object');
	});

	it('should throw error when bindFiles receives non-tracked object', () => {
		expect(() => {
			function App() {
				return <><input type="file" ref={bindFiles({ value: null })} /></>;
			}
			render(App);
		}).toThrow('bindFiles() argument is not a tracked object');
	});

	it('should throw error when bindValue receives a getter but not a setter', () => {
		expect(() => {
			function App() {
				return <>
					const value = track('');
					<input type="text" ref={bindValue(() => value.value)} />
				</>;
			}
			render(App);
		}).toThrow(
			'bindValue() second argument must be a set function when first argument is a get function',
		);
	});

	it('should throw error when bindValue receives a getter and setter not a function', () => {
		expect(() => {
			function App() {
				return <>
					const value = track('');
					// @ts-expect-error invalid argument
					<input type="text" ref={bindValue(() => value.value, 5)} />
				</>;
			}
			render(App);
		}).toThrow(
			'bindValue() second argument must be a set function when first argument is a get function',
		);
	});

	it(
		'should throw error when bindValue on select multiple receives a non-array tracked value',
		() => {
			expect(() => {
				function App() {
					return <>
						const value = track('2');
						<select multiple ref={bindValue(value)}>
							<option value="1">{'One'}</option>
							<option value="2">{'Two'}</option>
						</select>
					</>;
				}

				render(App);
				flushSync();
			}).toThrow(
				'Reactive bound value of a `<select multiple>` element should be an array, but it received a non-array value.',
			);
		},
	);

	it(
		'should throw error when bindValue on select multiple receives a non-array getter value',
		() => {
			expect(() => {
				function App() {
					return <>
						const value = track('2');
						<select multiple ref={bindValue(() => value.value, (v) => (value.value = v))}>
							<option value="1">{'One'}</option>
							<option value="2">{'Two'}</option>
						</select>
					</>;
				}

				render(App);
				flushSync();
			}).toThrow(
				'Reactive bound value of a `<select multiple>` element should be an array, but it received a non-array value.',
			);
		},
	);

	it(
		'should throw error when bindChecked receives non-tracked object with a getter but not a setter',
		() => {
			expect(() => {
				function App() {
					return <><input type="checkbox" ref={bindChecked(() => false)} /></>;
				}
				render(App);
			}).toThrow(
				'bindChecked() second argument must be a set function when first argument is a get function',
			);
		},
	);

	it(
		'should throw error when bindChecked receives non-tracked object with a getter and setter not a function',
		() => {
			expect(() => {
				function App() {
					return <><input type="checkbox" ref={bindChecked(() => false, true)} /></>;
				}
				render(App);
			}).toThrow(
				'bindChecked() second argument must be a set function when first argument is a get function',
			);
		},
	);

	it(
		'should throw error when bindIndeterminate receives non-tracked object with a getter and setter not a function',
		() => {
			expect(() => {
				function App() {
					return <><input type="checkbox" ref={bindIndeterminate(() => false, true)} /></>;
				}
				render(App);
			}).toThrow(
				'bindIndeterminate() second argument must be a set function when first argument is a get function',
			);
		},
	);

	it(
		'should throw error when bindGroup receives non-tracked object with a getter and setter not a function',
		() => {
			expect(() => {
				function App() {
					return <><input type="checkbox" value="a" ref={bindGroup(() => [], true)} /></>;
				}
				render(App);
			}).toThrow(
				'bindGroup() second argument must be a set function when first argument is a get function',
			);
		},
	);

	it(
		'should throw error when bindClientWidth receives non-tracked object with a getter and setter not a function',
		() => {
			expect(() => {
				function App() {
					return <><div ref={bindClientWidth(() => 0, true)} /></>;
				}
				render(App);
			}).toThrow(
				'bindClientWidth() second argument must be a set function when first argument is a get function',
			);
		},
	);

	it(
		'should throw error when bindClientHeight receives non-tracked object with a getter and setter not a function',
		() => {
			expect(() => {
				function App() {
					return <><div ref={bindClientHeight(() => 0, true)} /></>;
				}
				render(App);
			}).toThrow(
				'bindClientHeight() second argument must be a set function when first argument is a get function',
			);
		},
	);

	it(
		'should throw error when bindOffsetWidth receives non-tracked object with a getter and setter not a function',
		() => {
			expect(() => {
				function App() {
					return <><div ref={bindOffsetWidth(() => 0, true)} /></>;
				}
				render(App);
			}).toThrow(
				'bindOffsetWidth() second argument must be a set function when first argument is a get function',
			);
		},
	);

	it(
		'should throw error when bindOffsetHeight receives non-tracked object with a getter and setter not a function',
		() => {
			expect(() => {
				function App() {
					return <><div ref={bindOffsetHeight(() => 0, true)} /></>;
				}
				render(App);
			}).toThrow(
				'bindOffsetHeight() second argument must be a set function when first argument is a get function',
			);
		},
	);

	it(
		'should throw error when bindContentRect receives non-tracked object with a getter and setter not a function',
		() => {
			expect(() => {
				function App() {
					return <><div ref={bindContentRect(() => null, true)} /></>;
				}
				render(App);
			}).toThrow(
				'bindContentRect() second argument must be a set function when first argument is a get function',
			);
		},
	);

	it(
		'should throw error when bindContentBoxSize receives non-tracked object with a getter and setter not a function',
		() => {
			expect(() => {
				function App() {
					return <><div ref={bindContentBoxSize(() => null, true)} /></>;
				}
				render(App);
			}).toThrow(
				'bindContentBoxSize() second argument must be a set function when first argument is a get function',
			);
		},
	);

	it(
		'should throw error when bindBorderBoxSize receives non-tracked object with a getter and setter not a function',
		() => {
			expect(() => {
				function App() {
					return <><div ref={bindBorderBoxSize(() => null, true)} /></>;
				}
				render(App);
			}).toThrow(
				'bindBorderBoxSize() second argument must be a set function when first argument is a get function',
			);
		},
	);

	it(
		'should throw error when bindDevicePixelContentBoxSize receives non-tracked object with a getter and setter not a function',
		() => {
			expect(() => {
				function App() {
					return <><div ref={bindDevicePixelContentBoxSize(() => null, true)} /></>;
				}
				render(App);
			}).toThrow(
				'bindDevicePixelContentBoxSize() second argument must be a set function when first argument is a get function',
			);
		},
	);

	it(
		'should throw error when bindInnerHTML receives non-tracked object with a getter and setter not a function',
		() => {
			expect(() => {
				function App() {
					return <><div ref={bindInnerHTML(() => '', true)} /></>;
				}
				render(App);
			}).toThrow(
				'bindInnerHTML() second argument must be a set function when first argument is a get function',
			);
		},
	);

	it(
		'should throw error when bindInnerText receives non-tracked object with a getter and setter not a function',
		() => {
			expect(() => {
				function App() {
					return <><div ref={bindInnerText(() => '', true)} /></>;
				}
				render(App);
			}).toThrow(
				'bindInnerText() second argument must be a set function when first argument is a get function',
			);
		},
	);

	it(
		'should throw error when bindTextContent receives non-tracked object with a getter and setter not a function',
		() => {
			expect(() => {
				function App() {
					return <><div ref={bindTextContent(() => '', true)} /></>;
				}
				render(App);
			}).toThrow(
				'bindTextContent() second argument must be a set function when first argument is a get function',
			);
		},
	);

	it(
		'should throw error when bindNode receives non-tracked object with a getter and setter not a function',
		() => {
			expect(() => {
				function App() {
					return <><div ref={bindNode(() => null, true)} /></>;
				}
				render(App);
			}).toThrow(
				'bindNode() second argument must be a set function when first argument is a get function',
			);
		},
	);

	it(
		'should throw error when bindFiles receives non-tracked object with a getter and setter not a function',
		() => {
			expect(() => {
				function App() {
					return <><input type="file" ref={bindFiles(() => null, true)} /></>;
				}
				render(App);
			}).toThrow(
				'bindFiles() second argument must be a set function when first argument is a get function',
			);
		},
	);
});
