import { Component, T$, Ui, elemTo$, to$, toElem, hook, render } from '../src/index' import * as assert from 'assert' afterEach(function () { document.body.innerHTML = '' }) describe('hook', () => { describe('if object does not contain method', () => { it('should not do anything', () => { const fn = () => {} hook(undefined, '', fn) hook(null, '', fn) hook({ fn }, 'f', fn) assert.ok(true) }) }) describe('to setter', () => { class HasSetter { _val: number constructor (val: number) { this._val = val } get val (): number { return this._val } set val (value: number) { this._val = value } } it('should invoke callback whenever setter gets invoked', () => { const data: string[] = [] const obj = new HasSetter(0) obj.val = 1 hook(obj, 'val', () => data.push('foo')) assert.deepStrictEqual(data, []) obj.val = 2 assert.strictEqual(obj.val, 2) assert.deepStrictEqual(data, ['foo']) }) it('getter should still work', () => { const obj = new HasSetter(0) hook(obj, 'val', () => {}) obj.val = 3 assert.strictEqual(obj.val, 3) obj.val = 4 assert.strictEqual(obj.val, 4) }) }) describe('if property is a plain variable', () => { it('should override the variable by a property', () => { let sum = 0 const obj = { foo: 'foo' } assert.ok(Object.getOwnPropertyNames(obj).includes('foo')) hook(obj, 'foo', () => { sum += 1 }) assert.ok(!Object.getOwnPropertyNames(obj).includes('foo')) assert.strictEqual(sum, 0) obj.foo = 'bar' assert.strictEqual(obj.foo, 'bar') assert.strictEqual(sum, 1) obj.foo = 'baz' assert.strictEqual(obj.foo, 'baz') assert.strictEqual(sum, 2) }) }) describe('if object contains method', () => { it('should invoke callback whenever method gets called', () => { const data: string[] = [] const obj = { foo: () => data.push('foo') } obj.foo() assert.deepStrictEqual(data, ['foo']) hook(obj, 'foo', () => data.push('bar')) obj.foo() assert.deepStrictEqual(data, ['foo', 'foo', 'bar']) }) it('should not change the name of the method', () => { const obj = { foo: () => {} } assert.strictEqual(obj.foo.name, 'foo') hook(obj, 'foo', function bar () {}) assert.strictEqual(obj.foo.name, 'foo') }) it('replacement method should still return the original value', () => { const obj = { foo: () => 'foo' } assert.strictEqual(obj.foo(), 'foo') hook(obj, 'foo', () => 'bar') assert.strictEqual(obj.foo(), 'foo') }) }) }) describe('toElem', () => { describe('with incorrect number of top-level elements', () => { it('should throw error', () => { assert.throws(() => toElem('')) assert.throws(() => toElem('')) }) }) describe('with single top-level element', () => { const elem = toElem( '
' + 'Foo' + 'Bar' + 'Baz' + '
' ) assert.ok(elem) assert.strictEqual(elem.tagName, 'DIV') assert.strictEqual(elem.textContent, 'FooBarBaz') assert.strictEqual(elem.childElementCount, 3) assert.ok(elem.querySelector('.foo')) assert.ok(elem.querySelector('.bar')) assert.ok(elem.querySelector('.baz')) }) }) describe('to$ result', () => { const $ = to$( '
' + 'Foo' + 'Bar' + 'Baz' + '
' ) describe('with no input selector', () => { it('should return root element', () => { assert.strictEqual($().tagName, 'DIV') assert.strictEqual($().className, 'test') }) }) describe('with input selector', () => { it('should query selector', () => { assert.strictEqual($('.foo').textContent, 'Foo') assert.strictEqual($('.bar').textContent, 'Bar') assert.strictEqual($('.baz').textContent, 'Baz') }) }) }) describe('render', () => { describe('with element input', () => { const elem = toElem('') it('should return input element', () => { assert.strictEqual(elem, render(elem, document.body)) }) it('should insert element into DOM', () => { render(elem, document.body) assert.strictEqual(document.body.firstChild, elem) }) }) describe('component with ui', () => { class MyComponent implements Component { ui: Ui constructor () { this.ui = new Ui(() => {}) } render () { return toElem('
') } } it('should insert element into DOM', () => { const elem = render(new MyComponent(), document.body) assert.strictEqual(elem.tagName, 'HR') assert.strictEqual(elem, document.body.firstChild) }) }) describe('component without ui', () => { class MyComponent implements Component { render () { return toElem('
') } } it('should insert element into DOM', () => { const elem = render(new MyComponent(), document.body) assert.strictEqual(elem.tagName, 'HR') assert.strictEqual(elem, document.body.firstChild) }) }) }) describe('Ui usage example', () => { describe('Countdown (version 1)', () => { class Countdown { count: number constructor () { this.count = 10 } countdown () { this.count = this.count > 0 ? this.count - 1 : 0 } update ($: T$) { $().textContent = this.count > 0 ? this.count : 'boom!' } render () { const elem = toElem(``) as HTMLButtonElement elem.onclick = () => { this.countdown() this.update(elemTo$(elem)) } return elem } } it('should count down to 0 and BOOM!', () => { const elem = new Countdown().render() document.body.appendChild(elem) for (let i = 10; i > 0; i--) { assert.strictEqual(document.body.textContent, String(i)) elem.click() } assert.strictEqual(document.body.textContent, 'boom!') elem.click() assert.strictEqual(document.body.textContent, 'boom!') }) }) describe('Countdown (version 2)', () => { class Countdown { ui: Ui count: number constructor () { this.ui = new Ui($ => this.update($)) this.count = 10 } countdown () { this.count = this.count > 0 ? this.count - 1 : 0 } update ($: T$) { $().textContent = this.count > 0 ? this.count : 'boom!' } render () { const elem = toElem(``) as HTMLButtonElement elem.onclick = this.ui.update(() => this.countdown()) return elem } } it('should count down to 0 and BOOM!', () => { const elem = render(new Countdown(), document.body) as HTMLButtonElement for (let i = 10; i > 0; i--) { assert.strictEqual(document.body.textContent, String(i)) elem.click() } assert.strictEqual(document.body.textContent, 'boom!') elem.click() assert.strictEqual(document.body.textContent, 'boom!') }) }) describe('Countdown (version 3)', () => { class Countdown { ui: Ui count: number constructor () { this.count = 10 this.ui = new Ui($ => this.update($)) this.ui.watch(this, 'countdown') } countdown () { this.count = this.count > 0 ? this.count - 1 : 0 } update ($: T$) { $().textContent = this.count > 0 ? this.count : 'boom!' } render () { const elem = toElem(``) as HTMLButtonElement elem.onclick = () => this.countdown() return elem } } it('should count down to 0 and BOOM!', () => { const elem = render(new Countdown(), document.body) as HTMLButtonElement for (let i = 10; i > 0; i--) { assert.strictEqual(document.body.textContent, String(i)) elem.click() } assert.strictEqual(document.body.textContent, 'boom!') elem.click() assert.strictEqual(document.body.textContent, 'boom!') }) }) })