Skip to content

test: src/runtime/plugins/ is outside the vitest include patterns — the #301 crash could not have been caught #313

Description

@IgorShevchik

Follow-up from the review of #306. Not a regression — a structural gap surfaced by #301.

The gap

src/runtime/plugins/ contains three files — colors.ts, platform.ts, ui-version.ts — and none of them can be reached by the test runner. Both vitest projects list only three directories:

// vitest.config.ts:30-33 — project 'nuxt'
include: [
  'components/**/**.spec.ts',
  'composables/**.spec.ts',
  'utils/**/**.spec.ts'
]
// vitest.config.ts:53-56 — project 'vue'
include: [
  'components/**.spec.ts',
  'composables/**.spec.ts',
  'utils/**/**.spec.ts'
]

There is no plugins/** entry in either, and test/ contains no plugin spec at all.

This matters because the crash in #301 — injectHead(...).hooks.hookOnce is not a function, which broke app startup for every SPA user on Nuxt >= 4.5.1 — lived in src/runtime/plugins/colors.ts. The suite was green throughout: it never had the chance to fail.

Adding plugins/** to include is necessary but not sufficient

The vulnerable branch is gated on runtime state that component tests never reproduce:

// src/runtime/plugins/colors.ts:65
if (import.meta.client && nuxtApp.isHydrating && !nuxtApp.payload.serverRendered) {

A plain mountSuspended never enters it — which is corroborated by the fact that the full suite stayed green while the published package was crashing. So a spec has to either:

  • set nuxtApp.isHydrating = true and nuxtApp.payload.serverRendered = false explicitly before invoking the plugin — the repo already has this pattern, test/components/nuxt/LinkLocale.spec.ts:11-18 hand-patches nuxtApp.$localePath; or
  • extract the SPA branch of colors.ts into a named exported function that can be called directly with a mock head / nuxtApp, without booting Nuxt.

The second option is cleaner and makes the branch testable without fighting the runtime.

Cheap first step, independent of the above

A contract test pinning the shape this code depends on does not need any vitest.config.ts change — createHead from @unhead/vue/client is already imported by test/utils/mount.ts:4, so a spec under test/utils/ runs in both projects as-is.

Worth being careful about how it is written, though: asserting typeof head.hooks.hookOnce !== 'function' pins the absence of a method, and would fail if unhead ever reintroduced it even though nothing would be broken. Better to assert the behaviour we actually rely on — that registering with hook() and self-unhooking fires exactly once and does not throw.

Also uncovered

platform.ts and ui-version.ts sit in the same blind spot, and there is no e2e layer anywhere in .github/workflows/ci.yml (only lint -> typecheck -> test -> build) that would catch a "fails on real client-only boot" class of bug from the side.

Suggested scope

  1. Add 'plugins/**/**.spec.ts' to the nuxt project include.
  2. Add a contract spec for the unhead hooks shape under test/utils/.
  3. Add a spec for the colors.ts SPA branch — refactoring the branch into an exported function first, if that turns out simpler than faking the hydration state.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions