VueLynx Testing Library
The vue-lynx-testing-library package offers APIs like render, fireEvent, and getByText for testing Vue Lynx components, similar to Vue Test Utils and React Testing Library, with the dual-threaded architecture abstracted through @lynx-js/testing-environment.
Setup
From create-vue-lynx
Using create-vue-lynx sets up VueLynx Testing Library automatically, providing pre-configured testing support.
Adding to an existing project
Install the required dependencies:
npm add -D vitest jsdom @lynx-js/testing-environment @testing-library/dom @testing-library/jest-dom
yarn add -D vitest jsdom @lynx-js/testing-environment @testing-library/dom @testing-library/jest-dom
pnpm add -D vitest jsdom @lynx-js/testing-environment @testing-library/dom @testing-library/jest-dom
bun add -D vitest jsdom @lynx-js/testing-environment @testing-library/dom @testing-library/jest-dom
deno add -D npm:vitest npm:jsdom npm:@lynx-js/testing-environment npm:@testing-library/dom npm:@testing-library/jest-dom
For Vitest configuration, create a vitest.config.ts that aliases vue-lynx subpaths and registers the setup file:
vitest.config.ts
import { defineConfig } from 'vitest/config';
import path from 'node:path';
export default defineConfig({
test: {
environment: 'jsdom',
globals: true,
setupFiles: [path.resolve(__dirname, 'test/setup.ts')],
include: ['test/**/*.test.ts'],
alias: [
{
find: 'vue-lynx/entry-background',
replacement: path.resolve(
__dirname,
'node_modules/vue-lynx/runtime/dist/entry-background.js',
),
},
{
find: 'vue-lynx/main-thread',
replacement: path.resolve(
__dirname,
'node_modules/vue-lynx/main-thread/dist/entry-main.js',
),
},
{
find: 'vue-lynx/internal/ops',
replacement: path.resolve(
__dirname,
'node_modules/vue-lynx/internal/dist/ops.js',
),
},
{
find: /^vue-lynx$/,
replacement: path.resolve(
__dirname,
'node_modules/vue-lynx/runtime/dist/index.js',
),
},
],
},
});
Create a setup file that initializes the dual-thread testing environment. This runs before any test module is imported, so all Lynx globals are in place when Vue's runtime loads:
test/setup.ts
import { JSDOM } from 'jsdom';
import { LynxTestingEnv } from '@lynx-js/testing-environment';
// Create the testing environment
const jsdom = new JSDOM('<!DOCTYPE html><html><body></body></html>');
const lynxTestingEnv = new LynxTestingEnv(jsdom);
(globalThis as any).lynxTestingEnv = lynxTestingEnv;
// Wire Main Thread globals
lynxTestingEnv.switchToMainThread();
if (typeof (globalThis as any).registerWorkletInternal === 'undefined') {
(globalThis as any).registerWorkletInternal = () => {};
}
await import('vue-lynx/main-thread');
const mainThreadFns = {
renderPage: (globalThis as any).renderPage,
vuePatchUpdate: (globalThis as any).vuePatchUpdate,
processData: (globalThis as any).processData,
updatePage: (globalThis as any).updatePage,
updateGlobalProps: (globalThis as any).updateGlobalProps,
};
const mtGlobal = lynxTestingEnv.mainThread.globalThis as any;
Object.assign(mtGlobal, mainThreadFns);
// Wire Background Thread globals
lynxTestingEnv.switchToBackgroundThread();
await import('vue-lynx/entry-background');
const publishEventFn = (globalThis as any).publishEvent;
const bgGlobal = lynxTestingEnv.backgroundThread.globalThis as any;
bgGlobal.publishEvent = publishEventFn;
// Re-wire globals after env resets between tests
(globalThis as any).onSwitchedToMainThread = () => {
Object.assign(globalThis, mainThreadFns);
};
(globalThis as any).onSwitchedToBackgroundThread = () => {
if ((globalThis as any).lynxCoreInject?.tt) {
(globalThis as any).lynxCoreInject.tt.publishEvent = publishEventFn;
}
(globalThis as any).publishEvent = publishEventFn;
};
Examples
Quick Start
Follow the Arrange-Act-Assert pattern: prepare test data, perform operations, then assert results:
import { expect, it, vi } from 'vitest';
import { h, defineComponent } from 'vue-lynx';
import { render, fireEvent } from 'vue-lynx-testing-library';
it('basic', async () => {
const onClick = vi.fn();
const Button = defineComponent({
props: { onClick: Function },
setup(props, { slots }) {
return () => h('view', { bindtap: props.onClick }, slots.default?.());
},
});
// ARRANGE
const { container } = render(
defineComponent({
setup() {
return () =>
h(Button, { onClick }, () => [
h('text', null, 'Click me'),
]);
},
}),
);
expect(onClick).not.toHaveBeenCalled();
// ACT
fireEvent.tap(container.querySelector('view')!);
// ASSERT
expect(onClick).toBeCalledTimes(1);
expect(container.querySelector('text')!.textContent).toBe('Click me');
});
VueLynx Testing Library uses JSDOM to implement Element PAPI, so you can query rendered elements with container.querySelector() and assert their textContent.
Tip
When passing slot content to a component with h(), wrap children in a function: h(Comp, props, () => [children]). This follows Vue 3's recommended function slot pattern.
Basic rendering
The render method returns a RenderResult object with a container field containing the rendered result:
import { expect, it } from 'vitest';
import { h, defineComponent } from 'vue-lynx';
import { render } from 'vue-lynx-testing-library';
it('basic render', () => {
const Comp = defineComponent({
render() {
return h('view', { id: 'inner', style: { backgroundColor: 'yellow' } });
},
});
const { container } = render(Comp);
expect(container.querySelector('#inner')).not.toBeNull();
expect(container.querySelector('view')).not.toBeNull();
});
You can also use the @testing-library/dom queries bound to the container:
const { getByText } = render(Comp);
expect(getByText('Hello')).not.toBeNull();
Firing events
When using fireEvent, the event type is determined by the handler property name on the element. The format follows eventType:eventName (e.g., catchEvent:tap triggers a catch-type tap event). Event handler properties determine the event type:
You can construct Event objects directly or use the named helpers for automatic construction:
import { h, defineComponent } from 'vue-lynx';
import { render, fireEvent } from 'vue-lynx-testing-library';
import { vi, expect, it } from 'vitest';
it('fireEvent', async () => {
const handler = vi.fn();
const Comp = defineComponent({
setup() {
return () => h('view', null, [h('text', { catchtap: handler })]);
},
});
const { container } = render(Comp);
const textEl = container.querySelector('text')!;
expect(handler).toHaveBeenCalledTimes(0);
// Method 1: Use the named helper
fireEvent.tap(textEl, {
eventType: 'catchEvent',
key: 'value',
});
expect(handler).toHaveBeenCalledTimes(1);
// Method 2: Construct the Event object yourself
const event = new Event('catchEvent:tap');
Object.assign(event, {
eventType: 'catchEvent',
eventName: 'tap',
key: 'value2',
});
fireEvent(textEl, event);
expect(handler).toHaveBeenCalledTimes(2);
});
The following event helpers are available:
Testing reactivity
After changing reactive state, use await nextTick(); await nextTick() to wait for Vue's scheduler to flush and for the ops to be applied on the main thread. The convenience helper waitForUpdate() wraps both calls:
import { h, defineComponent, ref } from 'vue-lynx';
import { render, waitForUpdate } from 'vue-lynx-testing-library';
import { expect, it } from 'vitest';
it('updates text when ref changes', async () => {
const count = ref(0);
const Comp = defineComponent({
setup() {
return () => h('text', null, `Count: ${count.value}`);
},
});
const { container } = render(Comp);
expect(container.querySelector('text')!.textContent).toBe('Count: 0');
count.value = 42;
await waitForUpdate();
expect(container.querySelector('text')!.textContent).toBe('Count: 42');
});
Testing template refs
Vue Lynx sets a vue-ref-{id} attribute on elements, which can be used to verify ref assignment. The ShadowElement (background thread representation) provides NodesRef methods like invoke, setNativeProps, and animate:
import { h, defineComponent, ShadowElement } from 'vue-lynx';
import { render } from 'vue-lynx-testing-library';
import { expect, it } from 'vitest';
it('element ref', () => {
const Comp = defineComponent({
render() {
return h('view', null, [h('text', null, 'hello')]);
},
});
const { container } = render(Comp);
const view = container.querySelector('view')!;
// Vue Lynx sets vue-ref-{id} attribute for elements
expect(view.hasAttribute('vue-ref-2')).toBe(true);
});
it('ShadowElement has NodesRef methods', () => {
const el = new ShadowElement('view');
expect(typeof el.invoke).toBe('function');
expect(typeof el.setNativeProps).toBe('function');
expect(typeof el.animate).toBe('function');
});
Querying page elements
The render method returns @testing-library/dom query methods bound to the container, such as getByText:
import { h, defineComponent, ref } from 'vue-lynx';
import { render, waitForUpdate } from 'vue-lynx-testing-library';
import { expect, it } from 'vitest';
it('queries rendered elements', async () => {
const loaded = ref(false);
const Comp = defineComponent({
setup() {
return () =>
loaded.value
? h('text', { id: 'message' }, 'Hello World')
: h('text', null, 'Loading...');
},
});
const { container, getByText } = render(Comp);
expect(getByText('Loading...')).not.toBeNull();
loaded.value = true;
await waitForUpdate();
expect(container.querySelector('#message')!.textContent).toBe('Hello World');
});
Rerendering
The render method returns an object with a rerender method for testing different component states:
import { h, defineComponent } from 'vue-lynx';
import { render } from 'vue-lynx-testing-library';
import { expect, it } from 'vitest';
it('rerender will re-render your component', () => {
const Greeting = defineComponent({
props: { message: String },
render() {
return h('text', null, this.message);
},
});
const { container, rerender } = render(Greeting, { message: 'hi' });
expect(container.querySelector('text')!.textContent).toBe('hi');
{
const { container } = rerender(Greeting, { message: 'hey' });
expect(container.querySelector('text')!.textContent).toBe('hey');
}
});
Testing list
The list element renders list-item children. Items are described declaratively and the native list handles lazy loading:
import { h, defineComponent, ref } from 'vue-lynx';
import { render, waitForUpdate } from 'vue-lynx-testing-library';
import { expect, it } from 'vitest';
it('list', async () => {
const items = ref([0, 1, 2]);
const Comp = defineComponent({
setup() {
return () =>
h(
'list',
null,
items.value.map((item) =>
h('list-item', { key: item, 'item-key': item }, [
h('text', null, `${item}`),
]),
),
);
},
});
const { container } = render(Comp);
const list = container.querySelector('list');
expect(list).not.toBeNull();
// Add items reactively
items.value = [0, 1, 2, 3];
await waitForUpdate();
expect(container.querySelector('list')).not.toBeNull();
});
Testing Main Thread Script
Main Thread Script testing requires no special configuration. Note that background thread methods cannot be called directly from main thread scripts; place callback functions on globalThis for assertion:
import { h, defineComponent } from 'vue-lynx';
import { render } from 'vue-lynx-testing-library';
import { expect, it } from 'vitest';
it('main thread script', () => {
const Comp = defineComponent({
setup() {
return () =>
h('view', {
'main-thread-bindtap': {
_wkltId: 1,
_closure: {},
},
}, [
h('text', null, 'Hello Main Thread Script'),
]);
},
});
const { container } = render(Comp);
expect(container.querySelector('text')!.textContent).toBe('Hello Main Thread Script');
});
Info
In production, 'main thread' directive functions are transformed by the build plugin into worklet context objects. In tests, you can pass the worklet context directly as shown above, or use the SWC transform if your Vitest config includes the worklet loader.
More usage
For additional examples, see the test cases in the vue-lynx testing-library source code.
API Reference
See the full vue-lynx/testing-library API Reference for details on render, fireEvent, cleanup, waitForUpdate, and other exports.