Skip to content
23 changes: 9 additions & 14 deletions packages/v4/migration/Action/Action.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -43,7 +43,7 @@ registerComponents(Action, Target, Foo, Bar, Dialog, MountProbe);
afterEach(resetDom);

function at<T extends Base>(root: ParentNode, selector: string, name: string): T {
return getInstance<T>(root.querySelector(selector)!, name)!;
return getInstance<T>(root.querySelector(selector), name)!;
}

function click(el: Element): Event {
Expand Down Expand Up @@ -464,27 +464,22 @@ describe('Action — the component', () => {
const root = await mount(`
<div id="action" data-component="Action" data-on:click="() => consol.log()"></div>
`);
const details: Array<Record<string, unknown>> = [];
// 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<Record<string, unknown>>).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();
});
});

Expand Down
6 changes: 3 additions & 3 deletions packages/v4/migration/AnchorNav/AnchorNav.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<AnchorNavLink>(
root.querySelector('[data-component="AnchorNavLink"]')!,
root.querySelector('[data-component="AnchorNavLink"]'),
'AnchorNavLink',
)!;

Expand All @@ -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<AnchorNavLink>(
root.querySelector('[data-component="AnchorNavLink"]')!,
root.querySelector('[data-component="AnchorNavLink"]'),
'AnchorNavLink',
)!;

