mirror of
https://github.com/mattermost/mattermost-mobile.git
synced 2026-11-07 03:10:12 +00:00
Guard the composer against draft resurrection now that removeDraft enqueues a durable DELETE, and fail closed on unreconstructable burn-on-read sends. - Add per-composer editor_guard module (keyed by serverUrl:channelId:rootId): tracks genuine unsaved edits + a generation bumped on Send/Delete. Shared by the save site and the invalidation sites; avoids prop-drilling. - post_input: route blur/background/unmount through guardedSave, which no-ops when the editor is clean and discards its write (via a captured-generation staleness check) if Send/Delete invalidated the editor mid-save. Genuine typing marks dirty; an external/remote value change resets the baseline clean; unmount clears the entry. - draft_handler.clearDraft and deleteDraftConfirmation invalidate the editor so a pending lifecycle save cannot recreate a just-sent/deleted draft. - handleDraftUpdate discards stale markdown-image metadata when the editor generation changed during the async parse. - handle_send_message: fail-closed burn-on-read guard on both send paths — an enabled BoR send with invalid/missing durations shows a localized error, keeps the draft, and never creates a downgraded plain post. Adds one en.json string. Checks: npm run tsc clean; eslint --quiet clean; 126 Jest tests pass (editor guard, handle_send_message incl. BoR fail-closed, draft actions, draft utils). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
112 lines
3.4 KiB
TypeScript
112 lines
3.4 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import {
|
|
buildEditorKey,
|
|
captureEditorGeneration,
|
|
clearEditor,
|
|
invalidateEditor,
|
|
isEditorGenerationStale,
|
|
markEditorEdited,
|
|
markEditorSaved,
|
|
resetEditorBaseline,
|
|
shouldSaveEditor,
|
|
} from './editor_guard';
|
|
|
|
describe('editor_guard', () => {
|
|
const serverUrl = 'https://server.com';
|
|
const channelId = 'channel-id';
|
|
const rootId = '';
|
|
|
|
let key: string;
|
|
|
|
beforeEach(() => {
|
|
key = buildEditorKey(serverUrl, channelId, rootId);
|
|
|
|
// Ensure a fresh entry for each test.
|
|
clearEditor(key);
|
|
});
|
|
|
|
it('should build a composite key from serverUrl, channelId and rootId', () => {
|
|
expect(buildEditorKey('a', 'b', 'c')).toBe('a:b:c');
|
|
});
|
|
|
|
it('should default to clean, non-stale state on first access', () => {
|
|
expect(shouldSaveEditor(key)).toBe(false);
|
|
expect(captureEditorGeneration(key)).toBe(0);
|
|
});
|
|
|
|
it('should make shouldSaveEditor true after a genuine edit', () => {
|
|
markEditorEdited(key);
|
|
expect(shouldSaveEditor(key)).toBe(true);
|
|
});
|
|
|
|
it('should clear dirty flag and set baseline after markEditorSaved', () => {
|
|
markEditorEdited(key);
|
|
markEditorSaved(key, 'saved value');
|
|
expect(shouldSaveEditor(key)).toBe(false);
|
|
});
|
|
|
|
it('should clear dirty flag without signalling an edit on resetEditorBaseline', () => {
|
|
markEditorEdited(key);
|
|
resetEditorBaseline(key, 'external value');
|
|
expect(shouldSaveEditor(key)).toBe(false);
|
|
});
|
|
|
|
it('should bump generation and clear dirty flag on invalidateEditor', () => {
|
|
markEditorEdited(key);
|
|
const before = captureEditorGeneration(key);
|
|
|
|
invalidateEditor(key);
|
|
|
|
expect(captureEditorGeneration(key)).toBe(before + 1);
|
|
|
|
// Resurrection guard: a subsequent lifecycle save must be a no-op.
|
|
expect(shouldSaveEditor(key)).toBe(false);
|
|
});
|
|
|
|
it('should make a captured generation stale after invalidateEditor', () => {
|
|
const captured = captureEditorGeneration(key);
|
|
expect(isEditorGenerationStale(key, captured)).toBe(false);
|
|
|
|
invalidateEditor(key);
|
|
|
|
expect(isEditorGenerationStale(key, captured)).toBe(true);
|
|
});
|
|
|
|
it('should treat a captured generation as stale when the key is absent', () => {
|
|
const captured = captureEditorGeneration(key);
|
|
clearEditor(key);
|
|
expect(isEditorGenerationStale(key, captured)).toBe(true);
|
|
});
|
|
|
|
it('should remove state on clearEditor so a fresh entry is created afterwards', () => {
|
|
markEditorEdited(key);
|
|
invalidateEditor(key);
|
|
expect(captureEditorGeneration(key)).toBe(1);
|
|
|
|
clearEditor(key);
|
|
|
|
// Fresh entry: generation reset to 0, clean.
|
|
expect(captureEditorGeneration(key)).toBe(0);
|
|
expect(shouldSaveEditor(key)).toBe(false);
|
|
});
|
|
|
|
it('should keep distinct keys independent', () => {
|
|
const otherKey = buildEditorKey(serverUrl, 'other-channel', rootId);
|
|
clearEditor(otherKey);
|
|
|
|
markEditorEdited(key);
|
|
invalidateEditor(key);
|
|
|
|
expect(shouldSaveEditor(otherKey)).toBe(false);
|
|
expect(captureEditorGeneration(otherKey)).toBe(0);
|
|
|
|
markEditorEdited(otherKey);
|
|
expect(shouldSaveEditor(otherKey)).toBe(true);
|
|
expect(shouldSaveEditor(key)).toBe(false);
|
|
|
|
clearEditor(otherKey);
|
|
});
|
|
});
|