mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-11 09:51:40 +00:00
feat: add tests package (#7907)
* feat: add tests package * fix * fix * debug * debug * debug * fix * debug * fix: no concurrent * fix * test: add vite-app tests * test: add tests
This commit is contained in:
3
.github/workflows/test.yml
vendored
3
.github/workflows/test.yml
vendored
@@ -39,4 +39,7 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install
|
run: pnpm install
|
||||||
|
|
||||||
|
- name: Build packages
|
||||||
|
run: pnpm build --filter=shadcn
|
||||||
|
|
||||||
- run: pnpm test
|
- run: pnpm test
|
||||||
|
|||||||
@@ -46,7 +46,8 @@
|
|||||||
"release": "changeset version",
|
"release": "changeset version",
|
||||||
"pub:beta": "cd packages/shadcn && pnpm pub:beta",
|
"pub:beta": "cd packages/shadcn && pnpm pub:beta",
|
||||||
"pub:release": "cd packages/shadcn && pnpm pub:release",
|
"pub:release": "cd packages/shadcn && pnpm pub:release",
|
||||||
"test": "turbo run test --filter=!shadcn-ui --force"
|
"test": "turbo run test --filter=!shadcn-ui --force",
|
||||||
|
"tests:test": "pnpm --filter=tests test"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@9.0.6",
|
"packageManager": "pnpm@9.0.6",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
2
packages/tests/.eslintignore
Normal file
2
packages/tests/.eslintignore
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fixtures/
|
||||||
|
temp/
|
||||||
3
packages/tests/.gitignore
vendored
Normal file
3
packages/tests/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
temp/
|
||||||
|
*.log
|
||||||
|
.cache/
|
||||||
34
packages/tests/README.md
Normal file
34
packages/tests/README.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# Tests
|
||||||
|
|
||||||
|
This package contains integration tests that verify the shadcn CLI works correctly with a local registry. The tests run actual CLI commands against test fixtures to ensure files are created and updated properly.
|
||||||
|
|
||||||
|
## Running Tests
|
||||||
|
|
||||||
|
Run the following command from the root of the workspace:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm tests:test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Writing Tests
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
createFixtureTestDirectory,
|
||||||
|
fileExists,
|
||||||
|
npxShadcn,
|
||||||
|
} from "../utils/helpers"
|
||||||
|
|
||||||
|
describe("my test suite", () => {
|
||||||
|
it("should do something", async () => {
|
||||||
|
// Create a test directory from a fixture
|
||||||
|
const testDir = await createFixtureTestDirectory("next-app")
|
||||||
|
|
||||||
|
// Run CLI command
|
||||||
|
await npxShadcn(testDir, ["init", "--base-color=neutral"])
|
||||||
|
|
||||||
|
// Make assertions
|
||||||
|
expect(await fileExists(path.join(testDir, "components.json"))).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
BIN
packages/tests/fixtures/next-app/app/favicon.ico
vendored
Normal file
BIN
packages/tests/fixtures/next-app/app/favicon.ico
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
1
packages/tests/fixtures/next-app/app/globals.css
vendored
Normal file
1
packages/tests/fixtures/next-app/app/globals.css
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
22
packages/tests/fixtures/next-app/app/layout.tsx
vendored
Normal file
22
packages/tests/fixtures/next-app/app/layout.tsx
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import './globals.css'
|
||||||
|
import type { Metadata } from 'next'
|
||||||
|
import { Inter } from 'next/font/google'
|
||||||
|
|
||||||
|
const inter = Inter({ subsets: ['latin'] })
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Create Next App',
|
||||||
|
description: 'Generated by create next app',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<body className={inter.className}>{children}</body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}
|
||||||
3
packages/tests/fixtures/next-app/app/other.css
vendored
Normal file
3
packages/tests/fixtures/next-app/app/other.css
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
body {
|
||||||
|
background-color: red;
|
||||||
|
}
|
||||||
113
packages/tests/fixtures/next-app/app/page.tsx
vendored
Normal file
113
packages/tests/fixtures/next-app/app/page.tsx
vendored
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import Image from 'next/image'
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<main className="flex min-h-screen flex-col items-center justify-between p-24">
|
||||||
|
<div className="z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex">
|
||||||
|
<p className="fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30">
|
||||||
|
Get started by editing
|
||||||
|
<code className="font-mono font-bold">app/page.tsx</code>
|
||||||
|
</p>
|
||||||
|
<div className="fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:h-auto lg:w-auto lg:bg-none">
|
||||||
|
<a
|
||||||
|
className="pointer-events-none flex place-items-center gap-2 p-8 lg:pointer-events-auto lg:p-0"
|
||||||
|
href="https://vercel.com?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
By{' '}
|
||||||
|
<Image
|
||||||
|
src="/vercel.svg"
|
||||||
|
alt="Vercel Logo"
|
||||||
|
className="dark:invert"
|
||||||
|
width={100}
|
||||||
|
height={24}
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative flex place-items-center before:absolute before:h-[300px] before:w-[480px] before:-translate-x-1/2 before:rounded-full before:bg-gradient-radial before:from-white before:to-transparent before:blur-2xl before:content-[''] after:absolute after:-z-20 after:h-[180px] after:w-[240px] after:translate-x-1/3 after:bg-gradient-conic after:from-sky-200 after:via-blue-200 after:blur-2xl after:content-[''] before:dark:bg-gradient-to-br before:dark:from-transparent before:dark:to-blue-700 before:dark:opacity-10 after:dark:from-sky-900 after:dark:via-[#0141ff] after:dark:opacity-40 before:lg:h-[360px] z-[-1]">
|
||||||
|
<Image
|
||||||
|
className="relative dark:drop-shadow-[0_0_0.3rem_#ffffff70] dark:invert"
|
||||||
|
src="/next.svg"
|
||||||
|
alt="Next.js Logo"
|
||||||
|
width={180}
|
||||||
|
height={37}
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-32 grid text-center lg:max-w-5xl lg:w-full lg:mb-0 lg:grid-cols-4 lg:text-left">
|
||||||
|
<a
|
||||||
|
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||||
|
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<h2 className={`mb-3 text-2xl font-semibold`}>
|
||||||
|
Docs{' '}
|
||||||
|
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
||||||
|
->
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
|
||||||
|
Find in-depth information about Next.js features and API.
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||||
|
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<h2 className={`mb-3 text-2xl font-semibold`}>
|
||||||
|
Learn{' '}
|
||||||
|
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
||||||
|
->
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
|
||||||
|
Learn about Next.js in an interactive course with quizzes!
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||||
|
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<h2 className={`mb-3 text-2xl font-semibold`}>
|
||||||
|
Templates{' '}
|
||||||
|
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
||||||
|
->
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
|
||||||
|
Explore the Next.js 13 playground.
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||||
|
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<h2 className={`mb-3 text-2xl font-semibold`}>
|
||||||
|
Deploy{' '}
|
||||||
|
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
||||||
|
->
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
|
||||||
|
Instantly deploy your Next.js site to a shareable URL with Vercel.
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
4
packages/tests/fixtures/next-app/next.config.ts
vendored
Normal file
4
packages/tests/fixtures/next-app/next.config.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {}
|
||||||
|
|
||||||
|
module.exports = nextConfig
|
||||||
32
packages/tests/fixtures/next-app/package.json
vendored
Normal file
32
packages/tests/fixtures/next-app/package.json
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "my-app",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev --turbopack",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"lucide-react": "^0.527.0",
|
||||||
|
"next": "15.4.4",
|
||||||
|
"react": "19.1.0",
|
||||||
|
"react-dom": "19.1.0",
|
||||||
|
"tailwind-merge": "^3.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/eslintrc": "^3",
|
||||||
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"@types/node": "^20",
|
||||||
|
"@types/react": "^19",
|
||||||
|
"@types/react-dom": "^19",
|
||||||
|
"eslint": "^9",
|
||||||
|
"eslint-config-next": "15.4.4",
|
||||||
|
"tailwindcss": "^4",
|
||||||
|
"tw-animate-css": "^1.3.6",
|
||||||
|
"typescript": "^5"
|
||||||
|
}
|
||||||
|
}
|
||||||
5
packages/tests/fixtures/next-app/postcss.config.mjs
vendored
Normal file
5
packages/tests/fixtures/next-app/postcss.config.mjs
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
const config = {
|
||||||
|
plugins: ["@tailwindcss/postcss"],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
27
packages/tests/fixtures/next-app/tsconfig.json
vendored
Normal file
27
packages/tests/fixtures/next-app/tsconfig.json
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
12
packages/tests/fixtures/registry/example-component.json
vendored
Normal file
12
packages/tests/fixtures/registry/example-component.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||||
|
"name": "example-component",
|
||||||
|
"type": "registry:component",
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": "example/hello-world.tsx",
|
||||||
|
"type": "registry:component",
|
||||||
|
"content": "console.log('Hello, world!')"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
10
packages/tests/fixtures/registry/example-env-vars.json
vendored
Normal file
10
packages/tests/fixtures/registry/example-env-vars.json
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||||
|
"name": "example-env-vars",
|
||||||
|
"type": "registry:item",
|
||||||
|
"envVars": {
|
||||||
|
"APP_URL": "https://example.com",
|
||||||
|
"EMPTY_VAR": "",
|
||||||
|
"MULTILINE_VAR": "\"line1\nline2\nline3\""
|
||||||
|
}
|
||||||
|
}
|
||||||
13
packages/tests/fixtures/registry/example-item-to-root.json
vendored
Normal file
13
packages/tests/fixtures/registry/example-item-to-root.json
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||||
|
"name": "example-item-to-root",
|
||||||
|
"type": "registry:item",
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": "example/config.json",
|
||||||
|
"type": "registry:item",
|
||||||
|
"content": "{\"foo\": \"bar\"}",
|
||||||
|
"target": "~/config.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
13
packages/tests/fixtures/registry/example-item.json
vendored
Normal file
13
packages/tests/fixtures/registry/example-item.json
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||||
|
"name": "example-item",
|
||||||
|
"type": "registry:item",
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": "example/foo.txt",
|
||||||
|
"type": "registry:item",
|
||||||
|
"content": "Foo Bar",
|
||||||
|
"target": "path/to/foo.txt"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
18
packages/tests/fixtures/registry/example-style.json
vendored
Normal file
18
packages/tests/fixtures/registry/example-style.json
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||||
|
"name": "example-style",
|
||||||
|
"type": "registry:style",
|
||||||
|
"dependencies": ["@tabler/icons-react"],
|
||||||
|
"cssVars": {
|
||||||
|
"theme": {
|
||||||
|
"font-sans": "Inter, sans-serif"
|
||||||
|
},
|
||||||
|
"light": {
|
||||||
|
"brand": "oklch(20 14.3% 4.1%)",
|
||||||
|
"brand-foreground": "oklch(24 1.3% 10%)"
|
||||||
|
},
|
||||||
|
"dark": {
|
||||||
|
"brand": "oklch(24 1.3% 10%)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
packages/tests/fixtures/vite-app/eslint.config.js
vendored
Normal file
23
packages/tests/fixtures/vite-app/eslint.config.js
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import tseslint from 'typescript-eslint'
|
||||||
|
import { globalIgnores } from 'eslint/config'
|
||||||
|
|
||||||
|
export default tseslint.config([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
extends: [
|
||||||
|
js.configs.recommended,
|
||||||
|
tseslint.configs.recommended,
|
||||||
|
reactHooks.configs['recommended-latest'],
|
||||||
|
reactRefresh.configs.vite,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
13
packages/tests/fixtures/vite-app/index.html
vendored
Normal file
13
packages/tests/fixtures/vite-app/index.html
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Vite + React + TS</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
37
packages/tests/fixtures/vite-app/package.json
vendored
Normal file
37
packages/tests/fixtures/vite-app/package.json
vendored
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "vite-project",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"lucide-react": "^0.525.0",
|
||||||
|
"react": "^19.1.0",
|
||||||
|
"react-dom": "^19.1.0",
|
||||||
|
"tailwind-merge": "^3.3.1",
|
||||||
|
"tailwindcss": "^4.1.11"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.30.1",
|
||||||
|
"@types/node": "^24.1.0",
|
||||||
|
"@types/react": "^19.1.8",
|
||||||
|
"@types/react-dom": "^19.1.6",
|
||||||
|
"@vitejs/plugin-react": "^4.6.0",
|
||||||
|
"eslint": "^9.30.1",
|
||||||
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.20",
|
||||||
|
"globals": "^16.3.0",
|
||||||
|
"tw-animate-css": "^1.3.5",
|
||||||
|
"typescript": "~5.8.3",
|
||||||
|
"typescript-eslint": "^8.35.1",
|
||||||
|
"vite": "^7.0.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
42
packages/tests/fixtures/vite-app/src/App.css
vendored
Normal file
42
packages/tests/fixtures/vite-app/src/App.css
vendored
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
#root {
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
height: 6em;
|
||||||
|
padding: 1.5em;
|
||||||
|
will-change: filter;
|
||||||
|
transition: filter 300ms;
|
||||||
|
}
|
||||||
|
.logo:hover {
|
||||||
|
filter: drop-shadow(0 0 2em #646cffaa);
|
||||||
|
}
|
||||||
|
.logo.react:hover {
|
||||||
|
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes logo-spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
a:nth-of-type(2) .logo {
|
||||||
|
animation: logo-spin infinite 20s linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
padding: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-the-docs {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
35
packages/tests/fixtures/vite-app/src/App.tsx
vendored
Normal file
35
packages/tests/fixtures/vite-app/src/App.tsx
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import reactLogo from './assets/react.svg'
|
||||||
|
import viteLogo from '/vite.svg'
|
||||||
|
import './App.css'
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [count, setCount] = useState(0)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<a href="https://vite.dev" target="_blank">
|
||||||
|
<img src={viteLogo} className="logo" alt="Vite logo" />
|
||||||
|
</a>
|
||||||
|
<a href="https://react.dev" target="_blank">
|
||||||
|
<img src={reactLogo} className="logo react" alt="React logo" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<h1>Vite + React</h1>
|
||||||
|
<div className="card">
|
||||||
|
<button onClick={() => setCount((count) => count + 1)}>
|
||||||
|
count is {count}
|
||||||
|
</button>
|
||||||
|
<p>
|
||||||
|
Edit <code>src/App.tsx</code> and save to test HMR
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<p className="read-the-docs">
|
||||||
|
Click on the Vite and React logos to learn more
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App
|
||||||
1
packages/tests/fixtures/vite-app/src/index.css
vendored
Normal file
1
packages/tests/fixtures/vite-app/src/index.css
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
10
packages/tests/fixtures/vite-app/src/main.tsx
vendored
Normal file
10
packages/tests/fixtures/vite-app/src/main.tsx
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { StrictMode } from 'react'
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import './index.css'
|
||||||
|
import App from './App.tsx'
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
)
|
||||||
1
packages/tests/fixtures/vite-app/src/vite-env.d.ts
vendored
Normal file
1
packages/tests/fixtures/vite-app/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
33
packages/tests/fixtures/vite-app/tsconfig.app.json
vendored
Normal file
33
packages/tests/fixtures/vite-app/tsconfig.app.json
vendored
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true,
|
||||||
|
|
||||||
|
/* Paths */
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"#custom/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
13
packages/tests/fixtures/vite-app/tsconfig.json
vendored
Normal file
13
packages/tests/fixtures/vite-app/tsconfig.json
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"#custom/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
packages/tests/fixtures/vite-app/tsconfig.node.json
vendored
Normal file
25
packages/tests/fixtures/vite-app/tsconfig.node.json
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2023",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
14
packages/tests/fixtures/vite-app/vite.config.ts
vendored
Normal file
14
packages/tests/fixtures/vite-app/vite.config.ts
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import path from "path";
|
||||||
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react(), tailwindcss()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"#custom": path.resolve(__dirname, "./src"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
25
packages/tests/package.json
Normal file
25
packages/tests/package.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "tests",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"description": "Integration tests for shadcn CLI",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"shadcn": "workspace:*"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/fs-extra": "^11.0.1",
|
||||||
|
"@types/node": "^20.11.27",
|
||||||
|
"execa": "^7.0.0",
|
||||||
|
"fs-extra": "^11.1.0",
|
||||||
|
"rimraf": "^6.0.1",
|
||||||
|
"typescript": "^5.5.3",
|
||||||
|
"vite-tsconfig-paths": "^4.2.0",
|
||||||
|
"vitest": "^2.1.9",
|
||||||
|
"wait-port": "^1.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
273
packages/tests/src/tests/add.test.ts
Normal file
273
packages/tests/src/tests/add.test.ts
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
import path from "path"
|
||||||
|
import fs from "fs-extra"
|
||||||
|
import { describe, expect, it } from "vitest"
|
||||||
|
|
||||||
|
import {
|
||||||
|
createFixtureTestDirectory,
|
||||||
|
cssHasProperties,
|
||||||
|
fileExists,
|
||||||
|
npxShadcn,
|
||||||
|
} from "../utils/helpers"
|
||||||
|
|
||||||
|
describe("shadcn add", () => {
|
||||||
|
it("should add item to project", async () => {
|
||||||
|
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||||
|
await npxShadcn(fixturePath, ["init", "--base-color=neutral"])
|
||||||
|
await npxShadcn(fixturePath, ["add", "button"])
|
||||||
|
expect(
|
||||||
|
await fileExists(path.join(fixturePath, "components/ui/button.tsx"))
|
||||||
|
).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should add multiple items to project", async () => {
|
||||||
|
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||||
|
await npxShadcn(fixturePath, ["init", "--base-color=neutral"])
|
||||||
|
await npxShadcn(fixturePath, ["add", "button", "card"])
|
||||||
|
expect(
|
||||||
|
await fileExists(path.join(fixturePath, "components/ui/button.tsx"))
|
||||||
|
).toBe(true)
|
||||||
|
expect(
|
||||||
|
await fileExists(path.join(fixturePath, "components/ui/card.tsx"))
|
||||||
|
).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should add item with registryDependencies", async () => {
|
||||||
|
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||||
|
await npxShadcn(fixturePath, ["init", "--base-color=neutral"])
|
||||||
|
await npxShadcn(fixturePath, ["add", "alert-dialog"])
|
||||||
|
expect(
|
||||||
|
await fileExists(path.join(fixturePath, "components/ui/alert-dialog.tsx"))
|
||||||
|
).toBe(true)
|
||||||
|
expect(
|
||||||
|
await fileExists(path.join(fixturePath, "components/ui/button.tsx"))
|
||||||
|
).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should add item from url", async () => {
|
||||||
|
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||||
|
await npxShadcn(fixturePath, ["init", "--base-color=neutral"])
|
||||||
|
await npxShadcn(fixturePath, [
|
||||||
|
"add",
|
||||||
|
"https://ui.shadcn.com/r/styles/new-york-v4/login-01.json",
|
||||||
|
])
|
||||||
|
expect(
|
||||||
|
await fileExists(path.join(fixturePath, "components/ui/button.tsx"))
|
||||||
|
).toBe(true)
|
||||||
|
expect(
|
||||||
|
await fileExists(path.join(fixturePath, "components/ui/card.tsx"))
|
||||||
|
).toBe(true)
|
||||||
|
expect(
|
||||||
|
await fileExists(path.join(fixturePath, "components/ui/input.tsx"))
|
||||||
|
).toBe(true)
|
||||||
|
expect(
|
||||||
|
await fileExists(path.join(fixturePath, "components/ui/label.tsx"))
|
||||||
|
).toBe(true)
|
||||||
|
expect(
|
||||||
|
await fileExists(path.join(fixturePath, "components/login-form.tsx"))
|
||||||
|
).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should add component from local file", async () => {
|
||||||
|
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||||
|
await npxShadcn(fixturePath, ["init", "--base-color=neutral"])
|
||||||
|
await npxShadcn(fixturePath, [
|
||||||
|
"add",
|
||||||
|
"../../fixtures/registry/example-component.json",
|
||||||
|
])
|
||||||
|
|
||||||
|
const helloWorldContent = await fs.readFile(
|
||||||
|
path.join(fixturePath, "components/hello-world.tsx"),
|
||||||
|
"utf-8"
|
||||||
|
)
|
||||||
|
expect(helloWorldContent).toBe("console.log('Hello, world!')")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should add registry:page to the correct path", async () => {
|
||||||
|
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||||
|
await npxShadcn(fixturePath, ["init", "--base-color=neutral"])
|
||||||
|
await npxShadcn(fixturePath, ["add", "login-03"])
|
||||||
|
expect(await fileExists(path.join(fixturePath, "app/login/page.tsx"))).toBe(
|
||||||
|
true
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should add item with npm dependencies", async () => {
|
||||||
|
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||||
|
await npxShadcn(fixturePath, ["init", "--base-color=neutral"])
|
||||||
|
await npxShadcn(fixturePath, [
|
||||||
|
"add",
|
||||||
|
"../../fixtures/registry/example-style.json",
|
||||||
|
"--yes",
|
||||||
|
])
|
||||||
|
const packageJson = await fs.readJson(
|
||||||
|
path.join(fixturePath, "package.json")
|
||||||
|
)
|
||||||
|
expect(packageJson.dependencies["@tabler/icons-react"]).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should install cssVars", async () => {
|
||||||
|
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||||
|
await npxShadcn(fixturePath, ["init", "--base-color=neutral"])
|
||||||
|
await npxShadcn(fixturePath, [
|
||||||
|
"add",
|
||||||
|
"../../fixtures/registry/example-style.json",
|
||||||
|
"--yes",
|
||||||
|
])
|
||||||
|
|
||||||
|
const globalCssContent = await fs.readFile(
|
||||||
|
path.join(fixturePath, "app/globals.css"),
|
||||||
|
"utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
cssHasProperties(globalCssContent, [
|
||||||
|
{
|
||||||
|
selector: "@theme inline",
|
||||||
|
properties: {
|
||||||
|
"--font-sans": "Inter, sans-serif",
|
||||||
|
"--color-brand": "var(--brand)",
|
||||||
|
"--color-brand-foreground": "var(--brand-foreground)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: ":root",
|
||||||
|
properties: {
|
||||||
|
"--brand": "oklch(20 14.3% 4.1%)",
|
||||||
|
"--brand-foreground": "oklch(24 1.3% 10%)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: ".dark",
|
||||||
|
properties: {
|
||||||
|
"--brand": "oklch(24 1.3% 10%)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should add item with target", async () => {
|
||||||
|
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||||
|
await npxShadcn(fixturePath, ["init", "--base-color=neutral"])
|
||||||
|
await npxShadcn(fixturePath, [
|
||||||
|
"add",
|
||||||
|
"../../fixtures/registry/example-item.json",
|
||||||
|
])
|
||||||
|
expect(await fileExists(path.join(fixturePath, "path/to/foo.txt"))).toBe(
|
||||||
|
true
|
||||||
|
)
|
||||||
|
expect(
|
||||||
|
await fs.readFile(path.join(fixturePath, "path/to/foo.txt"), "utf-8")
|
||||||
|
).toBe("Foo Bar")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should add item with target to src", async () => {
|
||||||
|
const fixturePath = await createFixtureTestDirectory("vite-app")
|
||||||
|
await npxShadcn(fixturePath, ["init", "--base-color=neutral"])
|
||||||
|
await npxShadcn(fixturePath, [
|
||||||
|
"add",
|
||||||
|
"../../fixtures/registry/example-item.json",
|
||||||
|
])
|
||||||
|
expect(
|
||||||
|
await fileExists(path.join(fixturePath, "src/path/to/foo.txt"))
|
||||||
|
).toBe(true)
|
||||||
|
expect(
|
||||||
|
await fs.readFile(path.join(fixturePath, "src/path/to/foo.txt"), "utf-8")
|
||||||
|
).toBe("Foo Bar")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should add item with target to root", async () => {
|
||||||
|
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||||
|
await npxShadcn(fixturePath, ["init", "--base-color=neutral"])
|
||||||
|
await npxShadcn(fixturePath, [
|
||||||
|
"add",
|
||||||
|
"../../fixtures/registry/example-item-to-root.json",
|
||||||
|
])
|
||||||
|
expect(await fileExists(path.join(fixturePath, "config.json"))).toBe(true)
|
||||||
|
expect(await fs.readJson(path.join(fixturePath, "config.json"))).toEqual({
|
||||||
|
foo: "bar",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should add item with target to root when src", async () => {
|
||||||
|
const fixturePath = await createFixtureTestDirectory("vite-app")
|
||||||
|
await npxShadcn(fixturePath, ["init", "--base-color=neutral"])
|
||||||
|
await npxShadcn(fixturePath, [
|
||||||
|
"add",
|
||||||
|
"../../fixtures/registry/example-item-to-root.json",
|
||||||
|
])
|
||||||
|
expect(await fileExists(path.join(fixturePath, "config.json"))).toBe(true)
|
||||||
|
expect(await fs.readJson(path.join(fixturePath, "config.json"))).toEqual({
|
||||||
|
foo: "bar",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should add item with envVars", async () => {
|
||||||
|
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||||
|
await npxShadcn(fixturePath, ["init", "--base-color=neutral"])
|
||||||
|
await npxShadcn(fixturePath, [
|
||||||
|
"add",
|
||||||
|
"../../fixtures/registry/example-env-vars.json",
|
||||||
|
])
|
||||||
|
expect(await fileExists(path.join(fixturePath, ".env.local"))).toBe(true)
|
||||||
|
expect(await fs.readFile(path.join(fixturePath, ".env.local"), "utf-8"))
|
||||||
|
.toMatchInlineSnapshot(`
|
||||||
|
"APP_URL=https://example.com
|
||||||
|
EMPTY_VAR=
|
||||||
|
MULTILINE_VAR="line1
|
||||||
|
line2
|
||||||
|
line3"
|
||||||
|
"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should add NOT update existing envVars", async () => {
|
||||||
|
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||||
|
await npxShadcn(fixturePath, ["init", "--base-color=neutral"])
|
||||||
|
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(fixturePath, ".env.local"),
|
||||||
|
"APP_URL=https://foo.com"
|
||||||
|
)
|
||||||
|
|
||||||
|
await npxShadcn(fixturePath, [
|
||||||
|
"add",
|
||||||
|
"../../fixtures/registry/example-env-vars.json",
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(await fileExists(path.join(fixturePath, ".env.local"))).toBe(true)
|
||||||
|
expect(await fs.readFile(path.join(fixturePath, ".env.local"), "utf-8"))
|
||||||
|
.toMatchInlineSnapshot(`
|
||||||
|
"APP_URL=https://foo.com
|
||||||
|
|
||||||
|
EMPTY_VAR=
|
||||||
|
MULTILINE_VAR=line1
|
||||||
|
"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should use existing .env if it exists", async () => {
|
||||||
|
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||||
|
await npxShadcn(fixturePath, ["init", "--base-color=neutral"])
|
||||||
|
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(fixturePath, ".env"),
|
||||||
|
"APP_URL=https://foo.com"
|
||||||
|
)
|
||||||
|
|
||||||
|
await npxShadcn(fixturePath, [
|
||||||
|
"add",
|
||||||
|
"../../fixtures/registry/example-env-vars.json",
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(await fileExists(path.join(fixturePath, ".env.local"))).toBe(false)
|
||||||
|
expect(await fs.readFile(path.join(fixturePath, ".env"), "utf-8"))
|
||||||
|
.toMatchInlineSnapshot(`
|
||||||
|
"APP_URL=https://foo.com
|
||||||
|
|
||||||
|
EMPTY_VAR=
|
||||||
|
MULTILINE_VAR=line1
|
||||||
|
"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
})
|
||||||
131
packages/tests/src/tests/init.test.ts
Normal file
131
packages/tests/src/tests/init.test.ts
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import path from "path"
|
||||||
|
import fs from "fs-extra"
|
||||||
|
import { describe, expect, it } from "vitest"
|
||||||
|
|
||||||
|
import {
|
||||||
|
createFixtureTestDirectory,
|
||||||
|
fileExists,
|
||||||
|
npxShadcn,
|
||||||
|
readJson,
|
||||||
|
} from "../utils/helpers"
|
||||||
|
|
||||||
|
describe("shadcn init - next-app", () => {
|
||||||
|
it("should init with default configuration", async () => {
|
||||||
|
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||||
|
await npxShadcn(fixturePath, ["init", "--base-color=neutral"])
|
||||||
|
|
||||||
|
const componentsJsonPath = path.join(fixturePath, "components.json")
|
||||||
|
expect(await fileExists(componentsJsonPath)).toBe(true)
|
||||||
|
|
||||||
|
const componentsJson = await readJson(componentsJsonPath)
|
||||||
|
expect(componentsJson).toMatchObject({
|
||||||
|
style: "new-york",
|
||||||
|
rsc: true,
|
||||||
|
tsx: true,
|
||||||
|
tailwind: {
|
||||||
|
config: "",
|
||||||
|
css: "app/globals.css",
|
||||||
|
baseColor: "neutral",
|
||||||
|
cssVariables: true,
|
||||||
|
},
|
||||||
|
aliases: {
|
||||||
|
components: "@/components",
|
||||||
|
utils: "@/lib/utils",
|
||||||
|
ui: "@/components/ui",
|
||||||
|
lib: "@/lib",
|
||||||
|
hooks: "@/hooks",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(await fileExists(path.join(fixturePath, "lib/utils.ts"))).toBe(true)
|
||||||
|
|
||||||
|
const cssPath = path.join(fixturePath, "app/globals.css")
|
||||||
|
const cssContent = await fs.readFile(cssPath, "utf-8")
|
||||||
|
expect(cssContent).toContain("@layer base")
|
||||||
|
expect(cssContent).toContain(":root")
|
||||||
|
expect(cssContent).toContain(".dark")
|
||||||
|
expect(cssContent).toContain("tw-animate-css")
|
||||||
|
expect(cssContent).toContain("--background")
|
||||||
|
expect(cssContent).toContain("--foreground")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should init with custom base color", async () => {
|
||||||
|
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||||
|
await npxShadcn(fixturePath, ["init", "--base-color=zinc"])
|
||||||
|
|
||||||
|
const componentsJson = await readJson(
|
||||||
|
path.join(fixturePath, "components.json")
|
||||||
|
)
|
||||||
|
expect(componentsJson.style).toBe("new-york")
|
||||||
|
expect(componentsJson.tailwind.baseColor).toBe("zinc")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should init without CSS variables", async () => {
|
||||||
|
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||||
|
await npxShadcn(fixturePath, [
|
||||||
|
"init",
|
||||||
|
"--base-color=stone",
|
||||||
|
"--no-css-variables",
|
||||||
|
])
|
||||||
|
|
||||||
|
const componentsJson = await readJson(
|
||||||
|
path.join(fixturePath, "components.json")
|
||||||
|
)
|
||||||
|
expect(componentsJson.tailwind.cssVariables).toBe(false)
|
||||||
|
|
||||||
|
const cssPath = path.join(fixturePath, "app/globals.css")
|
||||||
|
const cssContent = await fs.readFile(cssPath, "utf-8")
|
||||||
|
expect(cssContent).not.toContain("--background")
|
||||||
|
expect(cssContent).not.toContain("--foreground")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should init with components", async () => {
|
||||||
|
const fixturePath = await createFixtureTestDirectory("next-app")
|
||||||
|
await npxShadcn(fixturePath, ["init", "--base-color=neutral", "button"])
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await fileExists(path.join(fixturePath, "components/ui/button.tsx"))
|
||||||
|
).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("shadcn init - vite-app", () => {
|
||||||
|
it("should init with custom alias and src", async () => {
|
||||||
|
const fixturePath = await createFixtureTestDirectory("vite-app")
|
||||||
|
await npxShadcn(fixturePath, ["init", "--base-color=gray", "alert-dialog"])
|
||||||
|
|
||||||
|
const componentsJson = await readJson(
|
||||||
|
path.join(fixturePath, "components.json")
|
||||||
|
)
|
||||||
|
expect(componentsJson.style).toBe("new-york")
|
||||||
|
expect(componentsJson.tailwind.baseColor).toBe("gray")
|
||||||
|
expect(componentsJson.aliases).toMatchObject({
|
||||||
|
components: "#custom/components",
|
||||||
|
utils: "#custom/lib/utils",
|
||||||
|
ui: "#custom/components/ui",
|
||||||
|
lib: "#custom/lib",
|
||||||
|
hooks: "#custom/hooks",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await fileExists(
|
||||||
|
path.join(fixturePath, "src/components/ui/alert-dialog.tsx")
|
||||||
|
)
|
||||||
|
).toBe(true)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await fileExists(path.join(fixturePath, "src/components/ui/button.tsx"))
|
||||||
|
).toBe(true)
|
||||||
|
|
||||||
|
const alertDialogContent = await fs.readFile(
|
||||||
|
path.join(fixturePath, "src/components/ui/alert-dialog.tsx"),
|
||||||
|
"utf-8"
|
||||||
|
)
|
||||||
|
expect(alertDialogContent).toContain(
|
||||||
|
'import { buttonVariants } from "#custom/components/ui/button"'
|
||||||
|
)
|
||||||
|
expect(alertDialogContent).toContain(
|
||||||
|
'import { cn } from "#custom/lib/utils"'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
106
packages/tests/src/utils/helpers.ts
Normal file
106
packages/tests/src/utils/helpers.ts
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import path from "path"
|
||||||
|
import { fileURLToPath } from "url"
|
||||||
|
import { execa } from "execa"
|
||||||
|
import fs from "fs-extra"
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
const FIXTURES_DIR = path.join(__dirname, "../../fixtures")
|
||||||
|
const TEMP_DIR = path.join(__dirname, "../../temp")
|
||||||
|
const CACHE_DIR = path.join(__dirname, "../../.cache")
|
||||||
|
const SHADCN_CLI_PATH = path.join(__dirname, "../../../shadcn/dist/index.js")
|
||||||
|
|
||||||
|
export async function fileExists(filePath: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await fs.access(filePath)
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function readJson(filePath: string): Promise<any> {
|
||||||
|
return fs.readJSON(filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createFixtureTestDirectory(fixtureName: string) {
|
||||||
|
const fixturePath = path.join(FIXTURES_DIR, fixtureName)
|
||||||
|
const testDir = path.join(
|
||||||
|
TEMP_DIR,
|
||||||
|
`test-${Date.now()}-${Math.random().toString(36).substring(7)}`
|
||||||
|
)
|
||||||
|
|
||||||
|
await fs.ensureDir(TEMP_DIR)
|
||||||
|
await fs.copy(fixturePath, testDir)
|
||||||
|
|
||||||
|
return testDir
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runCommand(
|
||||||
|
cwd: string,
|
||||||
|
args: string[],
|
||||||
|
options?: {
|
||||||
|
env?: Record<string, string>
|
||||||
|
input?: string
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const childProcess = execa("node", [SHADCN_CLI_PATH, ...args], {
|
||||||
|
cwd,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
FORCE_COLOR: "0",
|
||||||
|
CI: "true",
|
||||||
|
...options?.env,
|
||||||
|
},
|
||||||
|
input: options?.input,
|
||||||
|
reject: false,
|
||||||
|
timeout: 30000,
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await childProcess
|
||||||
|
|
||||||
|
return {
|
||||||
|
stdout: result.stdout || "",
|
||||||
|
stderr: result.stderr || "",
|
||||||
|
exitCode: result.exitCode ?? 0,
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
stdout: error.stdout || "",
|
||||||
|
stderr: error.stderr || error.message || "",
|
||||||
|
exitCode: error.exitCode ?? 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function npxShadcn(cwd: string, args: string[]) {
|
||||||
|
const { getRegistryUrl } = await import("./setup")
|
||||||
|
|
||||||
|
await fs.ensureDir(CACHE_DIR)
|
||||||
|
|
||||||
|
return runCommand(cwd, args, {
|
||||||
|
env: {
|
||||||
|
REGISTRY_URL: getRegistryUrl(),
|
||||||
|
SHADCN_CACHE_DIR: CACHE_DIR,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cssHasProperties(
|
||||||
|
cssContent: string,
|
||||||
|
checks: Array<{
|
||||||
|
selector: string
|
||||||
|
properties: Record<string, string>
|
||||||
|
}>
|
||||||
|
): boolean {
|
||||||
|
return checks.every(({ selector, properties }) => {
|
||||||
|
const escapedSelector = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
||||||
|
const regex = new RegExp(`${escapedSelector}\\s*{([^}]+)}`, "s")
|
||||||
|
const match = cssContent.match(regex)
|
||||||
|
const block = match ? match[1] : ""
|
||||||
|
|
||||||
|
return Object.entries(properties).every(([property, value]) =>
|
||||||
|
block.includes(`${property}: ${value};`)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
67
packages/tests/src/utils/registry.ts
Normal file
67
packages/tests/src/utils/registry.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import path from "path"
|
||||||
|
import { fileURLToPath } from "url"
|
||||||
|
import { execa, type ExecaChildProcess } from "execa"
|
||||||
|
import waitPort from "wait-port"
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
const ROOT_DIR = path.join(__dirname, "../../../..")
|
||||||
|
const PORT = 4000
|
||||||
|
|
||||||
|
export class Registry {
|
||||||
|
private process?: ExecaChildProcess
|
||||||
|
private _url?: string
|
||||||
|
|
||||||
|
get url(): string {
|
||||||
|
if (!this._url) {
|
||||||
|
throw new Error("Registry not started. Call start() first.")
|
||||||
|
}
|
||||||
|
return this._url
|
||||||
|
}
|
||||||
|
|
||||||
|
async start(): Promise<void> {
|
||||||
|
if (this.process) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.process = execa("pnpm", ["v4:dev"], {
|
||||||
|
cwd: ROOT_DIR,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
NODE_ENV: "development",
|
||||||
|
},
|
||||||
|
stdio: "pipe",
|
||||||
|
reject: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
await waitPort({
|
||||||
|
port: PORT,
|
||||||
|
host: "localhost",
|
||||||
|
timeout: 60000,
|
||||||
|
interval: 1000,
|
||||||
|
})
|
||||||
|
|
||||||
|
this._url = `http://localhost:${PORT}/r`
|
||||||
|
} catch (error) {
|
||||||
|
this.process.kill()
|
||||||
|
this.process = undefined
|
||||||
|
throw new Error(`Registry failed to start on port ${PORT}: ${error}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async stop(): Promise<void> {
|
||||||
|
if (!this.process) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.process.kill("SIGTERM")
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||||
|
|
||||||
|
if (!this.process.killed) {
|
||||||
|
this.process.kill("SIGKILL")
|
||||||
|
}
|
||||||
|
|
||||||
|
this.process = undefined
|
||||||
|
this._url = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
37
packages/tests/src/utils/setup.ts
Normal file
37
packages/tests/src/utils/setup.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/* eslint-disable turbo/no-undeclared-env-vars */
|
||||||
|
import path from "path"
|
||||||
|
import { fileURLToPath } from "url"
|
||||||
|
import { rimraf } from "rimraf"
|
||||||
|
import { afterAll, beforeAll } from "vitest"
|
||||||
|
|
||||||
|
import { Registry } from "./registry"
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
const TEMP_DIR = path.join(__dirname, "../../temp")
|
||||||
|
|
||||||
|
let globalRegistry: Registry | null = null
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await rimraf(TEMP_DIR)
|
||||||
|
|
||||||
|
if (!globalRegistry) {
|
||||||
|
globalRegistry = new Registry()
|
||||||
|
await globalRegistry.start()
|
||||||
|
|
||||||
|
process.env.TEST_REGISTRY_URL = globalRegistry.url
|
||||||
|
}
|
||||||
|
}, 120000)
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
if (globalRegistry) {
|
||||||
|
await globalRegistry.stop()
|
||||||
|
globalRegistry = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also clean up temp directory after all tests
|
||||||
|
await rimraf(TEMP_DIR)
|
||||||
|
})
|
||||||
|
|
||||||
|
export function getRegistryUrl(): string {
|
||||||
|
return process.env.TEST_REGISTRY_URL || "http://localhost:4000/r"
|
||||||
|
}
|
||||||
19
packages/tests/tsconfig.json
Normal file
19
packages/tests/tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "ESNext",
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2022"],
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"noEmit": true,
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"types": ["vitest/globals", "node"]
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"exclude": ["node_modules", "temp", "fixtures"]
|
||||||
|
}
|
||||||
19
packages/tests/vitest.config.ts
Normal file
19
packages/tests/vitest.config.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import tsconfigPaths from "vite-tsconfig-paths"
|
||||||
|
import { defineConfig } from "vitest/config"
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
testTimeout: 60000,
|
||||||
|
hookTimeout: 120000,
|
||||||
|
globals: true,
|
||||||
|
environment: "node",
|
||||||
|
setupFiles: ["./src/utils/setup.ts"],
|
||||||
|
maxConcurrency: 4,
|
||||||
|
isolate: true,
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
tsconfigPaths({
|
||||||
|
ignoreConfigErrors: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
1142
pnpm-lock.yaml
generated
1142
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,7 @@ export default defineConfig({
|
|||||||
"**/node_modules/**",
|
"**/node_modules/**",
|
||||||
"**/fixtures/**",
|
"**/fixtures/**",
|
||||||
"**/templates/**",
|
"**/templates/**",
|
||||||
|
"**/packages/tests/**",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
import { defineWorkspace } from "vitest/config"
|
import { defineWorkspace } from "vitest/config"
|
||||||
|
|
||||||
export default defineWorkspace(["./vitest.config.ts"])
|
export default defineWorkspace([
|
||||||
|
"./vitest.config.ts",
|
||||||
|
"./packages/tests/vitest.config.ts",
|
||||||
|
])
|
||||||
|
|||||||
Reference in New Issue
Block a user