WhatsApp stempelt beim Synchronisieren gelegentlich alte Nachrichten neu, im echten Export mehrere Nachrichten am 19.08.26 um 13:17. Die Position im Chat bildet die tatsächliche Reihenfolge verlässlich ab. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
170 lines
7.7 KiB
JavaScript
170 lines
7.7 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { test } from 'node:test';
|
|
import {
|
|
clipsInRange,
|
|
collectClips,
|
|
parseChat,
|
|
previousWorkWeek,
|
|
timestampFromFileName,
|
|
} from '../src/lib/whatsapp.js';
|
|
|
|
// Ausgedachte Zeilen im Format eines deutschen iPhone-Exports mit Medien,
|
|
// samt der unsichtbaren Richtungszeichen (\u200e), die WhatsApp einfügt.
|
|
const IOS_CHAT = [
|
|
'[21.09.26, 09:00:00] Anna Beispiel: Guten Morgen',
|
|
'\u200e[21.09.26, 10:15:30] Anna Beispiel: Ordnung am Platz \u200e<Anhang: 00000101-VIDEO-2026-09-21-10-15-30.mp4>',
|
|
'[21.09.26, 10:16:00] Ben Muster: Stark!',
|
|
'Zweite Zeile der Nachricht',
|
|
'\u200e[22.09.26, 08:00:00] ~\u202fCarla: \u200e<Anhang: 00000102-VIDEO-2026-09-22-08-00-00.mp4>',
|
|
// Außer der Reihe: im Chat später, laut Zeitstempel aber früher
|
|
'\u200e[21.09.26, 07:30:00] Ben Muster: Frühaufsteher \u200e<Anhang: 00000103-VIDEO-2026-09-21-07-30-00.mp4>',
|
|
'\u200e[22.09.26, 08:00:05] ~\u202fCarla: \u200e<Anhang: 00000104-PHOTO-2026-09-22-08-00-05.jpg>',
|
|
'[22.09.26, 09:00:00] Ben Muster: \u200eDiese Nachricht wurde gelöscht.',
|
|
'\u200e[23.09.26, 11:00:00] Anna Beispiel: Fehlt im Export \u200e<Anhang: 00000105-VIDEO-2026-09-23-11-00-00.mp4>',
|
|
'\u200e[28.09.26, 12:00:00] Anna Beispiel: Nächste Woche \u200e<Anhang: 00000106-VIDEO-2026-09-28-12-00-00.mp4>',
|
|
].join('\r\n');
|
|
|
|
const IOS_FILES = [
|
|
{ name: '_chat.txt', size: 1000 },
|
|
{ name: 'WhatsApp Chat/00000101-VIDEO-2026-09-21-10-15-30.mp4', size: 5000 },
|
|
{ name: 'WhatsApp Chat/00000102-VIDEO-2026-09-22-08-00-00.mp4', size: 7000 },
|
|
{ name: 'WhatsApp Chat/00000103-VIDEO-2026-09-21-07-30-00.mp4', size: 6000 },
|
|
{ name: 'WhatsApp Chat/00000104-PHOTO-2026-09-22-08-00-05.jpg', size: 100 },
|
|
{ name: 'WhatsApp Chat/00000106-VIDEO-2026-09-28-12-00-00.mp4', size: 8000 },
|
|
// Im Chat nicht erwähnt, Zeitpunkt nur aus dem Dateinamen
|
|
{ name: 'WhatsApp Chat/00000107-VIDEO-2026-09-22-15-00-00.mp4', size: 9000 },
|
|
];
|
|
|
|
test('iPhone-Export: Nachrichten, Absender, Zeitstempel und Anhänge', () => {
|
|
const messages = parseChat(IOS_CHAT);
|
|
assert.equal(messages.length, 9);
|
|
assert.deepEqual(messages[1], {
|
|
index: 1,
|
|
timestamp: '2026-09-21T10:15:30',
|
|
sender: 'Anna Beispiel',
|
|
text: 'Ordnung am Platz <Anhang: 00000101-VIDEO-2026-09-21-10-15-30.mp4>',
|
|
attachments: ['00000101-VIDEO-2026-09-21-10-15-30.mp4'],
|
|
});
|
|
assert.equal(messages[2].text, 'Stark!\nZweite Zeile der Nachricht');
|
|
assert.equal(messages[3].sender, '~ Carla');
|
|
assert.deepEqual(messages[6].attachments, []);
|
|
});
|
|
|
|
test('Videos folgen der Chatreihenfolge, Videos ohne Chatzeile dem Zeitstempel', () => {
|
|
const { clips } = collectClips(parseChat(IOS_CHAT), IOS_FILES);
|
|
assert.deepEqual(
|
|
clips.map((clip) => clip.file),
|
|
[
|
|
'00000101-VIDEO-2026-09-21-10-15-30.mp4',
|
|
'00000102-VIDEO-2026-09-22-08-00-00.mp4',
|
|
'00000103-VIDEO-2026-09-21-07-30-00.mp4',
|
|
'00000107-VIDEO-2026-09-22-15-00-00.mp4',
|
|
'00000106-VIDEO-2026-09-28-12-00-00.mp4',
|
|
],
|
|
);
|
|
});
|
|
|
|
test('Ein falsch nachgestempeltes Video bleibt an seinem Platz im Chat', () => {
|
|
// Wie im echten Export: WhatsApp hat das mittlere Video auf den 19. umgestempelt.
|
|
const chat = [
|
|
'\u200e[17.08.26, 09:48:18] Anna: Eins \u200e<Anhang: 00001338-VIDEO-2026-08-17-09-48-18.mp4>',
|
|
'\u200e[19.08.26, 13:17:37] Ben: Zwei \u200e<Anhang: 00001339-VIDEO-2026-08-19-13-17-37.mp4>',
|
|
'\u200e[17.08.26, 09:52:15] Carla: Drei \u200e<Anhang: 00001340-VIDEO-2026-08-17-09-52-15.mp4>',
|
|
].join('\n');
|
|
const files = [
|
|
{ name: '00001338-VIDEO-2026-08-17-09-48-18.mp4', size: 1 },
|
|
{ name: '00001339-VIDEO-2026-08-19-13-17-37.mp4', size: 2 },
|
|
{ name: '00001340-VIDEO-2026-08-17-09-52-15.mp4', size: 3 },
|
|
];
|
|
const { clips } = collectClips(parseChat(chat), files);
|
|
assert.deepEqual(clips.map((clip) => clip.caption), ['Eins', 'Zwei', 'Drei']);
|
|
});
|
|
|
|
test('Clips tragen Absender, Beschriftung und Pfad', () => {
|
|
const { clips } = collectClips(parseChat(IOS_CHAT), IOS_FILES);
|
|
const clip = clips.find((c) => c.file.startsWith('00000101'));
|
|
assert.equal(clip.sender, 'Anna Beispiel');
|
|
assert.equal(clip.caption, 'Ordnung am Platz');
|
|
assert.equal(clip.path, 'WhatsApp Chat/00000101-VIDEO-2026-09-21-10-15-30.mp4');
|
|
const orphan = clips.find((c) => c.file.startsWith('00000107'));
|
|
assert.equal(orphan.sender, null);
|
|
});
|
|
|
|
test('Im Chat genannte, aber fehlende Videos werden gemeldet', () => {
|
|
const { missing } = collectClips(parseChat(IOS_CHAT), IOS_FILES);
|
|
assert.deepEqual(missing.map((m) => m.file), ['00000105-VIDEO-2026-09-23-11-00-00.mp4']);
|
|
});
|
|
|
|
test('Bytegleiche Videos werden als Duplikat markiert', () => {
|
|
const chat = [
|
|
'\u200e[23.09.26, 10:03:49] Tom: Autostart \u200e<Anhang: 00000201-VIDEO-2026-09-23-10-03-49.mp4>',
|
|
'\u200e[23.09.26, 10:03:53] Tom: Autostart \u200e<Anhang: 00000202-VIDEO-2026-09-23-10-03-53.mp4>',
|
|
].join('\n');
|
|
const files = [
|
|
{ name: '00000201-VIDEO-2026-09-23-10-03-49.mp4', size: 4242 },
|
|
{ name: '00000202-VIDEO-2026-09-23-10-03-53.mp4', size: 4242 },
|
|
];
|
|
const { clips } = collectClips(parseChat(chat), files);
|
|
assert.equal(clips[0].duplicateOf, undefined);
|
|
assert.equal(clips[1].duplicateOf, '00000201-VIDEO-2026-09-23-10-03-49.mp4');
|
|
});
|
|
|
|
test('Android-Export (deutsch) mit Systemmeldung', () => {
|
|
const chat = [
|
|
'21.09.26, 10:15 - Anna: VID-20260921-WA0003.mp4 (Datei angehängt)',
|
|
'Ordnung am Platz',
|
|
'21.09.26, 10:20 - Ben hat die Gruppe verlassen',
|
|
'21.09.26, 11:00 - Ben: Hallo',
|
|
].join('\n');
|
|
const messages = parseChat(chat);
|
|
assert.equal(messages.length, 3);
|
|
assert.equal(messages[0].timestamp, '2026-09-21T10:15:00');
|
|
assert.deepEqual(messages[0].attachments, ['VID-20260921-WA0003.mp4']);
|
|
assert.equal(messages[1].sender, null);
|
|
|
|
const { clips } = collectClips(messages, [{ name: 'VID-20260921-WA0003.mp4', size: 1 }]);
|
|
assert.equal(clips[0].caption, 'Ordnung am Platz');
|
|
});
|
|
|
|
test('Englischer Export mit Monat zuerst und AM/PM', () => {
|
|
const messages = parseChat(
|
|
[
|
|
'9/21/26, 4:36\u202fPM - Anna: VID-20260921-WA0001.mp4 (file attached)',
|
|
'[9/22/26, 12:05:00 AM] Ben: <attached: 00000001-VIDEO-2026-09-22-00-05-00.mp4>',
|
|
].join('\n'),
|
|
);
|
|
assert.equal(messages[0].timestamp, '2026-09-21T16:36:00');
|
|
assert.equal(messages[1].timestamp, '2026-09-22T00:05:00');
|
|
assert.deepEqual(messages[1].attachments, ['00000001-VIDEO-2026-09-22-00-05-00.mp4']);
|
|
});
|
|
|
|
test('Schrägstrich-Datum mit Tag über 12 gilt als Tag zuerst', () => {
|
|
const messages = parseChat('24/09/2026, 16:36 - Anna: Hallo\n05/09/2026, 10:00 - Ben: Hi');
|
|
assert.equal(messages[0].timestamp, '2026-09-24T16:36:00');
|
|
assert.equal(messages[1].timestamp, '2026-09-05T10:00:00');
|
|
});
|
|
|
|
test('Zeitstempel aus Dateinamen', () => {
|
|
assert.equal(timestampFromFileName('00001338-VIDEO-2026-09-24-16-36-18.mp4'), '2026-09-24T16:36:18');
|
|
assert.equal(timestampFromFileName('VID-20260924-WA0012.mp4'), '2026-09-24T00:00:00');
|
|
assert.equal(timestampFromFileName('film.mp4'), null);
|
|
});
|
|
|
|
test('Zeitraumfilter schließt beide Tage ein', () => {
|
|
const { clips } = collectClips(parseChat(IOS_CHAT), IOS_FILES);
|
|
const week = clipsInRange(clips, '2026-09-21', '2026-09-25');
|
|
assert.equal(week.length, 4);
|
|
assert.ok(week.every((clip) => clip.timestamp < '2026-09-26'));
|
|
});
|
|
|
|
test('Vergangene Woche ist Montag bis Freitag der Vorwoche', () => {
|
|
// Dienstag, 29.09.2026
|
|
assert.deepEqual(previousWorkWeek(new Date(2026, 8, 29)), { from: '2026-09-21', to: '2026-09-25' });
|
|
// Montag, 28.09.2026
|
|
assert.deepEqual(previousWorkWeek(new Date(2026, 8, 28)), { from: '2026-09-21', to: '2026-09-25' });
|
|
// Sonntag, 27.09.2026 gehört noch zur Woche ab 21.09.
|
|
assert.deepEqual(previousWorkWeek(new Date(2026, 8, 27)), { from: '2026-09-14', to: '2026-09-18' });
|
|
// Über den Jahreswechsel
|
|
assert.deepEqual(previousWorkWeek(new Date(2027, 0, 5)), { from: '2026-12-28', to: '2027-01-01' });
|
|
});
|