diff --git a/test/index.test.ts b/test/index.test.ts index 7e2fe36..741bbda 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1,4 +1,3 @@ -// import jest globals import { describe, expect, it } from '@jest/globals' import { NodeEditor } from '../src/editor' @@ -52,5 +51,43 @@ describe('NodeEditor', () => { await expect(() => editor.addConnection(connectionData)).rejects.toThrowError() }) + + it('removeNode should remove a node', async () => { + const editor = new NodeEditor() + const nodeData = { id: '1', label: 'Node 1' } + + await editor.addNode(nodeData) + await editor.removeNode('1') + const nodes = editor.getNodes() + + expect(nodes).toHaveLength(0) + }) + + it('removeConnection should remove a connection', async () => { + const editor = new NodeEditor() + const connectionData = { id: '1', source: '1', target: '2' } + + await editor.addNode({ id: '1' }) + await editor.addNode({ id: '2' }) + await editor.addConnection(connectionData) + await editor.removeConnection('1') + const connections = editor.getConnections() + + expect(connections).toHaveLength(0) + }) + + it('should clear all nodes and connections', async () => { + const editor = new NodeEditor() + + await editor.addNode({ id: '1' }) + await editor.addNode({ id: '2' }) + await editor.addConnection({ id: '1', source: '1', target: '2' }) + await editor.clear() + const nodes = editor.getNodes() + const connections = editor.getConnections() + + expect(nodes).toHaveLength(0) + expect(connections).toHaveLength(0) + }) }) diff --git a/test/mocks/crypto.ts b/test/mocks/crypto.ts new file mode 100644 index 0000000..e248dbb --- /dev/null +++ b/test/mocks/crypto.ts @@ -0,0 +1,24 @@ +import { jest } from '@jest/globals' +import { Buffer } from 'buffer' + +export function mockCrypto(object: Record) { + // eslint-disable-next-line no-undef + globalThis.crypto = object as unknown as Crypto +} + +export function mockCryptoFromArray(array: Uint8Array) { + mockCrypto({ + getRandomValues: jest.fn().mockReturnValue(array) + }) +} + +export function mockCryptoFromBuffer(buffer: Buffer) { + mockCrypto({ + randomBytes: jest.fn().mockReturnValue(buffer) + }) +} + +export function resetCrypto() { + // eslint-disable-next-line no-undef, no-undefined + globalThis.crypto = undefined as unknown as Crypto +} diff --git a/test/presets/classic.test.ts b/test/presets/classic.test.ts new file mode 100644 index 0000000..7f29b20 --- /dev/null +++ b/test/presets/classic.test.ts @@ -0,0 +1,173 @@ +import { describe, expect, it } from '@jest/globals' + +import { mockCryptoFromArray, resetCrypto } from '../mocks/crypto' + +describe('ClassicPreset', () => { + // eslint-disable-next-line init-declarations + let preset!: typeof import('../../src/presets/classic') + + beforeEach(async () => { + mockCryptoFromArray(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8])) + preset = await import('../../src/presets/classic') + }) + + afterEach(() => { + resetCrypto() + }) + + describe('Node', () => { + it('is instantiable', () => { + expect(new preset.Node('A')).toBeInstanceOf(preset.Node) + }) + + it('should have an id', () => { + const node = new preset.Node('A') + + expect(node.id).toBeDefined() + }) + + it('should have a label', () => { + const node = new preset.Node('A') + + expect(node.label).toBe('A') + }) + + it('adds Input', () => { + const node = new preset.Node('A') + const input = new preset.Input(new preset.Socket('a')) + + node.addInput('a', input) + + expect(node.hasInput('a')).toBeTruthy() + expect(node.inputs['a']).toBe(input) + }) + + it('throws error if Input already exists', () => { + const node = new preset.Node('A') + + node.addInput('a', new preset.Input(new preset.Socket('a'))) + + expect(() => node.addInput('a', new preset.Input(new preset.Socket('a')))).toThrow() + }) + + it('removes Input', () => { + const node = new preset.Node('A') + + node.addInput('a', new preset.Input(new preset.Socket('a'))) + node.removeInput('a') + + expect(node.hasInput('a')).toBeFalsy() + }) + + it('adds Output', () => { + const node = new preset.Node('A') + const output = new preset.Output(new preset.Socket('a')) + + node.addOutput('a', output) + + expect(node.hasOutput('a')).toBeTruthy() + expect(node.outputs['a']).toBe(output) + }) + + it('throws error if Output already exists', () => { + const node = new preset.Node('A') + + node.addOutput('a', new preset.Output(new preset.Socket('a'))) + + expect(() => node.addOutput('a', new preset.Output(new preset.Socket('a')))).toThrow() + }) + + it('removes Output', () => { + const node = new preset.Node('A') + + node.addOutput('a', new preset.Output(new preset.Socket('a'))) + node.removeOutput('a') + + expect(node.hasOutput('a')).toBeFalsy() + }) + }) + + describe('Connection', () => { + it('Connection throws error if input not found', () => { + const a = new preset.Node('A') + const b = new preset.Node('B') + + a.addOutput('a', new preset.Output(new preset.Socket('a'))) + + expect(() => new preset.Connection(a, 'a', b, 'b')).toThrow() + }) + + it('Connection throws error if output not found', () => { + const a = new preset.Node('A') + const b = new preset.Node('B') + + b.addInput('b', new preset.Input(new preset.Socket('b'))) + + expect(() => new preset.Connection(a, 'a', b, 'b')).toThrow() + }) + + it('Connection is instantiable', () => { + const a = new preset.Node('A') + const b = new preset.Node('B') + const output = new preset.Output(new preset.Socket('b')) + const input = new preset.Input(new preset.Socket('a')) + + a.addOutput('a', output) + b.addInput('b', input) + + expect(new preset.Connection(a, 'a', b, 'b')).toBeInstanceOf(preset.Connection) + }) + }) + + describe('Control', () => { + it('adds Control to Node', () => { + const node = new preset.Node('A') + + node.addControl('ctrl', new preset.Control()) + + expect(node.hasControl('ctrl')).toBeTruthy() + }) + + it('throws error if Control already exists', () => { + const node = new preset.Node('A') + + node.addControl('ctrl', new preset.Control()) + + expect(() => node.addControl('ctrl', new preset.Control())).toThrow() + }) + + it('removes Control from Node', () => { + const node = new preset.Node('A') + + node.addControl('ctrl', new preset.Control()) + node.removeControl('ctrl') + + expect(node.hasControl('ctrl')).toBeFalsy() + }) + + it('adds Control to Input', () => { + const input = new preset.Input(new preset.Socket('a')) + + input.addControl(new preset.Control()) + + expect(input.control).toBeTruthy() + }) + + it('throws error if Control in Input already exists', () => { + const input = new preset.Input(new preset.Socket('a')) + + input.addControl(new preset.Control()) + + expect(() => input.addControl(new preset.Control())).toThrow() + }) + + it('removes Control from Input', () => { + const input = new preset.Input(new preset.Socket('a')) + + input.addControl(new preset.Control()) + input.removeControl() + + expect(input.control).toBeFalsy() + }) + }) +}) diff --git a/test/scope.test.ts b/test/scope.test.ts new file mode 100644 index 0000000..33d860d --- /dev/null +++ b/test/scope.test.ts @@ -0,0 +1,162 @@ +import { describe, expect, it, jest } from '@jest/globals' + +import { Scope } from '../src/scope' + +type Parent = { parent: string } +type Child = { child: number } + +describe('Scope', () => { + it('should create a new Scope instance', () => { + const scope = new Scope('test') + + expect(scope).toBeInstanceOf(Scope) + }) + + it('doesnt have a parent by default', () => { + const scope = new Scope('test') + + expect(scope.hasParent()).toBeFalsy() + }) + + describe('parent-child', () => { + it('should set a parent scope', () => { + const parent = new Scope('parent') + const child = new Scope('child') + + child.setParent(parent) + + expect(child.parentScope()).toBe(parent) + }) + + it('should use a nested scope', () => { + const parent = new Scope('parent') + const child = new Scope('child') + + parent.use(child) + expect(child.hasParent()).toBeTruthy() + expect(child.parentScope()).toBe(parent) + }) + + it('should throw an error when using a non-Scope instance', () => { + const parent = new Scope('parent') + const child = { signal: { emit: jest.fn() } } + + expect(() => parent.use(child as any)).toThrowError('cannot use non-Scope instance') + }) + + it('should throw an error when trying to access a parent without one', () => { + const scope = new Scope('test') + + expect(() => scope.parentScope()).toThrowError('cannot find parent') + }) + + it('should throw an error when trying to access a parent with the wrong type', () => { + class WrongScope extends Scope { } + const parent = new Scope('parent') + const child = new Scope('child') + + parent.use(child) + + expect(() => child.parentScope(WrongScope)).toThrowError('actual parent is not instance of type') + }) + }) + + describe('addPipe', () => { + it('should emit a signal', async () => { + const scope = new Scope('test') + const pipe = jest.fn<() => Parent>() + + scope.addPipe(pipe) + await scope.emit({ parent: 'test' }) + + expect(pipe).toHaveBeenCalledWith({ parent: 'test' }) + }) + + it('should return a promise from emit', async () => { + const scope = new Scope('test') + const signal = jest.fn<() => Parent>() + + scope.addPipe(signal) + const result = scope.emit({ parent: 'test' }) + + expect(result).toBeInstanceOf(Promise) + }) + + it('should return the result of the signal', async () => { + const scope = new Scope('test') + const signal = jest.fn<() => Parent>().mockReturnValue({ parent: 'test-result' }) + + scope.addPipe(signal) + const result = await scope.emit({ parent: 'test' }) + + expect(result).toEqual({ parent: 'test-result' }) + }) + + it('should return undefined if the signal returns undefined', async () => { + const scope = new Scope('test') + // eslint-disable-next-line no-undefined + const signal = jest.fn().mockReturnValue(undefined) + + scope.addPipe(signal) + const result = await scope.emit('test') + + expect(result).toBeUndefined() + }) + + it('should return the result of the signal with a parent', async () => { + const parent = new Scope('parent') + const child = new Scope('child') + const signal = jest.fn<() => Parent>().mockReturnValue({ parent: 'test-parent' }) + + parent.addPipe(signal) + parent.use(child) + const result = await child.emit({ child: 1 }) + + expect(result).toEqual({ child: 1 }) + }) + + it('should return the result of the signal with a parent and child', async () => { + const parent = new Scope('parent') + const child = new Scope('child') + const signal = jest.fn<() => Child>().mockReturnValue({ child: 1 }) + + parent.use(child) + child.addPipe(signal) + const result = await child.emit({ child: 2 }) + + expect(result).toEqual({ child: 1 }) + }) + + it('should transfer signals from parent to child', async () => { + const parent = new Scope('parent') + const child = new Scope('child') + const parentSignal = jest.fn<() => Parent>().mockReturnValue({ parent: 'test-parent' }) + const childSignal = jest.fn<() => Child>() + + parent.addPipe(parentSignal) + child.addPipe(childSignal) + parent.use(child) + + await parent.emit({ parent: 'test-parent' }) + + expect(childSignal).toHaveBeenCalledWith({ parent: 'test-parent' }) + }) + + it('should prevent execution of child signal if parent signal returns undefined', async () => { + const parent = new Scope('parent') + const child = new Scope('child') + // eslint-disable-next-line no-undefined + const parentSignal = jest.fn<() => Parent | undefined>().mockReturnValue(undefined) + const childSignal = jest.fn<() => Child>() + + parent.addPipe(parentSignal) + child.addPipe(childSignal) + parent.use(child) + + await parent.emit({ parent: 'test-parent' }) + + expect(childSignal).not.toHaveBeenCalled() + }) + }) +}) + diff --git a/test/utils.test.ts b/test/utils.test.ts new file mode 100644 index 0000000..ae40b9f --- /dev/null +++ b/test/utils.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it, jest } from '@jest/globals' +import { Buffer } from 'buffer' + +import { mockCryptoFromArray, mockCryptoFromBuffer, resetCrypto } from './mocks/crypto' + +describe('getUID', () => { + beforeEach(() => { + jest.resetModules() + }) + + afterEach(() => { + resetCrypto() + }) + + it('should return a unique id based on crypto.getRandomValues', async () => { + mockCryptoFromArray(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8])) + + const { getUID } = await import('../src/utils') + const uid = getUID() + + expect(uid).toHaveLength(16) + }) + + it('should return a unique id based on crypto.randomBytes', async () => { + mockCryptoFromBuffer(Buffer.from([1, 2, 3, 4, 5, 6, 7, 8])) + + const { getUID } = await import('../src/utils') + const uid = getUID() + + expect(uid).toHaveLength(16) + }) +})