The Plugin / GitHub / Settings labels open with siDeepseek, siGithub, and the harness's IconSettingsOutline14 gear, riding currentColor at the label's baseline. simple-icons is a dev-only dependency pinned to the harness ui-primitives' own 16.31.0 and tree-shakes into lib/client.js as exactly the two mark paths, so no runtime dependency is added and the marks are identical to the harness's own SiteGlyph artwork.
135 lines
6.5 KiB
TypeScript
135 lines
6.5 KiB
TypeScript
// PluginInfo (src/client/components/pluginInfo.tsx): static metadata rows
|
|
// plus the live npm latest-version check. fetch is stubbed per test and the
|
|
// modules are re-imported fresh (vi.resetModules) so the baked-in version
|
|
// define and the fetch TTL cache both start clean.
|
|
|
|
import { createElement as h } from 'react'
|
|
import assert from 'node:assert/strict'
|
|
import { afterEach, describe, test, vi } from 'vitest'
|
|
import type { makePluginInfo as makePluginInfoFn } from '../../../src/client/components/pluginInfo'
|
|
import { flush, makeKit, mount, query, queryAll, text } from '../helpers/kit'
|
|
|
|
const kit = makeKit()
|
|
|
|
type FetchBody = { version?: unknown }
|
|
|
|
function stubFetchOk(body: FetchBody) {
|
|
const fetchMock = vi.fn((_input: unknown) => Promise.resolve({ ok: true, json: () => Promise.resolve(body) }))
|
|
vi.stubGlobal('fetch', fetchMock)
|
|
return fetchMock
|
|
}
|
|
|
|
/** Fresh latestVersion + pluginInfo modules; `version` emulates the tsdown define. */
|
|
async function loadPluginInfo(version?: string): Promise<ReturnType<typeof makePluginInfoFn>> {
|
|
vi.resetModules()
|
|
if (version === undefined) delete (globalThis as Record<string, unknown>).__DSH_CTX_VERSION__
|
|
else (globalThis as Record<string, unknown>).__DSH_CTX_VERSION__ = version
|
|
await import('../../../src/client/latestVersion')
|
|
const mod = await import('../../../src/client/components/pluginInfo')
|
|
return mod.makePluginInfo(kit)
|
|
}
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals()
|
|
delete (globalThis as Record<string, unknown>).__DSH_CTX_VERSION__
|
|
})
|
|
|
|
describe('PluginInfo', () => {
|
|
test('dev build renders the static rows and never checks the registry', async () => {
|
|
const fetchMock = stubFetchOk({ version: '9.9.9' })
|
|
const PluginInfo = await loadPluginInfo() // PLUGIN_VERSION = 0.0.0-dev
|
|
const m = await mount(h(PluginInfo, {}))
|
|
await flush()
|
|
assert.equal(fetchMock.mock.calls.length, 0)
|
|
assert.ok(text(m.container).includes('Plugin Info'))
|
|
const rows = queryAll(m.container, '.lc-pi-row')
|
|
assert.equal(rows.length, 3)
|
|
// Every row's label leads with its site mark (DeepSeek / GitHub / the settings gear).
|
|
for (const row of rows) assert.equal(queryAll(row, '.lc-pi-label .lc-pi-labelicon').length, 1)
|
|
assert.equal(query(rows[0], '.lc-pi-label').textContent, 'Plugin')
|
|
assert.equal(query(rows[0], '.lc-pi-value').textContent, 'dsh-context (v0.0.0-dev)')
|
|
assert.equal(rows[0].getAttribute('href'), 'https://github.com/bowenliang123/dsh-context')
|
|
assert.equal(rows[0].getAttribute('target'), '_blank')
|
|
assert.equal(query(rows[1], '.lc-pi-label').textContent, 'GitHub')
|
|
assert.equal(query(rows[1], '.lc-pi-value').textContent, 'bowenliang123/dsh-context')
|
|
assert.equal(rows[1].getAttribute('href'), 'https://github.com/bowenliang123/dsh-context')
|
|
// The settings entry: a button (no href) below GitHub — the guarded jump runs on click.
|
|
assert.equal(rows[2].getAttribute('href'), null)
|
|
assert.equal(query(rows[2], '.lc-pi-label').textContent, 'Settings')
|
|
assert.equal(query(rows[2], '.lc-pi-value').textContent, 'Open in Settings')
|
|
// The tagline is the repo link too: hover underlines it, a click opens GitHub.
|
|
const hint = query(m.container, '.lc-pi-hint')
|
|
assert.ok(text(hint).includes('The best DSH context plugin'))
|
|
assert.equal(hint.getAttribute('href'), 'https://github.com/bowenliang123/dsh-context')
|
|
assert.equal(hint.getAttribute('target'), '_blank')
|
|
assert.equal(hint.getAttribute('rel'), 'noreferrer')
|
|
assert.equal(queryAll(m.container, '.lc-pi-update').length, 0)
|
|
// The settings entry runs the guarded jump; without the harness chrome in
|
|
// the jsdom document it silently no-ops — safe to click in tests.
|
|
assert.doesNotThrow(() => rows[2].click())
|
|
await m.unmount()
|
|
})
|
|
|
|
test('release build with a newer registry version appends the update chip', async () => {
|
|
const fetchMock = stubFetchOk({ version: '1.1.0' })
|
|
const PluginInfo = await loadPluginInfo('1.0.0')
|
|
const m = await mount(h(PluginInfo, {}))
|
|
await flush()
|
|
assert.equal(fetchMock.mock.calls.length, 1)
|
|
assert.equal(fetchMock.mock.calls[0][0], 'https://registry.npmjs.org/dsh-context/latest')
|
|
assert.ok(text(m.container).includes('dsh-context (v1.0.0)'))
|
|
assert.equal(query(m.container, '.lc-pi-update').textContent, '↑ v1.1.0')
|
|
await m.unmount()
|
|
})
|
|
|
|
test('a registry version that is not newer renders no chip', async () => {
|
|
stubFetchOk({ version: '1.0.0' })
|
|
const PluginInfo = await loadPluginInfo('1.0.0')
|
|
const m = await mount(h(PluginInfo, {}))
|
|
await flush()
|
|
assert.equal(queryAll(m.container, '.lc-pi-update').length, 0)
|
|
await m.unmount()
|
|
})
|
|
|
|
test('every registry failing narrows to null and renders no chip', async () => {
|
|
const fetchMock = vi.fn(() => Promise.resolve({ ok: false, json: () => Promise.resolve(null) }))
|
|
vi.stubGlobal('fetch', fetchMock)
|
|
const PluginInfo = await loadPluginInfo('1.0.0')
|
|
const m = await mount(h(PluginInfo, {}))
|
|
await flush()
|
|
// Both registries (official npm then npmmirror) are tried before giving up.
|
|
assert.equal(fetchMock.mock.calls.length, 2)
|
|
assert.equal(queryAll(m.container, '.lc-pi-update').length, 0)
|
|
await m.unmount()
|
|
})
|
|
|
|
test('falls back to npmmirror when the official registry fails', async () => {
|
|
const fetchMock = vi.fn((input: unknown) => {
|
|
const url = String(input)
|
|
if (url.startsWith('https://registry.npmjs.org/')) {
|
|
return Promise.resolve({ ok: false, json: () => Promise.resolve(null) })
|
|
}
|
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({ version: '1.1.0' }) })
|
|
})
|
|
vi.stubGlobal('fetch', fetchMock)
|
|
const PluginInfo = await loadPluginInfo('1.0.0')
|
|
const m = await mount(h(PluginInfo, {}))
|
|
await flush()
|
|
assert.equal(fetchMock.mock.calls.length, 2)
|
|
assert.equal(query(m.container, '.lc-pi-update').textContent, '↑ v1.1.0')
|
|
await m.unmount()
|
|
})
|
|
|
|
test('unmounting before the answer lands drops the late result', async () => {
|
|
let resolveFetch: ((res: { ok: boolean; json: () => Promise<FetchBody> }) => void) | null = null
|
|
vi.stubGlobal('fetch', vi.fn(() => new Promise((resolve) => { resolveFetch = resolve })))
|
|
const PluginInfo = await loadPluginInfo('1.0.0')
|
|
const m = await mount(h(PluginInfo, {}))
|
|
await m.unmount()
|
|
resolveFetch!({ ok: true, json: () => Promise.resolve({ version: '2.0.0' }) })
|
|
await flush()
|
|
// No state update after unmount, no act warning, no chip anywhere.
|
|
assert.equal(queryAll(document.body, '.lc-pi-update').length, 0)
|
|
})
|
|
})
|