feat(chat): preserve message drafts in session temp store

This commit is contained in:
2026-10-15 17:45:17 +02:00
parent e933782731
commit 697f538b6e
8 changed files with 426 additions and 16 deletions

View File

@@ -3,6 +3,7 @@ const assert = require('node:assert/strict')
const fs = require('node:fs')
const vm = require('node:vm')
const vue = require('vue')
const { createDraftHarness } = require('./helpers/chat-drafts.cjs')
const settle = () => new Promise(setImmediate)
const deferred = () => {
@@ -33,6 +34,7 @@ function fixture(override = () => undefined) {
function setup(api = fixture(), query = {}) {
const auth = vue.reactive({ user: { id: 'user-1' }, activeTenant: '1' })
const drafts = createDraftHarness({ auth })
const route = vue.reactive({ query })
const calls = [], toasts = [], timers = new Map()
const listeners = new Map()
@@ -47,6 +49,7 @@ function setup(api = fixture(), query = {}) {
ref: vue.ref, reactive: vue.reactive, computed: vue.computed, watch: vue.watch, nextTick: vue.nextTick,
useToast: () => ({ add: item => toasts.push(item) }),
useAuthStore: () => auth, useRoute: () => route,
useChatDraft: drafts.useChatDraft,
useRouter: () => ({ replace: async location => { route.query = location.query } }),
useNuxtApp: () => ({ $api: (url, options) => { calls.push({ url, options }); return api(url, options) } }),
onMounted: handler => { mount = handler }, onBeforeUnmount: handler => { unmount = handler },
@@ -54,7 +57,7 @@ function setup(api = fixture(), query = {}) {
clearInterval: id => timers.delete(id), document,
})
scope.run(() => vm.runInContext(source, context))
return { auth, route, calls, toasts, timers, listeners, document, mount, close: () => { unmount(); scope.stop() }, run: code => vm.runInContext(code, context) }
return { auth, route, calls, toasts, timers, listeners, document, mount, drafts, close: () => { unmount(); scope.stop(); drafts.close() }, run: code => vm.runInContext(code, context) }
}
test('only member rooms enter normal chat, with separate keys, groups and unread counts', async t => {
@@ -377,3 +380,69 @@ test('native image-only sends retain the draft after an error and clear it after
assert.equal(ui.run('draftImage.value'), null)
assert.ok(ui.calls.some(c => c.options?.body?.image?.name === 'Bild'))
})
test('native drafts survive switching chats, hiding a room and reloading its history', async t => {
const ui = setup(fixture(url => url.endsWith('/project-rooms') ? { rooms: [{ key: 'project_1', exists: true, type: 'project' }] } : undefined))
t.after(ui.close)
ui.mount()
await settle()
ui.run('draft.value = " Hallo\\nWelt 👋 "; draftImage.value = { dataUrl: "data:image/webp;base64,AAAA", name: "Skizze" }')
await ui.run('selectRoom(decoratedRooms.value.find(r => r.key === "project_1"))')
assert.equal(ui.run('draft.value'), '')
assert.equal(ui.run('draftImage.value'), null)
ui.run('draft.value = "Projektentwurf"')
await ui.run('selectRoom(decoratedRooms.value.find(r => r.key === "allgemein"))')
assert.equal(ui.run('draft.value'), ' Hallo\nWelt 👋 ')
assert.equal(ui.run('draftImage.value.name'), 'Skizze')
await ui.run('loadActiveRoom()')
assert.equal(ui.run('draft.value'), ' Hallo\nWelt 👋 ')
await ui.run('setRoomHidden(activeRoom.value, true)')
assert.equal(ui.run('draft.value'), 'Projektentwurf')
await ui.run('setRoomHidden(hiddenRooms.value[0], false)')
assert.equal(ui.run('draft.value'), ' Hallo\nWelt 👋 ')
})
test('failed sends retain the exact draft after switching away, and late success clears only the sent room', async t => {
const pending = deferred()
let fail = true
const ui = setup(fixture((url, options) => {
if (url.endsWith('/messages') && options?.method === 'POST') {
if (fail) throw { data: { message: 'Offline' } }
return pending.promise
}
}))
t.after(ui.close)
ui.mount()
await settle()
ui.run('draft.value = " Noch nicht fertig\\n "')
await ui.run('sendMessage()')
assert.equal(ui.run('draft.value'), ' Noch nicht fertig\n ')
fail = false
const sending = ui.run('sendMessage()')
await ui.run('selectRoom(decoratedRooms.value.find(r => r.source === "collaboration"))')
pending.resolve({ id: 2, body: 'Noch nicht fertig' })
await sending
await ui.run('selectRoom(decoratedRooms.value.find(r => r.key === "allgemein"))')
assert.equal(ui.run('draft.value'), '')
assert.equal(Object.keys(ui.drafts.storage).length, 0)
})
test('editing a room after returning to a pending send preserves the new draft', async t => {
const pending = deferred()
const ui = setup(fixture((url, options) => url.endsWith('/messages') && options?.method === 'POST' ? pending.promise : undefined))
t.after(ui.close)
ui.mount()
await settle()
ui.run('draft.value = "Vorheriger Entwurf"')
const sending = ui.run('sendMessage()')
await ui.run('selectRoom(decoratedRooms.value.find(r => r.source === "collaboration"))')
await ui.run('selectRoom(decoratedRooms.value.find(r => r.key === "allgemein"))')
assert.equal(ui.run('draftSending.value'), true)
await ui.run('sendMessage()')
assert.equal(ui.calls.filter(({ url, options }) => url.endsWith('/messages') && options?.method === 'POST').length, 1)
ui.run('draft.value = "Weitergeschrieben"')
pending.resolve({ id: 2, body: 'Vorheriger Entwurf' })
await sending
assert.equal(ui.run('draft.value'), 'Weitergeschrieben')
assert.equal(ui.run('draftSending.value'), false)
})