DSH-Arch-Setup/test/ui-controls.test.mjs
Шурупов Илья Викторович 500b5532a8 Add in-chat collapse and Zen controls
Hide workspace and agent content when the native sidebar rail is collapsed. Add persistent whole-composer collapse controls and a temporary Zen mode that removes side panels, chat header, and composer while keeping an in-chat exit control available. Cover state transitions, Escape handling, React rerender idempotence, and cleanup with a dependency-free DOM regression test.
2026-09-14 10:49:53 +03:00

152 lines
4.6 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) {
child.parentElement = this
this.children.push(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.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 seat = new Element('div')
seat.setAttribute('data-composer-seat', '')
body.appendChild(seat)
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-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 = seat.querySelector(':scope > .dsh-chat-controls')
assert(controls)
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')
documentListeners.get('keydown')?.({ key: 'Escape' })
assert.equal(documentElement.getAttribute('data-dsh-zen'), null)
assert.equal(zen.textContent, 'Zen')
cleanup()
assert.equal(seat.querySelector(':scope > .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"]'))
console.log('UI controls test passed')