Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .talismanrc
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,14 @@ fileignoreconfig:
- filecontent
- filename: package-lock.json
checksum: 29476e419f64cdf5cb6a41033148dae3eaacde840e0da8fdcf3690cf59b31899
- filename: __test__/find-render-content.test.ts
checksum: c2508ae1fb2b20f6fe3d354558704076ecdcbe7e1ece46addaa5eb8354e60233
- filename: __test__/json-to-html.test.ts
checksum: 8ef368136341314dc597a1e0a3a7b45ac9255897b376395db5e7e032b661682e
- filename: __test__/entry-editable.test.ts
checksum: 32e5e047c56ef0e74456eb6275c6ea8d2ffbef38901bc3eb97ae0a62e431f5a2
- filename: src/entry-editable.ts
checksum: 3ba7af9ed1c1adef2e2bd5610099716562bebb8ba750d4b41ddda99fc9eaf115
checksum: e130c19526679f220073ed9cc918fa28906ebf99c33c00cf8ee58f2178caddcf
- filename: .husky/pre-commit
checksum: 5baabd7d2c391648163f9371f0e5e9484f8fb90fa2284cfc378732ec3192c193
- filename: src/endpoints.ts
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Changelog

## [1.7.1](https://github.com/contentstack/contentstack-utils-javascript/tree/v1.7.1)
- Fix: Guard against null/undefined in getTag to prevent TypeError when addEditableTags/addTags processes entries with null content (Issue #193)

## [1.7.0](https://github.com/contentstack/contentstack-utils-javascript/tree/v1.7.0)
- Added option in addTags to disable lowercasing of locale

Expand Down
20 changes: 20 additions & 0 deletions __test__/attributes-to-string.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,4 +137,24 @@ describe('Attributes to String', () => {
expect(resultString).toEqual(' validKey="safeValue"');
done();
});

describe('Negative and corner cases', () => {
it('attributeToString with null should return empty or throw', () => {
try {
const result = attributeToString(null as any);
expect(result).toBe('');
} catch {
// In strict mode or some envs, for-in on null throws
}
});

it('attributeToString with undefined should return empty or throw', () => {
try {
const result = attributeToString(undefined as any);
expect(result).toBe('');
} catch {
// In strict mode or some envs, for-in on undefined throws
}
});
});
})
15 changes: 15 additions & 0 deletions __test__/default-options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,19 @@ describe('Default Option test', () => {
expect(assetDownloadFunction(assetContentBlank, embedAttributesText)).toEqual(`<a>${linkText}</a>`)
done()
})
})

describe('Default options negative and corner cases', () => {
it('should throw when item is null for entry block render', () => {
expect(() => entryBlockFunction(null as any, embedAttributes)).toThrow()
})

it('should throw when item is null for asset display render', () => {
expect(() => assetDisplaableFunction(null as any, embedAttributes)).toThrow()
})

it('should handle metadata with empty attributes', () => {
const emptyAttrs = { attributes: {} } as Metadata
expect(entryBlockFunction(entryContentBlank, emptyAttrs)).toContain(entryContentBlank.uid)
})
})
156 changes: 155 additions & 1 deletion __test__/entry-editable.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { addTags } from '../src/entry-editable'
import { addTags, getTag } from '../src/entry-editable'
import { entry_global_field, entry_global_field_multiple, entry_modular_block, entry_reference, entry_with_text, entry_with_applied_variants, entry_with_parent_path_variants } from './mock/entry-editable-mock'

describe('Entry editable test', () => {
Expand Down Expand Up @@ -519,6 +519,91 @@ describe('Entry editable test', () => {
})
})

