fix(plugins): retain displayed details during activation refresh
This commit is contained in:
1 parent
2bb84a5974
commit
f9bacf7b85
7 files changed
+39
-15
No files matched your search
@@ -153,12 +153,16 @@ it.skipIf(process.platform !== 'darwin')('keeps system requests pending until fr
|
||||
{ method: 'openPermissionSettings', params: { permission: 'accessibility' } },
|
||||
])
|
||||
expect(events.every(event => ['permissionStatus', 'requestPermission', 'openPermissionSettings', 'applicationIcons', 'getApp'].includes(event.method))).toBe(true)
|
||||
await plugins.locator('[data-detail-document] h1').waitFor()
|
||||
const trace = await page.evaluateHandle(() => {
|
||||
const errors: string[] = []
|
||||
const stable = [...document.querySelectorAll('[data-cua-settings] [role="alert"]')].map(node => node.textContent?.trim())
|
||||
const layouts: { height: number; sections: number[]; groups: number[] }[] = []
|
||||
const documents: number[] = []
|
||||
let frame = 0
|
||||
const measure = () => {
|
||||
const documentHeight = document.querySelector('[data-plugin-detail] [data-detail-document]')?.getBoundingClientRect().height ?? 0
|
||||
if (documents.at(-1) !== documentHeight) documents.push(documentHeight)
|
||||
const settings = document.querySelector('[data-cua-settings]')
|
||||
if (settings) {
|
||||
const height = settings.getBoundingClientRect().height
|
||||
@@ -177,7 +181,7 @@ it.skipIf(process.platform !== 'darwin')('keeps system requests pending until fr
|
||||
}
|
||||
})
|
||||
observer.observe(document.body, { childList: true, subtree: true, characterData: true })
|
||||
return { errors, layouts, stop: () => { observer.disconnect(); cancelAnimationFrame(frame) } }
|
||||
return { errors, layouts, documents, stop: () => { observer.disconnect(); cancelAnimationFrame(frame) } }
|
||||
})
|
||||
try {
|
||||
const toggle = page.locator('[data-plugin-panel]').getByRole('switch', { name: 'Enable Computer Use', exact: true })
|
||||
@@ -185,7 +189,7 @@ it.skipIf(process.platform !== 'darwin')('keeps system requests pending until fr
|
||||
await page.setViewportSize({ width, height: 1000 })
|
||||
if (width === 390) await page.locator('[data-sidebar-collapsed]').waitFor({ state: 'attached' })
|
||||
await page.locator('[data-animating]').waitFor({ state: 'hidden' })
|
||||
await trace.evaluate((value) => { value.layouts.length = 0 })
|
||||
await trace.evaluate((value) => { value.layouts.length = 0; value.documents.length = 0 })
|
||||
for (let cycle = 0; cycle < 3; cycle++) {
|
||||
await toggle.click()
|
||||
await expect.poll(() => toggle.getAttribute('aria-checked')).toBe('false')
|
||||
@@ -197,6 +201,9 @@ it.skipIf(process.platform !== 'darwin')('keeps system requests pending until fr
|
||||
}
|
||||
const layouts = await trace.evaluate(value => value.layouts)
|
||||
expect(layouts.length).toBeGreaterThan(0)
|
||||
const documentHeights = await trace.evaluate(value => value.documents)
|
||||
expect(documentHeights.length).toBeGreaterThan(0)
|
||||
expect(Math.max(...documentHeights) - Math.min(...documentHeights)).toBeLessThanOrEqual(1)
|
||||
const heights = layouts.map(layout => layout.height)
|
||||
expect(Math.max(...heights) - Math.min(...heights), JSON.stringify(layouts)).toBeLessThanOrEqual(1)
|
||||
}
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
en: 9312d8b1b381378f
|
||||
zh: 8d0ae7ad26ada2ac
|
||||
/deepseek-ai-dsh-client-ui-plugin-manager/use-this-package/switching-a-bundle:
|
||||
en: 4c82092cabed69da
|
||||
zh: 7e6d0d37e10f5132
|
||||
en: 58a4ec22fa624531
|
||||
zh: f838445232446850
|
||||
/deepseek-ai-dsh-client-ui-plugin-manager/use-this-package/switching-one-row-of-a-bundle:
|
||||
en: d3627d4dba5e6cc2
|
||||
zh: e0d0eada4c20331f
|
||||
|
||||
@@ -72,7 +72,7 @@ After the checks, the list toolbar says how many plugins can be updated, that al
|
||||
|
||||
### Switching a bundle
|
||||
|
||||
A bundle's page shows its full package name under the title. Its **Source** section, after the rows, names the spec that installs it elsewhere, as `pnpm add` accepts it, or **Built in** for an official bundle the installation ships, then its current version. A bundle's switch changes its layer selection. While the Host applies the change, the switch stays disabled and a spinner appears beside it; the action area permanently reserves 22px to the left of the switch. A persistent status region announces the pending change to screen readers. A profile with HMR recomposes before the operation completes; one without HMR, and a bundle a higher layer overrides, say so in a toast. A bundle the Host cannot read carries a problem tag and its reason on its page and cannot be switched on; one that provides the management components stays locked. The Host answers with error codes, which the page's dictionary words; pnpm's and the Loader's own diagnostics are shown as they are. The page excludes built-in profile bundles from cards even when the profile holds them as dependencies or the Host reports an error. The Host inventory remains complete; the Settings Plugins section's Plugin list tab inspects their plugins.
|
||||
A bundle's page shows its full package name under the title. Its **Source** section, after the rows, names the spec that installs it elsewhere, as `pnpm add` accepts it, or **Built in** for an official bundle the installation ships, then its current version. A bundle's switch changes its layer selection. A displayed detail document stays visible while a profile change triggers a fresh read; the result replaces it when ready. While the Host applies the change, the switch stays disabled and a spinner appears beside it; the action area permanently reserves 22px to the left of the switch. A persistent status region announces the pending change to screen readers. A profile with HMR recomposes before the operation completes; one without HMR, and a bundle a higher layer overrides, say so in a toast. A bundle the Host cannot read carries a problem tag and its reason on its page and cannot be switched on; one that provides the management components stays locked. The Host answers with error codes, which the page's dictionary words; pnpm's and the Loader's own diagnostics are shown as they are. The page excludes built-in profile bundles from cards even when the profile holds them as dependencies or the Host reports an error. The Host inventory remains complete; the Settings Plugins section's Plugin list tab inspects their plugins.
|
||||
|
||||
### Switching one row of a bundle
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ Host 将网络失败或超时归因于 GitHub 地址,且提供 npmmirror 时
|
||||
|
||||
### 切换一个组合包
|
||||
|
||||
组合包页面在标题下方显示完整包名。组件列表之后的「来源信息」给出在别处安装它所需的 spec(`pnpm add` 接受的形式),安装随附的官方组合包显示「内置」,随后是当前版本。组合包开关改变其层选择。Host 应用更改期间,开关保持禁用,旁边显示旋转指示器;操作区在开关左侧固定预留 22px。常驻状态区域向屏幕阅读器播报进行中的更改。启用了 HMR 的 profile 在操作完成前重组;没有 HMR 的 profile,以及被更高层覆盖的组合包,会以 toast 说明。Host 读不了的组合包带异常标签,其页面给出原因,且不能打开;提供管理组件的组合包保持锁定。Host 以错误码作答,由页面字典措辞;pnpm 与 Loader 自己的诊断原样显示。页面从卡片中排除内置 profile 组合包,即使 profile 将它们列为依赖或 Host 报告了异常。Host 清单仍保留完整数据;设置中「插件」分区的「插件列表」标签页负责查看它们的插件。
|
||||
组合包页面在标题下方显示完整包名。组件列表之后的「来源信息」给出在别处安装它所需的 spec(`pnpm add` 接受的形式),安装随附的官方组合包显示「内置」,随后是当前版本。组合包开关改变其层选择。profile 变更触发重新读取时,已显示的详情文档保持可见,直到新结果返回。Host 应用更改期间,开关保持禁用,旁边显示旋转指示器;操作区在开关左侧固定预留 22px。常驻状态区域向屏幕阅读器播报进行中的更改。启用了 HMR 的 profile 在操作完成前重组;没有 HMR 的 profile,以及被更高层覆盖的组合包,会以 toast 说明。Host 读不了的组合包带异常标签,其页面给出原因,且不能打开;提供管理组件的组合包保持锁定。Host 以错误码作答,由页面字典措辞;pnpm 与 Loader 自己的诊断原样显示。页面从卡片中排除内置 profile 组合包,即使 profile 将它们列为依赖或 Host 报告了异常。Host 清单仍保留完整数据;设置中「插件」分区的「插件列表」标签页负责查看它们的插件。
|
||||
|
||||
### 切换组合包里的一行
|
||||
|
||||
|
||||
@@ -38,9 +38,7 @@ export function BundleDetailSection(props: BundleDetailSectionProps): ReactNode
|
||||
const { name, version, fromNpm, language, details, t, load, retry, image } = props
|
||||
const key = bundleDetailKey(name, version, language)
|
||||
const state = details[key]
|
||||
// The key names the read completely; a read the controller forgot, after a profile change, starts again.
|
||||
const missing = state === undefined
|
||||
useEffect(() => { if (missing) load(name, version, language, fromNpm) }, [key, missing])
|
||||
useEffect(() => { load(name, version, language, fromNpm) }, [key, details])
|
||||
const labels = useDetailDocumentLabels({
|
||||
title: t('detailTitle'), loading: t('detailLoading'), retry: t('retry'), copy: t('copy'), copied: t('copied'),
|
||||
codeBlock: t('codeBlock.title'), wrap: t('codeBlock.wrap'), unwrap: t('codeBlock.unwrap'), footnotes: t('detailFootnotes'),
|
||||
|
||||
@@ -59,7 +59,7 @@ export class BundleDetailController {
|
||||
* @param fromNpm - whether the bundle's files are unavailable, so the Host reads its offered version from the npm file CDNs.
|
||||
*/
|
||||
load(name: string, version: string | undefined, language: string, fromNpm: boolean): void {
|
||||
if (this.store.getSnapshot()[bundleDetailKey(name, version, language)] === undefined) void this.read(name, version, language, fromNpm)
|
||||
if (!this.reads.has(bundleDetailKey(name, version, language))) void this.read(name, version, language, fromNpm)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -88,10 +88,10 @@ export class BundleDetailController {
|
||||
return new URL(`${DETAIL_FILE_ROUTE}?${query.toString()}`, this.base()).href
|
||||
}
|
||||
|
||||
/** Forget every read, so each shown bundle reads its document again; bundle files can change at any profile change. */
|
||||
/** Invalidate reads while retaining settled documents until their replacements arrive. */
|
||||
reset(): void {
|
||||
this.reads.clear()
|
||||
this.store.set({})
|
||||
this.store.set(Object.fromEntries(Object.entries(this.store.getSnapshot()).filter(([, value]) => value.status === 'ready')))
|
||||
}
|
||||
|
||||
/** Stop publishing late settlements. */
|
||||
@@ -103,7 +103,7 @@ export class BundleDetailController {
|
||||
const key = bundleDetailKey(name, version, language)
|
||||
const read = ++this.sequence
|
||||
this.reads.set(key, read)
|
||||
this.publish(key, { status: 'loading' })
|
||||
if (this.store.getSnapshot()[key]?.status !== 'ready') this.publish(key, { status: 'loading' })
|
||||
// The remote is captured before any wait: the plugin's context may be disposed while the comparison runs.
|
||||
const { remote } = this.ctx
|
||||
const current = (): boolean => !this.disposed && this.reads.get(key) === read
|
||||
|
||||
@@ -54,7 +54,7 @@ it('reports failures by reason, reads again on retry, and settles a retried key
|
||||
expect(state('a', '1.0.0', 'en')).toEqual({ status: 'ready', detail: FOUND })
|
||||
})
|
||||
|
||||
it('forgets every read on reset, drops answers of forgotten reads, and stops publishing after disposal', async () => {
|
||||
it('invalidates reads on reset, drops superseded answers, and stops publishing after disposal', async () => {
|
||||
const { controller, manager, state } = setup()
|
||||
let answer: (value: Answer<BundleDetail>) => void = () => {}
|
||||
manager.readBundleDetail.mockImplementationOnce(() => new Promise((resolve) => { answer = resolve }))
|
||||
@@ -80,7 +80,7 @@ it('forgets every read on reset, drops answers of forgotten reads, and stops pub
|
||||
controller.dispose()
|
||||
answer({ ok: true, value: FOUND })
|
||||
await flush()
|
||||
expect(state('a', '1.0.0', 'en')).toEqual({ status: 'loading' })
|
||||
expect(state('a', '1.0.0', 'en')).toEqual({ status: 'ready', detail: { status: 'none' } })
|
||||
})
|
||||
|
||||
it('asks the registry comparison only for bundles read from npm, and reads them and their images from npmmirror first where it won', async () => {
|
||||
@@ -123,3 +123,22 @@ it('addresses relative images through the Host route and leaves other destinatio
|
||||
expect(controller.image('x', 'README.md', destination, false)).toBeUndefined()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
it('retains a displayed document during invalidation and replaces it after one fresh read', async () => {
|
||||
const { controller, manager, state } = setup()
|
||||
controller.load('a', '1.0.0', 'en', false)
|
||||
await vi.waitFor(() => { expect(state('a', '1.0.0', 'en')).toEqual({ status: 'ready', detail: FOUND }) })
|
||||
const response = Promise.withResolvers<Answer<BundleDetail>>()
|
||||
onTestFinished(() => { response.resolve({ ok: true, value: FOUND }) })
|
||||
manager.readBundleDetail.mockImplementationOnce(() => response.promise)
|
||||
controller.reset()
|
||||
expect(state('a', '1.0.0', 'en')).toEqual({ status: 'ready', detail: FOUND })
|
||||
controller.load('a', '1.0.0', 'en', false)
|
||||
controller.load('a', '1.0.0', 'en', false)
|
||||
expect(manager.readBundleDetail).toHaveBeenCalledTimes(2)
|
||||
expect(state('a', '1.0.0', 'en')).toEqual({ status: 'ready', detail: FOUND })
|
||||
const replacement = { ...FOUND, markdown: '# Updated' }
|
||||
response.resolve({ ok: true, value: replacement })
|
||||
await vi.waitFor(() => { expect(state('a', '1.0.0', 'en')).toEqual({ status: 'ready', detail: replacement }) })
|
||||
})
|
||||
Reference in new issue
Block a user