Files
multi-simadmin/test/phase-two-ui.test.js
T

91 lines
5.3 KiB
JavaScript

import test from 'node:test'
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import { filterAndSortFleet, emptyFleetReason } from '../public/state/fleet-view-model.js'
import { instanceDraftPayload, requestDraft } from '../public/state/operation-draft.js'
import { resolveView } from '../public/router/view-router.js'
const root = new URL('../', import.meta.url)
const text = path => readFile(new URL(path, root), 'utf8')
test('fleet view model filters, searches and sorts without mutating source', () => {
const source = [{ id: 'z', name: 'Zulu', url: 'http://z', tags: ['备用'] }, { id: 'a', name: 'Alpha', url: 'http://a', tags: [] }]
const statuses = new Map([['z', { reachable: false, latencyMs: 80 }], ['a', { reachable: true, authenticated: true, latencyMs: 12 }]])
assert.deepEqual(filterAndSortFleet(source, statuses, { filter: 'online', sort: 'latency' }).map(x => x.id), ['a'])
assert.deepEqual(filterAndSortFleet(source, statuses, { query: '备用', sort: 'name' }).map(x => x.id), ['z'])
assert.deepEqual(source.map(x => x.id), ['z', 'a'])
assert.equal(emptyFleetReason({ total: 0 }), 'config')
assert.equal(emptyFleetReason({ total: 2, query: 'x', visible: 0 }), 'search')
assert.equal(emptyFleetReason({ total: 2, filter: 'offline', visible: 0 }), 'filter')
})
test('operation drafts enforce explicit password and request semantics', () => {
assert.deepEqual(instanceDraftPayload({ id: 'one', name: 'One', url: 'http://one', passwordAction: 'preserve' }, true).passwordAction, 'preserve')
assert.throws(() => instanceDraftPayload({ id: 'one', name: 'One', url: 'http://one', passwordAction: 'set', password: '' }, true), /密码/)
assert.throws(() => instanceDraftPayload({ id: 'one', name: 'One', url: 'http://one', passwordAction: 'clear' }, true), /确认/)
assert.deepEqual(requestDraft('GET', '/api/device', { ignored: true }), { method: 'GET', path: '/api/device' })
assert.equal(requestDraft('PATCH', '/api/data', { enabled: true }).body.enabled, true)
})
test('protected views fall back home without an active device', () => {
for (const view of ['detail', 'rawpage', 'api']) assert.equal(resolveView(view, false), 'home')
assert.equal(resolveView('rawpage', true), 'rawpage')
})
test('phase two shell, CRUD, API console and accessibility contracts exist', async () => {
const [html, app, css, responsive, api] = await Promise.all([
text('public/index.html'), text('public/app.js'), text('public/styles/components.css'), text('public/styles/responsive.css'), text('public/infrastructure/api-client.js'),
])
assert.doesNotMatch(html, /fonts\.googleapis|fonts\.gstatic/)
assert.match(html, /class="desktop-sidebar"/)
assert.match(html, /class="mobile-nav"/)
assert.match(html, /aria-live="polite"[^>]*role="status"/)
assert.match(html, /id="fatalState"[^>]*role="alert"/)
assert.match(html, /id="persistentError"[^>]*role="alert"/)
assert.match(html, /id="deleteDialog"[\s\S]*aria-labelledby="deleteDialogTitle"/)
assert.match(html, /id="apiDeleteDialog"[\s\S]*aria-labelledby="apiDeleteTitle"/)
assert.match(html, /id="passwordPreserve"[\s\S]*id="passwordSet"[\s\S]*id="passwordClear"/)
assert.match(html, /id="endpointSearch"/)
assert.match(html, /id="outputPretty"[\s\S]*id="outputRaw"[\s\S]*id="copyOutput"/)
assert.match(html, /<option>PUT<\/option>[\s\S]*<option>PATCH<\/option>/)
assert.match(html, /rel="noopener noreferrer"/)
assert.doesNotMatch(app, /\bconfirm\s*\(/)
assert.doesNotMatch(app, /role="button"/)
assert.match(app, /passwordAction/)
assert.match(app, /prepareConfirmation/)
assert.match(app, /x-confirmation-token/)
assert.match(app, /lastSuccessfulRefresh/)
assert.match(app, /requests\.isCurrent/)
assert.match(app, /prepared[\s\S]*requests\.isCurrent\('api-console'/)
assert.match(app, /try\s*\{[\s\S]*instanceDraftPayload/)
assert.match(app, /try\s*\{[\s\S]*parseBody/)
assert.match(app, /clearPasswordConfirm'\)\.checked=false/)
assert.match(app, /\[data-dialog-cancel\]/)
assert.match(app, /instanceDialogEpoch\s*\+=\s*1/)
assert.match(app, /deleteDialogEpoch\s*\+=\s*1/)
assert.match(app, /loginDialogEpoch\s*\+=\s*1/)
assert.match(app, /if\s*\(!result\.value\.authenticated\)/)
assert.match(app, /pendingApiRequest/)
assert.match(app, /isWriteMethod\(method\)[\s\S]*apiDeleteDialog'\)\.showModal/)
assert.match(html, /id="apiDeleteDetails"[\s\S]*id="apiDeleteBody"/)
assert.match(app, /passwordInput'\)\.value=''/)
assert.match(app, /loginError'\)\.hidden=true/)
assert.match(app, /loginBtn'\)\.addEventListener[\s\S]*submitLogin'\)\.disabled=false/)
assert.match(app, /function openDeviceDetail[\s\S]*submitLogin'\)\.disabled=false/)
assert.match(app, /function setActiveHome[\s\S]*submitLogin'\)\.disabled=false/)
assert.match(app, /document\.title/)
assert.match(app, /removeAttribute\('src'\)/)
assert.doesNotMatch(css, /minmax\(340px/)
assert.doesNotMatch(css, /\.island-card\{[^}]*box-shadow/s)
assert.doesNotMatch(css, /text-overflow\s*:\s*ellipsis|white-space\s*:\s*nowrap/)
assert.match(css, /overflow-wrap:break-word/)
assert.match(responsive, /max-width:767px/)
assert.match(responsive, /min-width:768px/)
assert.match(responsive, /min-width:1024px/)
assert.match(responsive, /min-width:1440px/)
assert.match(responsive, /min-height:44px/)
assert.match(api, /proxy-confirmations\/prepare/)
})