118 lines
5.9 KiB
JavaScript
118 lines
5.9 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import { readFile } from 'node:fs/promises'
|
|
import test from 'node:test'
|
|
import { buildSystemPluginClient } from '../tools/build-system-plugin-client.mjs'
|
|
|
|
const root = new URL('../../environment/resources/plugins/settings-navigation/package/', import.meta.url)
|
|
|
|
test('Settings Navigation is a browser-only optional System Plugin', async () => {
|
|
const metadata = JSON.parse(await readFile(new URL('package.json', root)))
|
|
const patch = JSON.parse(await readFile(new URL('cordis.patch.json', root)))
|
|
assert.equal(metadata.name, '@dsh-docker/settings-navigation')
|
|
assert.equal(metadata.dshDocker.description.zh, '优化 DSH 设置目录滚动和窄屏分级导航。')
|
|
assert.equal(metadata.dsh.client.platform, 'web')
|
|
assert.equal(metadata.dsh.client.inject.includes('@deepseek-ai/dsh-client-runtime'), false)
|
|
assert.equal(metadata.peerDependencies['@deepseek-ai/dsh-client-runtime'], undefined)
|
|
assert.equal(metadata.exports['./client'], './lib/client.bundle.js')
|
|
assert.equal(patch[0].insert[0].id, 'dsh-docker.settings-navigation.plugin')
|
|
assert.equal(patch[0].insert[0].name, metadata.name)
|
|
})
|
|
|
|
test('Settings Navigation checked-in bundle matches its source', async () => {
|
|
const bundle = await readFile(new URL('lib/client.bundle.js', root), 'utf8')
|
|
const style = await readFile(new URL('lib/style.module.css', root), 'utf8')
|
|
const rebuilt = await buildSystemPluginClient({
|
|
pluginId: '@dsh-docker/settings-navigation',
|
|
sourcePath: new URL('lib/client.js', root),
|
|
stylePath: new URL('lib/style.module.css', root),
|
|
})
|
|
assert.equal(bundle, rebuilt)
|
|
assert.doesNotThrow(() => new Function(bundle))
|
|
assert.match(bundle, /^window\.__ModuleLoader__\.load\(/)
|
|
assert.doesNotMatch(style, /corner-shape/)
|
|
assert.doesNotMatch(style, /\.header button:not\(\[aria-label\]\)/)
|
|
})
|
|
|
|
test('Settings Navigation recognizes the Settings shell without text or generated classes', async () => {
|
|
const source = await readFile(new URL('lib/client.js', root), 'utf8')
|
|
const matcher = source.slice(source.indexOf('function directChildren'), source.indexOf('\nfunction findSettingsDialog'))
|
|
const matchSettingsDialog = new Function(`${matcher}; return matchSettingsDialog`)()
|
|
|
|
class Element {
|
|
constructor(tagName, ownerDocument) {
|
|
this.tagName = tagName.toUpperCase()
|
|
this.ownerDocument = ownerDocument
|
|
this.children = []
|
|
this.attributes = new Map()
|
|
}
|
|
append(...children) { this.children.push(...children) }
|
|
contains(target) { return this === target || this.children.some(child => child.contains(target)) }
|
|
getAttribute(name) { return this.attributes.get(name) ?? null }
|
|
setAttribute(name, value) { this.attributes.set(name, value) }
|
|
querySelector(selector) {
|
|
return this.querySelectorAll(selector)[0] ?? null
|
|
}
|
|
querySelectorAll(selector) {
|
|
const result = []
|
|
for (const child of this.children) {
|
|
if (selector === 'button' && child.tagName === 'BUTTON') result.push(child)
|
|
result.push(...child.querySelectorAll(selector))
|
|
}
|
|
return result
|
|
}
|
|
}
|
|
const ids = new Map()
|
|
const document = { getElementById: id => ids.get(id) ?? null }
|
|
const dialog = new Element('div', document)
|
|
dialog.setAttribute('role', 'dialog')
|
|
dialog.setAttribute('aria-modal', 'true')
|
|
dialog.setAttribute('aria-labelledby', 'settings-title')
|
|
const nav = new Element('nav', document)
|
|
const title = new Element('div', document)
|
|
ids.set('settings-title', title)
|
|
const list = new Element('div', document)
|
|
const selected = new Element('button', document)
|
|
selected.setAttribute('aria-current', 'true')
|
|
list.append(selected)
|
|
nav.append(title, list)
|
|
const content = new Element('div', document)
|
|
const header = new Element('div', document)
|
|
header.append(new Element('button', document))
|
|
content.append(header, new Element('div', document))
|
|
dialog.append(nav, content)
|
|
|
|
assert.equal(matchSettingsDialog(dialog)?.current, selected)
|
|
dialog.append(new Element('aside', document))
|
|
assert.equal(matchSettingsDialog(dialog), null)
|
|
dialog.children.pop()
|
|
dialog.setAttribute('aria-labelledby', 'unknown')
|
|
assert.equal(matchSettingsDialog(dialog), null)
|
|
})
|
|
|
|
test('Settings Navigation owns compact navigation without moving native nodes', async () => {
|
|
const source = await readFile(new URL('lib/client.js', root), 'utf8')
|
|
const style = await readFile(new URL('lib/style.module.css', root), 'utf8')
|
|
assert.match(source, /const COMPACT_WIDTH = 640/)
|
|
assert.match(source, /shell\.overlay/)
|
|
assert.match(source, /IconChevronLeftOutline14/)
|
|
assert.match(source, /MutationObserverImpl/)
|
|
assert.match(source, /ResizeObserverImpl/)
|
|
assert.match(source, /attributeFilter: \['aria-current', 'aria-labelledby'\]/)
|
|
assert.match(source, /\['dialog', 'nav', 'navTitle', 'navList', 'content', 'header', 'options'\]/)
|
|
assert.match(source, /\.every\(key => next\[key\] === this\.match\[key\]\)/)
|
|
assert.match(source, /this\.view = match\.buttons\.indexOf\(match\.current\) > 0 \? 'detail' : 'directory'/)
|
|
assert.match(source, /this\.modeInitialized && compact && !this\.compact/)
|
|
assert.match(source, /this\.directoryScrollTop = this\.match\.navList\.scrollTop/)
|
|
assert.match(source, /current\?\.focus\(\{ preventScroll: true \}\)/)
|
|
assert.match(source, /this\.requestFrame\(\(\) => \{ navList\.scrollTop = scrollTop \}\)/)
|
|
assert.match(source, /delete this\.match\.dialog\.dataset\.dshSettingsNavigationMode/)
|
|
assert.match(source, /delete this\.match\.dialog\.dataset\.dshSettingsNavigationView/)
|
|
assert.match(source, /node\.classList\.remove\(className\)/)
|
|
assert.doesNotMatch(source, /textContent|innerHTML|appendChild\(match\.|replaceChild/)
|
|
assert.match(style, /scrollbar-gutter: stable/)
|
|
assert.match(style, /data-dsh-settings-navigation-view='directory'/)
|
|
assert.match(style, /data-dsh-settings-navigation-view='detail'/)
|
|
assert.match(style, /--dsw-alias-label-primary/)
|
|
assert.doesNotMatch(style, /#[0-9a-f]{3,8}\b/i)
|
|
})
|