phos 1.0.3 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/package.json +1 -1
- package/src/templates/backend/elysia/src/api/user_api.ts +313 -0
- package/src/templates/backend/elysia/src/function/helper.ts +111 -0
- package/src/templates/backend/elysia/src/service/user_service.ts +109 -0
- package/src/templates/backend/elysia/src/sql/user_sql.ts +191 -0
- package/src/templates/backend/elysia/src/types/user_type.ts +54 -0
- package/src/templates/backend/fastapi/src/api/user_api.py +163 -0
- package/src/templates/backend/fastapi/src/function/__init__.py +0 -0
- package/src/templates/backend/fastapi/src/function/helper.py +107 -0
- package/src/templates/backend/fastapi/src/service/user_service.py +94 -0
- package/src/templates/backend/fastapi/src/sql/user_sql.py +197 -0
- package/src/templates/backend/fastapi/src/types/user_type.py +64 -0
- package/src/templates/frontend/vue/.editorconfig +8 -0
- package/src/templates/frontend/vue/.gitattributes +1 -0
- package/src/templates/frontend/vue/.oxlintrc.json +10 -0
- package/src/templates/frontend/vue/.prettierrc.json +6 -0
- package/src/templates/frontend/vue/.vscode/extensions.json +11 -0
- package/src/templates/frontend/vue/README.md +73 -0
- package/src/templates/frontend/vue/e2e/tsconfig.json +4 -0
- package/src/templates/frontend/vue/e2e/vue.spec.ts +8 -0
- package/src/templates/frontend/vue/env.d.ts +1 -0
- package/src/templates/frontend/vue/eslint.config.ts +38 -0
- package/src/templates/frontend/vue/index.html +13 -0
- package/src/templates/frontend/vue/package.json +54 -0
- package/src/templates/frontend/vue/playwright.config.ts +110 -0
- package/src/templates/frontend/vue/public/favicon.ico +0 -0
- package/src/templates/frontend/vue/src/App.vue +11 -0
- package/src/templates/frontend/vue/src/__tests__/App.spec.ts +11 -0
- package/src/templates/frontend/vue/src/main.ts +12 -0
- package/src/templates/frontend/vue/src/router/index.ts +8 -0
- package/src/templates/frontend/vue/src/stores/counter.ts +12 -0
- package/src/templates/frontend/vue/tsconfig.app.json +12 -0
- package/src/templates/frontend/vue/tsconfig.json +14 -0
- package/src/templates/frontend/vue/tsconfig.node.json +19 -0
- package/src/templates/frontend/vue/tsconfig.vitest.json +11 -0
- package/src/templates/frontend/vue/vite.config.ts +20 -0
- package/src/templates/frontend/vue/vitest.config.ts +14 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import bcrypt
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from src.db import get_db
|
|
4
|
+
from src.types.user_type import User, CreateUserDto, UpdateUserDto
|
|
5
|
+
|
|
6
|
+
SALT_ROUNDS = 10
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def get_users():
|
|
10
|
+
query = """
|
|
11
|
+
SELECT id, email, username, full_name, password, avatar_url, bio, role, is_active, created_at, updated_at
|
|
12
|
+
FROM users
|
|
13
|
+
WHERE is_active = true
|
|
14
|
+
ORDER BY created_at DESC
|
|
15
|
+
"""
|
|
16
|
+
conn = await get_db()
|
|
17
|
+
result = await conn.fetch(query)
|
|
18
|
+
return result
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def get_user_by_id(user_id: int):
|
|
22
|
+
query = """
|
|
23
|
+
SELECT id, email, username, full_name, password, avatar_url, bio, role, is_active, created_at, updated_at
|
|
24
|
+
FROM users
|
|
25
|
+
WHERE id = $1
|
|
26
|
+
"""
|
|
27
|
+
conn = await get_db()
|
|
28
|
+
result = await conn.fetchrow(query, user_id)
|
|
29
|
+
return result
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
async def get_user_by_email(email: str):
|
|
33
|
+
query = """
|
|
34
|
+
SELECT id, email, username, full_name, password, avatar_url, bio, role, is_active, created_at, updated_at
|
|
35
|
+
FROM users
|
|
36
|
+
WHERE email = $1
|
|
37
|
+
"""
|
|
38
|
+
conn = await get_db()
|
|
39
|
+
result = await conn.fetchrow(query, email)
|
|
40
|
+
return result
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
async def get_user_by_username(username: str):
|
|
44
|
+
query = """
|
|
45
|
+
SELECT id, email, username, full_name, password, avatar_url, bio, role, is_active, created_at, updated_at
|
|
46
|
+
FROM users
|
|
47
|
+
WHERE username = $1
|
|
48
|
+
"""
|
|
49
|
+
conn = await get_db()
|
|
50
|
+
result = await conn.fetchrow(query, username)
|
|
51
|
+
return result
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
async def create_user(data: CreateUserDto):
|
|
55
|
+
hashed_password = bcrypt.hashpw(data.password.encode("utf-8"), bcrypt.gensalt(SALT_ROUNDS)).decode("utf-8")
|
|
56
|
+
|
|
57
|
+
query = """
|
|
58
|
+
INSERT INTO users (email, username, full_name, password, avatar_url, bio, role, is_active, created_at, updated_at)
|
|
59
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
60
|
+
RETURNING id, email, username, full_name, password, avatar_url, bio, role, is_active, created_at, updated_at
|
|
61
|
+
"""
|
|
62
|
+
conn = await get_db()
|
|
63
|
+
result = await conn.fetchrow(
|
|
64
|
+
query,
|
|
65
|
+
data.email,
|
|
66
|
+
data.username,
|
|
67
|
+
data.full_name,
|
|
68
|
+
hashed_password,
|
|
69
|
+
data.avatar_url,
|
|
70
|
+
data.bio,
|
|
71
|
+
data.role or "user",
|
|
72
|
+
)
|
|
73
|
+
return result
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
async def update_user(user_id: int, data: UpdateUserDto):
|
|
77
|
+
set_parts = []
|
|
78
|
+
values = []
|
|
79
|
+
param_index = 1
|
|
80
|
+
|
|
81
|
+
if data.email is not None:
|
|
82
|
+
set_parts.append(f"email = ${param_index}")
|
|
83
|
+
values.append(data.email)
|
|
84
|
+
param_index += 1
|
|
85
|
+
|
|
86
|
+
if data.username is not None:
|
|
87
|
+
set_parts.append(f"username = ${param_index}")
|
|
88
|
+
values.append(data.username)
|
|
89
|
+
param_index += 1
|
|
90
|
+
|
|
91
|
+
if data.full_name is not None:
|
|
92
|
+
set_parts.append(f"full_name = ${param_index}")
|
|
93
|
+
values.append(data.full_name)
|
|
94
|
+
param_index += 1
|
|
95
|
+
|
|
96
|
+
if data.password is not None:
|
|
97
|
+
hashed_password = bcrypt.hashpw(data.password.encode("utf-8"), bcrypt.gensalt(SALT_ROUNDS)).decode("utf-8")
|
|
98
|
+
set_parts.append(f"password = ${param_index}")
|
|
99
|
+
values.append(hashed_password)
|
|
100
|
+
param_index += 1
|
|
101
|
+
|
|
102
|
+
if data.avatar_url is not None:
|
|
103
|
+
set_parts.append(f"avatar_url = ${param_index}")
|
|
104
|
+
values.append(data.avatar_url)
|
|
105
|
+
param_index += 1
|
|
106
|
+
|
|
107
|
+
if data.bio is not None:
|
|
108
|
+
set_parts.append(f"bio = ${param_index}")
|
|
109
|
+
values.append(data.bio)
|
|
110
|
+
param_index += 1
|
|
111
|
+
|
|
112
|
+
if data.role is not None:
|
|
113
|
+
set_parts.append(f"role = ${param_index}")
|
|
114
|
+
values.append(data.role)
|
|
115
|
+
param_index += 1
|
|
116
|
+
|
|
117
|
+
if data.is_active is not None:
|
|
118
|
+
set_parts.append(f"is_active = ${param_index}")
|
|
119
|
+
values.append(data.is_active)
|
|
120
|
+
param_index += 1
|
|
121
|
+
|
|
122
|
+
if not set_parts:
|
|
123
|
+
return await get_user_by_id(user_id)
|
|
124
|
+
|
|
125
|
+
set_parts.append("updated_at = CURRENT_TIMESTAMP")
|
|
126
|
+
query = f"""
|
|
127
|
+
UPDATE users
|
|
128
|
+
SET {', '.join(set_parts)}
|
|
129
|
+
WHERE id = ${param_index}
|
|
130
|
+
RETURNING id, email, username, full_name, password, avatar_url, bio, role, is_active, created_at, updated_at
|
|
131
|
+
"""
|
|
132
|
+
values.append(user_id)
|
|
133
|
+
|
|
134
|
+
conn = await get_db()
|
|
135
|
+
result = await conn.fetchrow(query, *values)
|
|
136
|
+
return result
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
async def delete_user(user_id: int) -> bool:
|
|
140
|
+
query = """
|
|
141
|
+
DELETE FROM users
|
|
142
|
+
WHERE id = $1
|
|
143
|
+
"""
|
|
144
|
+
conn = await get_db()
|
|
145
|
+
result = await conn.execute(query, user_id)
|
|
146
|
+
return result == "DELETE 1"
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
async def soft_delete_user(user_id: int):
|
|
150
|
+
query = """
|
|
151
|
+
UPDATE users
|
|
152
|
+
SET is_active = false, updated_at = CURRENT_TIMESTAMP
|
|
153
|
+
WHERE id = $1
|
|
154
|
+
RETURNING id, email, username, full_name, password, avatar_url, bio, role, is_active, created_at, updated_at
|
|
155
|
+
"""
|
|
156
|
+
conn = await get_db()
|
|
157
|
+
result = await conn.fetchrow(query, user_id)
|
|
158
|
+
return result
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
async def search_users(keyword: str):
|
|
162
|
+
query = """
|
|
163
|
+
SELECT id, email, username, full_name, password, avatar_url, bio, role, is_active, created_at, updated_at
|
|
164
|
+
FROM users
|
|
165
|
+
WHERE is_active = true
|
|
166
|
+
AND (email ILIKE $1 OR username ILIKE $1 OR full_name ILIKE $1)
|
|
167
|
+
ORDER BY created_at DESC
|
|
168
|
+
"""
|
|
169
|
+
conn = await get_db()
|
|
170
|
+
result = await conn.fetch(query, f"%{keyword}%")
|
|
171
|
+
return result
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
|
175
|
+
return bcrypt.checkpw(plain_password.encode("utf-8"), hashed_password.encode("utf-8"))
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
user_table_schema = """
|
|
179
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
180
|
+
id SERIAL PRIMARY KEY,
|
|
181
|
+
email VARCHAR(255) UNIQUE NOT NULL,
|
|
182
|
+
username VARCHAR(100) UNIQUE NOT NULL,
|
|
183
|
+
full_name VARCHAR(255) NOT NULL,
|
|
184
|
+
password VARCHAR(255) NOT NULL,
|
|
185
|
+
avatar_url TEXT,
|
|
186
|
+
bio TEXT,
|
|
187
|
+
role VARCHAR(50) DEFAULT 'user',
|
|
188
|
+
is_active BOOLEAN DEFAULT true,
|
|
189
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
190
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
|
194
|
+
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
|
|
195
|
+
CREATE INDEX IF NOT EXISTS idx_users_role ON users(role);
|
|
196
|
+
CREATE INDEX IF NOT EXISTS idx_users_is_active ON users(is_active);
|
|
197
|
+
"""
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from pydantic import BaseModel, EmailStr, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class User(BaseModel):
|
|
7
|
+
id: int
|
|
8
|
+
email: str
|
|
9
|
+
username: str
|
|
10
|
+
full_name: str
|
|
11
|
+
password: str
|
|
12
|
+
avatar_url: Optional[str] = None
|
|
13
|
+
bio: Optional[str] = None
|
|
14
|
+
role: str = "user"
|
|
15
|
+
is_active: bool = True
|
|
16
|
+
created_at: datetime
|
|
17
|
+
updated_at: datetime
|
|
18
|
+
|
|
19
|
+
class Config:
|
|
20
|
+
from_attributes = True
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CreateUserDto(BaseModel):
|
|
24
|
+
email: EmailStr
|
|
25
|
+
username: str = Field(..., min_length=3, max_length=100)
|
|
26
|
+
full_name: str = Field(..., min_length=1, max_length=255)
|
|
27
|
+
password: str = Field(..., min_length=6, max_length=255)
|
|
28
|
+
avatar_url: Optional[str] = Field(None, max_length=1000)
|
|
29
|
+
bio: Optional[str] = Field(None, max_length=1000)
|
|
30
|
+
role: Optional[str] = Field(None, max_length=50)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class UpdateUserDto(BaseModel):
|
|
34
|
+
email: Optional[EmailStr] = None
|
|
35
|
+
username: Optional[str] = Field(None, min_length=3, max_length=100)
|
|
36
|
+
full_name: Optional[str] = Field(None, min_length=1, max_length=255)
|
|
37
|
+
password: Optional[str] = Field(None, min_length=6, max_length=255)
|
|
38
|
+
avatar_url: Optional[str] = Field(None, max_length=1000)
|
|
39
|
+
bio: Optional[str] = Field(None, max_length=1000)
|
|
40
|
+
role: Optional[str] = Field(None, max_length=50)
|
|
41
|
+
is_active: Optional[bool] = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class UserResponse(BaseModel):
|
|
45
|
+
id: int
|
|
46
|
+
email: str
|
|
47
|
+
username: str
|
|
48
|
+
full_name: str
|
|
49
|
+
avatar_url: Optional[str] = None
|
|
50
|
+
bio: Optional[str] = None
|
|
51
|
+
role: str
|
|
52
|
+
is_active: bool
|
|
53
|
+
created_at: datetime
|
|
54
|
+
updated_at: datetime
|
|
55
|
+
|
|
56
|
+
class Config:
|
|
57
|
+
from_attributes = True
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ApiResponse(BaseModel):
|
|
61
|
+
success: bool
|
|
62
|
+
data: Optional[any] = None
|
|
63
|
+
message: Optional[str] = None
|
|
64
|
+
error: Optional[str] = None
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
* text=auto eol=lf
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# vue
|
|
2
|
+
|
|
3
|
+
This template should help get you started developing with Vue 3 in Vite.
|
|
4
|
+
|
|
5
|
+
## Recommended IDE Setup
|
|
6
|
+
|
|
7
|
+
[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
|
8
|
+
|
|
9
|
+
## Recommended Browser Setup
|
|
10
|
+
|
|
11
|
+
- Chromium-based browsers (Chrome, Edge, Brave, etc.):
|
|
12
|
+
- [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
|
|
13
|
+
- [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters)
|
|
14
|
+
- Firefox:
|
|
15
|
+
- [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
|
|
16
|
+
- [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/)
|
|
17
|
+
|
|
18
|
+
## Type Support for `.vue` Imports in TS
|
|
19
|
+
|
|
20
|
+
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
|
|
21
|
+
|
|
22
|
+
## Customize configuration
|
|
23
|
+
|
|
24
|
+
See [Vite Configuration Reference](https://vite.dev/config/).
|
|
25
|
+
|
|
26
|
+
## Project Setup
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
npm install
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Compile and Hot-Reload for Development
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
npm run dev
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Type-Check, Compile and Minify for Production
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
npm run build
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Run Unit Tests with [Vitest](https://vitest.dev/)
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
npm run test:unit
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Run End-to-End Tests with [Playwright](https://playwright.dev)
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
# Install browsers for the first run
|
|
54
|
+
npx playwright install
|
|
55
|
+
|
|
56
|
+
# When testing on CI, must build the project first
|
|
57
|
+
npm run build
|
|
58
|
+
|
|
59
|
+
# Runs the end-to-end tests
|
|
60
|
+
npm run test:e2e
|
|
61
|
+
# Runs the tests only on Chromium
|
|
62
|
+
npm run test:e2e -- --project=chromium
|
|
63
|
+
# Runs the tests of a specific file
|
|
64
|
+
npm run test:e2e -- tests/example.spec.ts
|
|
65
|
+
# Runs the tests in debug mode
|
|
66
|
+
npm run test:e2e -- --debug
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Lint with [ESLint](https://eslint.org/)
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
npm run lint
|
|
73
|
+
```
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { test, expect } from '@playwright/test'
|
|
2
|
+
|
|
3
|
+
// See here how to get started:
|
|
4
|
+
// https://playwright.dev/docs/intro
|
|
5
|
+
test('visits the app root url', async ({ page }) => {
|
|
6
|
+
await page.goto('/')
|
|
7
|
+
await expect(page.locator('h1')).toHaveText('You did it!')
|
|
8
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { globalIgnores } from 'eslint/config'
|
|
2
|
+
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
|
|
3
|
+
import pluginVue from 'eslint-plugin-vue'
|
|
4
|
+
import pluginPlaywright from 'eslint-plugin-playwright'
|
|
5
|
+
import pluginVitest from '@vitest/eslint-plugin'
|
|
6
|
+
import pluginOxlint from 'eslint-plugin-oxlint'
|
|
7
|
+
import skipFormatting from 'eslint-config-prettier/flat'
|
|
8
|
+
|
|
9
|
+
// To allow more languages other than `ts` in `.vue` files, uncomment the following lines:
|
|
10
|
+
// import { configureVueProject } from '@vue/eslint-config-typescript'
|
|
11
|
+
// configureVueProject({ scriptLangs: ['ts', 'tsx'] })
|
|
12
|
+
// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup
|
|
13
|
+
|
|
14
|
+
export default defineConfigWithVueTs(
|
|
15
|
+
{
|
|
16
|
+
name: 'app/files-to-lint',
|
|
17
|
+
files: ['**/*.{vue,ts,mts,tsx}'],
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
|
|
21
|
+
|
|
22
|
+
...pluginVue.configs['flat/essential'],
|
|
23
|
+
vueTsConfigs.recommended,
|
|
24
|
+
|
|
25
|
+
{
|
|
26
|
+
...pluginPlaywright.configs['flat/recommended'],
|
|
27
|
+
files: ['e2e/**/*.{test,spec}.{js,ts,jsx,tsx}'],
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
{
|
|
31
|
+
...pluginVitest.configs.recommended,
|
|
32
|
+
files: ['src/**/__tests__/*'],
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
...pluginOxlint.buildFromOxlintConfigFile('.oxlintrc.json'),
|
|
36
|
+
|
|
37
|
+
skipFormatting,
|
|
38
|
+
)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<link rel="icon" href="/favicon.ico">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
<title>Vite App</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="app"></div>
|
|
11
|
+
<script type="module" src="/src/main.ts"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vue",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "run-p type-check \"build-only {@}\" --",
|
|
9
|
+
"preview": "vite preview",
|
|
10
|
+
"test:unit": "vitest",
|
|
11
|
+
"test:e2e": "playwright test",
|
|
12
|
+
"build-only": "vite build",
|
|
13
|
+
"type-check": "vue-tsc --build",
|
|
14
|
+
"lint": "run-s lint:*",
|
|
15
|
+
"lint:oxlint": "oxlint . --fix",
|
|
16
|
+
"lint:eslint": "eslint . --fix --cache",
|
|
17
|
+
"format": "prettier --write --experimental-cli src/"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"pinia": "^3.0.4",
|
|
21
|
+
"vue": "^3.5.27",
|
|
22
|
+
"vue-router": "^5.0.1"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@playwright/test": "^1.58.1",
|
|
26
|
+
"@tsconfig/node24": "^24.0.4",
|
|
27
|
+
"@types/jsdom": "^27.0.0",
|
|
28
|
+
"@types/node": "^24.10.9",
|
|
29
|
+
"@vitejs/plugin-vue": "^6.0.3",
|
|
30
|
+
"@vitejs/plugin-vue-jsx": "^5.1.3",
|
|
31
|
+
"@vitest/eslint-plugin": "^1.6.6",
|
|
32
|
+
"@vue/eslint-config-typescript": "^14.6.0",
|
|
33
|
+
"@vue/test-utils": "^2.4.6",
|
|
34
|
+
"@vue/tsconfig": "^0.8.1",
|
|
35
|
+
"eslint": "^9.39.2",
|
|
36
|
+
"eslint-config-prettier": "^10.1.8",
|
|
37
|
+
"eslint-plugin-oxlint": "~1.42.0",
|
|
38
|
+
"eslint-plugin-playwright": "^2.5.1",
|
|
39
|
+
"eslint-plugin-vue": "~10.7.0",
|
|
40
|
+
"jiti": "^2.6.1",
|
|
41
|
+
"jsdom": "^27.4.0",
|
|
42
|
+
"npm-run-all2": "^8.0.4",
|
|
43
|
+
"oxlint": "~1.42.0",
|
|
44
|
+
"prettier": "3.8.1",
|
|
45
|
+
"typescript": "~5.9.3",
|
|
46
|
+
"vite": "^7.3.1",
|
|
47
|
+
"vite-plugin-vue-devtools": "^8.0.5",
|
|
48
|
+
"vitest": "^4.0.18",
|
|
49
|
+
"vue-tsc": "^3.2.4"
|
|
50
|
+
},
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": "^20.19.0 || >=22.12.0"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import process from 'node:process'
|
|
2
|
+
import { defineConfig, devices } from '@playwright/test'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Read environment variables from file.
|
|
6
|
+
* https://github.com/motdotla/dotenv
|
|
7
|
+
*/
|
|
8
|
+
// require('dotenv').config();
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* See https://playwright.dev/docs/test-configuration.
|
|
12
|
+
*/
|
|
13
|
+
export default defineConfig({
|
|
14
|
+
testDir: './e2e',
|
|
15
|
+
/* Maximum time one test can run for. */
|
|
16
|
+
timeout: 30 * 1000,
|
|
17
|
+
expect: {
|
|
18
|
+
/**
|
|
19
|
+
* Maximum time expect() should wait for the condition to be met.
|
|
20
|
+
* For example in `await expect(locator).toHaveText();`
|
|
21
|
+
*/
|
|
22
|
+
timeout: 5000,
|
|
23
|
+
},
|
|
24
|
+
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
|
25
|
+
forbidOnly: !!process.env.CI,
|
|
26
|
+
/* Retry on CI only */
|
|
27
|
+
retries: process.env.CI ? 2 : 0,
|
|
28
|
+
/* Opt out of parallel tests on CI. */
|
|
29
|
+
workers: process.env.CI ? 1 : undefined,
|
|
30
|
+
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
|
31
|
+
reporter: 'html',
|
|
32
|
+
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
|
33
|
+
use: {
|
|
34
|
+
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
|
|
35
|
+
actionTimeout: 0,
|
|
36
|
+
/* Base URL to use in actions like `await page.goto('/')`. */
|
|
37
|
+
baseURL: process.env.CI ? 'http://localhost:4173' : 'http://localhost:5173',
|
|
38
|
+
|
|
39
|
+
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
|
40
|
+
trace: 'on-first-retry',
|
|
41
|
+
|
|
42
|
+
/* Only on CI systems run the tests headless */
|
|
43
|
+
headless: !!process.env.CI,
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
/* Configure projects for major browsers */
|
|
47
|
+
projects: [
|
|
48
|
+
{
|
|
49
|
+
name: 'chromium',
|
|
50
|
+
use: {
|
|
51
|
+
...devices['Desktop Chrome'],
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'firefox',
|
|
56
|
+
use: {
|
|
57
|
+
...devices['Desktop Firefox'],
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'webkit',
|
|
62
|
+
use: {
|
|
63
|
+
...devices['Desktop Safari'],
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
/* Test against mobile viewports. */
|
|
68
|
+
// {
|
|
69
|
+
// name: 'Mobile Chrome',
|
|
70
|
+
// use: {
|
|
71
|
+
// ...devices['Pixel 5'],
|
|
72
|
+
// },
|
|
73
|
+
// },
|
|
74
|
+
// {
|
|
75
|
+
// name: 'Mobile Safari',
|
|
76
|
+
// use: {
|
|
77
|
+
// ...devices['iPhone 12'],
|
|
78
|
+
// },
|
|
79
|
+
// },
|
|
80
|
+
|
|
81
|
+
/* Test against branded browsers. */
|
|
82
|
+
// {
|
|
83
|
+
// name: 'Microsoft Edge',
|
|
84
|
+
// use: {
|
|
85
|
+
// channel: 'msedge',
|
|
86
|
+
// },
|
|
87
|
+
// },
|
|
88
|
+
// {
|
|
89
|
+
// name: 'Google Chrome',
|
|
90
|
+
// use: {
|
|
91
|
+
// channel: 'chrome',
|
|
92
|
+
// },
|
|
93
|
+
// },
|
|
94
|
+
],
|
|
95
|
+
|
|
96
|
+
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
|
|
97
|
+
// outputDir: 'test-results/',
|
|
98
|
+
|
|
99
|
+
/* Run your local dev server before starting the tests */
|
|
100
|
+
webServer: {
|
|
101
|
+
/**
|
|
102
|
+
* Use the dev server by default for faster feedback loop.
|
|
103
|
+
* Use the preview server on CI for more realistic testing.
|
|
104
|
+
* Playwright will re-use the local server if there is already a dev-server running.
|
|
105
|
+
*/
|
|
106
|
+
command: process.env.CI ? 'npm run preview' : 'npm run dev',
|
|
107
|
+
port: process.env.CI ? 4173 : 5173,
|
|
108
|
+
reuseExistingServer: !process.env.CI,
|
|
109
|
+
},
|
|
110
|
+
})
|
|
Binary file
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { mount } from '@vue/test-utils'
|
|
4
|
+
import App from '../App.vue'
|
|
5
|
+
|
|
6
|
+
describe('App', () => {
|
|
7
|
+
it('mounts renders properly', () => {
|
|
8
|
+
const wrapper = mount(App)
|
|
9
|
+
expect(wrapper.text()).toContain('You did it!')
|
|
10
|
+
})
|
|
11
|
+
})
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ref, computed } from 'vue'
|
|
2
|
+
import { defineStore } from 'pinia'
|
|
3
|
+
|
|
4
|
+
export const useCounterStore = defineStore('counter', () => {
|
|
5
|
+
const count = ref(0)
|
|
6
|
+
const doubleCount = computed(() => count.value * 2)
|
|
7
|
+
function increment() {
|
|
8
|
+
count.value++
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return { count, doubleCount, increment }
|
|
12
|
+
})
|