payload
Version:
Node, React, Headless CMS and Application Framework built on Next.js
1,108 lines (1,107 loc) • 36.4 kB
JavaScript
import { describe, expect, it } from 'vitest';
import { mergeLocalizedData } from './mergeLocalizedData.js';
describe('mergeLocalizedData', ()=>{
const selectedLocales = [
'en'
];
const configBlockReferences = [];
describe('simple fields', ()=>{
it('should merge localized field values for selected locales', ()=>{
const fields = [
{
name: 'title',
type: 'text',
localized: true
}
];
const docWithLocales = {
title: {
en: 'English Title',
es: 'Spanish Title',
de: 'German Title'
}
};
const dataWithLocales = {
title: {
en: 'Updated English Title',
es: 'Updated Spanish Title'
}
};
const result = mergeLocalizedData({
configBlockReferences: [],
dataWithLocales,
docWithLocales,
fields,
selectedLocales
});
expect(result.title).toEqual({
en: 'Updated English Title',
es: 'Spanish Title',
de: 'German Title'
});
});
it('should keep doc value for non-localized fields', ()=>{
const fields = [
{
name: 'title',
type: 'text',
localized: false
}
];
const result = mergeLocalizedData({
configBlockReferences: [],
dataWithLocales: {
title: 'New Title'
},
docWithLocales: {
title: 'Old Title'
},
fields,
selectedLocales
});
expect(result.title).toBe('New Title');
const missingData = mergeLocalizedData({
configBlockReferences: [],
dataWithLocales: {},
docWithLocales: {
title: 'Old Title'
},
fields,
selectedLocales
});
expect(missingData.title).toBe('Old Title');
const updatedData = mergeLocalizedData({
configBlockReferences: [],
dataWithLocales: {
title: 'Updated Title'
},
docWithLocales: {},
fields,
selectedLocales
});
expect(updatedData.title).toBe('Updated Title');
});
});
describe('groups', ()=>{
it('should merge localized group with locale keys at top level', ()=>{
const fields = [
{
name: 'meta',
type: 'group',
localized: true,
fields: [
{
name: 'title',
type: 'text'
},
{
name: 'description',
type: 'text'
}
]
}
];
const docWithLocales = {
meta: {
en: {
title: 'EN Title',
description: 'EN Desc'
},
es: {
title: 'ES Title',
description: 'ES Desc'
}
}
};
const dataWithLocales = {
meta: {
en: {
title: 'Updated EN Title',
description: 'Updated EN Desc'
}
}
};
const result = mergeLocalizedData({
configBlockReferences: [],
dataWithLocales,
docWithLocales,
fields,
selectedLocales
});
expect(result.meta).toEqual({
en: {
title: 'Updated EN Title',
description: 'Updated EN Desc'
},
es: {
title: 'ES Title',
description: 'ES Desc'
}
});
});
it('should handle non-localized group with localized children', ()=>{
const fields = [
{
name: 'meta',
type: 'group',
localized: false,
fields: [
{
name: 'title',
type: 'text',
localized: true
},
{
name: 'version',
type: 'number',
localized: false
}
]
}
];
const docWithLocales = {
meta: {
title: {
en: 'EN Title',
es: 'ES Title'
},
version: 1
}
};
const dataWithLocales = {
meta: {
title: {
en: 'Updated EN Title'
},
version: 2
}
};
const result = mergeLocalizedData({
configBlockReferences: [],
dataWithLocales,
docWithLocales,
fields,
selectedLocales
});
expect(result.meta).toEqual({
title: {
en: 'Updated EN Title',
es: 'ES Title'
},
version: 2
});
});
});
describe('arrays', ()=>{
it('should merge localized array with locale keys at top level', ()=>{
const fields = [
{
name: 'items',
type: 'array',
localized: true,
fields: [
{
name: 'name',
type: 'text'
}
]
}
];
const docWithLocales = {
items: {
en: [
{
name: 'EN Item 1'
},
{
name: 'EN Item 2'
}
],
es: [
{
name: 'ES Item 1'
}
]
}
};
const dataWithLocales = {
items: {
en: [
{
name: 'Updated EN Item 1'
},
{
name: 'Updated EN Item 2'
}
]
}
};
const result = mergeLocalizedData({
configBlockReferences: [],
dataWithLocales,
docWithLocales,
fields,
selectedLocales
});
expect(result.items).toEqual({
en: [
{
name: 'Updated EN Item 1'
},
{
name: 'Updated EN Item 2'
}
],
es: [
{
name: 'ES Item 1'
}
]
});
});
it('should handle non-localized array with localized children', ()=>{
const fields = [
{
name: 'items',
type: 'array',
localized: false,
fields: [
{
name: 'name',
type: 'text',
localized: true
}
]
}
];
const docWithLocales = {
items: [
{
name: {
en: 'EN Item 1',
es: 'ES Item 1'
}
},
{
name: {
en: 'EN Item 2',
es: 'ES Item 2'
}
}
]
};
const dataWithLocales = {
items: [
{
name: {
en: 'Updated EN Item 1'
}
},
{
name: {
en: 'Updated EN Item 2'
}
}
]
};
const result = mergeLocalizedData({
configBlockReferences: [],
dataWithLocales,
docWithLocales,
fields,
selectedLocales
});
expect(result.items).toEqual([
{
name: {
en: 'Updated EN Item 1',
es: 'ES Item 1'
}
},
{
name: {
en: 'Updated EN Item 2',
es: 'ES Item 2'
}
}
]);
});
});
describe('blocks', ()=>{
it('should merge localized blocks with locale keys at top level', ()=>{
const fields = [
{
name: 'content',
type: 'blocks',
localized: true,
blocks: [
{
slug: 'text',
fields: [
{
name: 'text',
type: 'text'
}
]
}
]
}
];
const docWithLocales = {
content: {
en: [
{
blockType: 'text',
text: 'EN Text'
}
],
es: [
{
blockType: 'text',
text: 'ES Text'
}
]
}
};
const dataWithLocales = {
content: {
en: [
{
blockType: 'text',
text: 'Updated EN Text'
}
]
}
};
const result = mergeLocalizedData({
configBlockReferences: [],
dataWithLocales,
docWithLocales,
fields,
selectedLocales
});
expect(result.content).toEqual({
en: [
{
blockType: 'text',
text: 'Updated EN Text'
}
],
es: [
{
blockType: 'text',
text: 'ES Text'
}
]
});
});
it('should handle blocks with nested arrays', ()=>{
const fields = [
{
name: 'content',
type: 'blocks',
localized: true,
blocks: [
{
slug: 'nested',
fields: [
{
name: 'items',
type: 'array',
fields: [
{
name: 'name',
type: 'text'
}
]
}
]
}
]
}
];
const docWithLocales = {
content: {
en: [
{
blockType: 'nested',
items: [
{
name: 'EN Item 1'
}
]
}
],
es: [
{
blockType: 'nested',
items: [
{
name: 'ES Item 1'
}
]
}
]
}
};
const dataWithLocales = {
content: {
en: [
{
blockType: 'nested',
items: [
{
name: 'Updated EN Item 1'
}
]
}
]
}
};
const result = mergeLocalizedData({
configBlockReferences: [],
dataWithLocales,
docWithLocales,
fields,
selectedLocales
});
expect(result.content).toEqual({
en: [
{
blockType: 'nested',
items: [
{
name: 'Updated EN Item 1'
}
]
}
],
es: [
{
blockType: 'nested',
items: [
{
name: 'ES Item 1'
}
]
}
]
});
});
});
describe('tabs', ()=>{
it('should merge localized named tabs with locale keys at top level', ()=>{
const fields = [
{
type: 'tabs',
tabs: [
{
name: 'meta',
localized: true,
fields: [
{
name: 'title',
type: 'text'
}
]
}
]
}
];
const docWithLocales = {
meta: {
en: {
title: 'EN Title'
},
es: {
title: 'ES Title'
}
}
};
const dataWithLocales = {
meta: {
en: {
title: 'Updated EN Title'
}
}
};
const result = mergeLocalizedData({
configBlockReferences: [],
dataWithLocales,
docWithLocales,
fields,
selectedLocales
});
expect(result.meta).toEqual({
en: {
title: 'Updated EN Title'
},
es: {
title: 'ES Title'
}
});
});
it('should handle unnamed tabs with localized fields', ()=>{
const fields = [
{
type: 'tabs',
tabs: [
{
label: 'tab1',
fields: [
{
name: 'title',
type: 'text',
localized: true
}
]
}
]
}
];
const docWithLocales = {
title: {
en: 'EN Title',
es: 'ES Title'
}
};
const dataWithLocales = {
title: {
en: 'Updated EN Title'
}
};
const result = mergeLocalizedData({
configBlockReferences: [],
dataWithLocales,
docWithLocales,
fields,
selectedLocales
});
expect(result.title).toEqual({
en: 'Updated EN Title',
es: 'ES Title'
});
});
});
describe('deeply nested structures', ()=>{
it('should handle multiple levels of nesting with locale keys only at topmost localized field', ()=>{
const fields = [
{
name: 'outer',
type: 'group',
localized: true,
fields: [
{
name: 'inner',
type: 'group',
localized: false,
fields: [
{
name: 'items',
type: 'array',
localized: false,
fields: [
{
name: 'text',
type: 'text',
localized: false
}
]
}
]
}
]
}
];
const docWithLocales = {
outer: {
en: {
inner: {
items: [
{
text: 'EN Item 1'
}
]
}
},
es: {
inner: {
items: [
{
text: 'ES Item 1'
}
]
}
}
}
};
const dataWithLocales = {
outer: {
en: {
inner: {
items: [
{
text: 'Updated EN Item 1'
}
]
}
}
}
};
const result = mergeLocalizedData({
configBlockReferences: [],
dataWithLocales,
docWithLocales,
fields,
selectedLocales
});
expect(result.outer).toEqual({
en: {
inner: {
items: [
{
text: 'Updated EN Item 1'
}
]
}
},
es: {
inner: {
items: [
{
text: 'ES Item 1'
}
]
}
}
});
});
});
describe('multiple selected locales', ()=>{
it('should merge multiple locales when selected', ()=>{
const fields = [
{
name: 'title',
type: 'text',
localized: true
}
];
const docWithLocales = {
title: {
en: 'EN Title',
es: 'ES Title',
de: 'DE Title',
fr: 'FR Title'
}
};
const dataWithLocales = {
title: {
en: 'Updated EN Title',
es: 'Updated ES Title'
}
};
const result = mergeLocalizedData({
configBlockReferences: [],
dataWithLocales,
docWithLocales,
fields,
selectedLocales: [
'en',
'es'
]
});
expect(result.title).toEqual({
en: 'Updated EN Title',
es: 'Updated ES Title',
de: 'DE Title',
fr: 'FR Title'
});
});
});
describe('pass through fields, rows, collapsibles, unnamed tabs, unnamed groups', ()=>{
it('should not lose merged locale data when processing unnamed tabs', ()=>{
const fields = [
{
name: 'title',
type: 'text',
localized: true
},
{
type: 'tabs',
tabs: [
{
label: 'Other Fields',
fields: [
{
name: 'description',
type: 'text',
localized: true
}
]
}
]
}
];
const docWithLocales = {
title: {
en: 'English Title'
},
description: {
en: 'English Description'
}
};
const dataWithLocales = {
title: {
en: 'English Title',
es: 'Spanish Title'
},
description: {
en: 'English Description',
es: 'Spanish Description'
}
};
const result = mergeLocalizedData({
configBlockReferences: [],
dataWithLocales,
docWithLocales,
fields,
selectedLocales: [
'es'
]
});
expect(result.title).toEqual({
en: 'English Title',
es: 'Spanish Title'
});
expect(result.description).toEqual({
en: 'English Description',
es: 'Spanish Description'
});
});
it('should not lose other locale data when processing unnamed groups', ()=>{
// https://github.com/payloadcms/payload/issues/15642
const fields = [
{
name: 'textFieldRoot',
type: 'text'
},
{
name: 'textFieldRootLocalized',
type: 'text',
localized: true
},
// Unnamed group - fields at same data level as root
{
type: 'group',
fields: [
{
name: 'textFieldNested',
type: 'text'
},
{
name: 'textFieldNestedLocalized',
type: 'text',
localized: true
}
]
}
];
// Document already has English data published
const docWithLocales = {
textFieldRoot: 'Root Value',
textFieldRootLocalized: {
en: 'English Root Localized',
es: 'Spanish Root Localized'
},
textFieldNested: 'Nested Value',
textFieldNestedLocalized: {
en: 'English Nested Localized',
es: 'Spanish Nested Localized'
}
};
// Publishing only English locale with updated data
const dataWithLocales = {
textFieldRoot: 'Updated Root Value',
textFieldRootLocalized: {
en: 'Updated English Root Localized'
},
textFieldNested: 'Updated Nested Value',
textFieldNestedLocalized: {
en: 'Updated English Nested Localized'
}
};
const result = mergeLocalizedData({
configBlockReferences: [],
dataWithLocales,
docWithLocales,
fields,
selectedLocales: [
'en'
]
});
// Root non-localized field should be updated
expect(result.textFieldRoot).toBe('Updated Root Value');
// Root localized field should merge: update en, preserve es
expect(result.textFieldRootLocalized).toEqual({
en: 'Updated English Root Localized',
es: 'Spanish Root Localized'
});
// Nested non-localized field should be updated
expect(result.textFieldNested).toBe('Updated Nested Value');
// Nested localized field should merge: update en, preserve es
// This is the bug - es data is lost
expect(result.textFieldNestedLocalized).toEqual({
en: 'Updated English Nested Localized',
es: 'Spanish Nested Localized'
});
});
it('should not lose other locale data when processing row fields', ()=>{
const fields = [
{
type: 'row',
fields: [
{
name: 'rowFieldLocalized',
type: 'text',
localized: true
}
]
}
];
const docWithLocales = {
rowFieldLocalized: {
en: 'English Value',
es: 'Spanish Value'
}
};
const dataWithLocales = {
rowFieldLocalized: {
en: 'Updated English Value'
}
};
const result = mergeLocalizedData({
configBlockReferences: [],
dataWithLocales,
docWithLocales,
fields,
selectedLocales: [
'en'
]
});
expect(result.rowFieldLocalized).toEqual({
en: 'Updated English Value',
es: 'Spanish Value'
});
});
it('should preserve other locale data when updating through unnamed tabs', ()=>{
const fields = [
{
type: 'tabs',
tabs: [
{
label: 'Tab 1',
fields: [
{
name: 'tabFieldLocalized',
type: 'text',
localized: true
}
]
}
]
}
];
// Document has both en and es data
const docWithLocales = {
tabFieldLocalized: {
en: 'English Value',
es: 'Spanish Value'
}
};
// Only updating en
const dataWithLocales = {
tabFieldLocalized: {
en: 'Updated English Value'
}
};
const result = mergeLocalizedData({
configBlockReferences: [],
dataWithLocales,
docWithLocales,
fields,
selectedLocales: [
'en'
]
});
// es should be preserved
expect(result.tabFieldLocalized).toEqual({
en: 'Updated English Value',
es: 'Spanish Value'
});
});
});
describe('block metadata preservation', ()=>{
it('should preserve blockType, id, and blockName when existing doc has no blocks', ()=>{
// Reproduces the bug where autosave creates a doc without blocks,
// then blocks are added and publishSpecificLocale drops metadata
const fields = [
{
name: 'layout',
type: 'blocks',
blocks: [
{
slug: 'content',
fields: [
{
name: 'richText',
type: 'richText',
localized: true
}
]
}
]
}
];
// Main doc has no blocks (autosave wrote before blocks were added)
const docWithLocales = {};
const dataWithLocales = {
layout: [
{
blockType: 'content',
id: 'abc123',
blockName: 'My Content Block',
richText: {
en: 'Hello'
}
}
]
};
const result = mergeLocalizedData({
configBlockReferences: [],
dataWithLocales,
docWithLocales,
fields,
selectedLocales: [
'en'
]
});
expect(result.layout).toHaveLength(1);
expect(result.layout[0].blockType).toBe('content');
expect(result.layout[0].id).toBe('abc123');
expect(result.layout[0].blockName).toBe('My Content Block');
});
it('should preserve blockType and id when existing doc has undefined for block field', ()=>{
const fields = [
{
name: 'layout',
type: 'blocks',
blocks: [
{
slug: 'text',
fields: [
{
name: 'text',
type: 'text',
localized: true
}
]
}
]
}
];
// existingValue is undefined (field not present in existing doc)
const docWithLocales = {
layout: undefined
};
const dataWithLocales = {
layout: [
{
blockType: 'text',
id: 'def456',
text: {
en: 'English text',
es: 'Spanish text'
}
}
]
};
const result = mergeLocalizedData({
configBlockReferences: [],
dataWithLocales,
docWithLocales,
fields,
selectedLocales: [
'en'
]
});
expect(result.layout).toHaveLength(1);
expect(result.layout[0].blockType).toBe('text');
expect(result.layout[0].id).toBe('def456');
expect(result.layout[0].text).toEqual({
en: 'English text'
});
});
it('should preserve blockType when existing doc already has blocks', ()=>{
// Ensures the fix doesn't break the happy path
const fields = [
{
name: 'layout',
type: 'blocks',
blocks: [
{
slug: 'content',
fields: [
{
name: 'body',
type: 'text',
localized: true
}
]
}
]
}
];
const docWithLocales = {
layout: [
{
blockType: 'content',
id: 'existing-id',
body: {
en: 'Old English',
es: 'Old Spanish'
}
}
]
};
const dataWithLocales = {
layout: [
{
blockType: 'content',
id: 'existing-id',
body: {
en: 'New English',
es: 'New Spanish'
}
}
]
};
const result = mergeLocalizedData({
configBlockReferences: [],
dataWithLocales,
docWithLocales,
fields,
selectedLocales: [
'en'
]
});
expect(result.layout).toHaveLength(1);
expect(result.layout[0].blockType).toBe('content');
expect(result.layout[0].id).toBe('existing-id');
expect(result.layout[0].body).toEqual({
en: 'New English',
es: 'Old Spanish'
});
});
});
});
//# sourceMappingURL=mergeLocalizedData.spec.js.map