import { FormattedNodes } from './formattedText'; import { BaseResolvedNodes, HigherOrderFormattedNodesMap, resolveFormattedText, resolveFormattedTextNodes, } from './resolveFormattedTextNodes'; describe('higherOrderFormattedTextToBaseFormattedText', () => { const mockResolvers: HigherOrderFormattedNodesMap = { 'link-to-matrix-asset': jest.fn((node) => { return Promise.resolve({ type: 'text', value: `Link to asset ${node.matrixAssetId}` }); }), 'link-to-dam-asset': jest.fn((node) => { return Promise.resolve({ type: 'text', value: `Link to asset ${node.damObjectId}` }); }), 'matrix-image': jest.fn((node) => { return Promise.resolve({ type: 'text', value: `Image ${node.matrixAssetId}` }); }), 'dam-image': jest.fn((node) => { return Promise.resolve({ type: 'text', value: `Image ${node.damObjectId}` }); }), }; const testFormattedNodes: FormattedNodes = { tag: 'p', type: 'tag', children: [ { type: 'text', value: 'This is some ' }, { type: 'link-to-matrix-asset', matrixIdentifier: 'abc123', matrixDomain: 'example.com', matrixAssetId: '12345', target: '_blank', children: [{ type: 'text', value: 'linked text' }], }, { type: 'link-to-dam-asset', damObjectId: 'xyz-321', damSystemType: 'bynder', damSystemIdentifier: '321', target: '_blank', children: [{ type: 'text', value: 'linked text' }], }, { type: 'text', value: ' with an image ' }, { type: 'matrix-image', matrixIdentifier: 'abc123', matrixDomain: 'example.com', matrixAssetId: '54321', }, { type: 'dam-image', damObjectId: 'xyz-123', damSystemType: 'bynder', damSystemIdentifier: '123', }, { type: 'text', value: '.' }, ], }; const expectedFormattedNodes: FormattedNodes = { tag: 'p', type: 'tag', children: [ { type: 'text', value: 'This is some ' }, { type: 'text', value: 'Link to asset 12345' }, { type: 'text', value: 'Link to asset xyz-321' }, { type: 'text', value: ' with an image ' }, { type: 'text', value: 'Image 54321' }, { type: 'text', value: 'Image xyz-123' }, { type: 'text', value: '.' }, ], }; it('should replace higher order nodes with their resolved counterparts', async () => { const result = await resolveFormattedText(testFormattedNodes, mockResolvers); expect(result).toEqual(expectedFormattedNodes); }); it('should not process child nodes that are already string', async () => { const testFormattedNodes: FormattedNodes = { tag: 'p', type: 'tag', children: [ { type: 'text', value: 'This is some ' }, { type: 'link-to-matrix-asset', matrixIdentifier: 'abc123', matrixDomain: 'example.com', matrixAssetId: '12345', target: '_blank', children: ['linked text', '

More Text

'] as any, }, { type: 'text', value: '.' }, ], }; await expect( resolveFormattedText(testFormattedNodes, { 'link-to-matrix-asset': async (node) => ({ type: 'tag', tag: 'a', attributes: { href: node.matrixIdentifier, }, children: node.children, }), }), ).resolves.toEqual({ tag: 'p', type: 'tag', children: [ { type: 'text', value: 'This is some ' }, { type: 'tag', tag: 'a', attributes: { href: 'abc123', }, children: ['linked text', '

More Text

'], }, { type: 'text', value: '.' }, ], }); }); it('should handle layout nodes correctly', async () => { const testFormattedNodes: FormattedNodes = { tag: 'p', type: 'tag', children: [ { type: 'text', value: 'This is some ' }, { type: 'layout', layout: { name: 'test-layout', options: {}, }, content: {}, }, { type: 'text', value: ' with layout.' }, ], }; const expectedFormattedNodes: FormattedNodes = { tag: 'p', type: 'tag', children: [ { type: 'text', value: 'This is some ' }, { type: 'layout', layout: { name: 'test-layout', options: {}, }, content: {}, }, { type: 'text', value: ' with layout.' }, ], }; const result = await resolveFormattedText(testFormattedNodes, mockResolvers); expect(result).toEqual(expectedFormattedNodes); }); it('should handle multiple layout nodes in sequence', async () => { const testFormattedNodes: FormattedNodes = { tag: 'div', type: 'tag', children: [ { type: 'layout', layout: { name: 'layout-1', options: {}, }, content: {}, }, { type: 'text', value: 'Some text' }, { type: 'layout', layout: { name: 'layout-2', options: {}, }, content: {}, }, ], }; const expectedFormattedNodes: FormattedNodes = { tag: 'div', type: 'tag', children: [ { type: 'layout', layout: { name: 'layout-1', options: {}, }, content: {}, }, { type: 'text', value: 'Some text' }, { type: 'layout', layout: { name: 'layout-2', options: {}, }, content: {}, }, ], }; const result = await resolveFormattedText(testFormattedNodes, mockResolvers); expect(result).toEqual(expectedFormattedNodes); }); }); const testResolverMap: HigherOrderFormattedNodesMap = { 'link-to-matrix-asset': (node) => { return Promise.resolve({ type: 'text', value: `Link to asset ${node.matrixAssetId}` }); }, 'link-to-dam-asset': (node) => { return Promise.resolve({ type: 'text', value: `Link to asset ${node.damObjectId}` }); }, 'matrix-image': (node) => { return Promise.resolve({ type: 'text', value: `Image ${node.matrixAssetId}` }); }, 'dam-image': jest.fn((node) => { return Promise.resolve({ type: 'text', value: `Image ${node.damObjectId}` }); }), }; describe('resolveFormattedNodes', () => { it('should resolve all FormattedText items in an object', async () => { const testSchema = { type: 'object', properties: { normal: { type: 'string' }, number: { type: 'number' }, formattedText: { type: 'FormattedText' }, }, required: ['normal', 'number', 'formattedText'], }; const testItem = { normal: 'string', number: 312, formattedText: [ { type: 'link-to-matrix-asset', matrixIdentifier: 'abc123', matrixDomain: 'example.com', matrixAssetId: '12345', target: '_blank', children: [{ type: 'text', value: 'linked text' }], }, { type: 'link-to-dam-asset', damSystemType: 'Bynder', damSystemIdentifier: '333', damObjectId: 'xyz-333', target: '_blank', children: [{ type: 'text', value: 'linked text' }], }, { type: 'matrix-image', matrixIdentifier: 'abc123', matrixDomain: 'example.com', matrixAssetId: '54321', }, { type: 'text', value: 'This is some ', }, { type: 'tag', tag: 'p', children: [ { type: 'link-to-matrix-asset', matrixAssetId: '12345' }, { type: 'matrix-image', matrixAssetId: '54321' }, ], }, { type: 'dam-image', damSystemType: 'Bynder', damSystemIdentifier: '123', damObjectId: 'xyz-123', }, ], }; const expectedItem = { normal: 'string', number: 312, formattedText: [ { type: 'text', value: 'Link to asset 12345' }, { type: 'text', value: 'Link to asset xyz-333' }, { type: 'text', value: 'Image 54321' }, { type: 'text', value: 'This is some ' }, { type: 'tag', tag: 'p', children: [ { type: 'text', value: 'Link to asset 12345' }, { type: 'text', value: 'Image 54321' }, ], }, { type: 'text', value: 'Image xyz-123' }, ], }; const result = await resolveFormattedTextNodes(testItem, testSchema, testResolverMap); expect(result).toEqual(expectedItem); }); it('should not alter non resolvable array items', async () => { const testSchema = { type: 'object', properties: { strings: { title: 'strings', description: 'My fun strings', type: 'array', items: { title: 'A string', type: 'string', }, }, }, required: ['strings'], }; const testItem = { strings: ['a', 'b', 'c'], }; const expectedItem = { strings: ['a', 'b', 'c'], }; const result = await resolveFormattedTextNodes(testItem, testSchema, testResolverMap); expect(result).toEqual(expectedItem); }); it('should resolve errors to a text node if no handleError is passed', async () => { const testSchema = { type: 'object', properties: { normal: { type: 'string' }, number: { type: 'number' }, formattedText: { type: 'FormattedText' }, }, required: ['normal', 'number', 'formattedText'], }; const testItem = { normal: 'string', number: 312, formattedText: [ { type: 'link-to-matrix-asset', matrixIdentifier: 'abc123', matrixDomain: 'example.com', matrixAssetId: '12345', target: '_blank', children: [{ type: 'text', value: 'linked text' }], }, { type: 'matrix-image', matrixIdentifier: 'abc123', matrixDomain: 'example.com', matrixAssetId: '54321', }, { type: 'text', value: 'This is some text', }, ], }; const expectedItem = { normal: 'string', number: 312, formattedText: [{ type: 'text', value: 'Error resolving formatted text at #/formattedText: This is an error' }], }; const result = await resolveFormattedTextNodes(testItem, testSchema, { ...testResolverMap, 'link-to-matrix-asset': () => { return Promise.reject(new Error('This is an error')); }, }); expect(result).toEqual(expectedItem); }); it('should still resolve other nodes if one fails and no handleError is passed', async () => { const testSchema = { type: 'object', properties: { normal: { type: 'string' }, number: { type: 'number' }, goodFormattedText: { type: 'FormattedText' }, badFormattedText: { type: 'FormattedText' }, }, required: ['normal', 'number', 'formattedText'], }; const testItem = { normal: 'string', number: 312, goodFormattedText: [ { type: 'matrix-image', matrixIdentifier: 'abc123', matrixDomain: 'example.com', matrixAssetId: '54321', }, { type: 'text', value: 'This is some text', }, ], badFormattedText: [ { type: 'link-to-matrix-asset', matrixIdentifier: 'abc123', matrixDomain: 'example.com', matrixAssetId: '12345', target: '_blank', children: [{ type: 'text', value: 'linked text' }], }, ], }; const expectedItem = { normal: 'string', number: 312, goodFormattedText: [ { type: 'text', value: 'Image 54321' }, { type: 'text', value: 'This is some text' }, ], badFormattedText: [ { type: 'text', value: 'Error resolving formatted text at #/badFormattedText: This is an error' }, ], }; const result = await resolveFormattedTextNodes(testItem, testSchema, { ...testResolverMap, 'link-to-matrix-asset': () => { return Promise.reject(new Error('This is an error')); }, }); expect(result).toEqual(expectedItem); }); it('should run handleError if a resolver fails', async () => { const testSchema = { type: 'object', properties: { normal: { type: 'string' }, number: { type: 'number' }, formattedText: { type: 'FormattedText' }, }, required: ['normal', 'number', 'formattedText'], }; const testItem = { normal: 'string', number: 312, formattedText: [ { type: 'link-to-matrix-asset', matrixIdentifier: 'abc123', matrixDomain: 'example.com', matrixAssetId: '12345', target: '_blank', children: [{ type: 'text', value: 'linked text' }], }, { type: 'matrix-image', matrixIdentifier: 'abc123', matrixDomain: 'example.com', matrixAssetId: '54321', }, { type: 'text', value: 'This is some text', }, ], }; const expectedItem = { normal: 'string', number: 312, formattedText: [{ type: 'text', value: 'Error resolving formatted text at #/formattedText: This is an error' }], }; const handleError = jest.fn(() => { return { type: 'text', value: 'Error resolving formatted text at #/formattedText: This is an error', } as BaseResolvedNodes; }); const result = await resolveFormattedTextNodes( testItem, testSchema, { ...testResolverMap, 'link-to-matrix-asset': () => { return Promise.reject(new Error('This is an error')); }, }, { handleError }, ); expect(handleError).toHaveBeenCalled(); expect(result).toEqual(expectedItem); }); it('should run handleError if a resolver fails and error out if handleError throws', async () => { const testSchema = { type: 'object', properties: { normal: { type: 'string' }, number: { type: 'number' }, formattedText: { type: 'FormattedText' }, }, required: ['normal', 'number', 'formattedText'], }; const testItem = { normal: 'string', number: 312, formattedText: [ { type: 'link-to-matrix-asset', matrixIdentifier: 'abc123', matrixDomain: 'example.com', matrixAssetId: '12345', target: '_blank', children: [{ type: 'text', value: 'linked text' }], }, { type: 'matrix-image', matrixIdentifier: 'abc123', matrixDomain: 'example.com', matrixAssetId: '54321', }, { type: 'text', value: 'This is some text', }, ], }; const handleError = jest.fn(() => { throw new Error('This is an error'); }); await expect( resolveFormattedTextNodes( testItem, testSchema, { ...testResolverMap, 'link-to-matrix-asset': () => { return Promise.reject(new Error('a different error')); }, }, { handleError }, ), ).rejects.toThrow('This is an error'); }); it('should run handleError if a resolver fails and pass the error and pointer through to handleError', async () => { const testSchema = { type: 'object', properties: { normal: { type: 'string' }, number: { type: 'number' }, formattedText: { type: 'FormattedText' }, }, required: ['normal', 'number', 'formattedText'], }; const testItem = { normal: 'string', number: 312, formattedText: [ { type: 'link-to-matrix-asset', matrixIdentifier: 'abc123', matrixDomain: 'example.com', matrixAssetId: '12345', target: '_blank', children: [{ type: 'text', value: 'linked text' }], }, { type: 'matrix-image', matrixIdentifier: 'abc123', matrixDomain: 'example.com', matrixAssetId: '54321', }, { type: 'text', value: 'This is some text', }, ], }; const handleError = jest.fn(() => { throw new Error('This is an error'); }); await expect( resolveFormattedTextNodes( testItem, testSchema, { ...testResolverMap, 'link-to-matrix-asset': () => { return Promise.reject(new Error('a different error')); }, }, { handleError }, ), ).rejects.toThrow('This is an error'); expect(handleError).toHaveBeenCalledWith(new Error('a different error'), '#/formattedText'); }); it('should run default resolver if provided and no resolver is found', async () => { const testSchema = { type: 'object', properties: { normal: { type: 'string' }, number: { type: 'number' }, formattedText: { type: 'FormattedText' }, }, required: ['normal', 'number', 'formattedText'], }; const testItem = { normal: 'string', number: 312, formattedText: [ { type: 'link-to-matrix-asset', matrixIdentifier: 'abc123', matrixDomain: 'example.com', matrixAssetId: '12345', target: '_blank', children: [{ type: 'text', value: 'linked text' }], }, { type: 'matrix-image', matrixIdentifier: 'abc123', matrixDomain: 'example.com', matrixAssetId: '54321', }, { type: 'text', value: 'This is some text', }, ], }; const expectedItem = { normal: 'string', number: 312, formattedText: [ { type: 'text', value: 'Link to asset 12345' }, { type: 'text', value: 'Image 54321' }, { type: 'text', value: 'This is some text from the default resolver', }, ], }; const defaultResolver = jest.fn(() => { return Promise.resolve({ type: 'text', value: 'This is some text from the default resolver', } as BaseResolvedNodes); }); const result = await resolveFormattedTextNodes(testItem, testSchema, { ...testResolverMap, default: defaultResolver, }); expect(defaultResolver).toHaveBeenCalled(); expect(result).toEqual(expectedItem); }); it('should resolve a FormattedText value defined in a $ref', async () => { const testSchema = { type: 'object', properties: { normal: { type: 'string' }, number: { type: 'number' }, formattedText: { $ref: '#/definitions/FormattedText' }, }, required: ['normal', 'number', 'formattedText'], definitions: { FormattedText: { type: 'FormattedText' }, }, }; const testItem = { normal: 'string', number: 312, formattedText: [ { type: 'link-to-matrix-asset', matrixIdentifier: 'abc123', matrixDomain: 'example.com', matrixAssetId: '12345', target: '_blank', children: [{ type: 'text', value: 'linked text' }], }, { type: 'matrix-image', matrixIdentifier: 'abc123', matrixDomain: 'example.com', matrixAssetId: '54321', }, { type: 'text', value: 'This is some text', }, ], }; const expectedItem = { normal: 'string', number: 312, formattedText: [ { type: 'text', value: 'Link to asset 12345' }, { type: 'text', value: 'Image 54321' }, { type: 'text', value: 'This is some text' }, ], }; const result = await resolveFormattedTextNodes(testItem, testSchema, testResolverMap); expect(result).toEqual(expectedItem); }); it('should resolve a FormattedText value which is an empty array', async () => { const testSchema = { type: 'object', properties: { myProp: { type: 'FormattedText' }, }, required: [], }; await expect( resolveFormattedTextNodes({ myProp: [] }, testSchema, testResolverMap, { withJoin: '' }), ).resolves.toEqual({ myProp: '' }); }); it('should not resolve a FormattedText value which is a string', async () => { const testSchema = { type: 'object', properties: { myProp: { type: 'FormattedText' }, }, }; const testItem = { myProp: 'This is a string', }; const result = await resolveFormattedTextNodes(testItem, testSchema, testResolverMap); expect(result).toEqual(testItem); }); it('should ignore a FormattedText value which is null', async () => { const testSchema = { type: 'object', properties: { myProp: { type: 'FormattedText' }, }, }; const testItem = { myProp: null, }; const result = await resolveFormattedTextNodes(testItem, testSchema, testResolverMap); expect(result).toEqual(testItem); }); });