test(integrations): add Vue integration fixtures for Vite, Nuxt and Vike (#4882)

Mirror the lucide-react integration tests for `@lucide/vue`. Each fixture is
scaffolded with the framework's official generator (`pnpm create vue@latest`,
`pnpm create nuxt@latest`, `pnpm create vike@latest --vue`), trimmed of demo
boilerplate, and runs a production build, `vue-tsc`, and a Vitest Browser Mode
test in Chromium that renders the public `@lucide/vue` APIs (static icon,
per-instance props, `setLucideProps` context, alias vs canonical icon).

Nuxt is pinned to 4.4.8: 4.5.x requires Vite 8, which the workspace-wide
`vite: ^7.3.6` override does not allow.

Wire it up with a `test:integrations:vue` root script and an `integration`
job in the Lucide Vue workflow.


Claude-Session: https://claude.ai/code/session_01KmQsnSECZoNxh1GJ15BmdH

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Eric Fennis
2026-10-02 13:01:30 +02:00
committed by GitHub
co-authored by Claude
parent 3cb1625e46
commit 41991a82aa
36 changed files with 3885 additions and 2 deletions
+22
View File
@@ -10,6 +10,9 @@ on:
- packages/shared/**
- tools/build-icons/**
- tools/rollup-plugins/**
- integrations/lucide-vue/**
- package.json
- pnpm-workspace.yaml
- pnpm-lock.yaml
permissions:
@@ -63,3 +66,22 @@ jobs:
- name: Test
run: pnpm --filter @lucide/vue typecheck
integration:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
cache: 'pnpm'
node-version-file: 'package.json'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Install Chromium
run: pnpm --filter @lucide/integration-vue-vite exec playwright install --with-deps chromium
- name: Run integration checks
run: pnpm test:integrations:vue
+2
View File
@@ -19,6 +19,8 @@ stats
integrations/**/.react-router
integrations/**/.vitest-attachments
integrations/**/__screenshots__
integrations/**/.nuxt
integrations/**/.output
outlined
lucide-font
packages/**/src/icons/*.js
+2
View File
@@ -25,6 +25,8 @@ integrations/**/next-env.d.ts
integrations/**/routeTree.gen.ts
integrations/**/.vitest-attachments
integrations/**/__screenshots__
integrations/**/.nuxt
integrations/**/.output
# lucide-svelte
packages/svelte/src/icons/*.svelte
+18
View File
@@ -0,0 +1,18 @@
# Lucide Vue integrations
These fixtures exercise the built `@lucide/vue` package in real consumer frameworks.
Every fixture runs three checks:
- a framework production build;
- TypeScript with `vue-tsc` (`noEmit`);
- Vitest Browser Mode in Chromium.
From the repository root, install Chromium once and run the complete suite:
```sh
pnpm --filter @lucide/integration-vue-vite exec playwright install chromium
pnpm test:integrations:vue
```
The fixtures depend on `@lucide/vue` through `workspace:*`, but import only its public package entry point. The root command builds `@lucide/vue` before running any consumer checks.
+7
View File
@@ -0,0 +1,7 @@
<script setup lang="ts">
import IconShowcase from './components/IconShowcase.vue';
</script>
<template>
<IconShowcase />
</template>
@@ -0,0 +1,23 @@
<script setup lang="ts">
import { Camera, Droplet, Edit2, House, Pen } from '@lucide/vue';
import LucideProvider from './LucideProvider.vue';
</script>
<template>
<main aria-label="Lucide integration">
<Camera data-testid="static-icon" />
<Droplet
data-testid="custom-icon"
class="consumer-icon"
color="red"
:size="48"
:stroke-width="4"
absolute-stroke-width
/>
<LucideProvider>
<House data-testid="provider-icon" />
</LucideProvider>
<Pen data-testid="alias-icon" />
<Edit2 data-testid="canonical-icon" />
</main>
</template>
@@ -0,0 +1,9 @@
<script setup lang="ts">
import { setLucideProps } from '@lucide/vue';
setLucideProps({ color: 'purple', size: 32, strokeWidth: 3 });
</script>
<template>
<slot />
</template>
@@ -0,0 +1,11 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: '2025-07-15',
devtools: { enabled: true },
typescript: {
// `nuxt typecheck` only covers `app/**` and `tests/nuxt/**` by default; opt the browser spec
// and the vitest config in so they are type-checked too.
tsConfig: { include: ['../tests/**/*'] },
nodeTsConfig: { include: ['../vitest.config.*'] },
},
});
+27
View File
@@ -0,0 +1,27 @@
{
"name": "@lucide/integration-vue-nuxt",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"build": "nuxt build",
"typecheck": "nuxt typecheck",
"test:browser": "vitest run",
"check": "pnpm build && pnpm typecheck && pnpm test:browser"
},
"dependencies": {
"@lucide/vue": "workspace:*",
"nuxt": "4.4.8",
"vue": "3.5.42",
"vue-router": "5.3.1"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.8",
"@vitest/browser-playwright": "4.1.10",
"typescript": "^6.0.3",
"vite": "^7.3.6",
"vitest": "4.1.11",
"vitest-browser-vue": "3.1.0",
"vue-tsc": "^3.3.11"
}
}
@@ -0,0 +1,33 @@
import { expect, test } from 'vitest';
import { render } from 'vitest-browser-vue';
import App from '../app/app.vue';
test('renders Lucide public APIs in a real browser', async () => {
const screen = await render(App);
const staticIcon = screen.getByTestId('static-icon');
await expect.element(staticIcon).toBeVisible();
await expect.element(staticIcon).toHaveAttribute('width', '24');
await expect.element(staticIcon).toHaveAttribute('height', '24');
await expect.element(staticIcon).toHaveAttribute('viewBox', '0 0 24 24');
await expect.element(staticIcon).toHaveAttribute('aria-hidden', 'true');
const customIcon = screen.getByTestId('custom-icon');
await expect.element(customIcon).toBeVisible();
await expect.element(customIcon).toHaveAttribute('width', '48');
await expect.element(customIcon).toHaveAttribute('height', '48');
await expect.element(customIcon).toHaveAttribute('stroke', 'red');
await expect.element(customIcon).toHaveAttribute('stroke-width', '2');
await expect.element(customIcon).toHaveClass('consumer-icon');
const providerIcon = screen.getByTestId('provider-icon');
await expect.element(providerIcon).toHaveAttribute('width', '32');
await expect.element(providerIcon).toHaveAttribute('stroke', 'purple');
await expect.element(providerIcon).toHaveAttribute('stroke-width', '3');
const aliasIcon = screen.getByTestId('alias-icon');
const canonicalIcon = screen.getByTestId('canonical-icon');
await expect.element(aliasIcon).toBeVisible();
await expect.element(canonicalIcon).toBeVisible();
expect(aliasIcon.element().innerHTML).toBe(canonicalIcon.element().innerHTML);
});
@@ -0,0 +1,18 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"files": [],
"references": [
{
"path": "./.nuxt/tsconfig.app.json"
},
{
"path": "./.nuxt/tsconfig.server.json"
},
{
"path": "./.nuxt/tsconfig.shared.json"
},
{
"path": "./.nuxt/tsconfig.node.json"
}
]
}
@@ -0,0 +1,15 @@
import vue from '@vitejs/plugin-vue';
import { playwright } from '@vitest/browser-playwright';
import { defineConfig } from 'vitest/config';
export default defineConfig({
plugins: [vue()],
test: {
browser: {
enabled: true,
headless: true,
provider: playwright(),
instances: [{ browser: 'chromium' }],
},
},
});
@@ -0,0 +1,23 @@
<script setup lang="ts">
import { Camera, Droplet, Edit2, House, Pen } from '@lucide/vue';
import LucideProvider from './LucideProvider.vue';
</script>
<template>
<main aria-label="Lucide integration">
<Camera data-testid="static-icon" />
<Droplet
data-testid="custom-icon"
class="consumer-icon"
color="red"
:size="48"
:stroke-width="4"
absolute-stroke-width
/>
<LucideProvider>
<House data-testid="provider-icon" />
</LucideProvider>
<Pen data-testid="alias-icon" />
<Edit2 data-testid="canonical-icon" />
</main>
</template>
@@ -0,0 +1,9 @@
<script setup lang="ts">
import { setLucideProps } from '@lucide/vue';
setLucideProps({ color: 'purple', size: 32, strokeWidth: 3 });
</script>
<template>
<slot />
</template>
+27
View File
@@ -0,0 +1,27 @@
{
"name": "@lucide/integration-vue-vike",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"build": "vike build",
"typecheck": "vue-tsc --noEmit",
"test:browser": "vitest run",
"check": "pnpm build && pnpm typecheck && pnpm test:browser"
},
"dependencies": {
"@lucide/vue": "workspace:*",
"vike": "0.4.266",
"vike-vue": "0.9.13",
"vue": "3.5.42"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.8",
"@vitest/browser-playwright": "4.1.10",
"typescript": "^6.0.3",
"vite": "^7.3.6",
"vitest": "4.1.11",
"vitest-browser-vue": "3.1.0",
"vue-tsc": "^3.3.11"
}
}
@@ -0,0 +1,14 @@
import type { Config } from 'vike/types';
import vikeVue from 'vike-vue/config';
// Default config (can be overridden by pages)
// https://vike.dev/config
const config: Config = {
// https://vike.dev/head-tags
title: 'Lucide Vue Vike integration',
extends: [vikeVue],
};
export default config;
@@ -0,0 +1,7 @@
<script setup lang="ts">
import IconShowcase from '../../components/IconShowcase.vue';
</script>
<template>
<IconShowcase />
</template>
@@ -0,0 +1,33 @@
import { expect, test } from 'vitest';
import { render } from 'vitest-browser-vue';
import App from '../pages/index/+Page.vue';
test('renders Lucide public APIs in a real browser', async () => {
const screen = await render(App);
const staticIcon = screen.getByTestId('static-icon');
await expect.element(staticIcon).toBeVisible();
await expect.element(staticIcon).toHaveAttribute('width', '24');
await expect.element(staticIcon).toHaveAttribute('height', '24');
await expect.element(staticIcon).toHaveAttribute('viewBox', '0 0 24 24');
await expect.element(staticIcon).toHaveAttribute('aria-hidden', 'true');
const customIcon = screen.getByTestId('custom-icon');
await expect.element(customIcon).toBeVisible();
await expect.element(customIcon).toHaveAttribute('width', '48');
await expect.element(customIcon).toHaveAttribute('height', '48');
await expect.element(customIcon).toHaveAttribute('stroke', 'red');
await expect.element(customIcon).toHaveAttribute('stroke-width', '2');
await expect.element(customIcon).toHaveClass('consumer-icon');
const providerIcon = screen.getByTestId('provider-icon');
await expect.element(providerIcon).toHaveAttribute('width', '32');
await expect.element(providerIcon).toHaveAttribute('stroke', 'purple');
await expect.element(providerIcon).toHaveAttribute('stroke-width', '3');
const aliasIcon = screen.getByTestId('alias-icon');
const canonicalIcon = screen.getByTestId('canonical-icon');
await expect.element(aliasIcon).toBeVisible();
await expect.element(canonicalIcon).toBeVisible();
expect(aliasIcon.element().innerHTML).toBe(canonicalIcon.element().innerHTML);
});
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"strict": true,
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "Bundler",
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"types": ["vite/client"],
"noEmit": true,
"skipLibCheck": true,
"esModuleInterop": true
},
"exclude": ["dist"]
}
@@ -0,0 +1,7 @@
import vike from 'vike/plugin';
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vike(), vue()],
});
@@ -0,0 +1,15 @@
import vue from '@vitejs/plugin-vue';
import { playwright } from '@vitest/browser-playwright';
import { defineConfig } from 'vitest/config';
export default defineConfig({
plugins: [vue()],
test: {
browser: {
enabled: true,
headless: true,
provider: playwright(),
instances: [{ browser: 'chromium' }],
},
},
});
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
+18
View File
@@ -0,0 +1,18 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<title>Lucide Vue Vite integration</title>
</head>
<body>
<div id="app"></div>
<script
type="module"
src="/src/main.ts"
></script>
</body>
</html>
+29
View File
@@ -0,0 +1,29 @@
{
"name": "@lucide/integration-vue-vite",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"build": "vite build",
"typecheck": "vue-tsc --build",
"test:browser": "vitest run",
"check": "pnpm build && pnpm typecheck && pnpm test:browser"
},
"dependencies": {
"@lucide/vue": "workspace:*",
"vue": "3.5.42"
},
"devDependencies": {
"@tsconfig/node24": "^24.0.5",
"@types/node": "^26.1.1",
"@vitejs/plugin-vue": "^6.0.8",
"@vitest/browser-playwright": "4.1.10",
"@vue/tsconfig": "^0.9.1",
"playwright": "1.58.2",
"typescript": "^6.0.3",
"vite": "^7.3.6",
"vitest": "4.1.11",
"vitest-browser-vue": "3.1.0",
"vue-tsc": "^3.3.11"
}
}
+7
View File
@@ -0,0 +1,7 @@
<script setup lang="ts">
import IconShowcase from './IconShowcase.vue';
</script>
<template>
<IconShowcase />
</template>
@@ -0,0 +1,23 @@
<script setup lang="ts">
import { Camera, Droplet, Edit2, House, Pen } from '@lucide/vue';
import LucideProvider from './LucideProvider.vue';
</script>
<template>
<main aria-label="Lucide integration">
<Camera data-testid="static-icon" />
<Droplet
data-testid="custom-icon"
class="consumer-icon"
color="red"
:size="48"
:stroke-width="4"
absolute-stroke-width
/>
<LucideProvider>
<House data-testid="provider-icon" />
</LucideProvider>
<Pen data-testid="alias-icon" />
<Edit2 data-testid="canonical-icon" />
</main>
</template>
@@ -0,0 +1,9 @@
<script setup lang="ts">
import { setLucideProps } from '@lucide/vue';
setLucideProps({ color: 'purple', size: 32, strokeWidth: 3 });
</script>
<template>
<slot />
</template>
+4
View File
@@ -0,0 +1,4 @@
import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');
@@ -0,0 +1,33 @@
import { expect, test } from 'vitest';
import { render } from 'vitest-browser-vue';
import App from '../src/App.vue';
test('renders Lucide public APIs in a real browser', async () => {
const screen = await render(App);
const staticIcon = screen.getByTestId('static-icon');
await expect.element(staticIcon).toBeVisible();
await expect.element(staticIcon).toHaveAttribute('width', '24');
await expect.element(staticIcon).toHaveAttribute('height', '24');
await expect.element(staticIcon).toHaveAttribute('viewBox', '0 0 24 24');
await expect.element(staticIcon).toHaveAttribute('aria-hidden', 'true');
const customIcon = screen.getByTestId('custom-icon');
await expect.element(customIcon).toBeVisible();
await expect.element(customIcon).toHaveAttribute('width', '48');
await expect.element(customIcon).toHaveAttribute('height', '48');
await expect.element(customIcon).toHaveAttribute('stroke', 'red');
await expect.element(customIcon).toHaveAttribute('stroke-width', '2');
await expect.element(customIcon).toHaveClass('consumer-icon');
const providerIcon = screen.getByTestId('provider-icon');
await expect.element(providerIcon).toHaveAttribute('width', '32');
await expect.element(providerIcon).toHaveAttribute('stroke', 'purple');
await expect.element(providerIcon).toHaveAttribute('stroke-width', '3');
const aliasIcon = screen.getByTestId('alias-icon');
const canonicalIcon = screen.getByTestId('canonical-icon');
await expect.element(aliasIcon).toBeVisible();
await expect.element(canonicalIcon).toBeVisible();
expect(aliasIcon.element().innerHTML).toBe(canonicalIcon.element().innerHTML);
});
@@ -0,0 +1,18 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue", "tests/**/*"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
// Extra safety for array and object lookups, but may have false positives.
"noUncheckedIndexedAccess": true,
// Path mapping for cleaner imports.
"paths": {
"@/*": ["./src/*"]
},
// `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking.
// Specified here to keep it out of the root directory.
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo"
}
}
@@ -0,0 +1,11 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
]
}
@@ -0,0 +1,28 @@
// TSConfig for modules that run in Node.js environment via either transpilation or type-stripping.
{
"extends": "@tsconfig/node24/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"playwright.config.*",
"eslint.config.*"
],
"compilerOptions": {
// Most tools use transpilation instead of Node.js's native type-stripping.
// Bundler mode provides a smoother developer experience.
"module": "preserve",
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
// Include Node.js types and avoid accidentally including other `@types/*` packages.
"types": ["node"],
// Disable emitting output during `vue-tsc --build`, which is used for type-checking only.
"noEmit": true,
// `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking.
// Specified here to keep it out of the root directory.
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo"
}
}
@@ -0,0 +1,14 @@
import { fileURLToPath, URL } from 'node:url';
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
// https://vite.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
});
@@ -0,0 +1,17 @@
import { playwright } from '@vitest/browser-playwright';
import { defineConfig, mergeConfig } from 'vitest/config';
import viteConfig from './vite.config';
export default mergeConfig(
viteConfig,
defineConfig({
test: {
browser: {
enabled: true,
headless: true,
provider: playwright(),
instances: [{ browser: 'chromium' }],
},
},
}),
);
+1
View File
@@ -6,6 +6,7 @@
"test": "pnpm -r --filter './packages/**' test",
"test:update": "pnpm -r --filter './packages/**' --filter !'./packages/lucide-angular' test -- -u",
"test:integrations:react": "pnpm --filter lucide-react build && pnpm -r --workspace-concurrency=1 --filter './integrations/lucide-react/**' check",
"test:integrations:vue": "pnpm --filter @lucide/vue build && pnpm -r --workspace-concurrency=1 --filter './integrations/lucide-vue/**' check",
"test:font": "pnpm --filter build-font test",
"lucide": "pnpm --filter lucide",
"lucide-angular": "pnpm --filter lucide-angular",
+3336 -2
View File
File diff suppressed because it is too large Load Diff