Files
aiturk-hermes-ide/apps/chrome/tests/panel.test.mjs
T

104 lines
5.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import { createRequire } from 'node:module'
import { test } from 'node:test'
import { capturePageContext } from '../extension/page-context.js'
const { JSDOM } = createRequire(import.meta.url)('jsdom')
const eventHub = () => ({ listeners: [], addListener(callback) { this.listeners.push(callback) }, emit(value) { this.listeners.forEach(callback => callback(value)) } })
const tick = () => new Promise(resolve => setImmediate(resolve))
async function settle(condition) {
for (let count = 0; count < 100; count++) { if (condition()) return; await tick() }
assert.ok(condition(), 'UI did not settle')
}
async function panel(t, respond) {
const html = await readFile(new URL('../extension/panel.html', import.meta.url), 'utf8')
const dom = new JSDOM(html, { url: 'https://extension-fixture.test/panel.html' })
const calls = []
const port = { onMessage: eventHub(), onDisconnect: eventHub(), disconnect() {}, postMessage(message) {
calls.push(message)
queueMicrotask(() => respond(message, frame => port.onMessage.emit(frame), port))
} }
Object.assign(globalThis, { window: dom.window, document: dom.window.document, Option: dom.window.Option,
chrome: { runtime: { connectNative: () => port }, storage: { local: { get: async () => ({}), set: async () => {} } } } })
await import(`../extension/panel.js?test=${Math.random()}`)
t.after(() => { dom.window.close(); for (const key of ['window', 'document', 'Option', 'chrome']) delete globalThis[key] })
const get = id => dom.window.document.getElementById(id)
await settle(() => !get('sessions').disabled)
return { dom, calls, port, get, async select(id) {
get('sessions').value = id
get('sessions').dispatchEvent(new dom.window.Event('change'))
await settle(() => !get('sessions').disabled)
} }
}
function fixtureResponse(message, emit) {
const result = message.action === 'status' ? { profile: 'default' }
: message.action === 'sessions.list' ? { profile: 'default', sessions: [{ id: 'one', title: 'Birinci iş' }, { id: 'two', title: 'İkinci iş' }] }
: message.action === 'session.open' ? { messages: [{ role: 'user', text: '<img src=x onerror=alert(1)> önceden yazıldı' }], model: 'IDE modeli' }
: { messages: [] }
emit({ id: message.id, result })
}
test('panel safely renders real history and puts the submitted user before early streaming tokens', async t => {
const p = await panel(t, (message, emit) => {
if (message.action !== 'message.send') return fixtureResponse(message, emit)
emit({ event: 'message.start', sessionId: 'one' })
emit({ event: 'message.delta', sessionId: 'one', text: 'İlk yanıt' })
emit({ id: message.id, result: { accepted: true } })
})
await p.select('one')
assert.equal(p.get('transcript').querySelector('img'), null)
assert.ok(p.get('transcript').textContent.includes('<img'))
p.get('message').value = 'Buradan devam edelim'
p.get('composer').dispatchEvent(new p.dom.window.Event('submit', { cancelable: true }))
await settle(() => p.get('message').value === '')
const messages = [...p.get('transcript').querySelectorAll('.text')].map(node => node.textContent)
assert.deepEqual(messages.slice(-2), ['Buradan devam edelim', 'İlk yanıt'])
assert.equal(p.calls.filter(call => call.action === 'message.send').length, 1)
assert.equal(p.calls.find(call => call.action === 'message.send').params.sessionId, 'one')
})
test('task switching drops old events; disconnect keeps draft and does not retry a mutation', async t => {
const p = await panel(t, (message, emit, port) => {
if (message.action === 'message.send') { port.onDisconnect.emit(); return }
fixtureResponse(message, emit)
})
await p.select('one')
await p.select('two')
p.port.onMessage.emit({ event: 'message.delta', sessionId: 'one', text: 'YANLIŞ KONUŞMA' })
assert.ok(!p.get('transcript').textContent.includes('YANLIŞ'))
p.get('message').value = 'Kaybolmasın'
p.get('composer').dispatchEvent(new p.dom.window.Event('submit', { cancelable: true }))
await settle(() => p.get('notice').textContent.includes('Taslağınız korundu'))
assert.equal(p.get('message').value, 'Kaybolmasın')
assert.equal(p.calls.filter(call => call.action === 'message.send').length, 1)
assert.equal(p.get('send').disabled, true)
})
test('events arriving while history opens are not silently lost', async t => {
const p = await panel(t, (message, emit) => {
if (message.action === 'session.open') {
emit({ event: 'message.start', sessionId: message.params.sessionId })
emit({ event: 'message.delta', sessionId: message.params.sessionId, text: 'Açılırken gelen yanıt' })
}
fixtureResponse(message, emit)
})
await p.select('one')
assert.ok(p.get('transcript').textContent.includes('Açılırken gelen yanıt'))
})
test('explicit page capture excludes form values and strips URL credentials in query and fragment', t => {
const dom = new JSDOM('<body><p>Görünür içerik</p><input value="secret"><textarea>private draft</textarea><div hidden>hidden secret</div><script>private code</script></body>',
{ url: 'https://example.test/page?token=secret#private' })
Object.assign(globalThis, { window: dom.window, document: dom.window.document, location: dom.window.location,
NodeFilter: dom.window.NodeFilter, getComputedStyle: dom.window.getComputedStyle })
dom.window.HTMLElement.prototype.getClientRects = () => [{ width: 100 }]
t.after(() => { dom.window.close(); for (const key of ['window', 'document', 'location', 'NodeFilter', 'getComputedStyle']) delete globalThis[key] })
const context = capturePageContext()
assert.equal(context.text, 'Görünür içerik')
assert.equal(context.url, 'https://example.test/page')
assert.equal(context.selection, false)
})