DSH-Arch-Setup/test/ui-controls.test.mjs
Шурупов Илья Викторович 3eccc0411d Place chat controls before header utilities
Insert Chat and Zen immediately before the Git utility group in normal mode, then relocate them to the conversation root while Zen hides the header. Collapse the composer with measurement-safe clipping instead of display none to prevent incorrect empty textarea height after restoring it.
2026-09-14 11:40:14 +03:00

185 lines
6.8 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 = ''
}
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.startsWith('.')) return this.descendants().find((child) => child.hasClass(selector.slice(1))) ?? null
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 utilities = new Element('div')
utilities.className = 'fixture_headerUtilities'
titleRow.appendChild(utilities)
header.appendChild(titleRow)
const seat = new Element('div')
seat.setAttribute('data-composer-seat', '')
conversation.append(header, 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 === '.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()
const window = {
document,
localStorage: {
getItem: (key) => storage.get(key) ?? null,
setItem: (key, value) => storage.set(key, String(value)),
},
}
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(controls.parentElement, titleRow)
assert(controls.parentElement.children.indexOf(controls) < controls.parentElement.children.indexOf(utilities))
const collapse = controls.querySelector('.dsh-composer-collapse-toggle')
const zen = controls.querySelector('.dsh-zen-toggle')
assert.equal(collapse.textContent, '⌄ Chat')
assert.equal(zen.textContent, 'Zen')
collapse.click()
assert.equal(seat.getAttribute('data-dsh-composer-collapsed'), '1')
assert.equal(storage.get('dsh-ui-tweaks:composer-collapsed'), '1')
assert.equal(collapse.textContent, '⌃ Chat')
zen.click()
assert.equal(documentElement.getAttribute('data-dsh-zen'), '1')
assert.equal(zen.textContent, 'Exit Zen')
assert.equal(seat.getAttribute('data-dsh-composer-collapsed'), '1')
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, titleRow)
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(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('[data-composer-seat][data-dsh-composer-collapsed="1"]{max-height:0 !important'))
assert(!source.includes('[data-composer-seat][data-dsh-composer-collapsed="1"]{display:none'))
assert(source.includes('[data-phase] header[class*="_header"]{display:none !important}'))
console.log('UI controls test passed')