Revision control
Copy as Markdown
Other Tools
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
"use strict";
const {
calendarEventDraftSlice,
resetForCreate,
resetFromSource,
setSectionValidity,
updateSection,
} = ChromeUtils.importESModule(
"moz-src:///comm/mail/components/calendar/content/state/calendarEventDraftSlice.mjs"
);
const {
calendarEventCreateEditSessionSlice,
clearCalendarEventCreateEditSession,
operationFailed,
operationStarted,
operationSucceeded,
startEditSession,
} = ChromeUtils.importESModule(
"moz-src:///comm/mail/components/calendar/content/state/calendarEventCreateEditSessionSlice.mjs"
);
const { store } = ChromeUtils.importESModule(
"moz-src:///comm/mail/base/content/state/store.mjs"
);
const {
selectCanSave,
selectCurrentDraft,
selectDirtySections,
selectIsDirty,
} = ChromeUtils.importESModule(
"moz-src:///comm/mail/components/calendar/content/state/calendar-event-create-edit-selectors.mjs"
);
const sessionKey = "calendar-event-create-edit-state-test";
function sourceSnapshot() {
return {
event: {
calendarId: "calendar-id",
end: {
icalString: "20260824T110000Z",
isDate: false,
nativeTime: "1787569200000000",
timezoneId: "UTC",
},
eventId: "event-id",
recurrenceId: null,
start: {
icalString: "20260824T100000Z",
isDate: false,
nativeTime: "1787565600000000",
timezoneId: "UTC",
},
title: "Original title",
},
identity: {
calendarId: "calendar-id",
eventId: "event-id",
recurrenceId: null,
},
};
}
function reduceDraft(state, action) {
return calendarEventDraftSlice.reducer(state, action);
}
add_task(function test_sessionSliceIsInjectedIntoTheApplicationStore() {
store.dispatch(startEditSession({ sessionKey }));
Assert.equal(
store.getState().calendarEventCreateEditSession.sessions[sessionKey].mode,
"edit",
"The session state is available through the app store"
);
store.dispatch(clearCalendarEventCreateEditSession({ sessionKey }));
});
add_task(function test_sourceSnapshotStartsAnIsolatedDraft() {
const snapshot = sourceSnapshot();
const state = reduceDraft(
undefined,
resetFromSource({ sessionKey, snapshot })
);
const draft = state.sessions[sessionKey];
Assert.deepEqual(
draft.baseline,
{
calendarId: "calendar-id",
end: snapshot.event.end,
start: snapshot.event.start,
title: "Original title",
},
"The draft copies editable source values"
);
Assert.notStrictEqual(
draft.value,
snapshot.event,
"The draft does not keep the source data object"
);
Assert.notStrictEqual(
draft.value.start,
snapshot.event.start,
"Nested source values are copied too"
);
const changed = reduceDraft(
state,
updateSection({
patch: { title: "Changed title" },
section: "title",
sessionKey,
})
);
Assert.equal(
snapshot.event.title,
"Original title",
"The source data is unchanged"
);
Assert.equal(
changed.sessions[sessionKey].baseline.title,
"Original title",
"The starting values are unchanged"
);
const restored = reduceDraft(
changed,
updateSection({
patch: { title: "Original title" },
section: "title",
sessionKey,
})
);
Assert.deepEqual(
restored.sessions[sessionKey].dirtySections,
[],
"Restoring a section to its starting values clears its changed state"
);
});
add_task(function test_createDraftAcceptsProviderValuesWithoutDefaults() {
const initialDraft = {
calendarId: "provider-calendar",
start: { icalString: "20260824T100000Z" },
};
const state = reduceDraft(
undefined,
resetForCreate({ initialDraft, sessionKey })
);
Assert.deepEqual(
state.sessions[sessionKey].value,
initialDraft,
"The draft keeps values from field defaults"
);
Assert.deepEqual(
state.sessions[sessionKey].dirtySections,
[],
"Field default values set the starting values"
);
Assert.notStrictEqual(
state.sessions[sessionKey].value,
initialDraft,
"Values are copied before Redux stores them"
);
});
add_task(function test_dateTimeSectionStaysDirtyUntilBothValuesMatchBaseline() {
const snapshot = sourceSnapshot();
const { start, end } = snapshot.event;
const changedStart = {
...start,
icalString: "20260824T103000Z",
nativeTime: "1787567400000000",
};
const changedEnd = {
...end,
icalString: "20260824T120000Z",
nativeTime: "1787572800000000",
};
let state = reduceDraft(undefined, resetFromSource({ sessionKey, snapshot }));
for (const [patch, expectedDirtySections, message] of [
[
{ start: changedStart, end: changedEnd },
["dateTime"],
"The section is dirty when both values differ from the baseline",
],
[
{ start: changedStart, end },
["dateTime"],
"Restoring end leaves the section dirty while start differs",
],
[
{ start, end },
[],
"Restoring both values clears the section without Save",
],
]) {
state = reduceDraft(
state,
updateSection({ patch, section: "dateTime", sessionKey })
);
Assert.deepEqual(
state.sessions[sessionKey].dirtySections,
expectedDirtySections,
message
);
Assert.deepEqual(
state.sessions[sessionKey].value.start,
patch.start,
"The draft contains the supplied start value"
);
Assert.deepEqual(
state.sessions[sessionKey].value.end,
patch.end,
"The draft contains the supplied end value"
);
Assert.deepEqual(
state.sessions[sessionKey].baseline.start,
start,
"The start baseline is unchanged"
);
Assert.deepEqual(
state.sessions[sessionKey].baseline.end,
end,
"The end baseline is unchanged"
);
}
Assert.deepEqual(
snapshot,
sourceSnapshot(),
"The source snapshot is unchanged"
);
});
add_task(function test_dirtySectionsAndValidationResetForNewSource() {
let state = reduceDraft(
undefined,
resetFromSource({ sessionKey, snapshot: sourceSnapshot() })
);
state = reduceDraft(
state,
updateSection({
patch: {
start: sourceSnapshot().event.start,
end: {
...sourceSnapshot().event.end,
icalString: "20260824T120000Z",
nativeTime: "1787572800000000",
},
},
section: "dateTime",
sessionKey,
})
);
state = reduceDraft(
state,
setSectionValidity({
section: "dateTime",
sessionKey,
validity: { messageId: "calendar-date-invalid", valid: false },
})
);
Assert.deepEqual(
state.sessions[sessionKey].dirtySections,
["dateTime"],
"A section is changed when one value differs from its starting value"
);
state = reduceDraft(
state,
resetFromSource({
sessionKey,
snapshot: {
...sourceSnapshot(),
event: { ...sourceSnapshot().event, title: "Replacement source" },
},
})
);
Assert.deepEqual(
state.sessions[sessionKey].dirtySections,
[],
"New source data clears old changed state"
);
Assert.deepEqual(
state.sessions[sessionKey].validation,
{},
"New source data clears old validation results"
);
});
add_task(function test_selectorsReflectDraftAndOperationState() {
let draftState = reduceDraft(
undefined,
resetForCreate({ initialDraft: { title: "Draft" }, sessionKey })
);
let sessionState = calendarEventCreateEditSessionSlice.reducer(
undefined,
startEditSession({ sessionKey })
);
let state = {
calendarEventCreateEditSession: sessionState,
calendarEventDraft: draftState,
};
Assert.deepEqual(
selectCurrentDraft(state, sessionKey),
{ title: "Draft" },
"The function returns the session draft"
);
Assert.ok(selectCanSave(state, sessionKey), "A valid idle draft can save");
draftState = reduceDraft(
draftState,
setSectionValidity({
section: "title",
sessionKey,
validity: { valid: false },
})
);
state = { ...state, calendarEventDraft: draftState };
Assert.ok(
!selectCanSave(state, sessionKey),
"An invalid section blocks Save"
);
draftState = reduceDraft(
draftState,
setSectionValidity({
section: "title",
sessionKey,
validity: { valid: true },
})
);
state = { ...state, calendarEventDraft: draftState };
Assert.ok(selectCanSave(state, sessionKey), "A valid section allows Save");
draftState = reduceDraft(
draftState,
updateSection({
patch: { title: "Changed" },
section: "title",
sessionKey,
})
);
state = { ...state, calendarEventDraft: draftState };
Assert.ok(
selectIsDirty(state, sessionKey),
"The draft is dirty after a change"
);
Assert.deepEqual(
selectDirtySections(state, sessionKey),
["title"],
"The changed section is selected"
);
sessionState = calendarEventCreateEditSessionSlice.reducer(
sessionState,
operationStarted({ kind: "save", sessionKey })
);
state = { ...state, calendarEventCreateEditSession: sessionState };
Assert.ok(
!selectCanSave(state, sessionKey),
"Save in progress prevents another save"
);
sessionState = calendarEventCreateEditSessionSlice.reducer(
sessionState,
operationFailed({
error: { code: "save-failed" },
kind: "save",
sessionKey,
})
);
Assert.ok(
selectCanSave(
{ ...state, calendarEventCreateEditSession: sessionState },
sessionKey
),
"A failed save leaves the draft ready to retry"
);
sessionState = calendarEventCreateEditSessionSlice.reducer(
sessionState,
operationStarted({ kind: "save", sessionKey })
);
sessionState = calendarEventCreateEditSessionSlice.reducer(
sessionState,
operationSucceeded({ kind: "save", sessionKey })
);
Assert.equal(
sessionState.sessions[sessionKey].operation.status,
"idle",
"A successful operation clears temporary session state"
);
});