diff --git a/app.config.ts b/app.config.ts new file mode 100644 index 0000000..1e58d65 --- /dev/null +++ b/app.config.ts @@ -0,0 +1,7 @@ +export default defineAppConfig({ + ui: { + primary: "violet", + gray: "neutral", + strategy: "class", + }, +}); diff --git a/app/app.vue b/app/app.vue index 09f935b..ae0d903 100644 --- a/app/app.vue +++ b/app/app.vue @@ -1,6 +1,20 @@ + + diff --git a/app/components/ColorModeToggle.vue b/app/components/ColorModeToggle.vue new file mode 100644 index 0000000..868603f --- /dev/null +++ b/app/components/ColorModeToggle.vue @@ -0,0 +1,17 @@ + + + diff --git a/assets/css/main.css b/assets/css/main.css new file mode 100644 index 0000000..e4ea4c3 --- /dev/null +++ b/assets/css/main.css @@ -0,0 +1,12 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --ui-primary: 237 85% 62%; +} + +.dark { + --ui-primary: 237 85% 70%; +} + diff --git a/composables/useFixtureIO.ts b/composables/useFixtureIO.ts new file mode 100644 index 0000000..8a5ef41 --- /dev/null +++ b/composables/useFixtureIO.ts @@ -0,0 +1,25 @@ +import { useCounterStore } from '~/stores/counter' + +export type AppSnapshot = { + counter: { count: number } +} + +export function useFixtureIO() { + const exportAll = (): AppSnapshot => { + const counter = useCounterStore() + return { + counter: { count: counter.count } + } + } + + const importAll = (snapshot: AppSnapshot) => { + const counter = useCounterStore() + if (snapshot?.counter) { + counter.$patch({ count: snapshot.counter.count ?? 0 }) + } + } + + return { exportAll, importAll } +} + + diff --git a/nuxt.config.ts b/nuxt.config.ts index b6baa24..63867d7 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -1,5 +1,27 @@ +import { defineNuxtConfig } from "nuxt/config"; // https://nuxt.com/docs/api/configuration/nuxt-config export default defineNuxtConfig({ - compatibilityDate: '2025-07-15', - devtools: { enabled: true } -}) + compatibilityDate: "2025-07-15", + devtools: { enabled: true }, + + // Keep SSR on (default) with Nitro server + ssr: true, + + // Strict TypeScript + typescript: { + strict: true, + }, + + // Global CSS (Tailwind for @nuxt/ui) + css: ["~/assets/css/main.css"], + + modules: [ + "@pinia/nuxt", + "@nuxt/ui", + "@nuxtjs/color-mode", + "@nuxtjs/tailwindcss", + ], + + + // Nuxt UI minimal theme customizations live in app.config.ts +}); diff --git a/package-lock.json b/package-lock.json index 81b0ae5..67dd9a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ }, "devDependencies": { "@playwright/test": "^1.54.2", + "@tailwindcss/postcss": "^4.1.11", "@vitejs/plugin-vue": "^6.0.1", "@vue/test-utils": "^2.4.6", "typescript": "^5.9.2", diff --git a/package.json b/package.json index 89c20e3..c885af7 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,10 @@ "dev": "nuxt dev", "generate": "nuxt generate", "preview": "nuxt preview", - "postinstall": "nuxt prepare" + "postinstall": "nuxt prepare", + "test": "vitest --run", + "test:ui": "vitest", + "test:e2e": "playwright test" }, "dependencies": { "@nuxt/ui": "^3.3.0", @@ -22,6 +25,7 @@ }, "devDependencies": { "@playwright/test": "^1.54.2", + "@tailwindcss/postcss": "^4.1.11", "@vitejs/plugin-vue": "^6.0.1", "@vue/test-utils": "^2.4.6", "typescript": "^5.9.2", diff --git a/pages/index.vue b/pages/index.vue new file mode 100644 index 0000000..8bb75c4 --- /dev/null +++ b/pages/index.vue @@ -0,0 +1,62 @@ + + + diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..2ca18c9 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from '@playwright/test' + +export default defineConfig({ + webServer: { + command: 'npm run dev', + port: 3000, + timeout: 60_000, + reuseExistingServer: true + }, + use: { + baseURL: 'http://localhost:3000' + } +}) + + diff --git a/plugins/piniaPersistedState.client.ts b/plugins/piniaPersistedState.client.ts new file mode 100644 index 0000000..a7d6242 --- /dev/null +++ b/plugins/piniaPersistedState.client.ts @@ -0,0 +1,7 @@ +import { defineNuxtPlugin } from "#app"; +import { createPersistedState } from "pinia-plugin-persistedstate"; + +export default defineNuxtPlugin((nuxtApp) => { + // Register persisted state plugin for Pinia on client + nuxtApp.$pinia.use(createPersistedState()); +}); diff --git a/server/api/fixtures.get.ts b/server/api/fixtures.get.ts new file mode 100644 index 0000000..a05d740 --- /dev/null +++ b/server/api/fixtures.get.ts @@ -0,0 +1,10 @@ +import { defineEventHandler } from 'h3' +import { useFixtureIO } from '~/composables/useFixtureIO' + +export default defineEventHandler(() => { + // Export snapshot of in-memory state + const { exportAll } = useFixtureIO() + return exportAll() +}) + + diff --git a/server/api/fixtures.post.ts b/server/api/fixtures.post.ts new file mode 100644 index 0000000..0e1a7e7 --- /dev/null +++ b/server/api/fixtures.post.ts @@ -0,0 +1,11 @@ +import { defineEventHandler, readBody } from 'h3' +import { useFixtureIO, type AppSnapshot } from '~/composables/useFixtureIO' + +export default defineEventHandler(async (event) => { + const body = (await readBody(event)) as AppSnapshot + const { importAll } = useFixtureIO() + importAll(body) + return { ok: true } +}) + + diff --git a/stores/counter.ts b/stores/counter.ts new file mode 100644 index 0000000..d6afa81 --- /dev/null +++ b/stores/counter.ts @@ -0,0 +1,18 @@ +import { defineStore } from "pinia"; + +export const useCounterStore = defineStore("counter", { + state: () => ({ + count: 0, + }), + getters: { + doubleCount: (state) => state.count * 2, + }, + actions: { + increment() { + this.count += 1; + }, + }, + persist: { + paths: ["count"], + }, +}); diff --git a/tests/e2e/example.spec.ts b/tests/e2e/example.spec.ts new file mode 100644 index 0000000..aa0a368 --- /dev/null +++ b/tests/e2e/example.spec.ts @@ -0,0 +1,10 @@ +import { test, expect } from '@playwright/test' + +test('happy path: home loads and increments', async ({ page }) => { + await page.goto('/') + await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible() + await page.getByRole('button', { name: /Increment/ }).click() + await expect(page.getByText(/Count: 1/)).toBeVisible() +}) + + diff --git a/tests/example.spec.ts b/tests/example.spec.ts new file mode 100644 index 0000000..deefac5 --- /dev/null +++ b/tests/example.spec.ts @@ -0,0 +1,9 @@ +import { describe, it, expect } from 'vitest' + +describe('math', () => { + it('adds', () => { + expect(1 + 1).toBe(2) + }) +}) + + diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..74d19c6 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'vitest/config' +import vue from '@vitejs/plugin-vue' + +export default defineConfig({ + plugins: [vue()], + test: { + globals: true, + environment: 'jsdom' + } +}) + +