diff --git a/packages/v4/migration/Action/Action.spec.ts b/packages/v4/migration/Action/Action.spec.ts
index ab7e5e2b5..12d9a633c 100644
--- a/packages/v4/migration/Action/Action.spec.ts
+++ b/packages/v4/migration/Action/Action.spec.ts
@@ -7,7 +7,7 @@ import {
SWAP_MODES,
type BaseConfig,
} from '../../src/index.js';
-import { mount, resetDom, settle } from '../../src/test/index.js';
+import { captureDiagnostics, mount, resetDom, settle } from '../../src/test/index.js';
import { Dialog } from '../Dialog/Dialog.js';
import { Action } from './Action.js';
import { ActionEvent } from './ActionEvent.js';
@@ -43,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 {
@@ -464,27 +464,22 @@ describe('Action — the component', () => {
const root = await mount(`
`);
- const details: Array> = [];
- // Canceling suppresses the default sink, so the failure does not reach
- // `reportError()` and fail the run.
- const listener = (event: Event) => {
- details.push((event as CustomEvent>).detail);
- event.preventDefault();
- };
- document.addEventListener('js-toolkit:diagnostic', listener);
+ // The capture cancels every event, which suppresses the default sink, so
+ // the failure never reaches `reportError()` and fails the run.
+ const log = captureDiagnostics();
click(root.querySelector('#action') as Element);
- expect(details).toHaveLength(1);
- expect(details[0]).toMatchObject({
+ expect(log.entries).toHaveLength(1);
+ expect(log.entries[0]).toMatchObject({
severity: 'error',
code: 'action.effect-failed',
component: 'Action',
});
// The cause survives, which a bare `console.warn` never carried.
- expect(details[0].error).toBeInstanceOf(Error);
+ expect(log.entries[0].error).toBeInstanceOf(Error);
- document.removeEventListener('js-toolkit:diagnostic', listener);
+ log.stop();
});
});
diff --git a/packages/v4/migration/AnchorNav/AnchorNav.spec.ts b/packages/v4/migration/AnchorNav/AnchorNav.spec.ts
index dbf14befc..61e40606b 100644
--- a/packages/v4/migration/AnchorNav/AnchorNav.spec.ts
+++ b/packages/v4/migration/AnchorNav/AnchorNav.spec.ts
@@ -25,7 +25,7 @@ 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',
)!;
@@ -41,7 +41,7 @@ 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',
)!;
@@ -66,7 +66,7 @@ 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;
diff --git a/packages/v4/migration/AnchorNav/AnchorNavLink.spec.ts b/packages/v4/migration/AnchorNav/AnchorNavLink.spec.ts
index fbfbee046..08a0f8cb1 100644
--- a/packages/v4/migration/AnchorNav/AnchorNavLink.spec.ts
+++ b/packages/v4/migration/AnchorNav/AnchorNavLink.spec.ts
@@ -23,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/AnchorNav/AnchorNavTarget.spec.ts b/packages/v4/migration/AnchorNav/AnchorNavTarget.spec.ts
index f52a39b32..886544ac4 100644
--- a/packages/v4/migration/AnchorNav/AnchorNavTarget.spec.ts
+++ b/packages/v4/migration/AnchorNav/AnchorNavTarget.spec.ts
@@ -1,6 +1,5 @@
import { afterEach, describe, expect, it } from 'vitest';
-import { registerComponents } from '../../src/index.js';
-import { INSTANCES } from '../../src/protocol-symbols.js';
+import { getInstance, registerComponents } from '../../src/index.js';
import { resetDom, settle, waitFor } from '../../src/test/index.js';
import { AnchorNavTarget } from './AnchorNavTarget.js';
@@ -18,7 +17,7 @@ async function quiet(): Promise {
}
}
-const mountedState = (el: HTMLElement) => el[INSTANCES]?.get('AnchorNavTarget')?.$isMounted;
+const mountedState = (el: HTMLElement) => getInstance(el, 'AnchorNavTarget')?.$isMounted;
function render(style: string): HTMLElement {
const el = document.createElement('div');
diff --git a/packages/v4/migration/ClickOutside/ClickOutside.spec.ts b/packages/v4/migration/ClickOutside/ClickOutside.spec.ts
index 717867f65..8bb95eefc 100644
--- a/packages/v4/migration/ClickOutside/ClickOutside.spec.ts
+++ b/packages/v4/migration/ClickOutside/ClickOutside.spec.ts
@@ -79,7 +79,7 @@ describe('ClickOutside', () => {
outside.click();
const dropdown = getInstance(
- root.querySelector('[data-component="Dropdown"]')!,
+ root.querySelector('[data-component="Dropdown"]'),
'Dropdown',
)!;
expect(dropdown.closed).toHaveLength(1);
diff --git a/packages/v4/migration/Data/DataBind.spec.ts b/packages/v4/migration/Data/DataBind.spec.ts
index 7b71708b1..661dbc890 100644
--- a/packages/v4/migration/Data/DataBind.spec.ts
+++ b/packages/v4/migration/Data/DataBind.spec.ts
@@ -67,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', () => {
@@ -272,15 +272,9 @@ describe('DataBind — the element half', () => {
});
it('warns for a binding type that names nothing', async () => {
- const details: string[] = [];
- // Removed at the end: this listener cancels the default sink, and leaking
- // it silenced every later diagnostic in the file.
- const listener = (event: Event) => {
- const { detail } = event as CustomEvent<{ code: string; message: string }>;
- details.push(detail.code);
- event.preventDefault();
- };
- document.addEventListener(EVENTS.diagnostic, listener);
+ // Stopped at the end: the capture cancels the default sink, and leaking it
+ // silenced every later diagnostic in the file.
+ const log = captureDiagnostics();
const root = await mount(`
{
`);
// The typo used to be an attribute that silently did nothing at all.
- expect(details).toContain('attribute.unknown-qualifier');
+ expect(log.codes).toContain('attribute.unknown-qualifier');
expect(at
(root, '#d', 'DataBind').hasVirtualBindings).toBe(false);
- document.removeEventListener(EVENTS.diagnostic, listener);
+ log.stop();
});
it('fails quietly when a virtual expression throws', async () => {
diff --git a/packages/v4/migration/Data/DataDerived.spec.ts b/packages/v4/migration/Data/DataDerived.spec.ts
index 3f95defc3..8940d2ed9 100644
--- a/packages/v4/migration/Data/DataDerived.spec.ts
+++ b/packages/v4/migration/Data/DataDerived.spec.ts
@@ -30,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 a4e3ee1c2..304d0ea53 100644
--- a/packages/v4/migration/Data/DataScope.spec.ts
+++ b/packages/v4/migration/Data/DataScope.spec.ts
@@ -27,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 {
diff --git a/packages/v4/migration/Fetch/FetchShopifyPartial.spec.ts b/packages/v4/migration/Fetch/FetchShopifyPartial.spec.ts
index 7717fc765..78e5b6ed0 100644
--- a/packages/v4/migration/Fetch/FetchShopifyPartial.spec.ts
+++ b/packages/v4/migration/Fetch/FetchShopifyPartial.spec.ts
@@ -1,6 +1,6 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { getInstance, registerComponents } from '../../src/index.js';
-import { recordEvents, resetDom, settle } from '../../src/test/index.js';
+import { mount, recordEvents, resetDom, settle } from '../../src/test/index.js';
import { FETCH_EVENTS } from './Fetch.js';
import { FetchShopifyPartial } from './FetchShopifyPartial.js';
@@ -27,13 +27,15 @@ afterEach(async () => {
await resetDom();
});
-async function mount(html: string): Promise<{ root: HTMLElement; instance: FetchShopifyPartial }> {
- const root = document.createElement('div');
- root.innerHTML = html;
- document.body.append(root);
- await settle();
- const el = root.firstElementChild as HTMLElement;
- return { root, instance: getInstance(el, 'FetchShopifyPartial')! };
+/** {@link mount}, plus the one instance every test here goes on to drive. */
+async function mountWithInstance(
+ html: string,
+): Promise<{ root: HTMLElement; instance: FetchShopifyPartial }> {
+ const root = await mount(html);
+ return {
+ root,
+ instance: getInstance(root.firstElementChild, 'FetchShopifyPartial')!,
+ };
}
function stubClient(
@@ -54,7 +56,7 @@ function stubPartials(api: {
describe('FetchShopifyPartial', () => {
it('falls back to the base Fetch behaviour when no partials are configured', async () => {
const client = stubClient();
- const { root, instance } = await mount(
+ const { root, instance } = await mountWithInstance(
`old
`,
);
const { events } = recordEvents(root, ...Object.values(FETCH_EVENTS));
@@ -71,7 +73,7 @@ describe('FetchShopifyPartial', () => {
const apply = vi.fn();
const fetchPartials = vi.fn(async () => ({ shape: 'partial-update' }));
stubPartials({ fetch: fetchPartials, apply });
- const { root, instance } = await mount(
+ const { root, instance } = await mountWithInstance(
``,
);
const { events } = recordEvents(root, ...Object.values(FETCH_EVENTS));
@@ -106,7 +108,7 @@ describe('FetchShopifyPartial', () => {
FetchShopifyPartial.loadPartialsModule = async () => {
throw new Error('not installed');
};
- const { instance } = await mount(
+ const { instance } = await mountWithInstance(
`old
`,
);
@@ -120,7 +122,7 @@ describe('FetchShopifyPartial', () => {
const client = stubClient();
const fetchPartials = vi.fn(async () => ({}));
stubPartials({ fetch: fetchPartials, apply: vi.fn() });
- const { instance } = await mount(
+ const { instance } = await mountWithInstance(
`old
`,
);
@@ -135,7 +137,7 @@ describe('FetchShopifyPartial', () => {
const client = stubClient();
const fetchPartials = vi.fn(async () => ({}));
stubPartials({ fetch: fetchPartials, apply: vi.fn() });
- const { instance } = await mount(
+ const { instance } = await mountWithInstance(
`old
`,
);
@@ -150,7 +152,7 @@ describe('FetchShopifyPartial', () => {
const client = stubClient();
const fetchPartials = vi.fn(async () => ({}));
stubPartials({ fetch: fetchPartials, apply: vi.fn() });
- const { instance } = await mount(
+ const { instance } = await mountWithInstance(
`old
`,
);
@@ -166,7 +168,7 @@ describe('FetchShopifyPartial', () => {
const client = stubClient();
const fetchPartials = vi.fn(async () => ({}));
stubPartials({ fetch: fetchPartials, apply: vi.fn() });
- const { instance } = await mount(
+ const { instance } = await mountWithInstance(
`old
`,
);
@@ -181,7 +183,7 @@ describe('FetchShopifyPartial', () => {
const client = stubClient();
const fetchPartials = vi.fn(async () => ({}));
stubPartials({ fetch: fetchPartials, apply: vi.fn() });
- const { instance } = await mount(
+ const { instance } = await mountWithInstance(
``,
);
@@ -199,7 +201,7 @@ describe('FetchShopifyPartial', () => {
fetch: async () => ({}),
apply: () => Promise.reject(failure),
});
- const { root, instance } = await mount(
+ const { root, instance } = await mountWithInstance(
``,
);
const errors: unknown[] = [];
@@ -215,7 +217,7 @@ describe('FetchShopifyPartial', () => {
it('skips the history push for a popstate header given as a Headers instance', async () => {
stubPartials({ fetch: async () => ({}), apply: vi.fn() });
- const { instance } = await mount(
+ const { instance } = await mountWithInstance(
``,
);
const before = window.history.length;
@@ -232,7 +234,7 @@ describe('FetchShopifyPartial', () => {
it('still pushes history for a request that is not popstate-triggered', async () => {
stubPartials({ fetch: async () => ({}), apply: vi.fn() });
- const { instance } = await mount(
+ const { instance } = await mountWithInstance(
``,
);
const before = window.history.length;
@@ -248,7 +250,7 @@ describe('FetchShopifyPartial', () => {
partials: { fetch: vi.fn(async () => ({})), apply: vi.fn() },
}));
FetchShopifyPartial.loadPartialsModule = loadSpy;
- const { instance } = await mount(
+ const { instance } = await mountWithInstance(
``,
);
diff --git a/packages/v4/migration/Figure/FigureShopify.spec.ts b/packages/v4/migration/Figure/FigureShopify.spec.ts
index 0d9766c0b..9233ce424 100644
--- a/packages/v4/migration/Figure/FigureShopify.spec.ts
+++ b/packages/v4/migration/Figure/FigureShopify.spec.ts
@@ -19,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 c365a33b4..ccdc9ecaf 100644
--- a/packages/v4/migration/Figure/FigureTwicpics.spec.ts
+++ b/packages/v4/migration/Figure/FigureTwicpics.spec.ts
@@ -20,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 55d5e6735..fe7f29b68 100644
--- a/packages/v4/migration/FigureVideo/FigureVideo.spec.ts
+++ b/packages/v4/migration/FigureVideo/FigureVideo.spec.ts
@@ -100,24 +100,18 @@ describe('FigureVideo', () => {
it('settles and reports when the sources fail, instead of hanging forever', async () => {
const { el, video } = render(ONSCREEN);
- const details: Array> = [];
- const listener = (event: Event) => {
- details.push((event as CustomEvent>).detail);
- event.preventDefault();
- };
- document.addEventListener('js-toolkit:diagnostic', listener);
+ const log = captureDiagnostics();
await settle();
// v3 waits on `loadeddata` alone, so this never settled and `mounted()`
// never returned.
video.dispatchEvent(new Event('error'));
- await waitFor(() => details.length > 0);
+ await waitFor(() => log.codes.includes('figure-video.load-failed'));
- 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);
- document.removeEventListener('js-toolkit:diagnostic', listener);
+ log.stop();
});
it('warns and does not throw when the video ref is missing', async () => {
diff --git a/packages/v4/migration/FigureVideo/FigureVideoTwicpics.spec.ts b/packages/v4/migration/FigureVideo/FigureVideoTwicpics.spec.ts
index 060300f40..b035765c2 100644
--- a/packages/v4/migration/FigureVideo/FigureVideoTwicpics.spec.ts
+++ b/packages/v4/migration/FigureVideo/FigureVideoTwicpics.spec.ts
@@ -1,6 +1,6 @@
import { afterEach, describe, expect, it } from 'vitest';
import { getInstance, registerComponents } from '../../src/index.js';
-import { resetDom, settle } from '../../src/test/index.js';
+import { captureDiagnostics, resetDom, settle, waitFor } from '../../src/test/index.js';
import { FigureVideoTwicpics } from './FigureVideoTwicpics.js';
registerComponents(FigureVideoTwicpics);
@@ -20,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', () => {
@@ -33,12 +33,7 @@ describe('FigureVideoTwicpics — the loadSources override', () => {
`;
document.body.append(root);
- const details: Array> = [];
- const listener = (event: Event) => {
- details.push((event as CustomEvent>).detail);
- event.preventDefault();
- };
- document.addEventListener('js-toolkit:diagnostic', listener);
+ const log = captureDiagnostics();
await settle();
const el = root.firstElementChild as HTMLElement;
@@ -46,14 +41,11 @@ describe('FigureVideoTwicpics — the loadSources override', () => {
// This override replaces the base entirely, so fixing the base alone left
// it waiting on `canplaythrough` forever.
video.dispatchEvent(new Event('error'));
- for (let i = 0; i < 6; i += 1) {
- await settle();
- }
+ await waitFor(() => log.codes.includes('figure-video.load-failed'));
- expect(details.map((detail) => detail.code)).toContain('figure-video.load-failed');
expect(getInstance(el, 'FigureVideoTwicpics')!.hasLoaded).toBe(false);
- document.removeEventListener('js-toolkit:diagnostic', listener);
+ log.stop();
});
});
diff --git a/packages/v4/migration/InView/InView.spec.ts b/packages/v4/migration/InView/InView.spec.ts
index 92c99af5c..09844a615 100644
--- a/packages/v4/migration/InView/InView.spec.ts
+++ b/packages/v4/migration/InView/InView.spec.ts
@@ -1,6 +1,5 @@
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
-import { Base, registerComponents, type BaseConfig } from '../../src/index.js';
-import { INSTANCES } from '../../src/protocol-symbols.js';
+import { Base, getInstance, registerComponents, type BaseConfig } from '../../src/index.js';
import { recordEvents, resetDom, settle, waitFor } from '../../src/test/index.js';
import { InView } from './InView.js';
import { InViewOnce } from './InViewOnce.js';
@@ -79,7 +78,7 @@ describe('InView', () => {
it('re-emits `in-view` on each re-entry, from the same instance', async () => {
const el = render('InView', ONSCREEN);
await waitFor(() => log.events.length > 0);
- const instance = el[INSTANCES]?.get('InView');
+ const instance = getInstance(el, 'InView');
el.setAttribute('style', OFFSCREEN);
await waitFor(() => log.events.length > 1);
@@ -87,14 +86,14 @@ describe('InView', () => {
await waitFor(() => log.events.length > 2);
expect(types()).toEqual(['in-view', 'out-of-view', 'in-view']);
- expect(el[INSTANCES]?.get('InView')).toBe(instance);
+ expect(getInstance(el, 'InView')).toBe(instance);
});
it('does not instantiate the component until it is first seen', async () => {
const el = render('InView', OFFSCREEN);
await quiet();
- expect(el[INSTANCES]?.get('InView')).toBeUndefined();
+ expect(getInstance(el, 'InView')).toBeUndefined();
});
});
@@ -123,7 +122,7 @@ describe('InViewOnce', () => {
it('stays mounted after leaving the viewport, where v3 terminated', async () => {
const el = render('InViewOnce', ONSCREEN);
- const instance = await waitFor(() => el[INSTANCES]?.get('InViewOnce'));
+ const instance = await waitFor(() => getInstance(el, 'InViewOnce'));
el.setAttribute('style', OFFSCREEN);
await quiet();
@@ -153,7 +152,7 @@ describe('mount strategy gaps found by the port', () => {
await waitFor(() => log.events.length > 0);
expect(types()).toEqual(['in-view']);
- expect(el[INSTANCES]?.get('InView')?.$isMounted).toBe(true);
+ expect(getInstance(el, 'InView')?.$isMounted).toBe(true);
});
/** Subclasses must inherit the resolved mount strategy. */
@@ -161,7 +160,7 @@ describe('mount strategy gaps found by the port', () => {
const el = render('InViewSubclass', OFFSCREEN);
await quiet();
- expect(el[INSTANCES]?.get('InViewSubclass')).toBeUndefined();
+ expect(getInstance(el, 'InViewSubclass')).toBeUndefined();
});
});
@@ -177,19 +176,19 @@ describe('the strategy is per element, which the decorator never was', () => {
it('lets `data-mount` override the class default on one element', async () => {
const el = render('InView', OFFSCREEN, { 'data-mount': 'eager' });
- await waitFor(() => el[INSTANCES]?.get('InView')?.$isMounted);
+ await waitFor(() => getInstance(el, 'InView')?.$isMounted);
- expect(el[INSTANCES]?.get('InView')?.$isMounted).toBe(true);
+ expect(getInstance(el, 'InView')?.$isMounted).toBe(true);
expect(types()).toEqual(['in-view']);
});
it('lets `data-mount="in-view"` give the strategy to a component that never asked', async () => {
const el = render('InViewEagerProbe', OFFSCREEN, { 'data-mount': 'in-view' });
await quiet();
- expect(el[INSTANCES]?.get('InViewEagerProbe')).toBeUndefined();
+ expect(getInstance(el, 'InViewEagerProbe')).toBeUndefined();
el.setAttribute('style', ONSCREEN);
- await waitFor(() => el[INSTANCES]?.get('InViewEagerProbe'));
- expect((el[INSTANCES]?.get('InViewEagerProbe') as Eager | undefined)?.mounts).toBe(1);
+ await waitFor(() => getInstance(el, 'InViewEagerProbe'));
+ expect((getInstance(el, 'InViewEagerProbe') as Eager | undefined)?.mounts).toBe(1);
});
});
diff --git a/packages/v4/migration/Menu/Menu.spec.ts b/packages/v4/migration/Menu/Menu.spec.ts
index 521e5424d..c060fba1b 100644
--- a/packages/v4/migration/Menu/Menu.spec.ts
+++ b/packages/v4/migration/Menu/Menu.spec.ts
@@ -28,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
`,
);
await settle();
- const label = getInstance