Appearance
Testing
Lynx runs frontend tests with Vitest on the same build as the storefront. A test imports your component or store the way the theme does, so it runs against the exact code that theme ships, overrides included. Everything needed is installed with the theme's build tools: a theme or module adds no test package of its own.
Running the tests
bash
bin/magento lynx:static:test # every theme set up by lynx:theme:setup
bin/magento lynx:static:test OOCS_LynxVue # one theme
bin/magento lynx:static:test --list # the Lynx themes of this installationThe command runs the tests of each theme in turn and prints Vitest's report. A failing theme does not stop the others: at the end the command names the themes that failed and exits with code 1, so it can gate a deploy or a CI job. It takes the same --npm option and LYNX_NPM variable as lynx:static:deploy. It installs nothing: a theme not set up for the enabled modules stops with a message to run lynx:theme:setup.
✓ app/code/Vendor/Module/view/frontend/lynx/vue/test/stores/wishlistStore.test.ts (6 tests) 12ms
✓ app/code/Vendor/Module/view/frontend/lynx/vue/test/Components/WishlistButton.test.ts (5 tests) 48ms
Test Files 2 passed (2)
Tests 11 passed (11)
Tests passed in 2 theme(s).Inside a theme's lynx/ folder, npm run test runs the same tests, and npx vitest keeps watching and reruns them as you edit.
Where tests go
Tests sit in a test/ folder beside src/, in the same tree as the sources they test, named <File>.test.ts:
view/frontend/lynx/vue/
├── src/
│ ├── Components/WishlistButton.vue
│ └── stores/wishlistStore.ts
└── test/
├── Components/WishlistButton.test.ts
└── stores/wishlistStore.test.tsNothing under test/ is part of the build: it never reaches the bundle, the CSS or pub/static.
Import what you test by module name, as the theme does:
ts
import WishlistButton from 'Vendor_Module/Components/WishlistButton.vue'
import { useWishlistStore } from 'Vendor_Module/stores/wishlistStore'Tests and theme overrides
Tests follow the same file resolution as the sources. Each theme runs the tests of the files it actually builds:
| The file in the theme | The test that runs |
|---|---|
| Not overridden: the module's or the parent theme's | The module's or the parent theme's test |
| Overridden by the theme, with a test of its own | The theme's test only |
| Overridden by the theme, with no test of its own | None: the original test was written for the original file |
A theme overriding Vendor_Module/Components/WishlistButton.vue at <theme>/Vendor_Module/lynx/vue/src/Components/WishlistButton.vue places its test at <theme>/Vendor_Module/lynx/vue/test/Components/WishlistButton.test.ts. See Themes and Overrides for where theme overrides live.
Writing a component test
The build tools ship helpers that render a component the way a page does: inside Inertia, with page props, a fresh store and the store configuration your component reads.
ts
import { hydrate, mountNode } from '@oocs/lynx-vue/test'
import WishlistButton from 'Vendor_Module/Components/WishlistButton.vue'
import { describe, expect, it } from 'vitest'
const props = { product_id: 42, labels: { add: 'Add to Wish List' } }
describe('WishlistButton', () => {
it('adds the product to the wish list', async () => {
const wrapper = mountNode(WishlistButton, {
props,
page: { auth: { isLoggedIn: true } },
config: { currencyCode: 'USD' },
})
await wrapper.find('button').trigger('click')
expect(wrapper.text()).toContain('Added')
})
it('server-renders and hydrates the same markup', async () => {
const { html, warnings } = await hydrate(WishlistButton, { props })
expect(html).toContain('Add to Wish List')
expect(warnings).toEqual([])
})
})| Helper | What it does |
|---|---|
mountNode(Component, options) | Mounts the component as a layout node renders it. Returns a Vue Test Utils wrapper. |
renderSsr(Component, options) | The HTML the SSR renderer would send, rendered with window, document and the other browser-only globals removed. A component that touches them while rendering throws here instead of in production. |
hydrate(Component, options) | Renders the server HTML, hydrates it in the browser and returns every warning logged on the way, hydration mismatches included. |
The options are props (what your component provider returns), page (shared Inertia page props, as usePage() reads them), config (the store configuration read by config() from OOCS_LynxTheme/utils/storeConfig, such as priceFormat), and slots.
Between tests, everything the helpers mounted is unmounted, and cookies, local and session storage, the document, mocks and stubbed globals are reset. Stub the network with vi.stubGlobal('fetch', …) and Inertia visits with vi.spyOn(router, 'visit').
The hydrate check is worth one test per component: it catches the two mistakes that only show once SSR is on, browser-only code during render and markup that differs between server and browser. Leave it out for a component that only appears on pages Lynx never renders on the server: the cart, checkout, checkout success and failure, and the customer account pages.
Testing a store
Stores are plain modules: test their actions directly, without mounting anything.
ts
import { useWishlistStore } from 'Vendor_Module/stores/wishlistStore'
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, expect, it } from 'vitest'
beforeEach(() => setActivePinia(createPinia()))
it('counts the items', () => {
const store = useWishlistStore()
store.add(42)
expect(store.count).toBe(1)
})