diff --git a/packages/v4/DESIGN.md b/packages/v4/DESIGN.md index 7b2088ed..f7f87188 100644 --- a/packages/v4/DESIGN.md +++ b/packages/v4/DESIGN.md @@ -566,30 +566,33 @@ class Slider extends Base { - No global instance registry is added. The subscription stays active through unmount and mount cycles, for the whole life of the watching instance. - Unmounted instances announce from `document`, so one lazy, realm-shared listener serves every watcher and the document holds nothing but weak references to them. A listener per watcher would make every watching component immortal, since the document outlives the page's components. -### The page-wide lookup — `getInstances()` +### The page-wide lookup — `getInstances()` and its three siblings ```js -getInstances('Dialog').forEach((dialog) => dialog.close()); // page-wide -getInstances('Dialog', section); // one region -getInstances(el); // everything mounted on one element +getMountedInstances('Dialog').forEach((dialog) => dialog.close()); // the live ones +getInstances('Dialog', section); // every one built in a region +getUnmountedInstances('Dialog'); // built, then stood down +getInstance(el, 'Dialog'); // the one on this element, mounted or not +getInstances(el); // everything on one element ``` It derives the answer from the DOM. It keeps no registry of instances. -- A matching element with no instance is skipped. -- The filter is `$isMounted`, so an unmounted instance is never returned. +- **The name says which population it answers.** `getInstances()` returns every instance that exists; `getMountedInstances()` is the safe list to call a method on; `getUnmountedInstances()` is what a reversible `in-view` or `media:` strategy has stood down, plus a construct-then-mount-failure. The three take the same two overloads. +- **A matching element with no instance is skipped, and that is the whole narrowing.** `selectorFor(name)` over-matches on purpose — it lists the responsive spellings of `data-component` too — and the instance-map read is what removes an inactive declaration, because a breakpoint-withdrawn component is destroyed _and_ deleted from the map. A mount filter never did that work, which is why it is gone. +- **There is no `getMountedInstance`.** The singular returns one object; a caller reads `.$isMounted` on it. - `root` is a `ParentNode` and the call is `querySelectorAll`, so an element root searches its descendants and never matches itself. - `selectorFor(name)` on `/utils` is the one place that writes the name-to-selector contract. ### Where the instances live -An element publishes its instances under `Symbol.for('@studiometa/js-toolkit-v4/instances')`. It is not public API; `getInstances()` is. In a console, read them with one line: +An element publishes its instances under `Symbol.for('@studiometa/js-toolkit-v4/instances')`. It is not public API; the four lookups are, and between them they express every read the map answers. In a console, read them with one line: ```js $0[Symbol.for('@studiometa/js-toolkit-v4/instances')]; ``` -The element overload `getInstances(el)` answers "what is mounted here", in mount order and with the `$isMounted` filter. +The element overload `getInstances(el)` answers "what is here", in mount order. It reads the map directly and never the DOM, so it is the only form that reaches a **detached** element — the string form cannot, since `document.querySelectorAll()` does not see one. Pass the detached root as `root` when a name lookup has to reach inside it. ### Shared state — provide/inject @@ -1068,7 +1071,7 @@ See [RATIONALE.md — 12. Storage](./RATIONALE.md#12-storage). ## 13. Testing — the `/test` subpath -`@studiometa/js-toolkit-v4/test` ships the eight helpers a test of a component cannot write for itself: `mount(html)`, `settle()`, `frames(count?)`, `waitFor(predicate, options?)`, `resetDom()`, `captureDiagnostics(target?)`, `recordEvents(target, ...types)` and `resetRegistry()`. +`@studiometa/js-toolkit-v4/test` ships the nine helpers a test of a component cannot write for itself: `mount(html)`, `settle()`, `frames(count?)`, `countRequestedFrames(during)`, `waitFor(predicate, options?)`, `resetDom()`, `captureDiagnostics(target?)`, `recordEvents(target, ...types)` and `resetRegistry()`. - **The subpath exists because the timing recipe is not derivable.** "Has this component mounted and finished its writes?" is answered by five rounds of a 10ms timer followed by `defaultScheduler.whenIdle()`, and those two numbers encode the mount observer's delivery latency and the scheduler's lane order. Neither half works alone: `whenIdle()` can resolve before the observer has reported the element, and a timer can return between two lanes. `defaultScheduler` and `nextFrame` were already public, so the pieces shipped and the recipe did not. - **It depends on no test framework.** Nothing in the module imports a runner, an assertion library or a spy; it reads the DOM and the scheduler only, so it runs under Vitest, under Playwright and on a plain browser page. That is also why it is not on the root barrel: a page has no use for it, and the root export count stays what it was. @@ -1078,7 +1081,8 @@ See [RATIONALE.md — 12. Storage](./RATIONALE.md#12-storage). - **`captureDiagnostics()` reads the channel, and asserting on `console.warn` does not.** A recovered failure is reported on a cancelable event whose _default behaviour_ is the console line; a spy on that line cannot see the code, the severity or the reporting component, and it passes for the wrong diagnostic. The helper cancels each event as it arrives, which is the same act that suppresses the sink — so collecting and silencing are one step, not two, and no spy is involved. `target` defaults to `document`, which sees everything a connected element reported, because diagnostics bubble and compose. - **`recordEvents(target, ...types)` keeps the order and the payloads.** A component's contract is "`open` came before `opened`, and `opened` carried the height it measured", and a call-counting spy throws both away. Recording several types into one array is the only place their relative order is visible; the richer `{ type, detail }` shape ships because a caller wanting names alone can map, and the reverse is impossible. `$emit` dispatches synchronously but is almost never _called_ synchronously, so the count is awaited with `waitFor`, not read. - **`resetRegistry()` is the inverse `registerComponent()` deliberately lacks.** A page registers once and keeps it; a test suite is the one caller for which a page-wide registry surviving `resetDom()` is wrong, and the workaround it forces is a counter minting `Widget-1`, `Widget-2`. It is coarse rather than a targeted `unregisterComponent(name)` because the element→controller map is a `WeakMap`: it cannot be enumerated, so nothing can walk it to dispose live triggers, and a `querySelectorAll()` sweep would still miss detached elements. Clearing everything works because the mutation-observer path already disposes a controller as its element leaves the DOM — hence the call belongs _after_ `resetDom()`, and **never in an `afterEach`**, which would unregister the module-top-level registrations for every later test in the file. -- **What is deliberately not in it**: an instance lookup (`getInstances()` already answers it), frame counters, fetch stubs and pointer sequences. Each is either specific to one spec or already served by `vi.fn()` and `@vitest/browser`'s `userEvent`. +- **`countRequestedFrames(during)` patches a global, and says so.** It swaps `globalThis.requestAnimationFrame` for a counting wrapper that forwards to the original, and restores it in a `finally` — so it comes back whether `during` returns or throws. It is here despite the patch because the assertion it serves, "this did not schedule a frame per event", has no other seam: the framework's own scheduler makes the calls, so there is nothing of the component's for a spy to sit on. Two concurrent calls would nest their wrappers; do not. +- **What is deliberately not in it**: an instance lookup (`getInstance()` and `getInstances()` on the root barrel answer it), fetch stubs and pointer sequences. Each is either specific to one spec or already served by `vi.fn()` and `@vitest/browser`'s `userEvent`. See [RATIONALE.md — 13. Testing](./RATIONALE.md#13-testing). diff --git a/packages/v4/RATIONALE.md b/packages/v4/RATIONALE.md index 159f38d5..7016a1f3 100644 --- a/packages/v4/RATIONALE.md +++ b/packages/v4/RATIONALE.md @@ -319,6 +319,20 @@ The document already knows where the components are, and a second index of it ca There is no selector-strategy seam behind it. v4 resolves components through `data-component` alone, so name-to-selector is the only lookup shape that there will ever be, and `selectorFor(name)` is the one place that writes it down. +### Why one lookup became four + +`getInstances()` shipped with a silent `$isMounted` filter, and the filter was wrong twice over. It was invisible at the call site — `getInstances('Foo')` returning 2 where three elements declare `Foo`, one of them below its `media:` breakpoint, is not debuggable from the name — and it made the primitive unreachable, because the raw map read it hid had no public spelling. Fifty-two spec files answered that by importing a private `getInstance()` from `test-utils.ts`, which is the same finding `Action` produced one level up: a caller reaching past the public surface to write a lookup core could write in ten lines. + +So the population is named at the call site instead: `getInstances()`, `getMountedInstances()`, `getUnmountedInstances()`, and the singular `getInstance(el, name)`. One internal `collect()` does the traversal and a predicate does the difference, so DOM order, mount order and the map read are written once. + +**Dropping the filter resurrects nothing**, which is the fact the whole change rests on. The string form narrows three times — `querySelectorAll(selectorFor(name))`, then the `INSTANCES` read, then the mount check — and the middle step already does the work the last one is credited with. An inactive declaration has no instance, and a breakpoint-withdrawn one is destroyed _and_ deleted from the map by `reconcileElement()`. What the filter did hide was real, and small: the instances a reversible `in-view` or `media:` strategy stands down and keeps for the crossing back. That population now has a name. + +`selectorFor()`'s own doc comment credited the narrowing to the mount check, so it is rewritten. A comment that misattributes an invariant is how the filter comes back. + +There is no `getMountedInstance()`. The singular already returns one object, and a mounted-only variant would have to fold "no instance" and "not mounted" into the same `undefined`. + +The element form is the only one that reaches a detached element. `document.querySelectorAll()` does not see one, but the instance is still on it — so the asymmetry is documented rather than left to be discovered, and the escape hatch is passing the detached root as `root`. + ### Why the instances live under a symbol v3 stores `Map` under `el.__base__`. v4 stored `Map` under the same name. Two versions in one document then read the map of the other as their own: the teardown of v4 called `$unmount()` on the instances of v3 and on the `'terminated'` string that v3 leaves behind, which is a `TypeError`, while the child resolution of v3 accepted a v4 instance as one of its children. That blocked any migration page by page. `src/coexistence.spec.ts` mounts both versions in one document and holds the line. @@ -875,8 +889,14 @@ Its footgun is stated in the same place because the shape of a spec file guarant ### What was refused -`getInstance(el, name)` — the public `getInstances()` answers it, and a second spelling of a lookup is a second thing to keep true. A frame counter that patches `requestAnimationFrame` — one caller, and a global patch is a poor thing to hand out. Fetch stubs and pointer-event sequences — `vi.fn()` and `@vitest/browser`'s `userEvent` do both better, and each was shaped by the one spec that grew it. +Fetch stubs and pointer-event sequences — `vi.fn()` and `@vitest/browser`'s `userEvent` do both better, and each was shaped by the one spec that grew it. Those two refusals hold. + +Two others did not, and both are recorded here as reversals rather than quietly corrected. + +`getInstance(el, name)` was refused because "the public `getInstances()` answers it, and a second spelling of a lookup is a second thing to keep true". `getInstances()` did not answer it: it answered a filtered, plural version of it, and the fifty-two spec files importing a private `getInstance()` from `test-utils.ts` are the measurement. The refusal was also aiming at the wrong module — the fix was not a test helper but a missing core export, and it ships from the root barrel next to the plural forms. See "Why one lookup became four". + +A frame counter that patches `requestAnimationFrame` was refused for "one caller, and a global patch is a poor thing to hand out". The caller count was four by the time the module shipped, and the argument about the patch answered the wrong question. The patch is not a convenience a consumer could write around: the framework's own scheduler owns the `requestAnimationFrame` calls, so a component exposes no seam for a spy, and "this did not schedule a frame per event" has no other spelling. `countRequestedFrames()` ships on `./test` with the patch stated in its first line and restored in a `finally`. An event recorder was refused on that same list and then shipped as `recordEvents()`, which is worth recording as a reversal rather than quietly correcting. The refusal was right about `vi.fn()` replacing a _counter_ and wrong about what the seven spec files hand-rolling one were actually doing: they were recording `{ type, detail }` across several event names into one array, because the assertion is the sequence and its payloads, and that is the one thing a per-listener spy cannot express. Two shapes existed in the wild — `type` only, and `{ type, detail }` — and the richer one ships because mapping down is free and mapping up is impossible. -A re-export shim in `test-utils.ts` was refused too. The old file keeps the fixtures and the two helpers that stay source-only, and the specs move onto the new module in one pass; a shim would be both a compatibility layer and a temporary solution meant to be replaced. +A re-export shim in `test-utils.ts` was refused too, and that refusal held all the way to the file's deletion. The specs moved onto the new module in one pass; a shim would have been both a compatibility layer and a temporary solution meant to be replaced. `test-utils.ts` is gone now — the waits went to `src/test/index.ts`, the todo tree to `src/todo.fixtures.ts`, `getInstance()` to core and `countRequestedFrames()` to `./test` — and the two build-script exclusions that named the file went with it. diff --git a/packages/v4/migration/Accordion/Accordion.spec.ts b/packages/v4/migration/Accordion/Accordion.spec.ts index ab97d5e2..5b58b29b 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 fad610f5..ab7e5e2b 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/Action/ActionEvent.ts b/packages/v4/migration/Action/ActionEvent.ts index d9ef914a..9f65e161 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/AnchorNav/AnchorNav.spec.ts b/packages/v4/migration/AnchorNav/AnchorNav.spec.ts index cb59abbc..dbf14bef 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 4c0c1b30..fbfbee04 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 3fb280c9..7e951403 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 9c9d366d..717867f6 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 c6d9aa12..5aba5ade 100644 --- a/packages/v4/migration/Cursor/Cursor.spec.ts +++ b/packages/v4/migration/Cursor/Cursor.spec.ts @@ -1,7 +1,6 @@ import { afterEach, describe, expect, it } from 'vitest'; -import { registerComponents } from '../../src/index.js'; -import { countRequestedFrames, getInstance } from '../../src/test-utils.js'; -import { mount, resetDom, settle, waitFor } from '../../src/test/index.js'; +import { getInstance, registerComponents } from '../../src/index.js'; +import { countRequestedFrames, mount, resetDom, settle, waitFor } from '../../src/test/index.js'; import { Cursor } from './Cursor.js'; registerComponents(Cursor); @@ -19,7 +18,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 +189,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 e047d88e..7b71708b 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 6f24cff7..3f95defc 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 b86ed6ee..a4e3ee1c 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 7b5eac6c..f763e9ff 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 a7c328fa..62530056 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 8fc34cb4..4d882e49 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 af72221d..7717fc76 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 a017366e..424f257d 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 f78a2346..0d9766c0 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 2eb8fcaf..c365a33b 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 4e4706d0..55d5e673 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 e922e9ab..060300f4 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 f9f51c09..fb0d0d51 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 e16683e0..41833a17 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 b3329a87..521e5424 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 9d4ab63d..47a88e53 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 d3b05012..f50a42b6 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 1bd7ba3a..299fbc87 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 b2964350..de04c104 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 63a90ae2..81f4a65d 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 27829acb..6fb39116 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 5f9aac21..26dbae3e 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 4f3be762..e50fdff4 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 c329611d..74bf2675 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 54b887b8..ce2a0444 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/Sticky/Sticky.ts b/packages/v4/migration/Sticky/Sticky.ts index 1b3bea59..ecf3290b 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/migration/Timer/Timer.spec.ts b/packages/v4/migration/Timer/Timer.spec.ts index 63faa1b9..980506cc 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 317b69af..5cc3b7e5 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 a2ee15ef..011cf6fc 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 072f9781..ed4de1c1 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 eede45a2..5bb322ad 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 ea4d9cc6..5e042c08 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 c3acac2b..b0abfe16 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/package.json b/packages/v4/package.json index e8898a55..bc6260bd 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/scripts/build.js b/packages/v4/scripts/build.js index 9dc7fdf0..e94db53f 100644 --- a/packages/v4/scripts/build.js +++ b/packages/v4/scripts/build.js @@ -6,9 +6,10 @@ const pkgRoot = resolve(dirname(new URL(import.meta.url).pathname), '..'); const srcRoot = resolve(pkgRoot, 'src'); const outDir = resolve(pkgRoot, 'dist'); -// Every consumer module under `src/`. Specs, benchmarks, fixtures and test -// utilities stay source-only. `unbundle` keeps the emitted `dist/` tree -// one-to-one with the entries. +// Every consumer module under `src/`. Specs, benchmarks and fixtures stay +// source-only. `unbundle` keeps the emitted `dist/` tree one-to-one with the +// entries. `src/test/index.ts` is not excluded: it is the published `./test` +// subpath. const entryPoints = glob.sync( [ '**/*.ts', @@ -16,7 +17,6 @@ const entryPoints = glob.sync( '!**/*.spec.ts', '!**/*.bench.ts', '!**/*.fixtures.ts', - '!test-utils.ts', '!**/node_modules/**', ], { cwd: srcRoot, absolute: true }, diff --git a/packages/v4/scripts/check-package.js b/packages/v4/scripts/check-package.js index 25caa9a9..46515721 100644 --- a/packages/v4/scripts/check-package.js +++ b/packages/v4/scripts/check-package.js @@ -53,15 +53,13 @@ function assertPackageContent(metadata) { `Only package.json and built JavaScript, declarations and source maps are allowed:\n${unexpected.join('\n')}`, ); - const forbiddenTests = files.filter( - (path) => - path.startsWith('dist/test-utils.') || - /\.(?:spec|bench|fixtures)\.(?:js|js\.map|d\.ts)$/.test(path), + const forbiddenTests = files.filter((path) => + /\.(?:spec|bench|fixtures)\.(?:js|js\.map|d\.ts)$/.test(path), ); assert.deepEqual( forbiddenTests, [], - `Test utilities, specs, benchmarks and fixtures must not be packed:\n${forbiddenTests.join('\n')}`, + `Specs, benchmarks and fixtures must not be packed:\n${forbiddenTests.join('\n')}`, ); assert(fileSet.has('dist/index.js'), 'dist/index.js is missing from the package.'); diff --git a/packages/v4/src/Base.spec.ts b/packages/v4/src/Base.spec.ts index 3100e5c9..175823b5 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 ef490947..e28648a6 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 f6685c6b..0110518a 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 6c5044b9..de1a55d9 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 c826e1e6..7eca02cb 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 18bfe145..66971a1a 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/exports.spec.ts b/packages/v4/src/exports.spec.ts index 82d5aefc..09b56923 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/group.spec.ts b/packages/v4/src/group.spec.ts index b5ef34ac..467859e3 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/index.ts b/packages/v4/src/index.ts index 065af3aa..f4a777a5 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 042fc64d..363541a9 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 7b7c4679..a7f472fb 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/manifest.spec.ts b/packages/v4/src/manifest.spec.ts index 74703315..1f43103e 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 aad1a1cb..7d2a48ef 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/protocol-symbols.ts b/packages/v4/src/protocol-symbols.ts index 400e868b..58f17c12 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/registry.spec.ts b/packages/v4/src/registry.spec.ts index 523abf42..6b1f3307 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-components.spec.ts b/packages/v4/src/responsive-components.spec.ts index 6e5efe02..f99cd1ba 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/responsive-options.spec.ts b/packages/v4/src/responsive-options.spec.ts index ded20128..9582e87c 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