From 8d5525aeac0284131f5b83bd7d1011507293e03c Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Mon, 24 Aug 2026 16:59:33 +0200 Subject: [PATCH 1/6] feat(v4): make the instance lookup say which population it answers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `getInstances()` filtered on `$isMounted` and said nothing about it at the call site. `getInstances('Foo')` returning 2 where three elements declare `Foo` — one below its `media:` breakpoint — was undebuggable from the name, and the raw map read the filter hides had no public spelling at all, so 52 spec files reached past `getInstances()` for a private helper instead. Four exports replace the one, over a single internal `collect()` plus a predicate, so the traversal is written once: - `getInstances()` — every instance built, mounted or not. - `getMountedInstances()` — the live ones, what you may call a method on. - `getUnmountedInstances()` — built, then stood down. - `getInstance(el, name)` — one map read, no scan. Dropping the `$isMounted` filter resurrects nothing. The string form narrows three times, and the middle step already does the work the filter is credited with: `selectorFor(name)` over-matches by design, the `INSTANCES` read narrows to *constructed*, and `$isMounted` narrowed to *live*. An inactive declaration has no instance, and a breakpoint-withdrawn one is destroyed **and deleted from the map** by `reconcileElement()`. So the doc comment on `selectorFor()` — which attributed the narrowing to a mount check — is rewritten to name the map read instead, with the reason not to re-add the filter, because the filter would also hide the one population that is real: the instances a reversible `in-view` or `media:` strategy has legitimately unmounted and kept for the crossing back. There is deliberately no `getMountedInstance`. The singular returns one object; a caller who wants the live one reads `.$isMounted` on it, rather than trusting an `undefined` that would mean two things. Two asymmetries are now documented rather than discovered: the element form works on a detached element and the string form cannot reach one from `document`, and the string form is DOM order where the element form is mount order. Root barrel: 86 exports to 89. Subpath stubs regenerated with `npm run subpaths`. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01NnepYhqjPMcFCSb43PoHRM --- packages/v4/package.json | 12 + packages/v4/src/exports.spec.ts | 7 +- packages/v4/src/index.ts | 7 +- packages/v4/src/instances.spec.ts | 235 ++++++++++++++++-- packages/v4/src/instances.ts | 126 ++++++++-- packages/v4/src/protocol-symbols.ts | 9 +- packages/v4/src/subpaths/getInstance.ts | 1 + .../v4/src/subpaths/getMountedInstances.ts | 1 + .../v4/src/subpaths/getUnmountedInstances.ts | 1 + packages/v4/src/utils/selectors.ts | 10 +- packages/v4/test/package-node-consumer.js | 2 +- 11 files changed, 365 insertions(+), 46 deletions(-) create mode 100644 packages/v4/src/subpaths/getInstance.ts create mode 100644 packages/v4/src/subpaths/getMountedInstances.ts create mode 100644 packages/v4/src/subpaths/getUnmountedInstances.ts diff --git a/packages/v4/package.json b/packages/v4/package.json index e8898a55a..bc6260bdc 100644 --- a/packages/v4/package.json +++ b/packages/v4/package.json @@ -124,10 +124,22 @@ "types": "./dist/subpaths/createGroup.d.ts", "import": "./dist/subpaths/createGroup.js" }, + "./getInstance": { + "types": "./dist/subpaths/getInstance.d.ts", + "import": "./dist/subpaths/getInstance.js" + }, "./getInstances": { "types": "./dist/subpaths/getInstances.d.ts", "import": "./dist/subpaths/getInstances.js" }, + "./getMountedInstances": { + "types": "./dist/subpaths/getMountedInstances.d.ts", + "import": "./dist/subpaths/getMountedInstances.js" + }, + "./getUnmountedInstances": { + "types": "./dist/subpaths/getUnmountedInstances.d.ts", + "import": "./dist/subpaths/getUnmountedInstances.js" + }, "./defineManifest": { "types": "./dist/subpaths/defineManifest.d.ts", "import": "./dist/subpaths/defineManifest.js" diff --git a/packages/v4/src/exports.spec.ts b/packages/v4/src/exports.spec.ts index 82d5aefc0..09b569235 100644 --- a/packages/v4/src/exports.spec.ts +++ b/packages/v4/src/exports.spec.ts @@ -180,9 +180,10 @@ describe('the package entry points', () => { it('keeps the framework on the root entry, without the utils or removed exports', async () => { expect(typeof Base).toBe('function'); const root = (await import('@studiometa/js-toolkit-v4')) as Record; - // 84, plus `warn` and `reportDiagnostic`: the diagnostic channel is now - // reachable by a consumer with no instance to report as. - expect(Object.keys(root)).toHaveLength(86); + // 86, plus `getInstance`, `getMountedInstances` and + // `getUnmountedInstances`: the instance lookup now says at the call site + // which population it answers for, instead of filtering silently. + expect(Object.keys(root)).toHaveLength(89); expect(root.clamp).toBeUndefined(); expect(root.smoothTo).toBeUndefined(); for (const removed of [ diff --git a/packages/v4/src/index.ts b/packages/v4/src/index.ts index 065af3aa4..f4a777a5a 100644 --- a/packages/v4/src/index.ts +++ b/packages/v4/src/index.ts @@ -53,7 +53,12 @@ export { } from './dom-mutations.js'; export { EVENTS } from './events.js'; export { createGroup, type Group, type GroupMember } from './group.js'; -export { getInstances } from './instances.js'; +export { + getInstance, + getInstances, + getMountedInstances, + getUnmountedInstances, +} from './instances.js'; export { defineManifest, fromMetaGlob, diff --git a/packages/v4/src/instances.spec.ts b/packages/v4/src/instances.spec.ts index 042fc64d2..363541a9f 100644 --- a/packages/v4/src/instances.spec.ts +++ b/packages/v4/src/instances.spec.ts @@ -1,14 +1,51 @@ import { afterEach, describe, expect, it } from 'vitest'; -import { getInstances } from './instances.js'; +import { Base, type BaseConfig } from './Base.js'; +import { + getInstance, + getInstances, + getMountedInstances, + getUnmountedInstances, +} from './instances.js'; import { INSTANCES } from './protocol-symbols.js'; -import { getInstance } from './test-utils.js'; +import { registerComponent } from './registry.js'; import { renderTodoList, type TodoItem } from './todo.fixtures.js'; -import { resetDom, settle } from './test/index.js'; +import { resetDom, settle, waitFor } from './test/index.js'; afterEach(resetDom); -describe('getInstances', () => { - it('finds mounted instances page-wide, in DOM order', async () => { +const OFFSCREEN = 'position:absolute;top:300vh;left:0;width:50px;height:50px'; +const ONSCREEN = 'position:absolute;top:0;left:0;width:50px;height:50px'; + +/** + * A component on a reversible mount strategy. + * + * This is the whole reason `getInstances()` and `getMountedInstances()` are + * two functions: `in-view` unmounts its instance when the element leaves the + * viewport and keeps it in the element's map for the crossing back, so there + * is a real, reachable population of built-but-unmounted instances that a + * lookup must be able to name. + */ +class Reversible extends Base { + static config: BaseConfig = { name: 'Reversible', mountStrategy: 'in-view' }; +} + +registerComponent(Reversible); + +/** Mount a `Reversible`, then move it away so its strategy stands it down. */ +async function renderStoodDown(): Promise { + const el = document.createElement('div'); + el.setAttribute('data-component', 'Reversible'); + el.setAttribute('style', ONSCREEN); + document.body.append(el); + await waitFor(() => getMountedInstances('Reversible').length === 1); + + el.setAttribute('style', OFFSCREEN); + await waitFor(() => getInstance(el, 'Reversible')?.$isMounted === false); + return el; +} + +describe('getInstances by name', () => { + it('finds instances page-wide, in DOM order', async () => { renderTodoList({ items: ['one', 'two'] }); renderTodoList({ items: ['three'] }); await settle(); @@ -64,41 +101,159 @@ describe('getInstances', () => { expect(getInstances('TodoCount')).toHaveLength(2); }); - it('drops an instance unmounted with its subtree', async () => { + it('keeps an instance unmounted by hand', async () => { + const root = renderTodoList({ items: ['one'] }); + await settle(); + + const li = root.querySelector('[data-component="TodoItem"]') as HTMLElement; + const instance = getInstance(li, 'TodoItem'); + instance?.$unmount(); + + expect(instance?.$isMounted).toBe(false); + expect(getInstances('TodoItem')).toEqual([instance]); + }); +}); + +describe('getMountedInstances by name', () => { + it('returns the live instances, in DOM order', async () => { + renderTodoList({ items: ['one', 'two'] }); + await settle(); + + const elements = [...document.querySelectorAll('[data-component~="TodoItem"]')]; + expect(getMountedInstances('TodoItem').map((item) => item.$el)).toEqual(elements); + expect(getMountedInstances('TodoItem').every((item) => item.$isMounted)).toBe(true); + }); + + it('drops an instance unmounted by hand, which getInstances keeps', async () => { + const root = renderTodoList({ items: ['one'] }); + await settle(); + + const li = root.querySelector('[data-component="TodoItem"]') as HTMLElement; + getInstance(li, 'TodoItem')?.$unmount(); + + expect(getInstances('TodoItem')).toHaveLength(1); + expect(getMountedInstances('TodoItem')).toEqual([]); + }); + + it('takes the same root scope as getInstances', async () => { + const first = renderTodoList({ items: ['one', 'two'] }); + const second = renderTodoList({ items: ['three'] }); + await settle(); + + expect(getMountedInstances('TodoItem', first)).toHaveLength(2); + expect(getMountedInstances('TodoItem', second)).toHaveLength(1); + }); +}); + +describe('getUnmountedInstances by name', () => { + it('is empty while every instance is live', async () => { + renderTodoList({ items: ['one', 'two'] }); + await settle(); + + expect(getMountedInstances('TodoItem')).toHaveLength(2); + expect(getUnmountedInstances('TodoItem')).toEqual([]); + }); + + it('names the instance a reversible strategy stood down', async () => { + const el = await renderStoodDown(); + const instance = getInstance(el, 'Reversible'); + + // The strategy unmounts without deleting: the instance is kept for the + // crossing back, which is the whole population this function is for. + expect(instance?.$isMounted).toBe(false); + expect(getInstances('Reversible')).toEqual([instance]); + expect(getMountedInstances('Reversible')).toEqual([]); + expect(getUnmountedInstances('Reversible')).toEqual([instance]); + }); + + it('gives the instance back when the strategy mounts it again', async () => { + const el = await renderStoodDown(); + const instance = getInstance(el, 'Reversible'); + + el.setAttribute('style', ONSCREEN); + await waitFor(() => getMountedInstances('Reversible').length === 1); + + expect(getUnmountedInstances('Reversible')).toEqual([]); + expect(getMountedInstances('Reversible')).toEqual([instance]); + }); + + it('partitions getInstances together with getMountedInstances', async () => { + const root = renderTodoList({ items: ['one', 'two'] }); + await settle(); + + const li = root.querySelector('[data-component="TodoItem"]') as HTMLElement; + getInstance(li, 'TodoItem')?.$unmount(); + + const all = getInstances('TodoItem'); + const mounted = getMountedInstances('TodoItem'); + const unmounted = getUnmountedInstances('TodoItem'); + + expect(all).toHaveLength(2); + expect(mounted).toHaveLength(1); + expect(unmounted).toHaveLength(1); + // Every instance is in exactly one half, and the two halves add up. + for (const instance of all) { + expect(mounted.includes(instance)).toBe(!unmounted.includes(instance)); + } + }); + + it('never returns a declaration that was never constructed', async () => { + const el = document.createElement('div'); + el.setAttribute('data-component', 'Unregistered'); + document.body.append(el); + await settle(); + + // No instance is not the same as an unmounted instance, and the map read + // — not a mount check — is what tells the two apart. + expect(el[INSTANCES]).toBeUndefined(); + expect(getUnmountedInstances('Unregistered')).toEqual([]); + }); +}); + +describe('a detached element', () => { + it('is unreachable by name from the document, but reachable from its own root', async () => { const root = renderTodoList({ items: ['one', 'two'] }); await settle(); const li = root.querySelector('[data-component="TodoItem"]') as HTMLElement; const instance = getInstance(li, 'TodoItem'); - expect(getInstances('TodoItem')).toContain(instance); + expect(getMountedInstances('TodoItem')).toContain(instance); const detached = document.createElement('div'); detached.append(root); await settle(); - // Unmounted instances remain retained but are excluded by `$isMounted`. - expect(instance.$isMounted).toBe(false); + // Detaching unmounts the subtree but retains every instance. + expect(instance?.$isMounted).toBe(false); expect(getInstance(li, 'TodoItem')).toBe(instance); expect(detached.querySelectorAll('[data-component~="TodoItem"]')).toHaveLength(2); - expect(getInstances('TodoItem', detached)).toEqual([]); + + // The asymmetry: `document` cannot see the subtree, the detached root can. expect(getInstances('TodoItem')).toEqual([]); + expect(getInstances('TodoItem', detached)).toHaveLength(2); + expect(getUnmountedInstances('TodoItem', detached)).toHaveLength(2); + expect(getMountedInstances('TodoItem', detached)).toEqual([]); }); - it('drops an unmounted instance', async () => { + it('answers the element overload with no DOM at all', async () => { const root = renderTodoList({ items: ['one'] }); await settle(); const li = root.querySelector('[data-component="TodoItem"]') as HTMLElement; const instance = getInstance(li, 'TodoItem'); - instance.$unmount(); + li.remove(); + await settle(); - expect(instance.$isMounted).toBe(false); - expect(getInstances('TodoItem')).toEqual([]); + expect(li.isConnected).toBe(false); + expect(getInstances(li)).toEqual([instance]); + expect(getUnmountedInstances(li)).toEqual([instance]); + expect(getMountedInstances(li)).toEqual([]); + expect(getInstance(li, 'TodoItem')).toBe(instance); }); }); -describe('getInstances on an element', () => { - it('answers what is mounted on one element, in mount order', async () => { +describe('the element overload', () => { + it('answers what is on one element, in mount order', async () => { const root = renderTodoList({ items: ['one'] }); const li = root.querySelector('[data-component="TodoItem"]') as HTMLElement; li.setAttribute('data-component', 'TodoItem TodoCount'); @@ -109,6 +264,8 @@ describe('getInstances on an element', () => { 'TodoCount', ]); expect(getInstances(li)).toContain(getInstance(li, 'TodoItem')); + expect(getMountedInstances(li)).toEqual(getInstances(li)); + expect(getUnmountedInstances(li)).toEqual([]); }); it('never looks past the element', async () => { @@ -117,6 +274,9 @@ describe('getInstances on an element', () => { // `TodoList` is on `root`; its items are on descendants. expect(getInstances(root).map((instance) => instance.$config.name)).toEqual(['TodoList']); + expect(getMountedInstances(root).map((instance) => instance.$config.name)).toEqual([ + 'TodoList', + ]); }); it('returns nothing for an element carrying no instance', async () => { @@ -125,15 +285,52 @@ describe('getInstances on an element', () => { await settle(); expect(getInstances(el)).toEqual([]); + expect(getMountedInstances(el)).toEqual([]); + expect(getUnmountedInstances(el)).toEqual([]); }); - it('excludes an instance that is no longer mounted', async () => { + it('keeps an instance that is no longer mounted, and sorts it', async () => { const root = renderTodoList({ items: ['one'] }); await settle(); const li = root.querySelector('[data-component="TodoItem"]') as HTMLElement; - getInstance(li, 'TodoItem').$unmount(); + const instance = getInstance(li, 'TodoItem'); + instance?.$unmount(); + + expect(getInstances(li)).toEqual([instance]); + expect(getMountedInstances(li)).toEqual([]); + expect(getUnmountedInstances(li)).toEqual([instance]); + }); +}); + +describe('getInstance', () => { + it('reads one name off one element, mounted or not', async () => { + const root = renderTodoList({ items: ['one'] }); + await settle(); + + const li = root.querySelector('[data-component="TodoItem"]') as HTMLElement; + const instance = getInstance(li, 'TodoItem'); + expect(instance).toBeDefined(); + expect(instance?.$el).toBe(li); + expect(instance?.$isMounted).toBe(true); - expect(getInstances(li)).toEqual([]); + instance?.$unmount(); + expect(getInstance(li, 'TodoItem')).toBe(instance); + }); + + it('returns undefined for a name the element does not carry', async () => { + const root = renderTodoList({ items: ['one'] }); + await settle(); + + expect(getInstance(root, 'TodoItem')).toBeUndefined(); + }); + + it('returns undefined for an element carrying no instance', async () => { + const el = document.createElement('div'); + document.body.append(el); + await settle(); + + expect(el[INSTANCES]).toBeUndefined(); + expect(getInstance(el, 'TodoList')).toBeUndefined(); }); }); diff --git a/packages/v4/src/instances.ts b/packages/v4/src/instances.ts index 7b7c4679c..a7f472fb2 100644 --- a/packages/v4/src/instances.ts +++ b/packages/v4/src/instances.ts @@ -3,35 +3,127 @@ import { selectorFor } from './utils/selectors.js'; import type { Base } from './Base.js'; /** - * Return mounted instances of a component name in DOM order. - * The search includes only descendants of `root` and excludes unmounted instances. - */ -export function getInstances(name: string, root?: ParentNode): T[]; -/** - * Return every mounted instance on one element, in mount order. + * The one traversal behind the three plural lookups. * - * The element form is the reason this overload exists rather than a second - * export: both answer "which instances are mounted", and the argument picks the - * scope. It also keeps the `INSTANCES` read in one place, which matters more - * now that the key is a symbol and no longer spellable as `el.__base__`. + * Written once rather than three times, because the traversal is the part + * with the invariants — DOM order for a name, mount order for an element, the + * instance-map read in between — and the three exports differ only in which + * instances they keep. `accept` is that difference and nothing else. + * + * The string form narrows twice before `accept` ever runs. + * {@link selectorFor} over-matches on purpose: it lists the responsive + * spellings of `data-component` as well as the plain one, so an element that + * declares a name only above a breakpoint is still a candidate. The + * `INSTANCES` read is what removes it, because a declaration that is not + * currently active has no instance — the registry destroys a + * breakpoint-withdrawn one *and* deletes it from the map. So the map read, + * not `accept`, is what keeps an inactive declaration out of every result. */ -export function getInstances(el: Element): T[]; -export function getInstances( +function collect( target: string | Element, - root: ParentNode = document, + root: ParentNode, + accept: (instance: Base) => boolean, ): T[] { if (typeof target !== 'string') { - return [...(target[INSTANCES]?.values() ?? [])].filter( - (instance) => instance.$isMounted, - ) as T[]; + return [...(target[INSTANCES]?.values() ?? [])].filter(accept) as T[]; } const instances: T[] = []; for (const el of root.querySelectorAll(selectorFor(target))) { const instance = el[INSTANCES]?.get(target); - if (instance?.$isMounted) { + if (instance && accept(instance)) { instances.push(instance as T); } } return instances; } + +/** + * Every instance built for a component name, in DOM order, mounted or not. + * + * The search covers the descendants of `root` and never `root` itself, since + * it is a `querySelectorAll`. A **detached** element is therefore unreachable + * by this form even though it still carries its instance — pass the element + * to the overload below, or pass its detached root as `root`. + */ +export function getInstances(name: string, root?: ParentNode): T[]; +/** + * Every instance on one element, in mount order, mounted or not. + * + * The element form is the reason this overload exists rather than a second + * export: both answer "which instances are there", and the argument picks the + * scope. It also keeps the `INSTANCES` read in one place, which matters more + * now that the key is a symbol and no longer spellable as `el.__base__`. + * + * Unlike the string form it does not consult the DOM, so it answers for a + * detached element as readily as for a connected one. + */ +export function getInstances(el: Element): T[]; +export function getInstances( + target: string | Element, + root: ParentNode = document, +): T[] { + return collect(target, root, () => true); +} + +/** + * The live instances of a component name, in DOM order. + * + * This is the safe list to call a method on: every instance in it has run + * `mounted()` and has not yet run `unmounted()`. Prefer it over + * {@link getInstances} whenever the result is going to be *used* rather than + * counted or inspected. + * + * Scoping and the detached-element blind spot are {@link getInstances}'. + */ +export function getMountedInstances(name: string, root?: ParentNode): T[]; +/** The live instances on one element, in mount order. Works when detached. */ +export function getMountedInstances(el: Element): T[]; +export function getMountedInstances( + target: string | Element, + root: ParentNode = document, +): T[] { + return collect(target, root, (instance) => instance.$isMounted); +} + +/** + * The instances of a component name that were built and are not mounted, in + * DOM order. + * + * This population is small and specific. It is what a **reversible** mount + * strategy leaves behind: `in-view` and `media:` unmount their instance when + * the condition stops holding and keep it for the crossing back, so the + * instance stays in the element's map with `$isMounted === false`. A + * constructor that succeeded before a failing `mounted()` lands here too. + * + * What is *not* here: a declaration whose class never arrived, and a + * declaration withdrawn by a breakpoint. Neither has an instance at all. + * + * Scoping and the detached-element blind spot are {@link getInstances}'. + */ +export function getUnmountedInstances(name: string, root?: ParentNode): T[]; +/** The built-but-unmounted instances on one element, in mount order. */ +export function getUnmountedInstances(el: Element): T[]; +export function getUnmountedInstances( + target: string | Element, + root: ParentNode = document, +): T[] { + return collect(target, root, (instance) => !instance.$isMounted); +} + +/** + * The instance of `name` on `el`, mounted or not. + * + * One map read and no scan, which is why it is a function of its own rather + * than a filter over {@link getInstances}: the caller already holds the + * element and the name, so there is nothing left to search. It is also the + * answer to "is this element's instance there yet", which every plural form + * loses by returning a list. + * + * There is deliberately no `getMountedInstance`. The result is one object, so + * a caller who needs the live one reads `.$isMounted` on it — a second export + * would only hide that check behind a `undefined` that means two things. + */ +export function getInstance(el: Element, name: string): T | undefined { + return el[INSTANCES]?.get(name) as T | undefined; +} diff --git a/packages/v4/src/protocol-symbols.ts b/packages/v4/src/protocol-symbols.ts index 400e868b4..58f17c122 100644 --- a/packages/v4/src/protocol-symbols.ts +++ b/packages/v4/src/protocol-symbols.ts @@ -13,9 +13,12 @@ export const HANDLER_REGISTRATIONS: unique symbol = Symbol.for( * `$unmount()` on it. `Symbol.for` is realm-global, so evaluated copies of * this package still agree on the key. * - * Not public API — `getInstances()` is how consumers resolve instances, by - * name or by element. From a devtools console, where there is nothing to - * import, the realm-global key is the whole recipe: + * Not public API — `getInstance()`, `getInstances()`, + * `getMountedInstances()` and `getUnmountedInstances()` are how consumers + * resolve instances, by name, by element, or both. Between them they express + * every read this map answers, so nothing has to reach for the symbol. From a + * devtools console, where there is nothing to import, the realm-global key is + * the whole recipe: * `$0[Symbol.for('@studiometa/js-toolkit-v4/instances')]`. */ export const INSTANCES: unique symbol = Symbol.for('@studiometa/js-toolkit-v4/instances'); diff --git a/packages/v4/src/subpaths/getInstance.ts b/packages/v4/src/subpaths/getInstance.ts new file mode 100644 index 000000000..7757b3ab8 --- /dev/null +++ b/packages/v4/src/subpaths/getInstance.ts @@ -0,0 +1 @@ +export { getInstance, getInstance as default } from '../instances.js'; diff --git a/packages/v4/src/subpaths/getMountedInstances.ts b/packages/v4/src/subpaths/getMountedInstances.ts new file mode 100644 index 000000000..5a0b28d2f --- /dev/null +++ b/packages/v4/src/subpaths/getMountedInstances.ts @@ -0,0 +1 @@ +export { getMountedInstances, getMountedInstances as default } from '../instances.js'; diff --git a/packages/v4/src/subpaths/getUnmountedInstances.ts b/packages/v4/src/subpaths/getUnmountedInstances.ts new file mode 100644 index 000000000..372099dd4 --- /dev/null +++ b/packages/v4/src/subpaths/getUnmountedInstances.ts @@ -0,0 +1 @@ +export { getUnmountedInstances, getUnmountedInstances as default } from '../instances.js'; diff --git a/packages/v4/src/utils/selectors.ts b/packages/v4/src/utils/selectors.ts index bdbc51498..d4145d115 100644 --- a/packages/v4/src/utils/selectors.ts +++ b/packages/v4/src/utils/selectors.ts @@ -6,8 +6,14 @@ import { responsiveAttributeNames } from '../responsive-options.js'; * * `~=` gives whitespace-token matching, so `data-component="Action Dialog"` * declares several components on one element. Scoped attributes are included - * as discovery candidates; callers still check for a mounted instance, so an - * inactive declaration never appears in a lookup result. + * as discovery candidates, so this selector deliberately over-matches: an + * element that declares a name only above a breakpoint matches it below one + * too. The narrowing is the caller's read of the element's instance map, not + * a mount check — an inactive declaration has no instance, because a + * breakpoint-withdrawn component is destroyed and deleted from that map. Do + * not re-add an `$isMounted` filter here or at a call site to "fix" the + * over-matching: it would also hide every instance a reversible `in-view` or + * `media:` strategy has legitimately stood down. */ export function selectorFor(name: string): string { return [COMPONENT_ATTRIBUTE, ...responsiveAttributeNames(COMPONENT_ATTRIBUTE)] diff --git a/packages/v4/test/package-node-consumer.js b/packages/v4/test/package-node-consumer.js index 198d67f4d..9a0e3c75f 100644 --- a/packages/v4/test/package-node-consumer.js +++ b/packages/v4/test/package-node-consumer.js @@ -59,7 +59,7 @@ assert.equal(createGroup, toolkit.createGroup); assert.equal(createGroupDefault, createGroup); // Keep in step with the same count in `src/exports.spec.ts`. This one runs // under `check:package`, which `npm test` does not cover. -assert.equal(Object.keys(toolkit).length, 86); +assert.equal(Object.keys(toolkit).length, 89); // The diagnostic channel is reachable by a consumer with no instance to // report as, and both halves have to survive packing. assert.equal(typeof toolkit.warn, 'function'); From 50a019a9c206b837c7858e6fd76981e242e57dea Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Mon, 24 Aug 2026 17:02:58 +0200 Subject: [PATCH 2/6] refactor(v4): say which population each lookup call site wants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `getInstances()` no longer filters on `$isMounted`, so every existing call site had to be reread rather than renamed. Each of these picked the function that matches what the code does with the result, not the one that keeps the old behaviour by default. `getMountedInstances()`, because the result is *used*: - `ActionEvent.instances` builds the effect's name→instance map and the effect calls methods on it. - `ActionEvent.targets` resolves the components an event runs effects on. A stood-down `in-view` target must not be run against. - `Sticky.instances` stacks siblings by index and sums their heights. An unmounted `Sticky` has no position to contribute. - The `mount()`, `resetDom()`, `recordEvents()` and `resetRegistry()` specs on `src/test/index.spec.ts`, and the responsive-set spec, all assert "this component is live" — which `getInstances()` no longer says. `getInstances()`, kept, because the claim is "nothing was ever built": - `autoload.spec.ts` on a lazy declaration before its class arrives — the assertion is now stronger, since no filter can be hiding an instance. - The two post-`resetRegistry()` assertions, for the same reason. Worth recording: the suite was green both before and after this commit. No test distinguished the two populations at any of these sites, which is the argument for the rename — the filter was invisible to the callers and to their specs alike. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01NnepYhqjPMcFCSb43PoHRM --- packages/v4/migration/Action/ActionEvent.ts | 6 +++--- packages/v4/migration/Sticky/Sticky.ts | 4 ++-- packages/v4/src/responsive-components.spec.ts | 4 ++-- packages/v4/src/test/index.spec.ts | 18 +++++++++--------- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/v4/migration/Action/ActionEvent.ts b/packages/v4/migration/Action/ActionEvent.ts index d9ef914a4..9f65e1612 100644 --- a/packages/v4/migration/Action/ActionEvent.ts +++ b/packages/v4/migration/Action/ActionEvent.ts @@ -1,4 +1,4 @@ -import { getInstances, type Base } from '../../src/index.js'; +import { getMountedInstances, type Base } from '../../src/index.js'; import { MODIFIERS, parseEventDefinition, type Modifier } from '../event-modifiers.js'; import { getEffect, type EffectFunction } from './expression.js'; @@ -63,7 +63,7 @@ export class ActionEvent { /** Co-located mounted instances, recomputed for each event. */ get instances(): Map { return new Map( - getInstances(this.action.$el).map((instance) => [instance.$config.name, instance]), + getMountedInstances(this.action.$el).map((instance) => [instance.$config.name, instance]), ); } @@ -99,7 +99,7 @@ export class ActionEvent { // Ignore unparseable target parts. continue; } - for (const instance of getInstances(name)) { + for (const instance of getMountedInstances(name)) { if (!selector || instance.$el.matches(selector)) { targets.push({ [name]: instance }); } diff --git a/packages/v4/migration/Sticky/Sticky.ts b/packages/v4/migration/Sticky/Sticky.ts index 1b3bea59a..ecf3290b2 100644 --- a/packages/v4/migration/Sticky/Sticky.ts +++ b/packages/v4/migration/Sticky/Sticky.ts @@ -1,7 +1,7 @@ import { Base, component, - getInstances, + getMountedInstances, withResize, withScroll, write, @@ -67,7 +67,7 @@ export class Sticky extends withResize(withScro * and `unmounted()` — core's registry already answers this live. */ get instances(): Sticky[] { - return getInstances('Sticky'); + return getMountedInstances('Sticky'); } set y(value: number) { diff --git a/packages/v4/src/responsive-components.spec.ts b/packages/v4/src/responsive-components.spec.ts index 6e5efe02d..f99cd1ba0 100644 --- a/packages/v4/src/responsive-components.spec.ts +++ b/packages/v4/src/responsive-components.spec.ts @@ -2,7 +2,7 @@ import { afterEach, describe, expect, it } from 'vitest'; import { Base, type BaseConfig } from './Base.js'; import { DIAGNOSTICS } from './diagnostic-contract.js'; import { whenDOMSettled } from './dom-mutations.js'; -import { getInstances } from './instances.js'; +import { getMountedInstances } from './instances.js'; import { INSTANCES } from './protocol-symbols.js'; import { registerComponent, registerManifest } from './registry.js'; import { BREAKPOINTS, setBreakpoints } from './services/breakpoint.js'; @@ -109,7 +109,7 @@ describe('responsive component declarations', () => { [action.name, analytics.name, mobileMenu.name, mobileSearch.name].sort(), ); expect(instance(el, action.name)?.mounts).toBe(1); - expect(getInstances(mobileMenu.name)).toEqual([instance(el, mobileMenu.name)]); + expect(getMountedInstances(mobileMenu.name)).toEqual([instance(el, mobileMenu.name)]); }); it('replaces lower scoped sets, stops them on an empty override, and restores fresh identities', async () => { diff --git a/packages/v4/src/test/index.spec.ts b/packages/v4/src/test/index.spec.ts index 93f9086bc..2d741817c 100644 --- a/packages/v4/src/test/index.spec.ts +++ b/packages/v4/src/test/index.spec.ts @@ -2,7 +2,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; import * as subpath from '@studiometa/js-toolkit-v4/test'; import { Base } from '../Base.js'; import { warn } from '../diagnostics.js'; -import { getInstances } from '../instances.js'; +import { getInstances, getMountedInstances } from '../instances.js'; import { registerComponent, registerComponents, registerManifest } from '../registry.js'; import { defaultScheduler } from '../scheduler.js'; import { @@ -94,7 +94,7 @@ describe('mount()', () => { expect(root.parentElement).toBe(document.body); expect(root.tagName).toBe('DIV'); - const [subject] = getInstances('TestHelpersSubject', root); + const [subject] = getMountedInstances('TestHelpersSubject', root); expect(subject.mountedCalls).toBe(1); expect(subject.$el).toBe(root.firstElementChild); expect(subject.$el.textContent).toBe('written'); @@ -106,7 +106,7 @@ describe('mount()', () => { ); expect(root.children).toHaveLength(2); - expect(getInstances('TestHelpersSubject', root)).toHaveLength(2); + expect(getMountedInstances('TestHelpersSubject', root)).toHaveLength(2); }); }); @@ -210,7 +210,7 @@ describe('waitFor()', () => { describe('resetDom()', () => { it('empties the body and unmounts what was in it', async () => { const root = await mount('
'); - expect(getInstances('TestHelpersSubject', root)).toHaveLength(1); + expect(getMountedInstances('TestHelpersSubject', root)).toHaveLength(1); await resetDom(); @@ -276,7 +276,7 @@ describe('captureDiagnostics()', () => { describe('recordEvents()', () => { it("captures a component's emit, with its detail", async () => { const root = await mount('
'); - const [emitter] = getInstances('TestHelpersEmitter', root); + const [emitter] = getMountedInstances('TestHelpersEmitter', root); const log = recordEvents(root, 'ping'); emitter.ping(2); @@ -291,7 +291,7 @@ describe('recordEvents()', () => { it('keeps several types in one array, in delivery order', async () => { const root = await mount('
'); - const [emitter] = getInstances('TestHelpersEmitter', root); + const [emitter] = getMountedInstances('TestHelpersEmitter', root); const log = recordEvents(root, 'ping', 'pong'); emitter.pingLater(1); @@ -306,7 +306,7 @@ describe('recordEvents()', () => { it('ignores a type it was not asked for, and stops when stopped', async () => { const root = await mount('
'); - const [emitter] = getInstances('TestHelpersEmitter', root); + const [emitter] = getMountedInstances('TestHelpersEmitter', root); const log = recordEvents(root, 'ping'); emitter.$emit('pong'); @@ -349,7 +349,7 @@ describe('resetRegistry()', () => { expect(log.codes).toEqual([]); const root = await mount('
'); - const [instance] = getInstances('TestHelpersRecycled', root); + const [instance] = getMountedInstances('TestHelpersRecycled', root); expect(instance).toBeInstanceOf(second); }); @@ -381,7 +381,7 @@ describe('resetRegistry()', () => { }, ); const before = await mount('
'); - expect(getInstances('TestHelpersRetired', before)).toHaveLength(1); + expect(getMountedInstances('TestHelpersRetired', before)).toHaveLength(1); await resetDom(); resetRegistry(); From 65617606a13a0f3848ea6452880348592a155e0a Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Mon, 24 Aug 2026 17:11:07 +0200 Subject: [PATCH 3/6] test(v4): import getInstance from the public path in 51 spec files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `getInstance()` is core's now, so the specs that were reaching around the public surface for it stop doing that. `migration/**` takes it off the root barrel next to `registerComponents`, and `src/**` takes it off `instances.js` like every other core module it imports. Behaviour is identical — the same raw map read, no filter. The one real change is the type. The private helper was `(el: Element | null, name: string): T`, which lied twice: it accepted a `querySelector()` result and claimed the instance was always there. The public one is `(el: Element, name: string): T | undefined`, so a spec that knows the element and the instance exist now says so with `!`. That is the whole of the churn here, and it is worth its weight: the places where `!` had to go on the *argument* are exactly the places a `querySelector()` miss would have produced `undefined` from a helper whose return type said it could not. Where the spec was already handling the absence — `toBeUndefined()`, `toBeTruthy()`, `Boolean(...)`, a `waitFor()` predicate polling for the instance to appear — no assertion was added, because the point of the call there is that it may be `undefined`. `src/test-utils.js` still exports `countRequestedFrames`, so it survives this commit; four spec files still import it from there. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01NnepYhqjPMcFCSb43PoHRM --- .../v4/migration/Accordion/Accordion.spec.ts | 9 ++- packages/v4/migration/Action/Action.spec.ts | 12 +++- .../v4/migration/AnchorNav/AnchorNav.spec.ts | 15 ++-- .../migration/AnchorNav/AnchorNavLink.spec.ts | 5 +- .../v4/migration/Carousel/Carousel.spec.ts | 7 +- .../ClickOutside/ClickOutside.spec.ts | 15 ++-- packages/v4/migration/Cursor/Cursor.spec.ts | 8 +-- packages/v4/migration/Data/DataBind.spec.ts | 5 +- .../v4/migration/Data/DataDerived.spec.ts | 5 +- packages/v4/migration/Data/DataScope.spec.ts | 9 ++- packages/v4/migration/Dialog/Dialog.spec.ts | 29 ++++---- .../v4/migration/Draggable/Draggable.spec.ts | 7 +- packages/v4/migration/Fetch/Fetch.spec.ts | 5 +- .../Fetch/FetchShopifyPartial.spec.ts | 5 +- .../migration/Figure/AbstractFigure.spec.ts | 5 +- .../v4/migration/Figure/FigureShopify.spec.ts | 5 +- .../migration/Figure/FigureTwicpics.spec.ts | 5 +- .../migration/FigureVideo/FigureVideo.spec.ts | 7 +- .../FigureVideo/FigureVideoTwicpics.spec.ts | 7 +- .../v4/migration/Hoverable/Hoverable.spec.ts | 10 ++- .../migration/LazyInclude/LazyInclude.spec.ts | 17 +++-- packages/v4/migration/Menu/Menu.spec.ts | 9 ++- packages/v4/migration/Menu/MenuBtn.spec.ts | 5 +- packages/v4/migration/Menu/MenuList.spec.ts | 7 +- .../v4/migration/Prefetch/Prefetch.spec.ts | 33 +++++---- .../ScrollAnimation/ScrollAnimation.spec.ts | 9 ++- .../v4/migration/ScrollTo/ScrollTo.spec.ts | 5 +- packages/v4/migration/Slider/Slider.spec.ts | 5 +- .../v4/migration/Slider/SliderDots.spec.ts | 9 ++- .../v4/migration/Slider/SliderDrag.spec.ts | 7 +- .../migration/Slider/SliderProgress.spec.ts | 5 +- packages/v4/migration/Sticky/Sticky.spec.ts | 24 ++++--- packages/v4/migration/Timer/Timer.spec.ts | 5 +- .../v4/migration/Timer/TimerProgress.spec.ts | 5 +- packages/v4/migration/Toaster/Toast.spec.ts | 5 +- packages/v4/migration/Toaster/Toaster.spec.ts | 7 +- packages/v4/migration/Track/Track.spec.ts | 11 ++- .../v4/migration/Track/TrackEvent.spec.ts | 5 +- .../Transition/withTransition.spec.ts | 17 +++-- packages/v4/src/Base.spec.ts | 28 ++++---- packages/v4/src/config-extension.spec.ts | 16 ++--- packages/v4/src/context-subscription.spec.ts | 6 +- packages/v4/src/context.spec.ts | 6 +- packages/v4/src/decorators.spec.ts | 70 +++++++++---------- packages/v4/src/dom-mutations.spec.ts | 4 +- packages/v4/src/group.spec.ts | 6 +- packages/v4/src/manifest.spec.ts | 6 +- packages/v4/src/props.spec.ts | 6 +- packages/v4/src/registry.spec.ts | 26 +++---- packages/v4/src/responsive-options.spec.ts | 58 ++++++++------- packages/v4/src/services/mixin.spec.ts | 5 +- 51 files changed, 299 insertions(+), 303 deletions(-) diff --git a/packages/v4/migration/Accordion/Accordion.spec.ts b/packages/v4/migration/Accordion/Accordion.spec.ts index ab97d5e2f..5b58b29b7 100644 --- a/packages/v4/migration/Accordion/Accordion.spec.ts +++ b/packages/v4/migration/Accordion/Accordion.spec.ts @@ -1,6 +1,5 @@ import { afterEach, describe, expect, it } from 'vitest'; -import { registerComponent } from '../../src/index.js'; -import { getInstance } from '../../src/test-utils.js'; +import { getInstance, registerComponent } from '../../src/index.js'; import { resetDom, settle } from '../../src/test/index.js'; import { Accordion } from './Accordion.js'; import { AccordionItem } from './AccordionItem.js'; @@ -27,7 +26,7 @@ function render(): HTMLElement { function items(root: HTMLElement): AccordionItem[] { return [...root.querySelectorAll('[data-component="AccordionItem"]')].map((el) => - getInstance(el, 'AccordionItem'), + getInstance(el, 'AccordionItem')!, ); } @@ -36,7 +35,7 @@ describe('Accordion', () => { const root = render(); await settle(); - const accordion = getInstance(root, 'Accordion'); + const accordion = getInstance(root, 'Accordion')!; expect(accordion.items.size).toBe(3); expect(accordion.items.items.every((item) => item instanceof AccordionItem)).toBe(true); }); @@ -140,7 +139,7 @@ describe('Accordion', () => { const root = render(); await settle(); - const accordion = getInstance(root, 'Accordion'); + const accordion = getInstance(root, 'Accordion')!; root.querySelector('[data-component="AccordionItem"]')?.remove(); await settle(); expect(accordion.items.size).toBe(2); diff --git a/packages/v4/migration/Action/Action.spec.ts b/packages/v4/migration/Action/Action.spec.ts index fad610f53..ab7e5e2b5 100644 --- a/packages/v4/migration/Action/Action.spec.ts +++ b/packages/v4/migration/Action/Action.spec.ts @@ -1,6 +1,12 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { Base, registerComponents, swap, SWAP_MODES, type BaseConfig } from '../../src/index.js'; -import { getInstance } from '../../src/test-utils.js'; +import { + Base, + getInstance, + registerComponents, + swap, + SWAP_MODES, + type BaseConfig, +} from '../../src/index.js'; import { mount, resetDom, settle } from '../../src/test/index.js'; import { Dialog } from '../Dialog/Dialog.js'; import { Action } from './Action.js'; @@ -37,7 +43,7 @@ registerComponents(Action, Target, Foo, Bar, Dialog, MountProbe); afterEach(resetDom); function at(root: ParentNode, selector: string, name: string): T { - return getInstance(root.querySelector(selector), name); + return getInstance(root.querySelector(selector)!, name)!; } function click(el: Element): Event { diff --git a/packages/v4/migration/AnchorNav/AnchorNav.spec.ts b/packages/v4/migration/AnchorNav/AnchorNav.spec.ts index cb59abbc9..dbf14befc 100644 --- a/packages/v4/migration/AnchorNav/AnchorNav.spec.ts +++ b/packages/v4/migration/AnchorNav/AnchorNav.spec.ts @@ -1,6 +1,5 @@ import { afterEach, describe, expect, it } from 'vitest'; -import { registerComponents } from '../../src/index.js'; -import { getInstance } from '../../src/test-utils.js'; +import { getInstance, registerComponents } from '../../src/index.js'; import { mount, resetDom, settle, waitFor } from '../../src/test/index.js'; import { AnchorNav } from './AnchorNav.js'; import { AnchorNavLink } from './AnchorNavLink.js'; @@ -26,9 +25,9 @@ describe('AnchorNav', () => { it('enters the matching link once its target scrolls into view', async () => { const { root, target } = await render(); const link = getInstance( - root.querySelector('[data-component="AnchorNavLink"]'), + root.querySelector('[data-component="AnchorNavLink"]')!, 'AnchorNavLink', - ); + )!; target.setAttribute('style', ONSCREEN); await waitFor(() => link.state === 'entering'); @@ -42,9 +41,9 @@ describe('AnchorNav', () => { it('leaves the matching link once its target scrolls back out of view', async () => { const { root, target } = await render(); const link = getInstance( - root.querySelector('[data-component="AnchorNavLink"]'), + root.querySelector('[data-component="AnchorNavLink"]')!, 'AnchorNavLink', - ); + )!; target.setAttribute('style', ONSCREEN); await waitFor(() => link.state === 'entering'); @@ -67,9 +66,9 @@ describe('AnchorNav', () => { document.body.append(root); await settle(); const link = getInstance( - root.querySelector('[data-component="AnchorNavLink"]'), + root.querySelector('[data-component="AnchorNavLink"]')!, 'AnchorNavLink', - ); + )!; const target = root.querySelector('#one') as HTMLElement; target.setAttribute('style', ONSCREEN); diff --git a/packages/v4/migration/AnchorNav/AnchorNavLink.spec.ts b/packages/v4/migration/AnchorNav/AnchorNavLink.spec.ts index 4c0c1b30b..fbfbee046 100644 --- a/packages/v4/migration/AnchorNav/AnchorNavLink.spec.ts +++ b/packages/v4/migration/AnchorNav/AnchorNavLink.spec.ts @@ -1,6 +1,5 @@ import { afterEach, describe, expect, it } from 'vitest'; -import { registerComponents } from '../../src/index.js'; -import { getInstance } from '../../src/test-utils.js'; +import { getInstance, registerComponents } from '../../src/index.js'; import { resetDom, settle } from '../../src/test/index.js'; import { AnchorNavLink } from './AnchorNavLink.js'; @@ -24,7 +23,7 @@ async function render(): Promise { root.innerHTML = ``; document.body.append(root); await settle(); - return getInstance(root.firstElementChild, 'AnchorNavLink'); + return getInstance(root.firstElementChild!, 'AnchorNavLink')!; } describe('AnchorNavLink', () => { diff --git a/packages/v4/migration/Carousel/Carousel.spec.ts b/packages/v4/migration/Carousel/Carousel.spec.ts index 3fb280c9f..7e951403d 100644 --- a/packages/v4/migration/Carousel/Carousel.spec.ts +++ b/packages/v4/migration/Carousel/Carousel.spec.ts @@ -1,6 +1,5 @@ import { afterEach, describe, expect, it } from 'vitest'; -import { registerComponents } from '../../src/index.js'; -import { getInstance } from '../../src/test-utils.js'; +import { getInstance, registerComponents } from '../../src/index.js'; import { resetDom, settle, waitFor } from '../../src/test/index.js'; import { Carousel } from './Carousel.js'; import { CarouselBtn } from './CarouselBtn.js'; @@ -49,7 +48,7 @@ async function render({ return { root, el, - carousel: getInstance(el, 'Carousel'), + carousel: getInstance(el, 'Carousel')!, wrapper: el.querySelector('[data-component~="CarouselWrapper"]') as HTMLElement, }; } @@ -88,7 +87,7 @@ describe('getClosestIndex', () => { describe('slide positions', () => { it('centres each slide in its scroller, clamped to the scroll range', async () => { const { el } = await render({ count: 3 }); - const carousel = getInstance(el, 'Carousel'); + const carousel = getInstance(el, 'Carousel')!; // Each slide fills the scroller, so centring is the same as aligning — // and the arithmetic is core's now, through `scrollPosition({ align })`. diff --git a/packages/v4/migration/ClickOutside/ClickOutside.spec.ts b/packages/v4/migration/ClickOutside/ClickOutside.spec.ts index 9c9d366dc..717867f65 100644 --- a/packages/v4/migration/ClickOutside/ClickOutside.spec.ts +++ b/packages/v4/migration/ClickOutside/ClickOutside.spec.ts @@ -1,6 +1,11 @@ import { afterEach, describe, expect, it } from 'vitest'; -import { Base, registerComponents, type BaseConfig, type DelegatedEvent } from '../../src/index.js'; -import { getInstance } from '../../src/test-utils.js'; +import { + Base, + getInstance, + registerComponents, + type BaseConfig, + type DelegatedEvent, +} from '../../src/index.js'; import { resetDom, settle } from '../../src/test/index.js'; import { ClickOutside } from './ClickOutside.js'; @@ -44,7 +49,7 @@ async function render(): Promise<{ root, outside: root.querySelector('button:not([data-ref])') as HTMLElement, inside: el.querySelector('[data-ref="inner"]') as HTMLElement, - instance: getInstance(el, 'ClickOutside'), + instance: getInstance(el, 'ClickOutside')!, events, }; } @@ -74,9 +79,9 @@ describe('ClickOutside', () => { outside.click(); const dropdown = getInstance( - root.querySelector('[data-component="Dropdown"]'), + root.querySelector('[data-component="Dropdown"]')!, 'Dropdown', - ); + )!; expect(dropdown.closed).toHaveLength(1); expect(dropdown.closed[0].target).toBeInstanceOf(ClickOutside); expect(dropdown.closed[0].payload.event.type).toBe('click'); diff --git a/packages/v4/migration/Cursor/Cursor.spec.ts b/packages/v4/migration/Cursor/Cursor.spec.ts index c6d9aa129..3ab5cf8d4 100644 --- a/packages/v4/migration/Cursor/Cursor.spec.ts +++ b/packages/v4/migration/Cursor/Cursor.spec.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, it } from 'vitest'; -import { registerComponents } from '../../src/index.js'; -import { countRequestedFrames, getInstance } from '../../src/test-utils.js'; +import { getInstance, registerComponents } from '../../src/index.js'; +import { countRequestedFrames } from '../../src/test-utils.js'; import { mount, resetDom, settle, waitFor } from '../../src/test/index.js'; import { Cursor } from './Cursor.js'; @@ -19,7 +19,7 @@ async function mountCursor( const root = await mount( `
${inner}
`, ); - return { root, instance: getInstance(root.firstElementChild as HTMLElement, 'Cursor') }; + return { root, instance: getInstance(root.firstElementChild as HTMLElement, 'Cursor')! }; } function movePointer(target: EventTarget, x: number, y: number, buttons = 0): void { @@ -190,7 +190,7 @@ describe('Cursor', () => { other.append(root.firstElementChild as HTMLElement); await settle(); - const moved = getInstance(other.firstElementChild as HTMLElement, 'Cursor'); + const moved = getInstance(other.firstElementChild as HTMLElement, 'Cursor')!; expect(moved.motion().x).toBe(0); expect(moved.$el.style.transform).toContain('matrix(0, 0, 0, 0, 0, 0)'); }); diff --git a/packages/v4/migration/Data/DataBind.spec.ts b/packages/v4/migration/Data/DataBind.spec.ts index e047d88e3..7b71708b1 100644 --- a/packages/v4/migration/Data/DataBind.spec.ts +++ b/packages/v4/migration/Data/DataBind.spec.ts @@ -1,6 +1,5 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; -import { EVENTS, registerComponents } from '../../src/index.js'; -import { getInstance } from '../../src/test-utils.js'; +import { EVENTS, getInstance, registerComponents } from '../../src/index.js'; import { captureDiagnostics, mount, resetDom, settle } from '../../src/test/index.js'; import { DataBind } from './DataBind.js'; import { DataComputed } from './DataComputed.js'; @@ -68,7 +67,7 @@ function el(root: HTMLElement, selector: st } function at(root: HTMLElement, selector: string, name: string): T { - return getInstance(root.querySelector(selector), name) as T; + return getInstance(root.querySelector(selector)!, name)! as T; } describe('DataBind — the element half', () => { diff --git a/packages/v4/migration/Data/DataDerived.spec.ts b/packages/v4/migration/Data/DataDerived.spec.ts index 6f24cff7f..3f95defc3 100644 --- a/packages/v4/migration/Data/DataDerived.spec.ts +++ b/packages/v4/migration/Data/DataDerived.spec.ts @@ -1,6 +1,5 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; -import { registerComponents } from '../../src/index.js'; -import { getInstance } from '../../src/test-utils.js'; +import { getInstance, registerComponents } from '../../src/index.js'; import { mount, resetDom } from '../../src/test/index.js'; import { DataBind } from './DataBind.js'; import { DataComputed } from './DataComputed.js'; @@ -31,7 +30,7 @@ function el(root: HTMLElement, selector: st } function at(root: HTMLElement, selector: string, name: string): T { - return getInstance(root.querySelector(selector), name) as T; + return getInstance(root.querySelector(selector)!, name)! as T; } describe('DataModel', () => { diff --git a/packages/v4/migration/Data/DataScope.spec.ts b/packages/v4/migration/Data/DataScope.spec.ts index b86ed6ee3..a4e3ee1c2 100644 --- a/packages/v4/migration/Data/DataScope.spec.ts +++ b/packages/v4/migration/Data/DataScope.spec.ts @@ -1,6 +1,5 @@ import { afterEach, describe, expect, it } from 'vitest'; -import { registerComponents } from '../../src/index.js'; -import { getInstance } from '../../src/test-utils.js'; +import { getInstance, registerComponents } from '../../src/index.js'; import { mount, resetDom, settle } from '../../src/test/index.js'; import { DataBind } from './DataBind.js'; import { DataComputed } from './DataComputed.js'; @@ -28,7 +27,7 @@ function uniqueGroup(name: string): string { } function at(root: HTMLElement, selector: string, name: string): T { - return getInstance(root.querySelector(selector), name) as T; + return getInstance(root.querySelector(selector)!, name)! as T; } function el(root: HTMLElement, selector: string): T { @@ -307,8 +306,8 @@ describe('DataScope', () => { scope.append(el(root, '#bind')); await settle(); - const scopeInstance = getInstance(scope, 'DataScope'); - const rebound = getInstance(el(root, '#bind'), 'DataBind'); + const scopeInstance = getInstance(scope, 'DataScope')!; + const rebound = getInstance(el(root, '#bind'), 'DataBind')!; expect(rebound.dataRegistry).toBe(scopeInstance.registry); expect(rebound.group).toBe(group); expect(rebound.dataKey).toBe('first'); diff --git a/packages/v4/migration/Dialog/Dialog.spec.ts b/packages/v4/migration/Dialog/Dialog.spec.ts index 7b5eac6c2..f763e9ff9 100644 --- a/packages/v4/migration/Dialog/Dialog.spec.ts +++ b/packages/v4/migration/Dialog/Dialog.spec.ts @@ -1,6 +1,5 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; -import { registerComponent } from '../../src/index.js'; -import { getInstance } from '../../src/test-utils.js'; +import { getInstance, registerComponent } from '../../src/index.js'; import { resetDom, settle } from '../../src/test/index.js'; import { Transition } from '../Transition/Transition.js'; import { Dialog } from './Dialog.js'; @@ -36,7 +35,7 @@ describe('Dialog', () => { it('opens and closes the native dialog', async () => { const el = render(); await settle(); - const dialog = getInstance(el, 'Dialog'); + const dialog = getInstance(el, 'Dialog')!; await dialog.open(); expect(el.open).toBe(true); @@ -50,7 +49,7 @@ describe('Dialog', () => { it('is a no-op when already in the requested state', async () => { const el = render(); await settle(); - const dialog = getInstance(el, 'Dialog'); + const dialog = getInstance(el, 'Dialog')!; await dialog.close(); expect(el.open).toBe(false); @@ -62,11 +61,11 @@ describe('Dialog', () => { it('runs the transition children on open and close', async () => { const el = render(); await settle(); - const dialog = getInstance(el, 'Dialog'); + const dialog = getInstance(el, 'Dialog')!; const panel = el.querySelector('[data-component="Transition"]') as HTMLElement; expect(dialog.transitions).toHaveLength(1); - expect(getInstance(panel, 'Transition')).toBeInstanceOf(Transition); + expect(getInstance(panel, 'Transition')!).toBeInstanceOf(Transition); await dialog.open(); expect(panel.classList.contains('is-open')).toBe(true); @@ -79,7 +78,7 @@ describe('Dialog', () => { it('picks up a transition child inserted after mount', async () => { const el = render({ withTransition: false }); await settle(); - const dialog = getInstance(el, 'Dialog'); + const dialog = getInstance(el, 'Dialog')!; expect(dialog.transitions).toHaveLength(0); const added = document.createElement('div'); @@ -97,7 +96,7 @@ describe('Dialog', () => { it('closes through the component when Escape cancels the native dialog', async () => { const el = render(); await settle(); - const dialog = getInstance(el, 'Dialog'); + const dialog = getInstance(el, 'Dialog')!; const panel = el.querySelector('[data-component="Transition"]') as HTMLElement; await dialog.open(); @@ -116,7 +115,7 @@ describe('Dialog', () => { it('traps the tab key on the non-modal path only', async () => { const el = render({ modal: false, withTransition: false }); await settle(); - const dialog = getInstance(el, 'Dialog'); + const dialog = getInstance(el, 'Dialog')!; await dialog.open(); const last = el.querySelector('#last') as HTMLButtonElement; @@ -131,7 +130,7 @@ describe('Dialog', () => { it('releases the keydown listener on unmount', async () => { const el = render({ modal: false, withTransition: false }); await settle(); - const dialog = getInstance(el, 'Dialog'); + const dialog = getInstance(el, 'Dialog')!; await dialog.open(); const last = el.querySelector('#last') as HTMLButtonElement; @@ -149,23 +148,23 @@ describe('Dialog — the page scroll', () => { const second = render({ withTransition: false }); await settle(); - await getInstance(first, 'Dialog').open(); - await getInstance(second, 'Dialog').open(); + await getInstance(first, 'Dialog')!.open(); + await getInstance(second, 'Dialog')!.open(); expect(document.documentElement.style.overflow).toBe('hidden'); - await getInstance(second, 'Dialog').close(); + await getInstance(second, 'Dialog')!.close(); // The first one is still open: the page is not its to give back. expect(document.documentElement.style.overflow).toBe('hidden'); - await getInstance(first, 'Dialog').close(); + await getInstance(first, 'Dialog')!.close(); expect(document.documentElement.style.overflow).toBe(''); }); it('gives the scroll back when a dialog is unmounted while open', async () => { const el = render({ withTransition: false }); await settle(); - await getInstance(el, 'Dialog').open(); + await getInstance(el, 'Dialog')!.open(); expect(document.documentElement.style.overflow).toBe('hidden'); el.remove(); diff --git a/packages/v4/migration/Draggable/Draggable.spec.ts b/packages/v4/migration/Draggable/Draggable.spec.ts index a7c328fad..62530056d 100644 --- a/packages/v4/migration/Draggable/Draggable.spec.ts +++ b/packages/v4/migration/Draggable/Draggable.spec.ts @@ -1,6 +1,5 @@ import { afterEach, describe, expect, it } from 'vitest'; -import { registerComponents } from '../../src/index.js'; -import { getInstance } from '../../src/test-utils.js'; +import { getInstance, registerComponents } from '../../src/index.js'; import { recordEvents, resetDom, settle, waitFor } from '../../src/test/index.js'; import { Draggable } from './Draggable.js'; @@ -32,7 +31,7 @@ async function render(attributes = ''): Promise<{ root, el, target: el.querySelector('[data-ref="target"]') as HTMLElement, - instance: getInstance(el, 'Draggable'), + instance: getInstance(el, 'Draggable')!, }; } @@ -151,7 +150,7 @@ describe('Draggable — geometry', () => { other.append(el); await waitFor(() => getInstance(el, 'Draggable')?.bounds.xMax === 200); - expect(getInstance(el, 'Draggable').bounds.xMax).toBe(200); + expect(getInstance(el, 'Draggable')!.bounds.xMax).toBe(200); }); }); diff --git a/packages/v4/migration/Fetch/Fetch.spec.ts b/packages/v4/migration/Fetch/Fetch.spec.ts index 8fc34cb48..4d882e499 100644 --- a/packages/v4/migration/Fetch/Fetch.spec.ts +++ b/packages/v4/migration/Fetch/Fetch.spec.ts @@ -1,6 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { registerComponents } from '../../src/index.js'; -import { getInstance } from '../../src/test-utils.js'; +import { getInstance, registerComponents } from '../../src/index.js'; import { mount, recordEvents, resetDom, settle } from '../../src/test/index.js'; import { Fetch, FETCH_EVENTS, type FetchEmits } from './Fetch.js'; import { FetchShopifySection } from './FetchShopifySection.js'; @@ -42,7 +41,7 @@ async function mountFetch( ): Promise<{ root: HTMLElement; instance: T }> { const root = await mount(html); const el = root.firstElementChild as HTMLElement; - return { root, instance: getInstance(el, name) }; + return { root, instance: getInstance(el, name)! }; } /** Replace `window.fetch` and record every call. */ diff --git a/packages/v4/migration/Fetch/FetchShopifyPartial.spec.ts b/packages/v4/migration/Fetch/FetchShopifyPartial.spec.ts index af72221d6..7717fc765 100644 --- a/packages/v4/migration/Fetch/FetchShopifyPartial.spec.ts +++ b/packages/v4/migration/Fetch/FetchShopifyPartial.spec.ts @@ -1,6 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { registerComponents } from '../../src/index.js'; -import { getInstance } from '../../src/test-utils.js'; +import { getInstance, registerComponents } from '../../src/index.js'; import { recordEvents, resetDom, settle } from '../../src/test/index.js'; import { FETCH_EVENTS } from './Fetch.js'; import { FetchShopifyPartial } from './FetchShopifyPartial.js'; @@ -34,7 +33,7 @@ async function mount(html: string): Promise<{ root: HTMLElement; instance: Fetch document.body.append(root); await settle(); const el = root.firstElementChild as HTMLElement; - return { root, instance: getInstance(el, 'FetchShopifyPartial') }; + return { root, instance: getInstance(el, 'FetchShopifyPartial')! }; } function stubClient( diff --git a/packages/v4/migration/Figure/AbstractFigure.spec.ts b/packages/v4/migration/Figure/AbstractFigure.spec.ts index a017366ea..424f257db 100644 --- a/packages/v4/migration/Figure/AbstractFigure.spec.ts +++ b/packages/v4/migration/Figure/AbstractFigure.spec.ts @@ -1,6 +1,5 @@ import { afterEach, describe, expect, it } from 'vitest'; -import { registerComponents } from '../../src/index.js'; -import { getInstance } from '../../src/test-utils.js'; +import { getInstance, registerComponents } from '../../src/index.js'; import { resetDom, settle, waitFor } from '../../src/test/index.js'; import { Figure } from './Figure.js'; @@ -79,6 +78,6 @@ describe('Figure (AbstractFigure)', () => { // Polled, not assumed: `mounted()` fire-and-forgets the transition, and a // kept end state lands a few frames after the image has loaded. await waitFor(() => img.classList.contains('visible')); - expect(getInstance
(el, 'Figure').state).toBe('entering'); + expect(getInstance
(el, 'Figure')!.state).toBe('entering'); }); }); diff --git a/packages/v4/migration/Figure/FigureShopify.spec.ts b/packages/v4/migration/Figure/FigureShopify.spec.ts index f78a23466..0d9766c0b 100644 --- a/packages/v4/migration/Figure/FigureShopify.spec.ts +++ b/packages/v4/migration/Figure/FigureShopify.spec.ts @@ -1,6 +1,5 @@ import { afterEach, describe, expect, it } from 'vitest'; -import { registerComponents } from '../../src/index.js'; -import { getInstance } from '../../src/test-utils.js'; +import { getInstance, registerComponents } from '../../src/index.js'; import { resetDom, settle } from '../../src/test/index.js'; import { FigureShopify } from './FigureShopify.js'; @@ -20,7 +19,7 @@ async function render(attributes = ''): Promise { `; document.body.append(root); await settle(); - return getInstance(root.firstElementChild, 'FigureShopify'); + return getInstance(root.firstElementChild!, 'FigureShopify')!; } describe('FigureShopify', () => { diff --git a/packages/v4/migration/Figure/FigureTwicpics.spec.ts b/packages/v4/migration/Figure/FigureTwicpics.spec.ts index 2eb8fcaf5..c365a33b4 100644 --- a/packages/v4/migration/Figure/FigureTwicpics.spec.ts +++ b/packages/v4/migration/Figure/FigureTwicpics.spec.ts @@ -1,6 +1,5 @@ import { afterEach, describe, expect, it } from 'vitest'; -import { registerComponents } from '../../src/index.js'; -import { getInstance } from '../../src/test-utils.js'; +import { getInstance, registerComponents } from '../../src/index.js'; import { resetDom, settle } from '../../src/test/index.js'; import { FigureTwicpics } from './FigureTwicpics.js'; @@ -21,7 +20,7 @@ async function render( `; document.body.append(root); await settle(); - return getInstance(root.firstElementChild, 'FigureTwicpics'); + return getInstance(root.firstElementChild!, 'FigureTwicpics')!; } describe('FigureTwicpics', () => { diff --git a/packages/v4/migration/FigureVideo/FigureVideo.spec.ts b/packages/v4/migration/FigureVideo/FigureVideo.spec.ts index 4e4706d05..55d5e6735 100644 --- a/packages/v4/migration/FigureVideo/FigureVideo.spec.ts +++ b/packages/v4/migration/FigureVideo/FigureVideo.spec.ts @@ -1,6 +1,5 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; -import { registerComponents } from '../../src/index.js'; -import { getInstance } from '../../src/test-utils.js'; +import { getInstance, registerComponents } from '../../src/index.js'; import { captureDiagnostics, resetDom, settle, waitFor } from '../../src/test/index.js'; import { FigureVideo } from './FigureVideo.js'; @@ -86,7 +85,7 @@ describe('FigureVideo', () => { fireLoadedData(video); const instance = await waitFor(() => getInstance(el, 'FigureVideo')?.hasLoaded - ? getInstance(el, 'FigureVideo') + ? getInstance(el, 'FigureVideo')! : null, ); const spy = vi.spyOn(instance, 'load'); @@ -116,7 +115,7 @@ describe('FigureVideo', () => { expect(details.map((detail) => detail.code)).toContain('figure-video.load-failed'); // Left un-loaded, so a later mount cycle can retry. - expect(getInstance(el, 'FigureVideo').hasLoaded).toBe(false); + expect(getInstance(el, 'FigureVideo')!.hasLoaded).toBe(false); document.removeEventListener('js-toolkit:diagnostic', listener); }); diff --git a/packages/v4/migration/FigureVideo/FigureVideoTwicpics.spec.ts b/packages/v4/migration/FigureVideo/FigureVideoTwicpics.spec.ts index e922e9ab6..060300f40 100644 --- a/packages/v4/migration/FigureVideo/FigureVideoTwicpics.spec.ts +++ b/packages/v4/migration/FigureVideo/FigureVideoTwicpics.spec.ts @@ -1,6 +1,5 @@ import { afterEach, describe, expect, it } from 'vitest'; -import { registerComponents } from '../../src/index.js'; -import { getInstance } from '../../src/test-utils.js'; +import { getInstance, registerComponents } from '../../src/index.js'; import { resetDom, settle } from '../../src/test/index.js'; import { FigureVideoTwicpics } from './FigureVideoTwicpics.js'; @@ -21,7 +20,7 @@ async function render(attributes = ''): Promise { `; document.body.append(root); await settle(); - return getInstance(root.firstElementChild, 'FigureVideoTwicpics'); + return getInstance(root.firstElementChild!, 'FigureVideoTwicpics')!; } describe('FigureVideoTwicpics — the loadSources override', () => { @@ -52,7 +51,7 @@ describe('FigureVideoTwicpics — the loadSources override', () => { } expect(details.map((detail) => detail.code)).toContain('figure-video.load-failed'); - expect(getInstance(el, 'FigureVideoTwicpics').hasLoaded).toBe(false); + expect(getInstance(el, 'FigureVideoTwicpics')!.hasLoaded).toBe(false); document.removeEventListener('js-toolkit:diagnostic', listener); }); diff --git a/packages/v4/migration/Hoverable/Hoverable.spec.ts b/packages/v4/migration/Hoverable/Hoverable.spec.ts index f9f51c092..fb0d0d518 100644 --- a/packages/v4/migration/Hoverable/Hoverable.spec.ts +++ b/packages/v4/migration/Hoverable/Hoverable.spec.ts @@ -1,6 +1,10 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; -import { registerComponents, type ElementPointerProps, type RafProps } from '../../src/index.js'; -import { getInstance } from '../../src/test-utils.js'; +import { + getInstance, + registerComponents, + type ElementPointerProps, + type RafProps, +} from '../../src/index.js'; import { resetDom, settle } from '../../src/test/index.js'; import { Hoverable } from './Hoverable.js'; @@ -20,7 +24,7 @@ async function render(attributes = ''): Promise<{ el: HTMLElement; instance: Hov document.body.append(root); await settle(); const el = root.firstElementChild as HTMLElement; - return { el, instance: getInstance(el, 'Hoverable') }; + return { el, instance: getInstance(el, 'Hoverable')! }; } function progress(x: number, y: number): ElementPointerProps { diff --git a/packages/v4/migration/LazyInclude/LazyInclude.spec.ts b/packages/v4/migration/LazyInclude/LazyInclude.spec.ts index e16683e05..41833a175 100644 --- a/packages/v4/migration/LazyInclude/LazyInclude.spec.ts +++ b/packages/v4/migration/LazyInclude/LazyInclude.spec.ts @@ -1,6 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { Base, registerComponents, type BaseConfig } from '../../src/index.js'; -import { getInstance } from '../../src/test-utils.js'; +import { Base, getInstance, registerComponents, type BaseConfig } from '../../src/index.js'; import { captureDiagnostics, mount, resetDom, settle, waitFor } from '../../src/test/index.js'; import { LazyInclude } from './LazyInclude.js'; @@ -180,7 +179,10 @@ describe('LazyInclude', () => { const root = await mount( `
`, ); - const instance = getInstance(root.firstElementChild as HTMLElement, 'LazyInclude'); + const instance = getInstance( + root.firstElementChild as HTMLElement, + 'LazyInclude', + )!; expect(instance.hasLoaded).toBe(false); deferred.resolve('

remote

'); @@ -197,7 +199,10 @@ describe('LazyInclude', () => { const root = await mount( `
`, ); - const instance = getInstance(root.firstElementChild as HTMLElement, 'LazyInclude'); + const instance = getInstance( + root.firstElementChild as HTMLElement, + 'LazyInclude', + )!; deferred.resolve('

remote

'); await quiet(); @@ -274,7 +279,7 @@ describe('LazyInclude', () => { ); const el = root.firstElementChild as HTMLElement; await waitFor(() => client.mock.calls.length === 1); - expect(getInstance(el, 'LazyInclude').hasLoaded).toBe(false); + expect(getInstance(el, 'LazyInclude')!.hasLoaded).toBe(false); const other = document.createElement('section'); document.body.append(other); @@ -291,7 +296,7 @@ describe('LazyInclude', () => { `
`, ); const el = root.firstElementChild as HTMLElement; - await waitFor(() => getInstance(el, 'LazyInclude').hasLoaded); + await waitFor(() => getInstance(el, 'LazyInclude')!.hasLoaded); const other = document.createElement('section'); document.body.append(other); diff --git a/packages/v4/migration/Menu/Menu.spec.ts b/packages/v4/migration/Menu/Menu.spec.ts index b3329a875..521e5424d 100644 --- a/packages/v4/migration/Menu/Menu.spec.ts +++ b/packages/v4/migration/Menu/Menu.spec.ts @@ -1,6 +1,5 @@ import { afterEach, describe, expect, it } from 'vitest'; -import { registerComponents } from '../../src/index.js'; -import { getInstance } from '../../src/test-utils.js'; +import { getInstance, registerComponents } from '../../src/index.js'; import { resetDom, settle } from '../../src/test/index.js'; import { Menu } from './Menu.js'; import { MenuBtn } from './MenuBtn.js'; @@ -29,7 +28,7 @@ async function render(mode?: string): Promise<{ root: HTMLElement; menu: Menu }> root.innerHTML = menuMarkup(mode); document.body.append(root); await settle(); - return { root, menu: getInstance(root.querySelector('[data-component="Menu"]'), 'Menu') }; + return { root, menu: getInstance(root.querySelector('[data-component="Menu"]')!, 'Menu')! }; } describe('Menu', () => { @@ -134,8 +133,8 @@ describe('Menu', () => { `; document.body.append(root); await settle(); - const subA = getInstance(root.querySelector('#sub-a'), 'Menu'); - const subB = getInstance(root.querySelector('#sub-b'), 'Menu'); + const subA = getInstance(root.querySelector('#sub-a')!, 'Menu')!; + const subB = getInstance(root.querySelector('#sub-b')!, 'Menu')!; subA.open(); expect(subA.menuList?.isOpen).toBe(true); diff --git a/packages/v4/migration/Menu/MenuBtn.spec.ts b/packages/v4/migration/Menu/MenuBtn.spec.ts index 9d4ab63df..47a88e533 100644 --- a/packages/v4/migration/Menu/MenuBtn.spec.ts +++ b/packages/v4/migration/Menu/MenuBtn.spec.ts @@ -1,6 +1,5 @@ import { afterEach, describe, expect, it } from 'vitest'; -import { registerComponents } from '../../src/index.js'; -import { getInstance } from '../../src/test-utils.js'; +import { getInstance, registerComponents } from '../../src/index.js'; import { resetDom, settle } from '../../src/test/index.js'; import { MenuBtn } from './MenuBtn.js'; @@ -14,7 +13,7 @@ async function render(): Promise<{ el: HTMLElement; instance: MenuBtn }> { document.body.append(root); await settle(); const el = root.firstElementChild as HTMLElement; - return { el, instance: getInstance(el, 'MenuBtn') }; + return { el, instance: getInstance(el, 'MenuBtn')! }; } describe('MenuBtn', () => { diff --git a/packages/v4/migration/Menu/MenuList.spec.ts b/packages/v4/migration/Menu/MenuList.spec.ts index d3b05012d..f50a42b62 100644 --- a/packages/v4/migration/Menu/MenuList.spec.ts +++ b/packages/v4/migration/Menu/MenuList.spec.ts @@ -1,6 +1,5 @@ import { afterEach, describe, expect, it } from 'vitest'; -import { registerComponents } from '../../src/index.js'; -import { getInstance } from '../../src/test-utils.js'; +import { getInstance, registerComponents } from '../../src/index.js'; import { mount, resetDom, settle, waitFor } from '../../src/test/index.js'; import { MenuList } from './MenuList.js'; @@ -26,8 +25,8 @@ async function render(): Promise<{ `); return { root, - outer: getInstance(root.querySelector('#outer-list'), 'MenuList'), - nested: getInstance(root.querySelector('#nested-list'), 'MenuList'), + outer: getInstance(root.querySelector('#outer-list')!, 'MenuList')!, + nested: getInstance(root.querySelector('#nested-list')!, 'MenuList')!, outerLink: root.querySelector('#outer-link') as HTMLElement, nestedLink: root.querySelector('#nested-link') as HTMLElement, }; diff --git a/packages/v4/migration/Prefetch/Prefetch.spec.ts b/packages/v4/migration/Prefetch/Prefetch.spec.ts index 1bd7ba3a6..299fbc87e 100644 --- a/packages/v4/migration/Prefetch/Prefetch.spec.ts +++ b/packages/v4/migration/Prefetch/Prefetch.spec.ts @@ -1,6 +1,5 @@ import { afterEach, describe, expect, it } from 'vitest'; -import { registerComponents } from '../../src/index.js'; -import { getInstance } from '../../src/test-utils.js'; +import { getInstance, registerComponents } from '../../src/index.js'; import { mount, resetDom, settle, waitFor } from '../../src/test/index.js'; import { AbstractPrefetch } from './AbstractPrefetch.js'; import { PrefetchOnInteraction } from './PrefetchOnInteraction.js'; @@ -57,7 +56,7 @@ describe('AbstractPrefetch — is the URL prefetchable', () => { const instance = getInstance( root.firstElementChild as HTMLElement, 'AbstractPrefetch', - ); + )!; expect(instance.isPrefetchable).toBe(true); expect(instance.url?.href).toBe(href); @@ -70,7 +69,7 @@ describe('AbstractPrefetch — is the URL prefetchable', () => { const instance = getInstance( root.firstElementChild as HTMLElement, 'AbstractPrefetch', - ); + )!; expect(instance.isPrefetchable).toBe(false); }); @@ -82,7 +81,7 @@ describe('AbstractPrefetch — is the URL prefetchable', () => { const instance = getInstance( root.firstElementChild as HTMLElement, 'AbstractPrefetch', - ); + )!; expect(instance.isPrefetchable).toBe(false); }); @@ -92,7 +91,7 @@ describe('AbstractPrefetch — is the URL prefetchable', () => { const instance = getInstance( root.firstElementChild as HTMLElement, 'AbstractPrefetch', - ); + )!; expect(instance.isPrefetchable).toBe(false); }); @@ -104,7 +103,7 @@ describe('AbstractPrefetch — is the URL prefetchable', () => { const instance = getInstance( root.firstElementChild as HTMLElement, 'AbstractPrefetch', - ); + )!; expect(instance.isPrefetchable).toBe(false); }); @@ -118,7 +117,7 @@ describe('AbstractPrefetch — is the URL prefetchable', () => { const instance = getInstance( root.firstElementChild as HTMLElement, 'AbstractPrefetch', - ); + )!; expect(instance.url).toBeNull(); expect(instance.isPrefetchable).toBe(false); @@ -133,7 +132,7 @@ describe('AbstractPrefetch — the hint', () => { const instance = getInstance( root.firstElementChild as HTMLElement, 'AbstractPrefetch', - ); + )!; instance.prefetch(); await waitFor(() => hasPrefetchLink(href)); @@ -149,7 +148,7 @@ describe('AbstractPrefetch — the hint', () => { const instance = getInstance( root.firstElementChild as HTMLElement, 'AbstractPrefetch', - ); + )!; instance.prefetch(); await quiet(); @@ -163,7 +162,7 @@ describe('AbstractPrefetch — the hint', () => { const instance = getInstance( root.firstElementChild as HTMLElement, 'AbstractPrefetch', - ); + )!; let detail: { url: URL } | undefined; document.addEventListener('prefetched', (event) => { detail = (event as CustomEvent<{ url: URL }>).detail; @@ -186,7 +185,7 @@ describe('AbstractPrefetch — the hint', () => { `); const [first, second] = [...root.children].map((el) => - getInstance(el as HTMLElement, 'AbstractPrefetch'), + getInstance(el as HTMLElement, 'AbstractPrefetch')!, ); first.prefetch(); @@ -208,7 +207,7 @@ describe('AbstractPrefetch — the hint', () => { `); const instances = [...root.children].map((el) => - getInstance(el as HTMLElement, 'AbstractPrefetch'), + getInstance(el as HTMLElement, 'AbstractPrefetch')!, ); let count = 0; document.addEventListener('prefetched', () => { @@ -229,7 +228,7 @@ describe('AbstractPrefetch — the hint', () => { const instance = getInstance( root.firstElementChild as HTMLElement, 'AbstractPrefetch', - ); + )!; let count = 0; document.addEventListener('prefetched', () => { count += 1; @@ -280,7 +279,7 @@ describe('PrefetchOnInteraction', () => { getInstance( root.firstElementChild as HTMLElement, 'PrefetchOnInteraction', - ), + )!, ).toBeUndefined(); expect(hasPrefetchLink(href)).toBe(false); }); @@ -310,7 +309,7 @@ describe('PrefetchWhenVisible', () => { getInstance( root.firstElementChild as HTMLElement, 'PrefetchWhenVisible', - ), + )!, ).toBeUndefined(); expect(hasPrefetchLink(href)).toBe(false); }); @@ -335,7 +334,7 @@ describe('PrefetchWhenVisible', () => { ); const el = root.firstElementChild as HTMLElement; const instance = await waitFor(() => - getInstance(el, 'PrefetchWhenVisible'), + getInstance(el, 'PrefetchWhenVisible')!, ); expect(instance?.$isMounted).toBe(true); diff --git a/packages/v4/migration/ScrollAnimation/ScrollAnimation.spec.ts b/packages/v4/migration/ScrollAnimation/ScrollAnimation.spec.ts index b29643508..de04c1043 100644 --- a/packages/v4/migration/ScrollAnimation/ScrollAnimation.spec.ts +++ b/packages/v4/migration/ScrollAnimation/ScrollAnimation.spec.ts @@ -1,6 +1,5 @@ import { afterEach, describe, expect, it } from 'vitest'; -import { registerComponent } from '../../src/index.js'; -import { getInstance } from '../../src/test-utils.js'; +import { getInstance, registerComponent } from '../../src/index.js'; import { frames, resetDom, settle } from '../../src/test/index.js'; import { ScrollAnimationTarget } from './ScrollAnimationTarget.js'; import { ScrollAnimationTimeline } from './ScrollAnimationTimeline.js'; @@ -72,7 +71,7 @@ describe('ScrollAnimationTimeline', () => { const el = render({ targets: 2 }); await settle(); - const timeline = getInstance(el, 'ScrollAnimationTimeline'); + const timeline = getInstance(el, 'ScrollAnimationTimeline')!; expect(timeline.targets.size).toBe(2); const added = document.createElement('div'); @@ -155,7 +154,7 @@ describe('ScrollAnimationTimeline', () => { await scrollTo(start + (end - start) * 0.8); expect(Number(target.style.opacity)).toBeGreaterThan(0.5); - getInstance(target, 'ScrollAnimationTarget').$unmount(); + getInstance(target, 'ScrollAnimationTarget')!.$unmount(); await frames(4); expect(target.style.opacity).toBe('1'); }); @@ -163,7 +162,7 @@ describe('ScrollAnimationTimeline', () => { it('stops the frame subscription once the damped value settled', async () => { const el = render(); await settle(); - const timeline = getInstance(el, 'ScrollAnimationTimeline'); + const timeline = getInstance(el, 'ScrollAnimationTimeline')!; await scrollTo(bounds().end); expect((timeline as unknown as { __unsubscribeFrame: unknown }).__unsubscribeFrame).toBeNull(); diff --git a/packages/v4/migration/ScrollTo/ScrollTo.spec.ts b/packages/v4/migration/ScrollTo/ScrollTo.spec.ts index 63a90ae2d..81f4a65d0 100644 --- a/packages/v4/migration/ScrollTo/ScrollTo.spec.ts +++ b/packages/v4/migration/ScrollTo/ScrollTo.spec.ts @@ -1,6 +1,5 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; -import { registerComponents } from '../../src/index.js'; -import { getInstance } from '../../src/test-utils.js'; +import { getInstance, registerComponents } from '../../src/index.js'; import { resetDom, settle } from '../../src/test/index.js'; import { ScrollTo } from './ScrollTo.js'; @@ -27,7 +26,7 @@ async function render(href: string): Promise<{ el: HTMLAnchorElement; instance: document.body.append(root); await settle(); const el = root.querySelector('[data-component="ScrollTo"]') as HTMLAnchorElement; - return { el, instance: getInstance(el, 'ScrollTo') }; + return { el, instance: getInstance(el, 'ScrollTo')! }; } // Calling `onClick` directly, rather than dispatching a real click on a live diff --git a/packages/v4/migration/Slider/Slider.spec.ts b/packages/v4/migration/Slider/Slider.spec.ts index 27829acb2..6fb39116c 100644 --- a/packages/v4/migration/Slider/Slider.spec.ts +++ b/packages/v4/migration/Slider/Slider.spec.ts @@ -1,6 +1,5 @@ import { afterEach, describe, expect, it } from 'vitest'; -import { registerComponents } from '../../src/index.js'; -import { getInstance } from '../../src/test-utils.js'; +import { getInstance, registerComponents } from '../../src/index.js'; import { frames, resetDom, settle } from '../../src/test/index.js'; import { Slider } from './Slider.js'; import { SliderBtn } from './SliderBtn.js'; @@ -38,7 +37,7 @@ function render({ items = 3, options = '' } = {}): HTMLElement { function get(root: HTMLElement) { const buttons = [...root.querySelectorAll('[data-component="SliderBtn"]')]; return { - slider: getInstance(root, 'Slider'), + slider: getInstance(root, 'Slider')!, prev: buttons[0], next: buttons[1], current: root.querySelector('[data-ref="current"]') as HTMLElement, diff --git a/packages/v4/migration/Slider/SliderDots.spec.ts b/packages/v4/migration/Slider/SliderDots.spec.ts index 5f9aac212..26dbae3e7 100644 --- a/packages/v4/migration/Slider/SliderDots.spec.ts +++ b/packages/v4/migration/Slider/SliderDots.spec.ts @@ -1,6 +1,5 @@ import { afterEach, describe, expect, it } from 'vitest'; -import { registerComponents } from '../../src/index.js'; -import { getInstance } from '../../src/test-utils.js'; +import { getInstance, registerComponents } from '../../src/index.js'; import { frames, resetDom, settle } from '../../src/test/index.js'; import { Slider } from './Slider.js'; import { SliderDots } from './SliderDots.js'; @@ -38,8 +37,8 @@ async function ready(root: HTMLElement) { await frames(4); const dotsEl = root.querySelector('[data-component="SliderDots"]') as HTMLElement; return { - slider: getInstance(root, 'Slider'), - dots: getInstance(dotsEl, 'SliderDots'), + slider: getInstance(root, 'Slider')!, + dots: getInstance(dotsEl, 'SliderDots')!, buttons: [...dotsEl.querySelectorAll('[data-ref="dots[]"]')], }; } @@ -91,7 +90,7 @@ describe('SliderDots', () => { buttons[1].click(); await frames(4); - expect(getInstance(root, 'Slider').currentIndex).toBe(0); + expect(getInstance(root, 'Slider')!.currentIndex).toBe(0); }); it('picks up a dot added after mount', async () => { diff --git a/packages/v4/migration/Slider/SliderDrag.spec.ts b/packages/v4/migration/Slider/SliderDrag.spec.ts index 4f3be7628..e50fdff4f 100644 --- a/packages/v4/migration/Slider/SliderDrag.spec.ts +++ b/packages/v4/migration/Slider/SliderDrag.spec.ts @@ -1,6 +1,5 @@ import { afterEach, describe, expect, it } from 'vitest'; -import { registerComponents } from '../../src/index.js'; -import { getInstance } from '../../src/test-utils.js'; +import { getInstance, registerComponents } from '../../src/index.js'; import { frames, recordEvents, resetDom, settle } from '../../src/test/index.js'; import { Slider } from './Slider.js'; import { SliderDrag } from './SliderDrag.js'; @@ -38,8 +37,8 @@ async function ready(root: HTMLElement) { await frames(4); const track = root.querySelector('[data-component="SliderDrag"]') as HTMLElement; return { - slider: getInstance(root, 'Slider'), - drag: getInstance(track, 'SliderDrag'), + slider: getInstance(root, 'Slider')!, + drag: getInstance(track, 'SliderDrag')!, track, }; } diff --git a/packages/v4/migration/Slider/SliderProgress.spec.ts b/packages/v4/migration/Slider/SliderProgress.spec.ts index c329611df..74bf2675d 100644 --- a/packages/v4/migration/Slider/SliderProgress.spec.ts +++ b/packages/v4/migration/Slider/SliderProgress.spec.ts @@ -1,6 +1,5 @@ import { afterEach, describe, expect, it } from 'vitest'; -import { registerComponents } from '../../src/index.js'; -import { getInstance } from '../../src/test-utils.js'; +import { getInstance, registerComponents } from '../../src/index.js'; import { frames, resetDom, settle } from '../../src/test/index.js'; import { Slider } from './Slider.js'; import { SliderProgress } from './SliderProgress.js'; @@ -35,7 +34,7 @@ async function ready(root: HTMLElement) { await settle(); await frames(4); return { - slider: getInstance(root, 'Slider'), + slider: getInstance(root, 'Slider')!, bar: root.querySelector('[data-ref="progress"]') as HTMLElement, }; } diff --git a/packages/v4/migration/Sticky/Sticky.spec.ts b/packages/v4/migration/Sticky/Sticky.spec.ts index 54b887b8a..ce2a0444e 100644 --- a/packages/v4/migration/Sticky/Sticky.spec.ts +++ b/packages/v4/migration/Sticky/Sticky.spec.ts @@ -1,6 +1,10 @@ import { afterEach, describe, expect, it } from 'vitest'; -import { registerComponents, type InViewProps, type ScrollProps } from '../../src/index.js'; -import { getInstance } from '../../src/test-utils.js'; +import { + getInstance, + registerComponents, + type InViewProps, + type ScrollProps, +} from '../../src/index.js'; import { mount, resetDom, settle } from '../../src/test/index.js'; import { Sentinel } from '../Sentinel/index.js'; import { Sticky } from './Sticky.js'; @@ -50,8 +54,8 @@ describe('Sticky', () => { it('sizes its sentinel from the earlier instances sharing its relative ancestor', async () => { const root = await mount(`
${sticky()}${sticky()}
`); const [first, second] = root.querySelectorAll('[data-component="Sticky"]'); - const firstInstance = getInstance(first, 'Sticky'); - const secondInstance = getInstance(second, 'Sticky'); + const firstInstance = getInstance(first, 'Sticky')!; + const secondInstance = getInstance(second, 'Sticky')!; expect(firstInstance.sentinel?.$el.style.height).toBe('1px'); expect((first as HTMLElement).style.top).toBe('0px'); @@ -66,7 +70,7 @@ describe('Sticky', () => { const root = await mount(sticky()); const el = root.querySelector('[data-component="Sticky"]') as HTMLElement; const sentinelEl = root.querySelector('[data-component="Sentinel"]') as HTMLElement; - const instance = getInstance(el, 'Sticky'); + const instance = getInstance(el, 'Sticky')!; const inner = el.querySelector('[data-ref="inner"]') as HTMLElement; dispatchIntersected(sentinelEl, true, -5); @@ -81,7 +85,7 @@ describe('Sticky', () => { const root = await mount(sticky()); const sentinelEl = root.querySelector('[data-component="Sentinel"]') as HTMLElement; const el = root.querySelector('[data-component="Sticky"]') as HTMLElement; - const instance = getInstance(el, 'Sticky'); + const instance = getInstance(el, 'Sticky')!; // Still fully visible, entering from below: `y` is positive. dispatchIntersected(sentinelEl, true, 5); @@ -96,8 +100,8 @@ describe('Sticky', () => { const [firstSentinel, secondSentinel] = [ ...root.querySelectorAll('[data-component="Sentinel"]'), ] as HTMLElement[]; - const first = getInstance(firstEl, 'Sticky'); - const second = getInstance(secondEl, 'Sticky'); + const first = getInstance(firstEl, 'Sticky')!; + const second = getInstance(secondEl, 'Sticky')!; const secondInner = secondEl.querySelector('[data-ref="inner"]') as HTMLElement; dispatchIntersected(firstSentinel, true, -5); @@ -128,7 +132,7 @@ describe('Sticky', () => { `); const el = root.querySelector('[data-component="Sticky"]') as HTMLElement; const sentinelEl = root.querySelector('[data-component="Sentinel"]') as HTMLElement; - const instance = getInstance(el, 'Sticky'); + const instance = getInstance(el, 'Sticky')!; dispatchIntersected(sentinelEl, true, -5); expect(instance.isSticky).toBe(true); @@ -144,7 +148,7 @@ describe('Sticky', () => { const root = await mount(sticky()); const el = root.querySelector('[data-component="Sticky"]') as HTMLElement; const sentinelEl = root.querySelector('[data-component="Sentinel"]') as HTMLElement; - const instance = getInstance(el, 'Sticky'); + const instance = getInstance(el, 'Sticky')!; dispatchIntersected(sentinelEl, true, -5); instance.scrolled({ ...BASE_SCROLL_PROPS, deltaY: 0, directionY: 0 }); diff --git a/packages/v4/migration/Timer/Timer.spec.ts b/packages/v4/migration/Timer/Timer.spec.ts index 63faa1b91..980506cc0 100644 --- a/packages/v4/migration/Timer/Timer.spec.ts +++ b/packages/v4/migration/Timer/Timer.spec.ts @@ -1,6 +1,5 @@ import { afterEach, describe, expect, it } from 'vitest'; -import { registerComponents } from '../../src/index.js'; -import { getInstance } from '../../src/test-utils.js'; +import { getInstance, registerComponents } from '../../src/index.js'; import { recordEvents, resetDom, settle } from '../../src/test/index.js'; import { Timer } from './Timer.js'; @@ -28,7 +27,7 @@ function renderUnmounted(attributes = ''): HTMLElement { async function render(attributes = ''): Promise<{ el: HTMLElement; instance: Timer }> { const el = renderUnmounted(attributes); await settle(); - return { el, instance: getInstance(el, 'Timer') }; + return { el, instance: getInstance(el, 'Timer')! }; } /** Only the order of the names is asserted here, so the payloads drop out. */ diff --git a/packages/v4/migration/Timer/TimerProgress.spec.ts b/packages/v4/migration/Timer/TimerProgress.spec.ts index 317b69af7..5cc3b7e52 100644 --- a/packages/v4/migration/Timer/TimerProgress.spec.ts +++ b/packages/v4/migration/Timer/TimerProgress.spec.ts @@ -1,6 +1,5 @@ import { afterEach, describe, expect, it } from 'vitest'; -import { registerComponents } from '../../src/index.js'; -import { getInstance } from '../../src/test-utils.js'; +import { getInstance, registerComponents } from '../../src/index.js'; import { resetDom, settle } from '../../src/test/index.js'; import { TimerProgress } from './TimerProgress.js'; @@ -18,7 +17,7 @@ async function render(attributes = ''): Promise<{ el: HTMLElement; instance: Tim document.body.append(root); await settle(); const el = root.firstElementChild as HTMLElement; - return { el, instance: getInstance(el, 'TimerProgress') }; + return { el, instance: getInstance(el, 'TimerProgress')! }; } function recordProgress(el: HTMLElement): number[] { diff --git a/packages/v4/migration/Toaster/Toast.spec.ts b/packages/v4/migration/Toaster/Toast.spec.ts index a2ee15ef4..011cf6fc7 100644 --- a/packages/v4/migration/Toaster/Toast.spec.ts +++ b/packages/v4/migration/Toaster/Toast.spec.ts @@ -1,6 +1,5 @@ import { afterEach, describe, expect, it } from 'vitest'; -import { registerComponents } from '../../src/index.js'; -import { getInstance } from '../../src/test-utils.js'; +import { getInstance, registerComponents } from '../../src/index.js'; import { resetDom, settle } from '../../src/test/index.js'; import { Toast } from './Toast.js'; @@ -43,7 +42,7 @@ function renderUnmounted(attributes = ''): HTMLElement { async function render(attributes = ''): Promise<{ el: HTMLElement; instance: Toast }> { const el = renderUnmounted(attributes); await settle(); - return { el, instance: getInstance(el, 'Toast') }; + return { el, instance: getInstance(el, 'Toast')! }; } describe('Toast', () => { diff --git a/packages/v4/migration/Toaster/Toaster.spec.ts b/packages/v4/migration/Toaster/Toaster.spec.ts index 072f97810..ed4de1c1c 100644 --- a/packages/v4/migration/Toaster/Toaster.spec.ts +++ b/packages/v4/migration/Toaster/Toaster.spec.ts @@ -1,6 +1,5 @@ import { afterEach, describe, expect, it } from 'vitest'; -import { registerComponents } from '../../src/index.js'; -import { getInstance } from '../../src/test-utils.js'; +import { getInstance, registerComponents } from '../../src/index.js'; import { resetDom, settle } from '../../src/test/index.js'; import { Toast } from './Toast.js'; import { Toaster } from './Toaster.js'; @@ -42,7 +41,7 @@ async function render(): Promise<{ root: HTMLElement; instance: Toaster }> { await settle(); return { root, - instance: getInstance(root.querySelector('[data-component="Toaster"]'), 'Toaster'), + instance: getInstance(root.querySelector('[data-component="Toaster"]')!, 'Toaster')!, }; } @@ -84,7 +83,7 @@ describe('Toaster', () => { // written: the negated attribute name is what actually turns it off. expect(toast.hasAttribute('data-option-no-autostart')).toBe(true); - const toastInstance = getInstance(toast, 'Toast'); + const toastInstance = getInstance(toast, 'Toast')!; expect(toastInstance.timerId).toBeNull(); }); diff --git a/packages/v4/migration/Track/Track.spec.ts b/packages/v4/migration/Track/Track.spec.ts index eede45a23..5bb322ad8 100644 --- a/packages/v4/migration/Track/Track.spec.ts +++ b/packages/v4/migration/Track/Track.spec.ts @@ -1,6 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { registerComponents } from '../../src/index.js'; -import { getInstance } from '../../src/test-utils.js'; +import { getInstance, registerComponents } from '../../src/index.js'; import { captureDiagnostics, mount, resetDom, settle, waitFor } from '../../src/test/index.js'; import { Track } from './Track.js'; import { TrackContext } from './TrackContext.js'; @@ -387,7 +386,7 @@ describe('Track — lifecycle', () => { `
`, ); const el = root.firstElementChild as HTMLElement; - const track = getInstance(el, 'Track'); + const track = getInstance(el, 'Track')!; el.click(); expect(pushes()).toHaveLength(1); @@ -406,7 +405,7 @@ describe('Track — lifecycle', () => { `
`, ); const el = root.firstElementChild as HTMLElement; - const track = getInstance(el, 'Track'); + const track = getInstance(el, 'Track')!; el.dispatchEvent(new Event('input')); track.$unmount(); @@ -421,7 +420,7 @@ describe('Track — lifecycle', () => { `
`, ); const el = root.firstElementChild as HTMLElement; - const track = getInstance(el, 'Track'); + const track = getInstance(el, 'Track')!; track.$unmount(); el.setAttribute('data-track:click', '{"event": "after"}'); @@ -495,7 +494,7 @@ describe('Track — live rebinding through watchAttributes', () => { `
`, ); const el = root.firstElementChild as HTMLElement; - const track = getInstance(el, 'Track'); + const track = getInstance(el, 'Track')!; track.$unmount(); el.setAttribute('data-track:click', '{"event": "ignored"}'); diff --git a/packages/v4/migration/Track/TrackEvent.spec.ts b/packages/v4/migration/Track/TrackEvent.spec.ts index ea4d9cc6b..5e042c084 100644 --- a/packages/v4/migration/Track/TrackEvent.spec.ts +++ b/packages/v4/migration/Track/TrackEvent.spec.ts @@ -1,6 +1,5 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { registerComponents } from '../../src/index.js'; -import { getInstance } from '../../src/test-utils.js'; +import { getInstance, registerComponents } from '../../src/index.js'; import { captureDiagnostics, mount, resetDom } from '../../src/test/index.js'; import { parseEventDefinition } from '../event-modifiers.js'; import { Track } from './Track.js'; @@ -75,7 +74,7 @@ describe('parseEventDefinition', () => { it('applies the family default through the bound declaration', async () => { const el = await render('
'); - const [trackEvent] = getInstance(el, 'Track').trackEvents; + const [trackEvent] = getInstance(el, 'Track')!.trackEvents; expect(trackEvent.debounceDelay).toBe(300); expect(trackEvent.throttleDelay).toBe(16); }); diff --git a/packages/v4/migration/Transition/withTransition.spec.ts b/packages/v4/migration/Transition/withTransition.spec.ts index c3acac2ba..b0abfe16b 100644 --- a/packages/v4/migration/Transition/withTransition.spec.ts +++ b/packages/v4/migration/Transition/withTransition.spec.ts @@ -1,7 +1,6 @@ import { afterEach, describe, expect, it } from 'vitest'; -import { Base, registerComponents, type BaseConfig } from '../../src/index.js'; +import { Base, getInstance, registerComponents, type BaseConfig } from '../../src/index.js'; import { resolveConfig } from '../../src/Base.js'; -import { getInstance } from '../../src/test-utils.js'; import { mount, resetDom, settle } from '../../src/test/index.js'; import { Transition } from './Transition.js'; import { withTransition } from './withTransition.js'; @@ -56,7 +55,7 @@ describe('withTransition config merging', () => { ]); const el = await render('TransitionProbe', 'data-option-enter-to="on"'); - expect(getInstance(el, 'TransitionProbe').$options.enterTo).toBe('on'); + expect(getInstance(el, 'TransitionProbe')!.$options.enterTo).toBe('on'); }); /** @@ -76,7 +75,7 @@ describe('withTransition behaviour', () => { 'TransitionProbe', 'data-option-enter-to="visible" data-option-enter-keep="true"', ); - const instance = getInstance(el, 'TransitionProbe'); + const instance = getInstance(el, 'TransitionProbe')!; await instance.enter(); @@ -89,7 +88,7 @@ describe('withTransition behaviour', () => { 'TransitionProbe', 'data-option-enter-to="visible" data-option-enter-keep="true" data-option-leave-to="gone" data-option-leave-keep="true"', ); - const instance = getInstance(el, 'TransitionProbe'); + const instance = getInstance(el, 'TransitionProbe')!; await instance.toggle(); expect(el.classList.contains('visible')).toBe(true); @@ -110,7 +109,7 @@ describe('withTransition behaviour', () => { document.body.append(root); await settle(); const el = root.firstElementChild as HTMLElement; - const instance = getInstance(el, 'MultiProbe'); + const instance = getInstance(el, 'MultiProbe')!; const items = [...el.querySelectorAll('[data-ref="item[]"]')]; await instance.enter(); @@ -126,7 +125,7 @@ describe('withTransition behaviour', () => { 'TransitionProbe', 'data-option-enter-to="on" data-option-enter-keep="true"', ); - const instance = getInstance(el, 'TransitionProbe'); + const instance = getInstance(el, 'TransitionProbe')!; const other = document.createElement('div'); document.body.append(other); @@ -142,7 +141,7 @@ describe('withTransition behaviour', () => { 'TransitionProbe', 'data-option-enter-to="on" data-option-enter-keep="true" data-option-leave-to="off" data-option-leave-keep="true"', ); - const instance = getInstance(el, 'TransitionProbe'); + const instance = getInstance(el, 'TransitionProbe')!; const other = document.createElement('div'); document.body.append(other); @@ -160,7 +159,7 @@ describe('withTransition behaviour', () => { */ it('lets a consumer force an option the markup did not ask for', async () => { const el = await render('ForcedProbe', 'data-option-enter-to="visible"'); - const instance = getInstance(el, 'ForcedProbe'); + const instance = getInstance(el, 'ForcedProbe')!; expect(instance.$options.enterKeep).toBe(false); expect(instance.transitionOptions.enterKeep).toBe(true); diff --git a/packages/v4/src/Base.spec.ts b/packages/v4/src/Base.spec.ts index 3100e5c90..175823b52 100644 --- a/packages/v4/src/Base.spec.ts +++ b/packages/v4/src/Base.spec.ts @@ -11,9 +11,9 @@ import { } from './Base.js'; import { DIAGNOSTICS, type ToolkitDiagnosticDetail } from './diagnostic-contract.js'; import { EVENTS } from './events.js'; +import { getInstance } from './instances.js'; import { INSTANCES } from './protocol-symbols.js'; import { registerComponent } from './registry.js'; -import { getInstance } from './test-utils.js'; import { renderTodoList, TodoCount, TodoItem, TodoList } from './todo.fixtures.js'; import { captureDiagnostics, resetDom, settle } from './test/index.js'; @@ -61,7 +61,7 @@ describe('$emit and delegation', () => { const root = renderTodoList(); await settle(); - const list = getInstance(root, 'TodoList'); + const list = getInstance(root, 'TodoList')!; root.querySelector('[data-ref="remove"]')?.click(); await settle(); @@ -76,7 +76,7 @@ describe('$emit and delegation', () => { await settle(); const li = root.querySelector('[data-component="TodoItem"]'); - const instance = getInstance(li, 'TodoItem'); + const instance = getInstance(li!, 'TodoItem')!; const seen: unknown[] = []; root.addEventListener('ping', (event) => { seen.push((event as CustomEvent).detail); @@ -93,7 +93,7 @@ describe('$emit and delegation', () => { await settle(); const li = root.querySelector('[data-component="TodoItem"]'); - const instance = getInstance(li, 'TodoItem'); + const instance = getInstance(li!, 'TodoItem')!; const seen: unknown[] = []; root.addEventListener('ping', (event) => seen.push((event as CustomEvent).detail)); @@ -106,7 +106,7 @@ describe('$emit and delegation', () => { await settle(); const li = root.querySelector('[data-component="TodoItem"]'); - const instance = getInstance(li, 'TodoItem'); + const instance = getInstance(li!, 'TodoItem')!; const log = captureDiagnostics(); const seen: unknown[] = []; root.addEventListener('ping', (event) => seen.push((event as CustomEvent).detail)); @@ -521,7 +521,7 @@ describe('$options', () => { events.every((event) => event.detail.code === DIAGNOSTICS.component.lifecycleFailed), ).toBe(true); expect(events.every((event) => event.detail.component === 'ResilientOptions')).toBe(true); - expect(getInstance(reentrant, 'ReentrantOption').$isMounted).toBe(false); + expect(getInstance(reentrant, 'ReentrantOption')!.$isMounted).toBe(false); } finally { document.querySelectorAll('[data-component="ResilientOptions"]').forEach((el) => el.remove()); } @@ -797,7 +797,7 @@ describe('$refs', () => { expect(owner.$refs.item).toBe(root.querySelector('[data-ref="item"]')); await settle(); - expect(getInstance(root.lastElementChild, 'RefReadInserted').$isMounted).toBe(true); + expect(getInstance(root.lastElementChild!, 'RefReadInserted')!.$isMounted).toBe(true); }); }); @@ -1249,7 +1249,7 @@ describe('$query and $closest', () => { const root = renderTodoList(); await settle(); - const list = getInstance(root, 'TodoList'); + const list = getInstance(root, 'TodoList')!; const items = list.$query('TodoItem'); expect(items).toHaveLength(2); expect(items[0].$closest('TodoList')).toBe(list); @@ -1264,24 +1264,24 @@ describe('$watchChildren', () => { orphan.innerHTML = 'orphan '; document.body.append(orphan); await settle(); - expect(getInstance(orphan, 'TodoItem').$isMounted).toBe(true); + expect(getInstance(orphan, 'TodoItem')!.$isMounted).toBe(true); const root = renderTodoList({ items: [] }); await settle(); - const list = getInstance(root, 'TodoList'); + const list = getInstance(root, 'TodoList')!; expect(list.items.size).toBe(0); root.querySelector('[data-ref="list"]')?.append(orphan); await settle(); expect(list.items.size).toBe(1); - expect(list.items.items[0]).toBe(getInstance(orphan, 'TodoItem')); + expect(list.items.items[0]).toBe(getInstance(orphan, 'TodoItem')!); }); it('keeps the collection in DOM order', async () => { const root = renderTodoList({ items: ['a', 'b', 'c'] }); await settle(); - const list = getInstance(root, 'TodoList'); + const list = getInstance(root, 'TodoList')!; expect(list.items.items.map((item) => item.$el.textContent?.trim().charAt(0))).toEqual([ 'a', 'b', @@ -1626,9 +1626,9 @@ describe('lifecycle', () => { await settle(); const countInstance = getInstance( - root.querySelector('[data-component="TodoCount"]'), + root.querySelector('[data-component="TodoCount"]')!, 'TodoCount', - ); + )!; expect(countInstance.cleanupCalls).toBe(0); root.remove(); diff --git a/packages/v4/src/config-extension.spec.ts b/packages/v4/src/config-extension.spec.ts index ef490947c..e28648a69 100644 --- a/packages/v4/src/config-extension.spec.ts +++ b/packages/v4/src/config-extension.spec.ts @@ -14,8 +14,8 @@ import { afterEach, describe, expect, expectTypeOf, it } from 'vitest'; import { Base, type BaseConfig, type BaseProps } from './Base.js'; import { component } from './decorators.js'; import { DIAGNOSTICS } from './diagnostic-contract.js'; +import { getInstance } from './instances.js'; import { registerComponent } from './registry.js'; -import { getInstance } from './test-utils.js'; import { TodoItem } from './todo.fixtures.js'; import { captureDiagnostics, resetDom, settle } from './test/index.js'; @@ -71,7 +71,7 @@ describe('extending a component with extra config', () => { const el = render('NavControl', { 'data-option-show-compass': '' }); await settle(); - const instance = getInstance(el, 'NavControl'); + const instance = getInstance(el, 'NavControl')!; expect(instance.$config.name).toBe('NavControl'); expect(Object.keys(instance.$config.options ?? {})).toEqual([ 'position', @@ -100,7 +100,7 @@ describe('extending a component with extra config', () => { el.innerHTML = ''; await settle(); - const instance = getInstance(el, 'FullscreenControl'); + const instance = getInstance(el, 'FullscreenControl')!; expect(instance.$config.name).toBe('FullscreenControl'); expect(instance.$options.position).toBe('bottom-left'); expect(instance.$refs.handle).toBe(el.firstElementChild); @@ -126,8 +126,8 @@ describe('extending a component with extra config', () => { // A restated option replaces the parent definition whole; it does not // merge into it, so the derived default wins with nothing left behind. - expect(getInstance(left, 'LeftControl').$options.position).toBe('top-left'); - expect(getInstance(right, 'RightControl').$options.position).toBe('top-right'); + expect(getInstance(left, 'LeftControl')!.$options.position).toBe('top-left'); + expect(getInstance(right, 'RightControl')!.$options.position).toBe('top-right'); }); it('extends a class it cannot edit, in expression position', async () => { @@ -144,7 +144,7 @@ describe('extending a component with extra config', () => { const el = render('CompactVendor', { 'data-option-compact': '' }); await settle(); - const instance = getInstance(el, 'CompactVendor'); + const instance = getInstance(el, 'CompactVendor')!; expect(instance).toBeInstanceOf(Vendor); expect(instance.$options.compact).toBe(true); expect(Object.keys(instance.$config.options ?? {})).toEqual(['size', 'compact']); @@ -157,7 +157,7 @@ describe('extending a component with extra config', () => { const el = render('DecoratedControl'); await settle(); - const instance = getInstance(el, 'DecoratedControl'); + const instance = getInstance(el, 'DecoratedControl')!; expect(instance).toBeInstanceOf(AbstractControl); expect(instance.$config.name).toBe('DecoratedControl'); expect(Object.keys(instance.$config.options ?? {})).toEqual(['position', 'showZoom']); @@ -191,7 +191,7 @@ describe('what v3 did and v4 does not', () => { message: '"Widget" is already registered; the incoming declaration was ignored.', }, ]); - expect(getInstance(el, 'Widget')).not.toBeInstanceOf(UnnamedWidget); + expect(getInstance(el, 'Widget')!).not.toBeInstanceOf(UnnamedWidget); log.stop(); }); diff --git a/packages/v4/src/context-subscription.spec.ts b/packages/v4/src/context-subscription.spec.ts index f6685c6b1..0110518a9 100644 --- a/packages/v4/src/context-subscription.spec.ts +++ b/packages/v4/src/context-subscription.spec.ts @@ -4,8 +4,8 @@ import { subscribeContext } from './context-subscription.js'; import { createContext, provideContext, provideRootContext, type ContextKey } from './context.js'; import { DIAGNOSTICS, type ToolkitDiagnosticDetail } from './diagnostic-contract.js'; import { EVENTS } from './events.js'; +import { getInstance } from './instances.js'; import { registerComponent } from './registry.js'; -import { getInstance } from './test-utils.js'; import { resetDom, settle } from './test/index.js'; afterEach(resetDom); @@ -130,7 +130,7 @@ describe('subscribeContext', () => { `); await settle(); - const member = getInstance(root.querySelector('span'), 'ReanswerMember'); + const member = getInstance(root.querySelector('span')!, 'ReanswerMember')!; expect(member.seen).toEqual(['page']); root.querySelector('#scope')?.setAttribute('data-component', 'ReanswerScope'); @@ -165,7 +165,7 @@ describe('subscribeContext', () => { `); await settle(); - const member = getInstance(root.querySelector('span'), 'DistanceMember'); + const member = getInstance(root.querySelector('span')!, 'DistanceMember')!; expect(member.seen).toEqual(['inner']); root.querySelector('#outer')?.setAttribute('data-component', 'DistanceScope'); diff --git a/packages/v4/src/context.spec.ts b/packages/v4/src/context.spec.ts index 6c5044b90..de1a55d9f 100644 --- a/packages/v4/src/context.spec.ts +++ b/packages/v4/src/context.spec.ts @@ -11,8 +11,8 @@ import { } from './context.js'; import { DIAGNOSTICS, type ToolkitDiagnosticDetail } from './diagnostic-contract.js'; import { EVENTS } from './events.js'; +import { getInstance } from './instances.js'; import { registerComponent } from './registry.js'; -import { getInstance } from './test-utils.js'; import { renderTodoList } from './todo.fixtures.js'; import { resetDom, settle } from './test/index.js'; @@ -443,9 +443,9 @@ describe('provide/inject', () => { document.body.append(root); await settle(); - const counter = getInstance(root, 'Counter'); + const counter = getInstance(root, 'Counter')!; const button = root.querySelector('button'); - const control = getInstance(button, 'CounterBtn'); + const control = getInstance(button!, 'CounterBtn')!; button?.click(); button?.click(); diff --git a/packages/v4/src/decorators.spec.ts b/packages/v4/src/decorators.spec.ts index c826e1e67..7eca02cb2 100644 --- a/packages/v4/src/decorators.spec.ts +++ b/packages/v4/src/decorators.spec.ts @@ -14,9 +14,9 @@ import { isBaseConstructor } from './component-brand.js'; import { DIAGNOSTICS } from './diagnostic-contract.js'; import { createContext, signal, type Signal } from './context.js'; import { children, component, inject, on, provide, read, write } from './decorators.js'; +import { getInstance } from './instances.js'; import { registerComponent, registerComponents } from './registry.js'; import { defaultScheduler } from './scheduler.js'; -import { getInstance } from './test-utils.js'; import { TodoItem } from './todo.fixtures.js'; import { captureDiagnostics, resetDom, settle } from './test/index.js'; @@ -374,7 +374,7 @@ describe('@component', () => { const root = render(); await settle(); - expect(getInstance(root, 'DecoParent').$isMounted).toBe(true); + expect(getInstance(root, 'DecoParent')!.$isMounted).toBe(true); }); /** @@ -592,7 +592,7 @@ describe('@component', () => { }); await settle(); - const instance = getInstance(el, 'ImmediateRegistration'); + const instance = getInstance(el, 'ImmediateRegistration')!; expect(instance.$isMounted).toBe(true); expect(instance.$options.tone).toBe('loud'); expect(instance.$refs.label).toBe(el.querySelector('span')); @@ -642,11 +642,11 @@ describe('@on', () => { const root = render(); await settle(); - const parent = getInstance(root, 'DecoParent'); + const parent = getInstance(root, 'DecoParent')!; const child = getInstance( - root.querySelector('[data-component="DecoChild"]'), + root.querySelector('[data-component="DecoChild"]')!, 'DecoChild', - ); + )!; child.ping(); expect(parent.received).toHaveLength(1); @@ -658,11 +658,11 @@ describe('@on', () => { const root = render(); await settle(); - const parent = getInstance(root, 'DecoParent'); + const parent = getInstance(root, 'DecoParent')!; const child = getInstance( - root.querySelector('[data-component="DecoChild"]'), + root.querySelector('[data-component="DecoChild"]')!, 'DecoChild', - ); + )!; child.ping(); child.$emit('pong'); @@ -674,7 +674,7 @@ describe('@on', () => { const root = render(); await settle(); - const parent = getInstance(root, 'DecoParent'); + const parent = getInstance(root, 'DecoParent')!; root.click(); expect(parent.clicks).toBe(1); }); @@ -705,7 +705,7 @@ describe('@on', () => { document.body.append(root); await settle(); - const instance = getInstance(root, name); + const instance = getInstance(root, name)!; root.click(); instance.$emit('picked', { id: 'a' }); @@ -716,11 +716,11 @@ describe('@on', () => { const root = render(); await settle(); - const parent = getInstance(root, 'DecoParent'); + const parent = getInstance(root, 'DecoParent')!; const child = getInstance( - root.querySelector('[data-component="DecoChild"]'), + root.querySelector('[data-component="DecoChild"]')!, 'DecoChild', - ); + )!; child.ping(); expect(parent.byClass).toHaveLength(1); @@ -733,7 +733,7 @@ describe('@on', () => { const root = render(); await settle(); - const parent = getInstance(root, 'DecoParent'); + const parent = getInstance(root, 'DecoParent')!; window.dispatchEvent(new Event('load')); expect(parent.windowLoads).toHaveLength(1); @@ -749,7 +749,7 @@ describe('@on', () => { const root = render(); await settle(); - const parent = getInstance(root, 'DecoParent'); + const parent = getInstance(root, 'DecoParent')!; root.remove(); await settle(); window.dispatchEvent(new Event('load')); @@ -768,9 +768,9 @@ describe('@on', () => { document.body.append(root); await settle(); - const parent = getInstance(root, 'GlobalNames'); + const parent = getInstance(root, 'GlobalNames')!; const childEl = root.querySelector('[data-component="Window"]') as HTMLElement; - const child = getInstance(childEl, 'Window'); + const child = getInstance(childEl, 'Window')!; window.dispatchEvent(new Event('resize')); expect(parent.globalResizes).toHaveLength(1); @@ -789,7 +789,7 @@ describe('@on', () => { document.body.append(root); await settle(); - const instance = getInstance(root, 'DotList'); + const instance = getInstance(root, 'DotList')!; (root.querySelectorAll('i')[1] as HTMLElement).click(); expect(instance.clicked).toEqual([1]); expect(instance.magic).toEqual([1]); @@ -812,7 +812,7 @@ describe('@on', () => { const log = captureDiagnostics(); await settle(); - const instance = getInstance(root, 'NsDots'); + const instance = getInstance(root, 'NsDots')!; (root.querySelectorAll('i')[1] as HTMLElement).click(); expect(instance.clicked).toEqual([1]); @@ -831,7 +831,7 @@ describe('@on', () => { const log = captureDiagnostics(); await settle(); - const instance = getInstance(root, 'DotMismatch'); + const instance = getInstance(root, 'DotMismatch')!; (root.querySelectorAll('i')[1] as HTMLElement).click(); expect(instance.clicked).toEqual([]); @@ -848,12 +848,12 @@ describe('@on', () => { document.body.append(root); await settle(); - const parent = getInstance(root, 'SubTargetParent'); - const sub = getInstance(root.querySelector('[data-component="SubKind"]'), 'SubKind'); + const parent = getInstance(root, 'SubTargetParent')!; + const sub = getInstance(root.querySelector('[data-component="SubKind"]')!, 'SubKind')!; const base = getInstance( - root.querySelector('[data-component="BaseKind"]'), + root.querySelector('[data-component="BaseKind"]')!, 'BaseKind', - ); + )!; sub.$emit('ping'); expect(parent.seen).toHaveLength(1); @@ -882,7 +882,7 @@ describe('@on', () => { const root = render(); await settle(); - const parent = getInstance(root, 'DecoParent'); + const parent = getInstance(root, 'DecoParent')!; root.remove(); await settle(); root.click(); @@ -900,7 +900,7 @@ describe('@children', () => { const root = render(2); await settle(); - const parent = getInstance(root, 'DecoParent'); + const parent = getInstance(root, 'DecoParent')!; expect(parent.kids.size).toBe(2); root.querySelector('[data-component="DecoChild"]')?.remove(); @@ -1041,11 +1041,11 @@ describe('@on stacked with @read / @write', () => { const root = renderPhased(); await settle(); - const instance = getInstance(root, 'PhasedHandlers'); + const instance = getInstance(root, 'PhasedHandlers')!; const child = getInstance( - root.querySelector('[data-component="PhasedChild"]'), + root.querySelector('[data-component="PhasedChild"]')!, 'PhasedChild', - ); + )!; window.dispatchEvent(new Event('resize')); window.dispatchEvent(new Event('scroll')); @@ -1074,7 +1074,7 @@ describe('@on stacked with @read / @write', () => { document.body.append(root); await settle(); - const instance = getInstance(root, 'SinglyDecorated'); + const instance = getInstance(root, 'SinglyDecorated')!; window.dispatchEvent(new Event('resize')); expect(instance.resizes).toBe(1); }); @@ -1085,7 +1085,7 @@ describe('@on stacked with @read / @write', () => { document.body.append(root); await settle(); - const instance = getInstance(root, 'SinglyDecorated'); + const instance = getInstance(root, 'SinglyDecorated')!; window.dispatchEvent(new Event('scroll')); expect(instance.scrolls).toBe(0); @@ -1099,11 +1099,11 @@ describe('@provide / @inject', () => { const root = render(2); await settle(); - const parent = getInstance(root, 'DecoParent'); + const parent = getInstance(root, 'DecoParent')!; const child = getInstance( - root.querySelector('[data-component="DecoChild"]'), + root.querySelector('[data-component="DecoChild"]')!, 'DecoChild', - ); + )!; expect(parent.total.value).toBe(2); expect(child.total).toBe(parent.total); diff --git a/packages/v4/src/dom-mutations.spec.ts b/packages/v4/src/dom-mutations.spec.ts index 18bfe1456..66971a1af 100644 --- a/packages/v4/src/dom-mutations.spec.ts +++ b/packages/v4/src/dom-mutations.spec.ts @@ -12,10 +12,10 @@ import { type AttributeChange, } from './dom-mutations.js'; import { EVENTS } from './events.js'; +import { getInstance } from './instances.js'; import { INSTANCES } from './protocol-symbols.js'; import { registerComponent } from './registry.js'; import { SWAP_MODES, swap } from './swap.js'; -import { getInstance } from './test-utils.js'; import { resetDom } from './test/index.js'; let counter = 0; @@ -310,7 +310,7 @@ describe('watchAttributes', () => { el.setAttribute(VIRTUAL_ATTRIBUTE, 'open()'); document.body.append(el); await whenDOMSettled(); - const instance = getInstance(el, name); + const instance = getInstance(el, name)!; el.setAttribute('data-component', ''); el.setAttribute(VIRTUAL_ATTRIBUTE, 'close()'); diff --git a/packages/v4/src/group.spec.ts b/packages/v4/src/group.spec.ts index b5ef34acf..467859e33 100644 --- a/packages/v4/src/group.spec.ts +++ b/packages/v4/src/group.spec.ts @@ -3,8 +3,8 @@ import { Base, type BaseConfig, type BaseProps, type MountedReturn } from './Bas import { createContext, type Signal } from './context.js'; import { subscribeContext } from './context-subscription.js'; import { createGroup, type Group } from './group.js'; +import { getInstance } from './instances.js'; import { registerComponents } from './registry.js'; -import { getInstance } from './test-utils.js'; import { mount, resetDom, settle } from './test/index.js'; afterEach(resetDom); @@ -211,11 +211,11 @@ function disclosureMarkup(id: string, open = false): string { } function disclosure(root: ParentNode, id: string): Disclosure { - return getInstance(root.querySelector(`#${id}`), 'Disclosure'); + return getInstance(root.querySelector(`#${id}`)!, 'Disclosure')!; } function group(root: ParentNode, id: string): DisclosureGroup { - return getInstance(root.querySelector(`#${id}`), 'DisclosureGroup'); + return getInstance(root.querySelector(`#${id}`)!, 'DisclosureGroup')!; } describe('a group of disclosures', () => { diff --git a/packages/v4/src/manifest.spec.ts b/packages/v4/src/manifest.spec.ts index 747033150..1f43103e5 100644 --- a/packages/v4/src/manifest.spec.ts +++ b/packages/v4/src/manifest.spec.ts @@ -9,8 +9,8 @@ import { type ModuleRecord, type WebpackContextLike, } from './manifest.js'; +import { getInstance } from './instances.js'; import { registerManifest } from './registry.js'; -import { getInstance } from './test-utils.js'; import { captureDiagnostics, resetDom, settle } from './test/index.js'; class Widget {} @@ -200,7 +200,7 @@ describe('defineManifest with registerManifest', () => { document.body.append(named, fallback); await settle(); - expect(getInstance(named, 'ManifestNamed')).toBeInstanceOf(ManifestNamed); - expect(getInstance(fallback, 'ManifestDefault')).toBeInstanceOf(ManifestDefault); + expect(getInstance(named, 'ManifestNamed')!).toBeInstanceOf(ManifestNamed); + expect(getInstance(fallback, 'ManifestDefault')!).toBeInstanceOf(ManifestDefault); }); }); diff --git a/packages/v4/src/props.spec.ts b/packages/v4/src/props.spec.ts index aad1a1cb5..7d2a48ef2 100644 --- a/packages/v4/src/props.spec.ts +++ b/packages/v4/src/props.spec.ts @@ -1,7 +1,7 @@ import { describe, expect, expectTypeOf, it } from 'vitest'; import { Base, type BaseProps } from './Base.js'; +import { getInstance } from './instances.js'; import { registerComponent } from './registry.js'; -import { getInstance } from './test-utils.js'; import { resetDom, settle } from './test/index.js'; /** @@ -174,7 +174,7 @@ describe('a component declared with a props type parameter', () => { `; await settle(); - const instance = getInstance(document.querySelector('form'), 'Extensible'); + const instance = getInstance(document.querySelector('form')!, 'Extensible')!; expect(instance.$options.target).toBe('here'); expect(instance.$refs.btn).toBeInstanceOf(HTMLButtonElement); expect(instance.$refs.items).toHaveLength(2); @@ -189,7 +189,7 @@ describe('a component declared with a props type parameter', () => { await settle(); const el = document.querySelector('form') as HTMLFormElement; - const instance = getInstance(el, 'Extensible'); + const instance = getInstance(el, 'Extensible')!; const seen: unknown[] = []; el.addEventListener('go', (event) => seen.push((event as CustomEvent).detail)); instance.$emit('go', { at: 3 }); diff --git a/packages/v4/src/registry.spec.ts b/packages/v4/src/registry.spec.ts index 523abf425..6b1f33072 100644 --- a/packages/v4/src/registry.spec.ts +++ b/packages/v4/src/registry.spec.ts @@ -2,9 +2,9 @@ import { afterEach, describe, expect, it } from 'vitest'; import { Base, type BaseConfig } from './Base.js'; import { DIAGNOSTICS, type ToolkitDiagnosticDetail } from './diagnostic-contract.js'; import { EVENTS } from './events.js'; +import { getInstance } from './instances.js'; import { INSTANCES } from './protocol-symbols.js'; import { registerComponent } from './registry.js'; -import { getInstance } from './test-utils.js'; import { renderTodoList, TodoItem, TodoList } from './todo.fixtures.js'; import { captureDiagnostics, resetDom, settle } from './test/index.js'; @@ -15,7 +15,7 @@ describe('registry', () => { const root = renderTodoList(); await settle(); - const list = getInstance(root, 'TodoList'); + const list = getInstance(root, 'TodoList')!; expect(list.$isMounted).toBe(true); expect(list.items.size).toBe(2); @@ -26,26 +26,26 @@ describe('registry', () => { await settle(); expect(list.items.size).toBe(3); - expect(getInstance(li, 'TodoItem').$isMounted).toBe(true); + expect(getInstance(li, 'TodoItem')!.$isMounted).toBe(true); }); it('unmounts on removal and remounts the same instance on re-insertion', async () => { const root = renderTodoList(); await settle(); - const list = getInstance(root, 'TodoList'); + const list = getInstance(root, 'TodoList')!; const li = root.querySelector('[data-component="TodoItem"]') as HTMLElement; - const instance = getInstance(li, 'TodoItem'); + const instance = getInstance(li, 'TodoItem')!; li.remove(); await settle(); expect(instance.$isMounted).toBe(false); - expect(getInstance(li, 'TodoItem')).toBe(instance); + expect(getInstance(li, 'TodoItem')!).toBe(instance); expect(list.items.size).toBe(1); root.querySelector('[data-ref="list"]')?.append(li); await settle(); - expect(getInstance(li, 'TodoItem')).toBe(instance); + expect(getInstance(li, 'TodoItem')!).toBe(instance); expect(instance.$isMounted).toBe(true); expect(list.items.size).toBe(2); }); @@ -58,7 +58,7 @@ describe('registry', () => { el.setAttribute('data-component', 'TodoItem'); await settle(); - expect(getInstance(el, 'TodoItem').$isMounted).toBe(true); + expect(getInstance(el, 'TodoItem')!.$isMounted).toBe(true); }); it('reconciles token changes without disturbing retained components', async () => { @@ -67,8 +67,8 @@ describe('registry', () => { document.body.append(el); await settle(); - const item = getInstance(el, 'TodoItem'); - const count = getInstance(el, 'TodoCount'); + const item = getInstance(el, 'TodoItem')!; + const count = getInstance(el, 'TodoCount')!; expect(item.$isMounted).toBe(true); expect(count.$isMounted).toBe(true); @@ -85,7 +85,7 @@ describe('registry', () => { el.setAttribute('data-component', 'TodoItem'); document.body.append(el); await settle(); - const first = getInstance(el, 'TodoItem'); + const first = getInstance(el, 'TodoItem')!; el.removeAttribute('data-component'); await settle(); @@ -94,7 +94,7 @@ describe('registry', () => { el.setAttribute('data-component', 'TodoItem'); await settle(); - const second = getInstance(el, 'TodoItem'); + const second = getInstance(el, 'TodoItem')!; expect(second).not.toBe(first); expect(second.$isMounted).toBe(true); }); @@ -259,7 +259,7 @@ describe('registry', () => { message: '"MergedName" is already registered; the incoming declaration was ignored.', }, ]); - expect(getInstance(el, 'MergedName')).toBeInstanceOf(Named); + expect(getInstance(el, 'MergedName')!).toBeInstanceOf(Named); log.stop(); }); }); diff --git a/packages/v4/src/responsive-options.spec.ts b/packages/v4/src/responsive-options.spec.ts index ded20128d..9582e87c9 100644 --- a/packages/v4/src/responsive-options.spec.ts +++ b/packages/v4/src/responsive-options.spec.ts @@ -2,9 +2,9 @@ import { afterEach, describe, expect, it } from 'vitest'; import { Base, type OptionChange } from './Base.js'; import { DIAGNOSTICS } from './diagnostic-contract.js'; import { EVENTS } from './events.js'; +import { getInstance } from './instances.js'; import { registerComponent, registerManifest } from './registry.js'; import { BREAKPOINTS, setBreakpoints } from './services/breakpoint.js'; -import { getInstance } from './test-utils.js'; import { captureDiagnostics, resetDom, settle } from './test/index.js'; /** Select test breakpoints without changing the viewport. */ @@ -138,7 +138,7 @@ describe('responsive options', () => { data-option-label:large="wide">

`, ); await settle(); - const label = getInstance