DSH-Arch-Setup/test/ui-controls.test.mjs
Шурупов Илья Викторович 35f26fa32b Repair stale native composer layout
After removing the former collapse feature, force a one-time reflow of DSH input mirrors and textareas and refresh the conversation composer-height variable. Restore every temporary inline style immediately and mark each mounted input so normal prompt behavior remains entirely native afterward.
2026-09-14 13:54:52 +03:00

237 lines
9.4 KiB
JavaScript

import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import vm from 'node:vm'
class Element {
constructor(tagName) {
this.tagName = tagName
this.attributes = new Map()
this.children = []
this.listeners = new Map()
this.dataset = {}
this.parentElement = null
this.className = ''
this.textContent = ''
this.title = ''
this.type = ''
this.clientHeight = 32
this.offsetHeight = 32
this.isConnected = true
const properties = new Map()
this.style = {
position: '',
height: '',
setProperty: (name, value) => properties.set(name, String(value)),
getPropertyValue: (name) => properties.get(name) ?? '',
}
}
append(...children) {
for (const child of children) this.appendChild(child)
}
appendChild(child) {
if (child.parentElement) child.parentElement.children = child.parentElement.children.filter((candidate) => candidate !== child)
child.parentElement = this
this.children.push(child)
return child
}
insertBefore(child, reference) {
if (child.parentElement) child.parentElement.children = child.parentElement.children.filter((candidate) => candidate !== child)
const index = this.children.indexOf(reference)
child.parentElement = this
this.children.splice(index < 0 ? this.children.length : index, 0, child)
return child
}
addEventListener(name, listener) {
this.listeners.set(name, listener)
}
click() {
this.listeners.get('click')?.({ preventDefault() {}, stopPropagation() {}, target: this })
}
setAttribute(name, value) {
this.attributes.set(name, String(value))
}
getAttribute(name) {
return this.attributes.get(name) ?? null
}
removeAttribute(name) {
this.attributes.delete(name)
}
querySelector(selector) {
if (selector === ':scope > .dsh-chat-controls') return this.children.find((child) => child.hasClass('dsh-chat-controls')) ?? null
if (selector === 'header [class*="_headerUtilities"]') return this.descendants().find((child) => child.tagName === 'div' && child.className.includes('_headerUtilities')) ?? null
if (selector === '[data-input-mirror]') return this.descendants().find((child) => child.attributes.has('data-input-mirror')) ?? null
if (selector === 'textarea') return this.descendants().find((child) => child.tagName === 'textarea') ?? null
if (selector.startsWith('.')) return this.descendants().find((child) => child.hasClass(selector.slice(1))) ?? null
return null
}
querySelectorAll(selector) {
if (selector.startsWith('.')) return this.descendants().filter((child) => child.hasClass(selector.slice(1)))
return []
}
closest(selector) {
let current = this
while (current) {
if (selector === '[data-phase]' && current.attributes.has('data-phase')) return current
if (selector === '[data-composer-seat]' && current.attributes.has('data-composer-seat')) return current
current = current.parentElement
}
return null
}
hasClass(name) {
return this.className.split(/\s+/).includes(name)
}
descendants() {
return this.children.flatMap((child) => [child, ...child.descendants()])
}
remove() {
if (!this.parentElement) return
this.parentElement.children = this.parentElement.children.filter((child) => child !== this)
this.parentElement = null
}
}
const documentElement = new Element('html')
const head = new Element('head')
const body = new Element('body')
const conversation = new Element('div')
conversation.setAttribute('data-phase', 'active')
const header = new Element('header')
const titleRow = new Element('div')
const headerActions = new Element('div')
const git = new Element('div')
git.className = 'dsh-git-working-dirs'
const utilities = new Element('div')
utilities.className = 'fixture_headerUtilities'
headerActions.appendChild(git)
titleRow.append(headerActions, utilities)
header.appendChild(titleRow)
const seat = new Element('div')
seat.setAttribute('data-composer-seat', '')
seat.setAttribute('data-dsh-composer-collapsed', '1')
const inputScroll = new Element('div')
inputScroll.setAttribute('data-input-scroll', '')
const input = new Element('textarea')
const mirror = new Element('div')
mirror.setAttribute('data-input-mirror', '')
inputScroll.append(input, mirror)
seat.appendChild(inputScroll)
const nestedPhase = new Element('div')
nestedPhase.setAttribute('data-phase', 'streaming')
const strayControls = new Element('div')
strayControls.className = 'dsh-chat-controls'
nestedPhase.appendChild(strayControls)
conversation.append(header, nestedPhase, seat)
body.appendChild(conversation)
const documentListeners = new Map()
const allElements = () => [documentElement, head, body, ...head.descendants(), ...body.descendants()]
const document = {
documentElement,
head,
body,
createElement: (tagName) => new Element(tagName),
getElementById: (id) => allElements().find((element) => element.id === id) ?? null,
querySelectorAll(selector) {
if (selector === '[data-phase]') return allElements().filter((element) => element.attributes.has('data-phase'))
if (selector === '[data-composer-seat]') return allElements().filter((element) => element.attributes.has('data-composer-seat'))
if (selector === '[data-input-scroll]') return allElements().filter((element) => element.attributes.has('data-input-scroll'))
if (selector === '[data-dsh-composer-repaired="1"]') return allElements().filter((element) => element.getAttribute('data-dsh-composer-repaired') === '1')
if (selector === '.dsh-chat-controls') return allElements().filter((element) => element.hasClass('dsh-chat-controls'))
if (selector === '.dsh-hdr-menu-trigger' || selector === '[data-dsh-hdr-menu]' || selector === '[class*="_headerUtilities"]') return []
return []
},
addEventListener: (name, listener) => documentListeners.set(name, listener),
removeEventListener: (name) => documentListeners.delete(name),
}
const storage = new Map([['dsh-ui-tweaks:composer-collapsed', '1']])
const window = {
document,
localStorage: {
getItem: (key) => storage.get(key) ?? null,
setItem: (key, value) => storage.set(key, String(value)),
removeItem: (key) => storage.delete(key),
},
}
window.window = window
let definition
window.__ModuleLoader__ = { load: (candidate) => { definition = candidate } }
class MutationObserver {
observe() {}
disconnect() {}
}
const source = await readFile(new URL('../plugins/dsh-ui-tweaks/lib/client.js', import.meta.url), 'utf8')
vm.runInNewContext(source, {
window,
document,
MutationObserver,
requestAnimationFrame: (callback) => callback(),
setTimeout,
})
const plugin = definition.factory(() => {})
let cleanup
plugin.apply({ effect: (start) => { cleanup = start() } })
const controls = conversation.querySelector('.dsh-chat-controls')
assert(controls)
assert.equal(document.querySelectorAll('.dsh-chat-controls').length, 1)
assert.equal(strayControls.parentElement, null)
assert.equal(controls.parentElement, headerActions)
assert(controls.parentElement.children.indexOf(controls) < controls.parentElement.children.indexOf(git))
const zen = controls.querySelector('.dsh-zen-toggle')
assert.equal(controls.querySelector('.dsh-composer-collapse-toggle'), null)
assert.equal(zen.textContent, 'Zen')
assert.equal(seat.getAttribute('data-dsh-composer-collapsed'), null)
assert.equal(storage.has('dsh-ui-tweaks:composer-collapsed'), false)
assert.equal(inputScroll.getAttribute('data-dsh-composer-repaired'), '1')
assert.equal(mirror.style.position, '')
assert.equal(input.style.height, '')
assert.equal(conversation.style.getPropertyValue('--dsh-composer-height'), '32px')
zen.click()
assert.equal(documentElement.getAttribute('data-dsh-zen'), '1')
assert.equal(zen.textContent, 'Exit Zen')
assert.equal(seat.getAttribute('data-dsh-composer-collapsed'), null)
assert.equal(controls.parentElement, conversation)
assert.equal(controls.getAttribute('data-floating'), '1')
documentListeners.get('keydown')?.({ key: 'Escape' })
assert.equal(documentElement.getAttribute('data-dsh-zen'), null)
assert.equal(zen.textContent, 'Zen')
assert.equal(controls.parentElement, headerActions)
assert(controls.parentElement.children.indexOf(controls) < controls.parentElement.children.indexOf(git))
assert.equal(controls.getAttribute('data-floating'), null)
cleanup()
assert.equal(conversation.querySelector('.dsh-chat-controls'), null)
assert.equal(seat.getAttribute('data-dsh-composer-collapsed'), null)
assert.equal(inputScroll.getAttribute('data-dsh-composer-repaired'), null)
assert.equal(documentElement.getAttribute('data-dsh-zen'), null)
assert(source.includes('[data-sidebar-collapsed] [class*="_sidebarCol"] [class*="_regionArea"]'))
assert(source.includes('[class*="_detailsCol"]{visibility:hidden !important;pointer-events:none !important;border:0 !important}'))
assert(!source.includes('[class*="_detailsCol"],html[data-dsh-zen="1"] [class*="_handle"]{display:none'))
assert(source.includes('.dsh-chat-controls{z-index:12;display:flex;flex:none'))
assert(source.includes('.dsh-chat-controls[data-floating="1"]{position:absolute;right:24px;top:12px'))
assert(!source.includes('dsh-composer-collapse-toggle'))
assert(!source.includes('[data-composer-seat][data-dsh-composer-collapsed="1"]'))
assert(source.includes('[data-composer-seat]{position:absolute !important;left:0;right:0;bottom:0;visibility:hidden'))
assert(source.includes('[data-phase] header[class*="_header"]{display:none !important}'))
console.log('UI controls test passed')