feat(client): add an update capsule to the Insights page head
The Context tab's Plugin-info card already hinted at a newer npm release; the Context Insights page head had no such hint. Add one beside the balance capsule, in that page's own capsule idiom rather than the card's inline chip: label plus the version in the warn token, linking the release notes through the page's portaled hover bubble. The registry check moves out of pluginInfo into a useNewerVersion hook both surfaces read, so the second hint costs no new fetch path, state, or mechanism — one TTL-cached read still answers both. Its own coverage moves with it into latestVersion.spec, leaving each surface's spec asserting only what that surface renders. The two head capsules share one CSS chrome instead of a copy, and the head's gap drops to 8px so the capsule cluster tightens; the title keeps its wider 16px lead-in, and the fold at narrow widths leaves every wrapped row on the same left edge with no overflow.
This commit is contained in:
1 parent
3fef32e61d
commit
018e92964f
10 files changed
+203
-63
No files matched your search
@@ -42,6 +42,7 @@ import { makeOverviewUsage } from './overviewUsage'
|
||||
import { makeStatsTiming } from './statsTiming'
|
||||
import { makeOverviewCard } from './overviewCard'
|
||||
import { makeOverviewSkills } from './overviewSkills'
|
||||
import { makeUpdatePill } from './updatePill'
|
||||
import { IconSettings } from '../primitives'
|
||||
|
||||
export interface OverviewPanelProps {
|
||||
@@ -64,6 +65,7 @@ export function makeOverviewPanel(ctx: ClientCtx, kit: ViewKit): (props: Overvie
|
||||
const StatsTiming = makeStatsTiming(kit, Donut)
|
||||
const OverviewUsage = makeOverviewUsage(kit)
|
||||
const OverviewSkills = makeOverviewSkills(kit)
|
||||
const UpdatePill = makeUpdatePill(kit)
|
||||
const ErrorBoundary = makeErrorBoundary(t)
|
||||
|
||||
// Read per render: the slot outlet re-renders on a locale switch.
|
||||
@@ -175,6 +177,7 @@ export function makeOverviewPanel(ctx: ClientCtx, kit: ViewKit): (props: Overvie
|
||||
</button>
|
||||
{/* Renders nothing until a live figure lands, so the heading row never reflows for it. */}
|
||||
<BalanceCapsule />
|
||||
<UpdatePill />
|
||||
{/* One wrapper so the head's right cluster pushes once; a bare `.lc-gran` sibling would split the free space. */}
|
||||
<div className="lc-ov-headctl">
|
||||
<DateRange
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/** The card beside Context stats introducing the plugin. Metadata is baked in from package.json via tsdown `define`;
|
||||
* one live npm-registry check appends an `↑ vX.Y.Z` chip when newer. */
|
||||
|
||||
import { useEffect, useState, type ReactElement, type ReactNode } from 'react'
|
||||
import type { ReactElement, ReactNode } from 'react'
|
||||
import { IconSettings } from '../primitives'
|
||||
import { siDeepseek, siGithub, type SimpleIcon } from 'simple-icons'
|
||||
import { fetchLatestVersion, isNewerVersion } from '../latestVersion'
|
||||
import { useNewerVersion } from '../latestVersion'
|
||||
import { PLUGIN_NAME, PLUGIN_REPO, PLUGIN_REPO_SHORT, PLUGIN_VERSION } from '../meta'
|
||||
import { openPluginSettings } from '../settingsJump'
|
||||
import type { ViewKit } from '../viewkit'
|
||||
@@ -28,15 +28,7 @@ export function makePluginInfo(kit: ViewKit): () => ReactElement {
|
||||
</a>
|
||||
)
|
||||
return function PluginInfo(): ReactElement {
|
||||
const [latest, setLatest] = useState<string | null>(null)
|
||||
useEffect(() => {
|
||||
if (PLUGIN_VERSION.includes('-dev')) return
|
||||
let on = true
|
||||
// fetchLatestVersion never rejects (every failure narrows to null); the `on` flag drops late results.
|
||||
void fetchLatestVersion().then((v) => { if (on && v) setLatest(v) })
|
||||
return () => { on = false }
|
||||
}, [])
|
||||
const update = latest !== null && isNewerVersion(latest, PLUGIN_VERSION) ? latest : null
|
||||
const update = useNewerVersion()
|
||||
const nameText = PLUGIN_NAME + ' (v' + PLUGIN_VERSION + ')'
|
||||
const nameValue: ReactNode[] = [nameText]
|
||||
if (update) nameValue.push(<span key="update" className="lc-pi-update">{'↑ v' + update}</span>)
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/** The Context Insights page's update capsule — the newer registry version, linking its release notes.
|
||||
* Renders NOTHING without one, so the heading row never reflows for it. */
|
||||
|
||||
import type { ReactElement } from 'react'
|
||||
import { useNewerVersion } from '../latestVersion'
|
||||
import { PLUGIN_REPO } from '../meta'
|
||||
import type { ViewKit } from '../viewkit'
|
||||
|
||||
/** The release notes behind the capsule's click. */
|
||||
const RELEASES_URL = PLUGIN_REPO + '/releases/latest'
|
||||
|
||||
export function makeUpdatePill(kit: ViewKit): () => ReactElement | null {
|
||||
const { t } = kit
|
||||
return function UpdatePill(): ReactElement | null {
|
||||
const update = useNewerVersion()
|
||||
if (update === null) return null
|
||||
const tip = t('update.tip', { version: 'v' + update })
|
||||
return (
|
||||
<a
|
||||
className="lc-ov-update"
|
||||
href={RELEASES_URL}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
aria-label={tip}
|
||||
data-lc-tip={tip}
|
||||
data-lc-tip-side="bottom"
|
||||
>
|
||||
<span className="lc-ov-update-label">{t('update.label')}</span>
|
||||
<span className="lc-ov-update-version">↑ v{update}</span>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -480,6 +480,8 @@ export const DICT_EN = {
|
||||
'balance.title': 'DeepSeek balance',
|
||||
'balance.tip.granted': 'Granted balance',
|
||||
'balance.tip.toppedUp': 'Topped-up balance',
|
||||
'update.label': 'Update',
|
||||
'update.tip': 'Update {version} is available — click for the release notes',
|
||||
'ov.heat.title': 'Activity',
|
||||
'ov.heat.sub': 'Filter by date',
|
||||
'ov.heat.empty': 'No activity yet — daily usage appears once sessions make model requests',
|
||||
|
||||
@@ -479,6 +479,8 @@ export const DICT_ZH = {
|
||||
'balance.title': 'DeepSeek 余额',
|
||||
'balance.tip.granted': '赠送余额',
|
||||
'balance.tip.toppedUp': '充值余额',
|
||||
'update.label': '新版本',
|
||||
'update.tip': '新版本 {version} 已发布,点击查看更新说明',
|
||||
'ov.heat.title': '活跃度',
|
||||
'ov.heat.sub': '按日期筛选',
|
||||
'ov.heat.empty': '暂无活跃数据 — 会话产生模型请求后,这里会显示每日用量',
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/** Lazy 1h TTL check for the Plugin-info card: npm first, npmmirror as fallback, one at a time; all
|
||||
/** Lazy 1h TTL check for the update hints: npm first, npmmirror as fallback, one at a time; all
|
||||
* sources failing resolves null. */
|
||||
|
||||
import { PLUGIN_NAME } from './meta'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { PLUGIN_NAME, PLUGIN_VERSION } from './meta'
|
||||
|
||||
const REGISTRY_HOSTS = [
|
||||
'https://registry.npmjs.org',
|
||||
@@ -48,3 +49,17 @@ export function isNewerVersion(latest: string, current: string): boolean {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/** The newer registry version for this build, or null — the read behind every surface's update hint. A dev
|
||||
* build never asks; the 1h TTL cache answers every later mount from the first fetch. */
|
||||
export function useNewerVersion(): string | null {
|
||||
const [latest, setLatest] = useState<string | null>(null)
|
||||
useEffect(() => {
|
||||
if (PLUGIN_VERSION.includes('-dev')) return
|
||||
let on = true
|
||||
// fetchLatestVersion never rejects (every failure narrows to null); the `on` flag drops late results.
|
||||
void fetchLatestVersion().then((v) => { if (on && v) setLatest(v) })
|
||||
return () => { on = false }
|
||||
}, [])
|
||||
return latest !== null && isNewerVersion(latest, PLUGIN_VERSION) ? latest : null
|
||||
}
|
||||
@@ -3,18 +3,22 @@
|
||||
.lc-ov-pagescroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; scrollbar-gutter: stable; }
|
||||
/* No width cap: the shipped first-level pages frame at 960px, this page adapts to the pane. */
|
||||
.lc-ov-pagecontent { padding: 0 clamp(24px, 4vw, 48px) 48px; }
|
||||
.lc-ov-pagehead { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; padding-top: 28px; margin-bottom: 24px; }
|
||||
.lc-ov-pagehead { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; padding-top: 28px; margin-bottom: 24px; }
|
||||
/* macOS hiddenInset titlebar clearance, so first-level pages align with the frame. */
|
||||
[data-platform='darwin'] .lc-ov-pagehead { padding-top: calc(28px + var(--dsh-frame-top-clearance, 0px)); }
|
||||
.lc-ov-pagetitle { flex: 0 1 auto; min-width: 0; margin: 0; font-size: 20px; line-height: 28px; font-weight: 500; }
|
||||
/* The capsules cluster at the head's 8px; only the title keeps the wider 16px lead-in. */
|
||||
.lc-ov-pagetitle { flex: 0 1 auto; min-width: 0; margin: 0 8px 0 0; font-size: 20px; line-height: 28px; font-weight: 500; }
|
||||
.lc-ov-settings { flex: none; display: inline-flex; align-items: center; gap: 6px; box-sizing: border-box; border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-2); border-radius: 999px; padding: 3px 12px; font: inherit; font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-secondary); white-space: nowrap; cursor: pointer; transition: border-color var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease-in-out), color var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease-in-out); }
|
||||
/* One wrapper around the picker and range group, so the head keeps a single auto-margin push. */
|
||||
.lc-ov-headctl { margin-left: auto; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||||
.lc-ov-settings:hover { border-color: var(--dsw-alias-label-secondary); color: var(--dsw-alias-label-primary); }
|
||||
.lc-ov-balance { flex: none; display: inline-flex; align-items: center; gap: 6px; max-width: 100%; padding: 2px 10px; border-radius: 999px; background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l2); font-size: 12px; line-height: 18px; text-decoration: none; color: inherit; }
|
||||
/* The head's two capsules share one chrome; only the update hint's version wears the warn token. */
|
||||
.lc-ov-balance, .lc-ov-update { flex: none; display: inline-flex; align-items: center; gap: 6px; max-width: 100%; padding: 2px 10px; border-radius: 999px; background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l2); font-size: 12px; line-height: 18px; text-decoration: none; color: inherit; white-space: nowrap; }
|
||||
.lc-ov-balance:hover .lc-ov-balance-value { text-decoration: underline; }
|
||||
.lc-ov-balance-label { color: var(--dsw-alias-label-secondary); white-space: nowrap; }
|
||||
.lc-ov-balance-value { font-variant-numeric: tabular-nums; font-weight: 600; white-space: nowrap; }
|
||||
.lc-ov-balance-label, .lc-ov-update-label { color: var(--dsw-alias-label-secondary); }
|
||||
.lc-ov-balance-value, .lc-ov-update-version { font-variant-numeric: tabular-nums; font-weight: 600; }
|
||||
.lc-ov-update:hover { border-color: var(--dsw-alias-state-warn-primary); }
|
||||
.lc-ov-update-version { color: var(--dsw-alias-state-warn-primary); }
|
||||
|
||||
.lc-hovertip { position: fixed; z-index: 300; max-width: min(320px, 50vw); white-space: pre-line; }
|
||||
|
||||
|
||||
@@ -6,9 +6,7 @@ import { flush, makeKit, mount, query, queryAll, text } from '../helpers/kit'
|
||||
|
||||
const kit = makeKit()
|
||||
|
||||
type FetchBody = { version?: unknown }
|
||||
|
||||
function stubFetchOk(body: FetchBody) {
|
||||
function stubFetchOk(body: { version?: unknown }) {
|
||||
const fetchMock = vi.fn((_input: unknown) => Promise.resolve({ ok: true, json: () => Promise.resolve(body) }))
|
||||
vi.stubGlobal('fetch', fetchMock)
|
||||
return fetchMock
|
||||
@@ -85,45 +83,4 @@ describe('PluginInfo', () => {
|
||||
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)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,68 @@
|
||||
// UpdatePill (src/client/components/updatePill.tsx): the Insights head's newer-version capsule. What it
|
||||
// renders and where it links; the check itself (dev skip, not-newer, registry failure, late result) is
|
||||
// useNewerVersion's, covered once in tests/client/latestVersion.spec.ts.
|
||||
|
||||
import { createElement as h } from 'react'
|
||||
import assert from 'node:assert/strict'
|
||||
import { afterEach, describe, test, vi } from 'vitest'
|
||||
import type { makeUpdatePill as makeUpdatePillFn } from '../../../src/client/components/updatePill'
|
||||
import { flush, makeKit, mount, query, queryAll } from '../helpers/kit'
|
||||
|
||||
/** Fresh latestVersion + updatePill modules — resetModules so the baked-in version define and the fetch
|
||||
* TTL cache start clean; `version` emulates the tsdown define. */
|
||||
async function loadUpdatePill(version: string, active: 'en' | 'zh' = 'en'): Promise<ReturnType<typeof makeUpdatePillFn>> {
|
||||
vi.resetModules()
|
||||
;(globalThis as Record<string, unknown>).__DSH_CTX_VERSION__ = version
|
||||
await import('../../../src/client/latestVersion')
|
||||
const mod = await import('../../../src/client/components/updatePill')
|
||||
return mod.makeUpdatePill(makeKit(active))
|
||||
}
|
||||
|
||||
/** A registry one minor ahead of the running build. */
|
||||
function stubRegistry(): void {
|
||||
vi.stubGlobal('fetch', async () => ({ ok: true, json: async () => ({ version: '1.1.0' }) }))
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
delete (globalThis as Record<string, unknown>).__DSH_CTX_VERSION__
|
||||
})
|
||||
|
||||
describe('UpdatePill', () => {
|
||||
test('a newer version paints the capsule linking the release notes', async () => {
|
||||
stubRegistry()
|
||||
const UpdatePill = await loadUpdatePill('1.0.0')
|
||||
const m = await mount(h(UpdatePill, {}))
|
||||
await flush()
|
||||
const pill = query(m.container, '.lc-ov-update')
|
||||
assert.equal(pill.getAttribute('href'), 'https://github.com/bowenliang123/dsh-context/releases/latest')
|
||||
assert.equal(pill.getAttribute('target'), '_blank')
|
||||
assert.equal(pill.getAttribute('rel'), 'noreferrer')
|
||||
assert.equal(query(pill, '.lc-ov-update-label').textContent, 'Update')
|
||||
assert.equal(query(pill, '.lc-ov-update-version').textContent, '↑ v1.1.0')
|
||||
// The tip rides the page's portaled hover bubble, below the pill in the head.
|
||||
assert.equal(pill.getAttribute('data-lc-tip'), 'Update v1.1.0 is available — click for the release notes')
|
||||
assert.equal(pill.getAttribute('data-lc-tip-side'), 'bottom')
|
||||
assert.equal(pill.getAttribute('aria-label'), pill.getAttribute('data-lc-tip'))
|
||||
await m.unmount()
|
||||
})
|
||||
|
||||
test('the Chinese dictionary rides the same capsule', async () => {
|
||||
stubRegistry()
|
||||
const UpdatePill = await loadUpdatePill('1.0.0', 'zh')
|
||||
const m = await mount(h(UpdatePill, {}))
|
||||
await flush()
|
||||
assert.equal(query(m.container, '.lc-ov-update-label').textContent, '新版本')
|
||||
assert.equal(query(m.container, '.lc-ov-update-version').textContent, '↑ v1.1.0')
|
||||
await m.unmount()
|
||||
})
|
||||
|
||||
test('the build being current leaves the head unpainted', async () => {
|
||||
stubRegistry()
|
||||
const UpdatePill = await loadUpdatePill('1.1.0')
|
||||
const m = await mount(h(UpdatePill, {}))
|
||||
await flush()
|
||||
assert.equal(queryAll(m.container, '.lc-ov-update').length, 0)
|
||||
await m.unmount()
|
||||
})
|
||||
})
|
||||
@@ -1,10 +1,13 @@
|
||||
// Latest-version check (src/client/latestVersion.ts): registry fetch with a 1h TTL and the semver comparator.
|
||||
// Latest-version check (src/client/latestVersion.ts): registry fetch with a 1h TTL, the semver comparator,
|
||||
// and the useNewerVersion hook every surface's update hint reads.
|
||||
// The fetch is stubbed per test — the suite never hits the network — and each fetch test re-imports the module
|
||||
// (vi.resetModules) to reset the module-level cache. Two registries are tried in order (official npm, then npmmirror).
|
||||
|
||||
import { createElement as h } from 'react'
|
||||
import assert from 'node:assert/strict'
|
||||
import { afterEach, describe, test, vi } from 'vitest'
|
||||
import { isNewerVersion } from '../../src/client/latestVersion'
|
||||
import { flush, mount } from './helpers/kit'
|
||||
|
||||
const HOUR_MS = 60 * 60 * 1000
|
||||
|
||||
@@ -12,6 +15,7 @@ afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
vi.useRealTimers()
|
||||
vi.resetModules()
|
||||
delete (globalThis as Record<string, unknown>).__DSH_CTX_VERSION__
|
||||
})
|
||||
|
||||
async function freshModule(): Promise<typeof import('../../src/client/latestVersion')> {
|
||||
@@ -154,3 +158,63 @@ describe('isNewerVersion', () => {
|
||||
assert.equal(isNewerVersion('x.y', '0.0.0'), false)
|
||||
})
|
||||
})
|
||||
|
||||
/** Mount the hook through a throwaway component; `version` emulates the tsdown define. The settled value is
|
||||
* what the hook last reported — an empty string stands for null, and a late setState still re-renders. */
|
||||
async function renderUseNewerVersion(version?: string): Promise<{ settled: () => string; unmount: () => Promise<void> }> {
|
||||
vi.resetModules()
|
||||
if (version === undefined) delete (globalThis as Record<string, unknown>).__DSH_CTX_VERSION__
|
||||
else (globalThis as Record<string, unknown>).__DSH_CTX_VERSION__ = version
|
||||
const { useNewerVersion } = await import('../../src/client/latestVersion')
|
||||
const seen: string[] = []
|
||||
function Probe(): null {
|
||||
seen.push(useNewerVersion() ?? '')
|
||||
return null
|
||||
}
|
||||
const m = await mount(h(Probe, {}))
|
||||
await flush()
|
||||
return { settled: () => seen[seen.length - 1], unmount: () => m.unmount() }
|
||||
}
|
||||
|
||||
describe('useNewerVersion', () => {
|
||||
test('a newer registry version is served to the caller', async () => {
|
||||
vi.stubGlobal('fetch', async () => ({ ok: true, json: async () => ({ version: '1.1.0' }) }))
|
||||
const { settled, unmount } = await renderUseNewerVersion('1.0.0')
|
||||
assert.equal(settled(), '1.1.0')
|
||||
await unmount()
|
||||
})
|
||||
|
||||
test('a dev build never checks the registry', async () => {
|
||||
const fetchMock = vi.fn()
|
||||
vi.stubGlobal('fetch', fetchMock)
|
||||
const { settled, unmount } = await renderUseNewerVersion() // PLUGIN_VERSION = 0.0.0-dev
|
||||
assert.equal(fetchMock.mock.calls.length, 0)
|
||||
assert.equal(settled(), '')
|
||||
await unmount()
|
||||
})
|
||||
|
||||
test('a version that is not newer stays null', async () => {
|
||||
vi.stubGlobal('fetch', async () => ({ ok: true, json: async () => ({ version: '1.0.0' }) }))
|
||||
const { settled, unmount } = await renderUseNewerVersion('1.0.0')
|
||||
assert.equal(settled(), '')
|
||||
await unmount()
|
||||
})
|
||||
|
||||
test('every registry failing stays null', async () => {
|
||||
vi.stubGlobal('fetch', async () => ({ ok: false, json: async () => ({}) }))
|
||||
const { settled, unmount } = await renderUseNewerVersion('1.0.0')
|
||||
assert.equal(settled(), '')
|
||||
await unmount()
|
||||
})
|
||||
|
||||
test('unmounting before the answer lands drops the late result', async () => {
|
||||
let resolveFetch: ((res: { ok: boolean; json: () => Promise<{ version: string }> }) => void) | null = null
|
||||
vi.stubGlobal('fetch', vi.fn(() => new Promise((resolve) => { resolveFetch = resolve })))
|
||||
const { settled, unmount } = await renderUseNewerVersion('1.0.0')
|
||||
await unmount()
|
||||
resolveFetch!({ ok: true, json: () => Promise.resolve({ version: '2.0.0' }) })
|
||||
await flush()
|
||||
// No state update after unmount, no act warning, and the value never lands.
|
||||
assert.equal(settled(), '')
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user