Skip to content

test: add frontend test infrastructure (vitest + vue-test-utils)#50

Open
moodyjmz wants to merge 15 commits into
mainfrom
test/vitest-harness
Open

test: add frontend test infrastructure (vitest + vue-test-utils)#50
moodyjmz wants to merge 15 commits into
mainfrom
test/vitest-harness

Conversation

@moodyjmz

@moodyjmz moodyjmz commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Stands up frontend test infrastructure and covers all of current main's src/. Closes #47.

  • Harness — Vitest + @vue/test-utils + jsdom. Standalone vitest.config.ts (leaves vite.config.ts untouched to avoid conflict with feat/wopi-phase-5), shared vitest.setup.ts (Nextcloud global mocks + ResizeObserver stub), a makeNode()/makeCreator() fixture factory, and .nvmrc pinned to Node 24.
  • Behaviour-preserving refactor — extracted the pure logic out of OfficeOverview.vue into src/utils/: filterFiles (mine/shared/all + search + sort), validateFilename, and the category helpers. This makes the logic unit-testable without mounting and turns currentUid into a parameter instead of a module-scope read.
  • Coverage — every file in src/: config, officeFiles, templates, the extracted utils, FileCard, TemplateSection (including the ResizeObserver/scrollLeft jsdom gotchas), OfficeOverview rendering/wiring, and App. Includes the regression pin for the external-storage "Mine" fix ("Mine" filter shows external storage files as owned by everyone #46): an external-storage file owned by the current user is excluded from mine.
  • CI.github/workflows/test-unit.yml, same path-filtered changes/job/summary shape as the existing lint workflows, running npm run test:unit on PRs.
  • Dev-workflow guard — a pre-commit hook (no husky; wired via an npm prepare script) that blocks hand-committed js//css/ assets, plus a README note documenting the source-only / /compile convention.

Notes for reviewers

  • npm-build will go red, and that's expected — this is a source-only PR; the built assets come from the /compile bot, per the README "Committing changes" section added here.

🤖 Generated with Claude Code

@moodyjmz moodyjmz force-pushed the test/vitest-harness branch from d73ec83 to 888a656 Compare July 3, 2026 21:41
@moodyjmz moodyjmz self-assigned this Jul 3, 2026
moodyjmz and others added 10 commits July 8, 2026 23:45
.nvmrc said 20, package.json's engines.node already said ^24.0.0. The
mismatch surfaced while setting up the Vitest harness: Node 25's native
localStorage global silently breaks jsdom's implementation under Vitest,
Node 24 doesn't have that problem.

Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
vitest + @vue/test-utils + jsdom, sharing the app's Vite/Vue transform
pipeline via @vitejs/plugin-vue. Shared mocks for the four Nextcloud
runtime globals (l10n, auth, initial-state, router) and a ResizeObserver
stub live in vitest.setup.ts so future specs don't re-stub them.

Adds a makeNode() fixture factory (src/test-utils/fixtures.ts) so
owner/mount-type permutations — the axis the Mine filter and external
storage exclusion depend on — are a one-line call, not a hand-built Node.

Spec glob is src/**/*.spec.ts; no specs yet.

Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
padded-blocks in the ResizeObserver stub class, and a JSDoc block on
makeNode() that eslint-plugin-jsdoc wanted full @param tags for — a plain
comment says the same thing without the noise.

Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move validateFilename() to src/utils/validateFilename.ts and
categoryName()/categoryMimes() (with MIME_CATEGORIES/ALL_OFFICE_MIMES) to
src/utils/fileCategories.ts. Behaviour-preserving — OfficeOverview.vue
calls the same functions, just imported instead of defined inline.

filterFiles (the Mine-filter logic) is extracted separately, with
#46's external-storage exclusion fix applied.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Covers config.ts (localStorage round-trip), officeFiles.ts
(filterByMimes, DAV SEARCH caching/invalidation, XML escaping),
templates.ts (OCS envelope unwrapping), the newly-extracted
validateFilename/fileCategories utils, and FileCard.vue (first
component mount, zero Nextcloud globals to stub).

Adds makeCreator() to the fixture factory alongside makeNode().

filterFiles and OfficeOverview.vue coverage follow in later commits.

Also adds an eslintrc override: n/no-unpublished-import doesn't apply
to *.spec.ts, which legitimately import test-only devDependencies.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Move filteredFiles' mine/shared/all + search + sortNodes pipeline to
filterFiles() in src/utils/fileFilters.ts. currentUid becomes a
parameter instead of a module-scope read.

Includes the external-storage mount-type exclusion from #46: most
storage backends' getOwner() defaults to whoever is currently browsing
when there's no single real owner, so external/external-session mounts
must be excluded from "Mine" the same way group/shared mounts are.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Table-tests mine/shared/all, search-on-top-of-filter, category
filtering, and sort. The regression case for #46: a file owned by
currentUid on an external/external-session mount is excluded from
"mine".

Also pins a real, surprising, pre-existing (not introduced here)
sortNodes() behaviour: sortingMode 'mtime' internally flips
sortingOrder, so 'desc' actually sorts non-favourites oldest-first.
Characterizing what main already does, not endorsing it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Mounts the real component via shallowMount() with the standard
resetModules()+dynamic-import dance (module-level getCurrentUser()/
fetchAll() side effects, officeFiles.ts's cachedNodes singleton).
Covers rendering states (loading/empty/error/no-results/grid-vs-list)
and wiring (openFile, openInFiles, toggleViewMode, doCreateFromTemplate).

Component lookups match by { name }, not by imported reference:
resetModules() gives the freshly re-imported OfficeOverview.vue its own
module instances, so an identity-based findComponent(RealComponent)
silently fails to match.

Harness extensions this needed, all in vitest.config.ts/setup.ts so
future specs get them for free:
- css: true and inlining @nextcloud/vue/router/files — they self-import
  CSS (breaks under Node's ESM loader otherwise), and leaving consumers
  inlined while @nextcloud/router stayed externalized meant
  @nextcloud/files/dav.mjs resolved the real router instead of the
  mocked one.
- @nextcloud/l10n and @nextcloud/router mocks now preserve the rest of
  each module via importOriginal() — @nextcloud/vue's internals need
  more than the one function each we were overriding (isRTL,
  generateRemoteUrl, ...).
- config.global.renderStubDefaultSlot, plus a generic
  stubRenderingAllSlots() helper for components whose *named* slots
  matter (NcAppNavigation's #search, NcEmptyContent's #description) —
  shallowMount's auto-stubs render neither by default.
- Top-level @nextcloud/vue layout components (NcContent/NcAppNavigation/
  NcAppContent) need real Nextcloud page globals/DOM structure to mount
  for real; confirmed empirically, hence shallowMount over mount().

fixtures.ts's makeNode() now sets id (Node.fileid only derives from a
numeric backing id) — without it every fileid-based assertion was
silently comparing "undefined" to "undefined".

Two real, pre-existing product behaviours pinned as characterization,
not fixed (out of scope for a behaviour-preserving refactor):
- sortNodes() with sortingMode 'mtime' internally flips sortingOrder,
  so the "desc" call already in main sorts non-favourites oldest-first.
- If getTemplates() itself rejects, creators stays empty and the UI
  shows "No office suite installed", not the "Failed to load files"
  error state — that only reaches the error branch when the second
  fetch (getAllOfficeFiles) is what fails. Misleading for a network
  error, but not this refactor's problem to fix.

Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
(cherry picked from commit eec63a3)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
js/ and css/ are generated bundles, committed by the nextcloud-command bot via /compile in CI, never by hand. A tracked .githooks/pre-commit rejects staged js/css on dev machines; it steps aside when CI=true so the bot's asset commit is unaffected. Wired through a 'prepare' script so it auto-installs on npm install, no husky dependency. See README 'Committing changes'.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
@moodyjmz moodyjmz force-pushed the test/vitest-harness branch from 888a656 to 94f89fd Compare July 8, 2026 22:07
moodyjmz and others added 3 commits July 9, 2026 00:29
Full mount (not shallow) — NcButton/NcIconSvgWrapper need no
page-level Nextcloud globals to render, unlike OfficeOverview.vue's
NcContent/NcAppNavigation.

Covers items (blank card always first), nameWithoutExt (ordinary name,
dotfile, no-extension), click wiring (blank vs template card select
args), template preview (previewUrl present/absent, broken-image
fallback to icon), and themeType/palette mapping (known mime, unmapped
mime, empty mimetypes).

Scroll-arrow tests need scrollLeft/scrollWidth/clientWidth overridden
via Object.defineProperty (jsdom reports these as 0 by default) and
Element.prototype.scrollBy stubbed (jsdom doesn't implement it at
all), then assert canScrollLeft/canScrollRight and the scroll-step
math: max(CARD_WIDTH+CARD_GAP, clientWidth-(CARD_WIDTH+CARD_GAP)).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
One-line wrapper (<OfficeOverview />) — a single shallowMount smoke
test is proportionate. Stubs templates.ts/officeFiles.ts locally since
OfficeOverview.vue still runs fetchAll() at module-evaluation time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Same three-job changes/test/summary shape as the existing
lint-eslint.yml/npm-build.yml workflows, same action pins — this repo
already has a convention for path-filtered PR checks.

Path filter deliberately isn't src/** alone: a vitest.config.ts or
setup-file change with no src/ diff would otherwise skip the check
while still being able to break every test. Watches
.github/workflows/**, src/**, vitest.config.ts, vitest.setup.ts,
package.json, package-lock.json and tsconfig.json.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
@moodyjmz moodyjmz marked this pull request as ready for review July 9, 2026 21:40
moodyjmz and others added 2 commits July 9, 2026 23:49
Add a "Running tests" section (npm run test:unit, why nvm use matters
— Node 25's native localStorage breaks jsdom under Vitest) and note
the pre-commit hook that blocks staged js/css under "Committing
changes". Update the architecture diagram to show TemplateSection.vue/
FileCard.vue and the src/utils/ extraction as their own nodes instead
of everything living inside OfficeOverview.vue.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
NcEmptyContent's description is a named slot (#description), never a
prop — stubRenderingAllSlots' generic slot-forwarding template already
handles it regardless of what's declared in props. Listing it there
did nothing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

3. to review Waiting for reviews enhancement New feature request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add frontend test infrastructure (Vitest)

1 participant