describe('Null and undefined content handling (Issue #193)', () => {
it('should not throw when entry has a null field value', done => {
const entryWithNullField: any = {
"uid": "entry_uid_null",
"locale": "en-us",
"title": "Valid title",
"description": null
}

expect(() => addTags(entryWithNullField, 'content_type', false)).not.toThrow()
expect((entryWithNullField as any)['$']['title']).toEqual('data-cslp=content_type.entry_uid_null.en-us.title')
done()
})

it('should not throw when entry has an undefined field value', done => {
const entryWithUndefinedField: any = {
"uid": "entry_uid_undef",
"locale": "en-us",
"title": "Valid title",
"description": undefined
}

expect(() => addTags(entryWithUndefinedField, 'content_type', false)).not.toThrow()
expect((entryWithUndefinedField as any)['$']['title']).toEqual('data-cslp=content_type.entry_uid_undef.en-us.title')
done()
})

it('should return empty tags for a null entry', done => {
const nullEntry: any = null

expect(() => addTags(nullEntry, 'content_type', false)).not.toThrow()
done()
})

it('should return empty tags for an undefined entry', done => {
const undefinedEntry: any = undefined

expect(() => addTags(undefinedEntry, 'content_type', false)).not.toThrow()
done()
})

it('should handle entry with multiple null field values', done => {
const entryWithMultipleNulls: any = {
"uid": "entry_uid_multi_null",
"locale": "en-us",
"title": "Valid title",
"field_a": null,
"field_b": null,
"field_c": "valid"
}

expect(() => addTags(entryWithMultipleNulls, 'content_type', true)).not.toThrow()
expect((entryWithMultipleNulls as any)['$']['title']).toEqual({'data-cslp': 'content_type.entry_uid_multi_null.en-us.title'})
expect((entryWithMultipleNulls as any)['$']['field_c']).toEqual({'data-cslp': 'content_type.entry_uid_multi_null.en-us.field_c'})
done()
})

it('should handle entry with nested null object', done => {
const entryWithNestedNull: any = {
"uid": "entry_uid_nested_null",
"locale": "en-us",
"title": "Valid title",
"group": null
}

expect(() => addTags(entryWithNestedNull, 'content_type', false)).not.toThrow()
expect((entryWithNestedNull as any)['$']['title']).toEqual('data-cslp=content_type.entry_uid_nested_null.en-us.title')
done()
})

it('getTag with null content returns empty object (covers null guard)', done => {
const appliedVariants = { _applied_variants: {}, shouldApplyVariant: false, metaKey: '' }
const result = getTag(null as any, 'some.prefix', false, 'en-us', appliedVariants)
expect(result).toEqual({})
done()
})

it('getTag with undefined content returns empty object (covers null guard)', done => {
const appliedVariants = { _applied_variants: {}, shouldApplyVariant: false, metaKey: '' }
const result = getTag(undefined as any, 'some.prefix', true, 'en-us', appliedVariants)
expect(result).toEqual({})
done()
})
})

describe('useLowerCaseLocale option', () => {
it('should preserve locale casing when useLowerCaseLocale is false', done => {
const entry = {
Expand Down Expand Up @@ -560,4 +645,73 @@ describe('Entry editable test', () => {
})
})

describe('Negative and corner cases', () => {
it('addTags with empty entry object should not throw and should set $ with empty-like tags', done => {
const emptyEntry = { uid: 'e1', locale: 'en-us' }
expect(() => addTags(emptyEntry, 'ct', false)).not.toThrow()
expect((emptyEntry as any).$).toBeDefined()
expect(typeof (emptyEntry as any).$).toBe('object')
done()
})

it('addTags with empty string contentTypeUid should normalize to lowercase', done => {
const entry = { uid: 'u1', locale: 'en-us', title: 't' }
addTags(entry, '', false)
expect((entry as any)['$']['title']).toContain('.title')
done()
})

it('addTags with options as undefined should not throw', done => {
const entry = { uid: 'u1', locale: 'en-us', title: 't' }
expect(() => addTags(entry, 'ct', false, 'en-us', undefined)).not.toThrow()
expect((entry as any)['$']['title']).toBeDefined()
done()
})

it('getTag with empty object content should return empty tags object', done => {
const appliedVariants = { _applied_variants: {}, shouldApplyVariant: false, metaKey: '' }
const result = getTag({}, 'prefix', false, 'en-us', appliedVariants)
expect(result).toEqual({})
done()
})

it('getTag with content that has only $ key should skip $ and return empty', done => {
const appliedVariants = { _applied_variants: {}, shouldApplyVariant: false, metaKey: '' }
const result = getTag({ $: 'ignored' }, 'prefix', false, 'en-us', appliedVariants)
expect(result).toEqual({})
done()
})

it('getTag with appliedVariants._applied_variants null should not throw (getParentVariantisedPath safety)', done => {
const content = { field: 'value' }
const appliedVariants = { _applied_variants: null as any, shouldApplyVariant: true, metaKey: 'field' }
expect(() => getTag(content, 'prefix', false, 'en-us', appliedVariants)).not.toThrow()
const result = getTag(content, 'prefix', false, 'en-us', appliedVariants)
expect(result).toHaveProperty('field')
done()
})

it('getTag with appliedVariants.metaKey empty and shouldApplyVariant true should still produce tags', done => {
const content = { title: 'hello' }
const appliedVariants = { _applied_variants: {}, shouldApplyVariant: true, metaKey: '' }
const result = getTag(content, 'ct.uid.en-us', false, 'en-us', appliedVariants)
expect(result).toHaveProperty('title')
expect((result as any).title).toContain('title')
done()
})

it('addTags with entry that has empty array field should not throw', done => {
const entry: any = { uid: 'u1', locale: 'en-us', items: [] }
expect(() => addTags(entry, 'ct', false)).not.toThrow()
expect((entry as any)['$']['items']).toBeDefined()
done()
})

it('addTags with entry that has nested null object value should not throw', done => {
const entry: any = { uid: 'u1', locale: 'en-us', title: 't', nested: null }
expect(() => addTags(entry, 'ct', true)).not.toThrow()
expect((entry as any)['$']['title']).toEqual({ 'data-cslp': expect.stringContaining('title') })
done()
})
})
})
43 changes: 43 additions & 0 deletions __test__/enumerate-entries.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { enumerate, enumerateContents } from '../src/helper/enumerate-entries';

describe('enumerate', () => {
it('should not throw when entries is empty array', () => {
const process = jest.fn();
expect(() => enumerate([], process)).not.toThrow();
expect(process).not.toHaveBeenCalled();
});

it('should call process for each entry', () => {
const entries = [{ uid: '1' }, { uid: '2' }];
const process = jest.fn();
enumerate(entries, process);
expect(process).toHaveBeenCalledTimes(2);
expect(process).toHaveBeenNthCalledWith(1, { uid: '1' });
expect(process).toHaveBeenNthCalledWith(2, { uid: '2' });
});
});

describe('enumerateContents', () => {
it('should return content as string when content is not array and type is not doc', () => {
const content = { type: 'paragraph', children: [] } as any;
const result = enumerateContents(content);
expect(result).toEqual(content);
});

it('should return array of strings when content is array of docs', () => {
const doc: any = { type: 'doc', children: [] };
const result = enumerateContents([doc, doc]);
expect(Array.isArray(result)).toBe(true);
expect((result as string[]).length).toBe(2);
expect((result as string[])[0]).toBe('');
expect((result as string[])[1]).toBe('');
});

it('should throw when content is null', () => {
expect(() => enumerateContents(null as any)).toThrow();
});

it('should throw when content is undefined', () => {
expect(() => enumerateContents(undefined as any)).toThrow();
});
});
37 changes: 37 additions & 0 deletions __test__/find-embedded-objects.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,43 @@ describe('findGQLEmbeddedItems edge cases', () => {
const result = findGQLEmbeddedItems(null as any, null as any);
expect(result).toEqual([]);
});

it('should return empty array if items is undefined', () => {
const result = findGQLEmbeddedItems({ itemType: 'entry', itemUid: 'uid', contentTypeUid: 'ct', attributes: {} } as any, undefined as any);
expect(result).toEqual([]);
});
});

