Chat-Baustein: Option requestHeaders fuer Hochladen und Entfernen (Ticketsystem mit JWT)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
48dd19b75a
commit
a6ac4510cd
2 files changed
+111
-2
No files matched your search
+27
-2
@@ -535,7 +535,9 @@
|
||||
/**
|
||||
* Eingabezeile mit Anhaengen und Sprachaufnahme.
|
||||
* options: root, dropZone, uploadUrl (POST roh, DELETE uploadUrl/id), send({ text, attachmentIds }) -> message,
|
||||
* onSent(message), placeholder, placeholderShort (schmale Bildschirme), disabled, disabledText
|
||||
* onSent(message), placeholder, placeholderShort (schmale Bildschirme), disabled, disabledText,
|
||||
* requestHeaders() -> Objekt oder Promise (zusaetzliche Header fuer Hochladen und Entfernen, z. B. das
|
||||
* Bearer-JWT des Ticketsystems; ohne die Option wie bisher nur das Cookie)
|
||||
*/
|
||||
function create(options) {
|
||||
const opts = { placeholder: 'Nachricht schreiben…', disabled: false, disabledText: '', onSent() {}, ...options }
|
||||
@@ -708,9 +710,15 @@
|
||||
return item
|
||||
}
|
||||
|
||||
function extraHeaders() {
|
||||
return Promise.resolve(opts.requestHeaders ? opts.requestHeaders() : {}).then((headers) => headers || {})
|
||||
}
|
||||
|
||||
function deleteOnServer(item) {
|
||||
if (!item.meta || !item.meta.id) return
|
||||
fetch(`${opts.uploadUrl}/${encodeURIComponent(item.meta.id)}`, { method: 'DELETE', credentials: 'same-origin' }).catch(() => {})
|
||||
extraHeaders()
|
||||
.then((headers) => fetch(`${opts.uploadUrl}/${encodeURIComponent(item.meta.id)}`, { method: 'DELETE', credentials: 'same-origin', headers }))
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
function dropItem(item) {
|
||||
@@ -743,9 +751,26 @@
|
||||
item.status = 'uploading'
|
||||
item.progress = 0
|
||||
item.error = ''
|
||||
renderTray()
|
||||
extraHeaders().then(
|
||||
(headers) => {
|
||||
// Waehrend des Wartens entfernt oder Eingabezeile weg: nicht mehr hochladen
|
||||
if (destroyed || !items.includes(item)) return
|
||||
sendUpload(item, headers)
|
||||
},
|
||||
(err) => {
|
||||
item.status = 'error'
|
||||
item.error = (err && err.message) || 'Anmeldung abgelaufen. Bitte Seite neu laden.'
|
||||
afterUpload(item)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function sendUpload(item, headers) {
|
||||
const xhr = new XMLHttpRequest()
|
||||
item.xhr = xhr
|
||||
xhr.open('POST', opts.uploadUrl)
|
||||
for (const [name, value] of Object.entries(headers)) xhr.setRequestHeader(name, value)
|
||||
xhr.setRequestHeader('Content-Type', 'application/octet-stream')
|
||||
xhr.setRequestHeader('X-File-Name', encodeURIComponent(item.name))
|
||||
if (item.hint) xhr.setRequestHeader('X-Attachment-Kind', item.hint)
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
// Playwright-Checks Chat-Baustein: Option requestHeaders (Ticketsystem mit JWT) fuer Hochladen und Entfernen.
|
||||
// Nicht Teil von npm test. Aufruf: node test/ui/composer-headers.ui.mjs
|
||||
import http from 'node:http'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { chromium } from '/root/.npm/_npx/db89d7302a373f10/node_modules/playwright-core/index.mjs'
|
||||
|
||||
const pub = path.join(path.dirname(fileURLToPath(import.meta.url)), '../../public')
|
||||
// Testseite: ?mode=ok (async Header), none (ohne Option), fail (Header verworfen)
|
||||
const PAGE = `<!doctype html><html lang="de"><head><meta charset="utf-8"><link rel="stylesheet" href="/chat-composer.css"></head>
|
||||
<body style="background:#111;color:#fff"><div id="zone"><div id="root"></div></div><script src="/chat-composer.js"></script><script>
|
||||
const mode = new URLSearchParams(location.search).get('mode')
|
||||
const opts = { root: document.getElementById('root'), dropZone: document.getElementById('zone'), uploadUrl: '/upload',
|
||||
send: async () => ({ id: 'm1', text: '', attachments: [] }) }
|
||||
if (mode === 'ok') opts.requestHeaders = async () => { await new Promise((r) => setTimeout(r, 20)); return { Authorization: 'Bearer test' } }
|
||||
if (mode === 'fail') opts.requestHeaders = () => Promise.reject(new Error('JWT weg'))
|
||||
window.composer = ChatComposer.create(opts)
|
||||
</script></body></html>`
|
||||
|
||||
const server = http.createServer(async (req, res) => {
|
||||
const url = new URL(req.url, 'http://x')
|
||||
if (url.pathname === '/composer-test.html') {
|
||||
res.writeHead(200, { 'content-type': 'text/html' })
|
||||
return res.end(PAGE)
|
||||
}
|
||||
try {
|
||||
const file = path.join(pub, url.pathname)
|
||||
const data = await readFile(file)
|
||||
res.writeHead(200, { 'content-type': file.endsWith('.js') ? 'text/javascript' : 'text/css' })
|
||||
res.end(data)
|
||||
} catch {
|
||||
res.writeHead(404)
|
||||
res.end()
|
||||
}
|
||||
})
|
||||
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve))
|
||||
const base = `http://127.0.0.1:${server.address().port}`
|
||||
|
||||
let failed = 0
|
||||
const check = (name, ok, detail = '') => {
|
||||
console.log(`${ok ? 'OK ' : 'FAIL'} ${name}${detail ? ` (${detail})` : ''}`)
|
||||
if (!ok) failed += 1
|
||||
}
|
||||
|
||||
const browser = await chromium.launch()
|
||||
try {
|
||||
const run = async (mode) => {
|
||||
const page = await browser.newPage()
|
||||
page.setDefaultTimeout(30000)
|
||||
const calls = []
|
||||
await page.route('**/upload**', (route) => {
|
||||
const req = route.request()
|
||||
calls.push({ method: req.method(), path: new URL(req.url()).pathname, auth: req.headers().authorization || null })
|
||||
if (req.method() === 'DELETE') return route.fulfill({ status: 204, body: '' })
|
||||
return route.fulfill({ status: 201, contentType: 'application/json', body: JSON.stringify({ attachment: { id: 'a1', name: 'notiz.txt', type: 'text/plain', size: 5, kind: 'file' } }) })
|
||||
})
|
||||
await page.goto(`${base}/composer-test.html?mode=${mode}`)
|
||||
await page.locator('#root input[type=file]:not([accept])').setInputFiles({ name: 'notiz.txt', mimeType: 'text/plain', buffer: Buffer.from('hallo') })
|
||||
await page.waitForTimeout(800)
|
||||
return { page, calls }
|
||||
}
|
||||
|
||||
const ok = await run('ok')
|
||||
const up = ok.calls.find((c) => c.method === 'POST')
|
||||
check('mit_header_upload', up?.auth === 'Bearer test', JSON.stringify(up))
|
||||
await ok.page.click('.chat-tray-remove')
|
||||
await ok.page.waitForTimeout(500)
|
||||
const del = ok.calls.find((c) => c.method === 'DELETE')
|
||||
check('mit_header_delete', del?.path === '/upload/a1' && del?.auth === 'Bearer test', JSON.stringify(del))
|
||||
|
||||
const none = await run('none')
|
||||
const up2 = none.calls.find((c) => c.method === 'POST')
|
||||
check('ohne_option_kein_header', Boolean(up2) && up2.auth === null, JSON.stringify(up2))
|
||||
|
||||
const fail = await run('fail')
|
||||
const text = await fail.page.textContent('#root')
|
||||
check('header_fehler', fail.calls.length === 0 && text.includes('JWT weg'), `${fail.calls.length} Aufrufe, Text: ${text.slice(0, 120)}`)
|
||||
} finally {
|
||||
await browser.close()
|
||||
server.close()
|
||||
}
|
||||
console.log(failed ? `${failed} Check(s) fehlgeschlagen` : 'alle Checks ok')
|
||||
process.exit(failed ? 1 : 0)
|
||||
Reference in new issue
Block a user