144 lines
7.3 KiB
JavaScript
144 lines
7.3 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-document-editor/package/', import.meta.url)
|
|
|
|
test('Settings Document Editor is an optional DSH web 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-document-editor')
|
|
assert.equal(metadata.dshDocker.description.zh, '在浏览器中查看和编辑 DSH 配置文件。')
|
|
assert.equal(metadata.dsh.client.platform, 'web')
|
|
assert.ok(metadata.dsh.client.inject.includes('@deepseek-ai/dsh-api-remotes'))
|
|
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-document-editor.plugin')
|
|
})
|
|
|
|
test('Settings Document Editor checked-in bundle matches its source', async () => {
|
|
const bundle = await readFile(new URL('lib/client.bundle.js', root), 'utf8')
|
|
const rebuilt = await buildSystemPluginClient({
|
|
pluginId: '@dsh-docker/settings-document-editor',
|
|
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\(/)
|
|
})
|
|
|
|
test('Settings Document Editor replaces and restores both pathless DSH open actions', async () => {
|
|
const source = await readFile(new URL('lib/client.js', root), 'utf8')
|
|
assert.match(source, /\/_dsh_platform\/plugin-api\/v1\/settings-document/)
|
|
assert.doesNotMatch(source, /\/_dsh_platform\/api\/v1\/settings-document/)
|
|
assert.match(source, /interceptDocumentOpen\([\s\S]*connection\.api\.settings,[\s\S]*'openDocument'/)
|
|
assert.match(source, /interceptDocumentOpen\([\s\S]*child\.remote\.settings,[\s\S]*'openSettingsDocument'/)
|
|
assert.match(source, /ctx\.inject\(\['remote\.settings'\]/)
|
|
assert.match(source, /\{ ok: true, value: \{ opened: true \} \}/)
|
|
assert.match(source, /\{ result: \{ ok: true, value: \{ opened: true \} \} \}/)
|
|
assert.doesNotMatch(source, /openDocument\(.*path|body:.*path/s)
|
|
assert.match(source, /settings\.dshDocumentEditor/)
|
|
assert.match(source, /shell\.overlay/)
|
|
assert.match(source, /createPortal\([\s\S]*document\.body\)/)
|
|
assert.match(source, /zh: \{[\s\S]*en: \{/)
|
|
assert.match(source, /error\.status === 409/)
|
|
assert.match(source, /event\.key\.toLowerCase\(\) === 's'/)
|
|
assert.match(source, /className: css\.lineNumbers/)
|
|
assert.match(source, /lineNumbers\.current\.scrollTop = event\.currentTarget\.scrollTop/)
|
|
assert.match(source, /wrap: 'off'/)
|
|
})
|
|
|
|
test('Settings Document Editor intercepts old and getter-backed open methods with their native result shapes', async () => {
|
|
const source = await readFile(new URL('lib/client.js', root), 'utf8')
|
|
const definition = source.slice(source.indexOf('function interceptDocumentOpen'), source.indexOf('\n\nexport function apply'))
|
|
const interceptDocumentOpen = new Function(`${definition}; return interceptDocumentOpen`)()
|
|
let opens = 0
|
|
const controller = { open: async () => { opens += 1 } }
|
|
|
|
for (const [method, result, getterBacked] of [
|
|
['openDocument', { result: { ok: true, value: { opened: true } } }, false],
|
|
['openSettingsDocument', { ok: true, value: { opened: true } }, true],
|
|
]) {
|
|
const original = async () => ({ original: true })
|
|
const target = getterBacked ? {} : { [method]: original }
|
|
if (getterBacked) Object.defineProperty(target, method, {
|
|
configurable: true,
|
|
enumerable: true,
|
|
get: () => original,
|
|
})
|
|
const restore = interceptDocumentOpen(target, method, controller, result)
|
|
assert.deepEqual(await target[method](), result)
|
|
restore()
|
|
assert.equal(target[method], original)
|
|
}
|
|
assert.equal(opens, 2)
|
|
})
|
|
|
|
test('Settings Document Editor follows DSH tokens and has a mobile layout', async () => {
|
|
const style = await readFile(new URL('lib/style.module.css', root), 'utf8')
|
|
assert.match(style, /--dsw-alias-label-primary/)
|
|
assert.match(style, /--dsw-alias-border-l2/)
|
|
assert.match(style, /@media \(max-width: 640px\)/)
|
|
assert.match(style, /height: 100dvh/)
|
|
assert.match(style, /\.editorFrame \{[\s\S]*grid-template-columns: auto minmax\(0, 1fr\)/)
|
|
assert.match(style, /\.lineNumbers \{[\s\S]*text-align: right/)
|
|
assert.match(style, /\.editorFrame:focus-within \{ border-color: var\(--dsw-alias-border-l2\); \}/)
|
|
assert.match(style, /\.lineNumbers \{[\s\S]*background: color-mix\(in srgb, var\(--dsw-alias-bg-base\) 72%, var\(--dsw-alias-bg-module-platform\)\)/)
|
|
assert.match(style, /\.overlay,[\s\S]*?\.overlay \*::after \{[^}]*corner-shape: round;/)
|
|
assert.doesNotMatch(style, /--dsw-alias-state-business-primary/)
|
|
assert.equal((style.match(/ui-monospace/g) ?? []).length, 2)
|
|
assert.doesNotMatch(style, /#[0-9a-f]{3,8}\b/i)
|
|
})
|
|
|
|
test('Settings Document Editor state discards stale loads and rejects stale saves', async () => {
|
|
const source = await readFile(new URL('lib/client.js', root), 'utf8')
|
|
const definition = source.slice(source.indexOf('class DocumentController'), source.indexOf('function Editor('))
|
|
const calls = []
|
|
const responses = [
|
|
{ content: 'language: zh\n', revision: 'a'.repeat(64), exists: true },
|
|
{ content: 'language: en\n', revision: 'b'.repeat(64), exists: true },
|
|
]
|
|
const DocumentController = new Function('request', `${definition}; return DocumentController`)(async (method, body) => {
|
|
calls.push([method, body])
|
|
return responses.shift()
|
|
})
|
|
const controller = new DocumentController()
|
|
await controller.open()
|
|
assert.equal(controller.state.content, 'language: zh\n')
|
|
controller.edit('language: en\n')
|
|
await controller.save()
|
|
assert.equal(controller.state.content, 'language: en\n')
|
|
assert.equal(controller.state.savedContent, 'language: en\n')
|
|
assert.deepEqual(calls.map(call => call[0]), ['GET', 'PUT'])
|
|
|
|
controller.close()
|
|
controller.state.content = 'stale'
|
|
const conflict = Object.assign(new Error('changed'), { status: 409 })
|
|
const FailingController = new Function('request', `${definition}; return DocumentController`)(async () => { throw conflict })
|
|
const failing = new FailingController()
|
|
failing.state.content = 'old content'
|
|
failing.state.savedContent = 'old content'
|
|
failing.state.revision = 'c'.repeat(64)
|
|
await failing.open()
|
|
assert.equal(failing.state.content, '')
|
|
assert.equal(failing.state.revision, null)
|
|
assert.equal(failing.state.error, 'changed')
|
|
|
|
let conflictCall = 0
|
|
const ConflictController = new Function('request', `${definition}; return DocumentController`)(async () => {
|
|
conflictCall += 1
|
|
if (conflictCall === 1) return { content: 'one: 1\n', revision: 'd'.repeat(64), exists: true }
|
|
throw conflict
|
|
})
|
|
const stale = new ConflictController()
|
|
await stale.open()
|
|
stale.edit('one: 2\n')
|
|
await stale.save()
|
|
assert.equal(stale.state.conflict, true)
|
|
assert.equal(stale.state.savedContent, 'one: 1\n')
|
|
assert.equal(stale.state.content, 'one: 2\n')
|
|
})
|