Skip to content
Open
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
62 changes: 62 additions & 0 deletions packages/decap-cms-core/src/__tests__/backend.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,68 @@ describe('Backend', () => {
expect(backend.entryToRaw).toHaveBeenCalledTimes(1);
expect(backend.entryToRaw).toHaveBeenCalledWith(collection, newEntry);
});

it('should preserve slug when preSave event handler modifies file collection entry', async () => {
const implementation = {
init: jest.fn(() => implementation),
persistEntry: jest.fn(() => implementation),
};

const config = {
backend: {
commit_messages: 'commit-messages',
},
};

// File collection with a single file
const collection = Map({
name: 'settings',
type: FILES,
files: List([
Map({
name: 'config',
file: 'data/config.json',
fields: List([Map({ name: 'title', widget: 'string' })]),
}),
]),
});

const originalEntry = Map({
slug: 'config',
path: 'data/config.json',
data: Map({ title: 'original' }),
meta: Map({ path: 'data/config.json' }),
});

const entryDraft = Map({
entry: originalEntry,
});

const user = { login: 'login', name: 'name' };
const backend = new Backend(implementation, { config, backendName: 'github' });

backend.currentUser = jest.fn().mockResolvedValue(user);
backend.entryToRaw = jest.fn().mockReturnValue('content');

// Mock invokePreSaveEvent to simulate a preSave handler that modifies data
// This is what happens when custom widgets or event handlers modify entry data
// The key is that it returns the FULL entry with slug, not just the data
backend.invokePreSaveEvent = jest.fn().mockImplementation(async entry => {
// Simulate a preSave handler modifying the data field
return entry.setIn(['data', 'title'], 'modified');
});

await backend.persistEntry({ config, collection, entryDraft });

// Verify entryToRaw was called with an entry that has the slug
expect(backend.entryToRaw).toHaveBeenCalledTimes(1);
const entryPassedToRaw = backend.entryToRaw.mock.calls[0][1];

// Critical assertion: slug must be preserved
expect(entryPassedToRaw.get('slug')).toBe('config');
expect(entryPassedToRaw.get('path')).toBe('data/config.json');
expect(entryPassedToRaw.getIn(['data', 'title'])).toBe('modified');
});
});

describe('persistMedia', () => {
Expand Down
6 changes: 3 additions & 3 deletions packages/decap-cms-core/src/lib/__tests__/registry.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ describe('registry', () => {
});
});

it(`should return an updated entry's DataMap`, async () => {
it(`should return the complete updated entry object`, async () => {
const { registerEventListener, invokeEvent } = require('../registry');

const event = 'preSave';
Expand Down Expand Up @@ -233,7 +233,7 @@ describe('registry', () => {
expect(handler1).toHaveBeenCalledWith(data, options);
expect(handler2).toHaveBeenCalledWith(dataAfterFirstHandlerExecution, options);

expect(result).toEqual(dataAfterSecondHandlerExecution.entry.get('data'));
expect(result).toEqual(dataAfterSecondHandlerExecution.entry);
});

it('should allow multiple events to not return a value', async () => {
Expand All @@ -254,7 +254,7 @@ describe('registry', () => {

expect(handler1).toHaveBeenCalledWith(data, options);
expect(handler2).toHaveBeenCalledWith(data, options);
expect(result).toEqual(data.entry.get('data'));
expect(result).toEqual(data.entry);
});
});
});
Expand Down
5 changes: 4 additions & 1 deletion packages/decap-cms-core/src/lib/registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,10 @@ export async function invokeEvent({ name, data }) {
_data = { ...data, entry };
}
}
return _data.entry.get('data');
// Return the full entry object with all metadata (slug, path, meta, etc.)
// rather than just the data payload. Callers like invokePreSaveEvent expect
// the complete entry object to be preserved through the event handler chain.
return _data.entry;
}

export function removeEventListener({ name, handler }) {
Expand Down