Expand All @@ -66,7 +66,7 @@ describe('AnchorNav', () => {
document.body.append(root);
await settle();
const link = getInstance<AnchorNavLink>(
root.querySelector('[data-component="AnchorNavLink"]')!,
root.querySelector('[data-component="AnchorNavLink"]'),
'AnchorNavLink',
)!;
const target = root.querySelector('#one') as HTMLElement;
Expand Down
2 changes: 1 addition & 1 deletion packages/v4/migration/AnchorNav/AnchorNavLink.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ async function render(): Promise<AnchorNavLink> {
root.innerHTML = `<a data-component="AnchorNavLink" href="#section-one" ${OPTIONS_ATTRS}></a>`;
document.body.append(root);
await settle();
return getInstance<AnchorNavLink>(root.firstElementChild!, 'AnchorNavLink')!;
return getInstance<AnchorNavLink>(root.firstElementChild, 'AnchorNavLink')!;
}

describe('AnchorNavLink', () => {
Expand Down
5 changes: 2 additions & 3 deletions packages/v4/migration/AnchorNav/AnchorNavTarget.spec.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -18,7 +17,7 @@ async function quiet(): Promise<void> {
}
}

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');
Expand Down
2 changes: 1 addition & 1 deletion packages/v4/migration/ClickOutside/ClickOutside.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ describe('ClickOutside', () => {
outside.click();

const dropdown = getInstance<Dropdown>(
root.querySelector('[data-component="Dropdown"]')!,
root.querySelector('[data-component="Dropdown"]'),
'Dropdown',
)!;
expect(dropdown.closed).toHaveLength(1);
Expand Down
18 changes: 6 additions & 12 deletions packages/v4/migration/Data/DataBind.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ function el<T extends HTMLElement = HTMLElement>(root: HTMLElement, selector: st
}

function at<T>(root: HTMLElement, selector: string, name: string): T {
return getInstance<never>(root.querySelector(selector)!, name)! as T;
return getInstance<never>(root.querySelector(selector), name)! as T;
}

describe('DataBind — the element half', () => {
Expand Down Expand Up @@ -272,26 +272,20 @@ 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(`
<div id="d" data-component="DataBind" data-option-group="${uniqueGroup('v')}"
data-bind:txet="value"></div>
`);

// 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<DataBind>(root, '#d', 'DataBind').hasVirtualBindings).toBe(false);

document.removeEventListener(EVENTS.diagnostic, listener);
log.stop();
});

it('fails quietly when a virtual expression throws', async () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/v4/migration/Data/DataDerived.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ function el<T extends HTMLElement = HTMLElement>(root: HTMLElement, selector: st
}

function at<T>(root: HTMLElement, selector: string, name: string): T {
return getInstance<never>(root.querySelector(selector)!, name)! as T;
return getInstance<never>(root.querySelector(selector), name)! as T;
}

describe('DataModel', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/v4/migration/Data/DataScope.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ function uniqueGroup(name: string): string {
}

function at<T>(root: HTMLElement, selector: string, name: string): T {
return getInstance<never>(root.querySelector(selector)!, name)! as T;
return getInstance<never>(root.querySelector(selector), name)! as T;
}

function el<T extends HTMLElement = HTMLElement>(root: HTMLElement, selector: string): T {
Expand Down
42 changes: 22 additions & 20 deletions packages/v4/migration/Fetch/FetchShopifyPartial.spec.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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<FetchShopifyPartial>(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<FetchShopifyPartial>(root.firstElementChild, 'FetchShopifyPartial')!,
};
}

function stubClient(
Expand All @@ -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(
`<a data-component="FetchShopifyPartial" href="/page" id="a"><div id="a">old</div></a>`,
);
const { events } = recordEvents(root, ...Object.values(FETCH_EVENTS));
Expand All @@ -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(
`<a data-component="FetchShopifyPartial" href="/page" data-option-partials="main, header"></a>`,
);
const { events } = recordEvents(root, ...Object.values(FETCH_EVENTS));
Expand Down Expand Up @@ -106,7 +108,7 @@ describe('FetchShopifyPartial', () => {
FetchShopifyPartial.loadPartialsModule = async () => {
throw new Error('not installed');
};
const { instance } = await mount(
const { instance } = await mountWithInstance(
`<a data-component="FetchShopifyPartial" href="/page" data-option-partials="main"><div id="a">old</div></a>`,
);

Expand All @@ -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(
`<a data-component="FetchShopifyPartial" href="/page" data-option-partials="main"><div id="a">old</div></a>`,
);

Expand All @@ -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(
`<a data-component="FetchShopifyPartial" href="/page" data-option-partials="main"><div id="a">old</div></a>`,
);

Expand All @@ -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(
`<a data-component="FetchShopifyPartial" href="/page" data-option-partials="main"><div id="a">old</div></a>`,
);

Expand All @@ -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(
`<a data-component="FetchShopifyPartial" href="/page" data-option-partials="main"><div id="a">old</div></a>`,
);

Expand All @@ -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(
`<a data-component="FetchShopifyPartial" href="/page" data-option-partials="main"></a>`,
);

Expand All @@ -199,7 +201,7 @@ describe('FetchShopifyPartial', () => {
fetch: async () => ({}),
apply: () => Promise.reject(failure),
});
const { root, instance } = await mount(
const { root, instance } = await mountWithInstance(
`<a data-component="FetchShopifyPartial" href="/page" data-option-partials="main"></a>`,
);
const errors: unknown[] = [];
Expand All @@ -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(
`<a data-component="FetchShopifyPartial" href="/page" data-option-partials="main" data-option-history></a>`,
);
const before = window.history.length;
Expand All @@ -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(
`<a data-component="FetchShopifyPartial" href="/page" data-option-partials="main" data-option-history></a>`,
);
const before = window.history.length;
Expand All @@ -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(
`<a data-component="FetchShopifyPartial" href="/page" data-option-partials="main"></a>`,
);

Expand Down
2 changes: 1 addition & 1 deletion packages/v4/migration/Figure/FigureShopify.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ async function render(attributes = ''): Promise<FigureShopify> {
</div>`;
document.body.append(root);
await settle();
return getInstance<FigureShopify>(root.firstElementChild!, 'FigureShopify')!;
return getInstance<FigureShopify>(root.firstElementChild, 'FigureShopify')!;
}

describe('FigureShopify', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/v4/migration/Figure/FigureTwicpics.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ async function render(
</div>`;
document.body.append(root);
await settle();
return getInstance<FigureTwicpics>(root.firstElementChild!, 'FigureTwicpics')!;
return getInstance<FigureTwicpics>(root.firstElementChild, 'FigureTwicpics')!;
}

describe('FigureTwicpics', () => {
Expand Down
12 changes: 3 additions & 9 deletions packages/v4/migration/FigureVideo/FigureVideo.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Record<string, unknown>> = [];
const listener = (event: Event) => {
details.push((event as CustomEvent<Record<string, unknown>>).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<FigureVideo>(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 () => {
Expand Down
Loading
Loading