describe('findEmbeddedItems negative and corner cases', () => {
it('should return empty array when entry is null', () => {
const metadata = { itemType: 'entry' as const, itemUid: 'uid', contentTypeUid: 'ct', attributes: {} };
expect(findEmbeddedItems(metadata as any, null as any)).toEqual([]);
});

it('should return empty array when entry has no _embedded_items', () => {
const entry = { uid: 'e1', title: 'Entry' };
const metadata = { itemType: 'entry' as const, itemUid: 'uid', contentTypeUid: 'ct', attributes: {} };
expect(findEmbeddedItems(metadata as any, entry as any)).toEqual([]);
});

it('should return empty array when object (metadata) is null', () => {
expect(findEmbeddedItems(null as any, entryEmbeddedEntries)).toEqual([]);
});
});

describe('findRenderString negative and corner cases', () => {
it('should return empty string when metadata is undefined', () => {
expect(findRenderString(entryEmbeddedEntries._embedded_items.rich_text_editor[0], undefined as any)).toEqual('');
});

it('should throw when item is null and metadata is valid (default render accesses item)', () => {
const metadata = { styleType: 'block', attributes: {} } as any;
expect(() => findRenderString(null as any, metadata)).toThrow();
});

it('should throw when both item and metadata are null (accesses metadata.styleType)', () => {
expect(() => findRenderString(null as any, null as any)).toThrow();
});
});

function makeFindEntry(uid: string = '', contentTypeUid: string = '', embeddeditems?: EmbeddedItem[]) {
Expand Down
38 changes: 38 additions & 0 deletions __test__/find-render-content.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,44 @@ describe('Find Render content test', () => {
})
})

describe('Find Render Content negative and corner cases', () => {
it('getContent with null object should throw when accessing object[key]', () => {
expect(() => getContent(['key'], null as any, () => '')).toThrow()
})

it('getContent with undefined object should throw when accessing object[key]', () => {
expect(() => getContent(['key'], undefined as any, () => '')).toThrow()
})

it('getContent with empty keys array should not call render', done => {
const entry = { rich_text_editor: 'content' }
const render = jest.fn(() => 'replaced')
getContent([], entry, render)
expect(render).not.toHaveBeenCalled()
done()
})

it('findRenderContent with path that does not exist on entry should not throw', done => {
const entry = { uid: 'e1', title: 'Only these' }
expect(() => findRenderContent('nonexistent.path', entry, (c: string | string[]) => c)).not.toThrow()
done()
})

it('findRenderContent with single key that does not exist should not call render', done => {
const entry = { uid: 'e1' }
const render = jest.fn((c: any) => c)
findRenderContent('missing_key', entry, render)
expect(render).not.toHaveBeenCalled()
done()
})

it('getContent when intermediate key is null should not throw', done => {
const entry: any = { level1: null }
expect(() => getContent(['level1', 'level2'], entry, () => 'x')).not.toThrow()
done()
})
})

function findContent(path: string, renders: (content: string| string[]) => string| string[]) {
findRenderContent(path, entryMultipleContent, renders)
}
Loading
Loading