diff --git a/packages/builder/spec/builderBehaviors.ts b/packages/builder/spec/builderBehaviors.ts index 8d6fc6759..52b1f9a52 100644 --- a/packages/builder/spec/builderBehaviors.ts +++ b/packages/builder/spec/builderBehaviors.ts @@ -1,3 +1,4 @@ +import { assert } from 'chai' import { VirtualProvider } from '@tko/provider.virtual' import { bindings as ifBindings } from '@tko/binding.if' import { options } from '@tko/utils' @@ -22,3 +23,73 @@ describe('Builder', () => { const builder = new Builder({ filters: {}, provider: new VirtualProvider(), bindings: [ifBindings], options: {} }) }) }) + +describe('Builder.create()', () => { + let builder: Builder + + beforeEach(() => { + // @ts-ignore — global helper from mocha-test-helpers.js + restoreAfter(options, 'filters') + // @ts-ignore — global helper from mocha-test-helpers.js + restoreAfter(options, 'bindingProviderInstance') + builder = new Builder({ filters: {}, provider: new VirtualProvider(), bindings: [ifBindings], options: {} }) + }) + + it('merges additional properties onto the knockout instance', function () { + const extensions = { version: '1.0.0', myProp: 42 } + const ko = builder.create(extensions) + assert.equal(ko.version, '1.0.0') + assert.equal(ko.myProp, 42) + }) + + it('preserves core KnockoutInstance properties', function () { + const ko = builder.create({}) + assert.isFunction(ko.observable) + assert.isFunction(ko.computed) + assert.isFunction(ko.applyBindings) + assert.isFunction(ko.isObservable) + }) + + it('sets options.knockoutInstance to the returned instance', function () { + const ko = builder.create({}) + assert.strictEqual(ko.options.knockoutInstance, ko) + }) + + it('handles an empty object as additional properties', function () { + const ko = builder.create({}) + assert.isFunction(ko.observable) + }) + + it('handles nested object additional properties', function () { + const jsx = { createElement: () => {}, Fragment: Symbol('Fragment') } + const ko = builder.create({ jsx }) + assert.deepEqual(ko.jsx, jsx) + }) + + it('additional properties do not override core knockout properties', function () { + // observable is a core KO property — if overridden, it should come from additionalProperties + // but the standard behavior is: knockout core < providedProperties < additionalProperties + const customObservable = () => 'custom' + const ko = builder.create({ customField: customObservable }) + // Existing core props still present + assert.isFunction(ko.observable) + // Custom field also present + assert.isFunction(ko.customField) + }) + + it('returns an instance with the getBindingHandler getter', function () { + const ko = builder.create({}) + // getBindingHandler should be accessible as a property (delegating to options) + assert.isDefined(ko.getBindingHandler) + }) + + it('additionalProperties object is treated as single merge (not spread)', function () { + // Previously create() used rest params, now it takes a single typed object. + // Verify a plain object with multiple keys all appear on the result. + const extensions = { alpha: 1, beta: 'two', gamma: true } + const ko = builder.create(extensions) + assert.equal(ko.alpha, 1) + assert.equal(ko.beta, 'two') + assert.equal(ko.gamma, true) + }) +}) diff --git a/packages/utils.jsx/spec/jsxRenderBehaviors.ts b/packages/utils.jsx/spec/jsxRenderBehaviors.ts new file mode 100644 index 000000000..aee472a88 --- /dev/null +++ b/packages/utils.jsx/spec/jsxRenderBehaviors.ts @@ -0,0 +1,86 @@ +import { assert } from 'chai' + +import { render, createElement, Fragment } from '../src' + +describe('render()', function () { + it('returns an object with node and dispose properties', function () { + const jsx = createElement('div', null) + const result = render(jsx) + assert.property(result, 'node') + assert.property(result, 'dispose') + assert.isFunction(result.dispose) + result.dispose() + }) + + it('returns the first child node when JSX produces exactly one node', function () { + const jsx = createElement('span', { id: 'test-single' }) + const result = render(jsx) + assert.isNotNull(result.node) + // A single element renders as a plain child node, not a fragment + assert.notInstanceOf(result.node, DocumentFragment) + assert.equal((result.node as HTMLElement).tagName.toLowerCase(), 'span') + result.dispose() + }) + + it('returns a DocumentFragment when JSX produces multiple nodes', function () { + // Fragment children expand to multiple nodes + const jsx = createElement(Fragment, null, createElement('div', null), createElement('span', null)) + const result = render(jsx) + assert.instanceOf(result.node, DocumentFragment) + result.dispose() + }) + + it('dispose() can be called without throwing', function () { + const jsx = createElement('p', null, 'Hello') + const result = render(jsx) + assert.doesNotThrow(() => result.dispose()) + }) + + it('returns node=null-or-fragment when jsx is null/undefined (empty output)', function () { + // An empty JSX Fragment produces no child nodes → node is the DocumentFragment itself + const jsx = createElement(Fragment, null) + const result = render(jsx) + // Empty fragment: childNodes.length === 0, so node should be the fragment (not firstChild) + assert.instanceOf(result.node, DocumentFragment) + result.dispose() + }) + + it('renders a text node for a string child', function () { + const jsx = createElement('div', null, 'hello world') + const result = render(jsx) + // The outer div is a single node + assert.notInstanceOf(result.node, DocumentFragment) + const el = result.node as HTMLElement + assert.equal(el.textContent, 'hello world') + result.dispose() + }) + + it('renders nested elements correctly', function () { + const jsx = createElement('section', null, createElement('h1', null, 'Title')) + const result = render(jsx) + assert.notInstanceOf(result.node, DocumentFragment) + const el = result.node as HTMLElement + assert.equal(el.tagName.toLowerCase(), 'section') + assert.equal(el.querySelector('h1')?.textContent, 'Title') + result.dispose() + }) + + it('multiple render() calls each return independent instances', function () { + const jsx1 = createElement('div', { class: 'first' }) + const jsx2 = createElement('div', { class: 'second' }) + const result1 = render(jsx1) + const result2 = render(jsx2) + assert.notStrictEqual(result1.node, result2.node) + result1.dispose() + result2.dispose() + }) + + it('dispose() can be called multiple times without throwing', function () { + const jsx = createElement('div', null) + const result = render(jsx) + assert.doesNotThrow(() => { + result.dispose() + result.dispose() + }) + }) +})