feat: Initial clean release of CHM
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
这个提交包含在:
@@ -0,0 +1,140 @@
|
||||
import { setupDatabaseAndSynchronizer, switchClient } from '../../testing/test-utils';
|
||||
import Note from '../../models/Note';
|
||||
import Setting from '../../models/Setting';
|
||||
import ConflictNoteState from '../../models/ConflictNoteState';
|
||||
import loadConflictData, { ConflictDataStatus } from './loadConflictData';
|
||||
import { ConflictNoteStateEntity } from '../database/types';
|
||||
|
||||
// The remote version stays as the original note, and the conflict note is the copy of the local version
|
||||
const createConflictNote = async (localBody: string, remoteBody = '', localTitle = 'Title', remoteTitle = 'Title') => {
|
||||
const original = await Note.save({ title: remoteTitle, body: remoteBody });
|
||||
return Note.save({ title: localTitle, body: localBody, is_conflict: 1, conflict_original_id: original.id });
|
||||
};
|
||||
|
||||
const saveState = async (noteId: string, state: Partial<ConflictNoteStateEntity>) => {
|
||||
await ConflictNoteState.save({
|
||||
note_id: noteId,
|
||||
base_body: '',
|
||||
base_title: '',
|
||||
remote_updated_time: 0,
|
||||
...state,
|
||||
});
|
||||
};
|
||||
|
||||
describe('loadConflictData', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
await setupDatabaseAndSynchronizer(1);
|
||||
await switchClient(1);
|
||||
Setting.setValue('featureFlag.conflictResolution', true);
|
||||
});
|
||||
|
||||
test('should three-way merge a note that has a base', async () => {
|
||||
const note = await createConflictNote('one\ntwo\nthree', 'one\ntwo\nTHREE');
|
||||
await saveState(note.id, { base_body: 'one\ntwo\nthree' });
|
||||
|
||||
const data = await loadConflictData(note.id);
|
||||
|
||||
expect(data.status).toBe(ConflictDataStatus.Ok);
|
||||
expect(data.sections.some(s => s.type === 'conflict')).toBe(false);
|
||||
expect(data.sections.some(s => s.type === 'auto-merged')).toBe(true);
|
||||
});
|
||||
|
||||
test('should report a conflict section when both sides changed the same line', async () => {
|
||||
const note = await createConflictNote('one\nMINE', 'one\nTHEIRS');
|
||||
await saveState(note.id, { base_body: 'one\ntwo' });
|
||||
|
||||
const data = await loadConflictData(note.id);
|
||||
|
||||
expect(data.status).toBe(ConflictDataStatus.Ok);
|
||||
const conflict = data.sections.find(s => s.type === 'conflict');
|
||||
expect(conflict).toBeTruthy();
|
||||
expect(conflict.localText).toBe('MINE');
|
||||
expect(conflict.remoteText).toBe('THEIRS');
|
||||
});
|
||||
|
||||
test('should fall back to a two-way diff when there is no base', async () => {
|
||||
const note = await createConflictNote('same\nMINE', 'same\nTHEIRS');
|
||||
await saveState(note.id, { base_body: '' });
|
||||
|
||||
const data = await loadConflictData(note.id);
|
||||
|
||||
expect(data.status).toBe(ConflictDataStatus.Ok);
|
||||
expect(data.sections.filter(s => s.type === 'unchanged').length).toBeGreaterThan(0);
|
||||
expect(data.sections.some(s => s.type === 'conflict')).toBe(true);
|
||||
expect(data.sections.some(s => s.type === 'auto-merged')).toBe(false);
|
||||
});
|
||||
|
||||
test('should report the title versions and whether they differ', async () => {
|
||||
const note = await createConflictNote('body', 'body', 'My title', 'Their title');
|
||||
await saveState(note.id, {});
|
||||
|
||||
const data = await loadConflictData(note.id);
|
||||
|
||||
expect(data.localTitle).toBe('My title');
|
||||
expect(data.remoteTitle).toBe('Their title');
|
||||
expect(data.titleConflict).toBe(true);
|
||||
});
|
||||
|
||||
test('should not flag a title conflict when both titles match', async () => {
|
||||
const note = await createConflictNote('body', 'body', 'Same', 'Same');
|
||||
await saveState(note.id, {});
|
||||
|
||||
const data = await loadConflictData(note.id);
|
||||
|
||||
expect(data.titleConflict).toBe(false);
|
||||
});
|
||||
|
||||
test('should recompute on demand rather than reading stored sections', async () => {
|
||||
const note = await createConflictNote('one\ntwo', 'one\ntwo');
|
||||
await saveState(note.id, { base_body: 'one\ntwo' });
|
||||
|
||||
const before = await loadConflictData(note.id);
|
||||
expect(before.sections.every(s => s.type === 'unchanged')).toBe(true);
|
||||
|
||||
await Note.save({ id: note.conflict_original_id, body: 'one\nTWO' });
|
||||
|
||||
const after = await loadConflictData(note.id);
|
||||
expect(after.sections.some(s => s.type === 'auto-merged')).toBe(true);
|
||||
});
|
||||
|
||||
test('should be unavailable when the feature flag is off', async () => {
|
||||
Setting.setValue('featureFlag.conflictResolution', false);
|
||||
const note = await createConflictNote('one\ntwo', 'one\nTWO');
|
||||
await saveState(note.id, { base_body: 'one\ntwo' });
|
||||
|
||||
const data = await loadConflictData(note.id);
|
||||
|
||||
expect(data.status).toBe(ConflictDataStatus.Unavailable);
|
||||
expect(data.sections).toEqual([]);
|
||||
});
|
||||
|
||||
test('should be unavailable when there is no state row', async () => {
|
||||
const note = await createConflictNote('body');
|
||||
|
||||
const data = await loadConflictData(note.id);
|
||||
|
||||
expect(data.status).toBe(ConflictDataStatus.Unavailable);
|
||||
expect(data.sections).toEqual([]);
|
||||
});
|
||||
|
||||
test('should be unavailable when the note does not exist', async () => {
|
||||
const data = await loadConflictData('7a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d');
|
||||
|
||||
expect(data.status).toBe(ConflictDataStatus.Unavailable);
|
||||
});
|
||||
|
||||
test.each([
|
||||
['encrypted', { encryption_applied: 1 }],
|
||||
['locked', { is_locked: 1 }],
|
||||
])('should be unavailable when the note is %s', async (_label, fields) => {
|
||||
const note = await createConflictNote('body', 'other');
|
||||
await Note.save({ id: note.id, ...fields });
|
||||
await saveState(note.id, { base_body: 'body' });
|
||||
|
||||
const data = await loadConflictData(note.id);
|
||||
|
||||
expect(data.status).toBe(ConflictDataStatus.Unavailable);
|
||||
});
|
||||
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户