← Vue Menu · ← Prev: API & Composables
npm install -D vitest @vue/test-utils happy-dom
# vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
test: {
environment: 'happy-dom',
globals: true, // no need to import test/expect
},
})
# Run tests
npx vitest # watch mode
npx vitest run # single run (CI)
npx vitest --coverage # with coverage report
// composables/useCounter.js
import { ref } from 'vue'
export function useCounter(initial = 0) {
const count = ref(initial)
const inc = () => count.value++
const dec = () => count.value--
return { count, inc, dec }
}
// tests/useCounter.test.js
import { describe, it, expect } from 'vitest'
import { useCounter } from '@/composables/useCounter'
describe('useCounter', () => {
it('starts at initial value', () => {
const { count } = useCounter(5)
expect(count.value).toBe(5)
})
it('increments correctly', () => {
const { count, inc } = useCounter(0)
inc()
inc()
expect(count.value).toBe(2)
})
it('decrements correctly', () => {
const { count, dec } = useCounter(10)
dec()
expect(count.value).toBe(9)
})
})
// components/ProductCard.vue (simplified)
// <template><div><h3>{{product.name}}</h3>
// <button @click="$emit('delete', product.id)">Delete</button>
// </div></template>
// tests/ProductCard.test.js
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import ProductCard from '@/components/ProductCard.vue'
const mockProduct = { id: 1, name: 'Test Phone', price: 299 }
describe('ProductCard', () => {
it('renders product name', () => {
const wrapper = mount(ProductCard, {
props: { product: mockProduct }
})
expect(wrapper.find('h3').text()).toBe('Test Phone')
})
it('emits delete event with product id', async () => {
const wrapper = mount(ProductCard, {
props: { product: mockProduct }
})
await wrapper.find('button').trigger('click')
expect(wrapper.emitted('delete')).toBeTruthy()
expect(wrapper.emitted('delete')[0]).toEqual([1])
})
it('shows price formatted', () => {
const wrapper = mount(ProductCard, { props: { product: mockProduct } })
expect(wrapper.text()).toContain('299')
})
})
// tests/useProductStore.test.js
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useProductStore } from '@/stores/useProductStore'
// Mock the API
vi.mock('@/services/api', () => ({
default: {
get: vi.fn().mockResolvedValue({
data: [{ id: 1, name: 'Phone', price: 299 }]
}),
delete: vi.fn().mockResolvedValue({}),
}
}))
describe('useProductStore', () => {
beforeEach(() => {
setActivePinia(createPinia()) // fresh store each test
})
it('fetches products', async () => {
const store = useProductStore()
await store.fetchAll()
expect(store.products).toHaveLength(1)
expect(store.products[0].name).toBe('Phone')
})
it('removes product', async () => {
const store = useProductStore()
store.products = [{ id: 1, name: 'Phone' }, { id: 2, name: 'Laptop' }]
await store.removeProduct(1)
expect(store.products).toHaveLength(1)
expect(store.products[0].id).toBe(2)
})
})
# Build npm run build # outputs to dist/ npm run preview # preview built version locally # dist/ contents: dist/ ├── index.html ├── assets/ │ ├── index-a1b2c3.js ← bundled JS (hashed for cache busting) │ ├── index-x4y5z6.css ← bundled CSS │ └── logo-abcdef.png # Environment variables for production # .env.production VITE_API_BASE_URL=https://api.yourdomain.com # Build with specific mode npm run build -- --mode staging
| Platform | Command | Cost | Notes |
|---|---|---|---|
| GitHub Pages | Push to gh-pages branch | Free | Static only |
| Netlify | drag & drop dist/ or CLI | Free tier | Auto-deploy from GitHub |
| Vercel | vercel deploy | Free tier | Best DX, preview URLs |
| Nginx VPS | Copy dist/ to /var/www/ | VPS cost | Full control, SPA routing config |
| Firebase Hosting | firebase deploy | Free tier | Google CDN |
# netlify.toml (SPA routing fix)
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
# .github/workflows/deploy.yml
name: Deploy to Netlify
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: npm ci && npm run build
- uses: netlify/actions/cli@master
with:
args: deploy --prod --dir=dist
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
# /etc/nginx/sites-available/vue-app
server {
listen 80;
server_name yourdomain.com;
root /var/www/vue-app/dist;
index index.html;
# SPA: all routes → index.html
location / {
try_files $uri $uri/ /index.html;
}
# Cache static assets aggressively (hashed filenames)
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
gzip on;
gzip_types text/css application/javascript;
}
# Analyze bundle size
npm install -D rollup-plugin-visualizer
# vite.config.js
import { visualizer } from 'rollup-plugin-visualizer'
plugins: [vue(), visualizer({ open: true })]
# Code splitting — already automatic with lazy imports
const ProductsPage = () => import('@/pages/ProductsPage.vue')
# Preload important routes
const routes = [
{ path: '/products', component: ProductsPage,
props: true },
]
# Tips:
# - Use v-show over v-if for frequently toggled elements
# - Use :key on v-for to enable efficient patching
# - Use computed for derived values (cached)
# - Avoid large reactive objects — keep state minimal
# - Use defineAsyncComponent for heavy components
CDN Basics → Vite + SFC → Composition API → Router → Pinia → API → Testing → Deploy
Vue.js 17 Lessons မြောက် — အားကုန် Vue ကိုသင်ပြီး Real Projects ဆောက်ဖို့ Ready !