Appearance
Naming
Every name in the codebase tells a story. Consistent naming makes intent visible at a glance, speeds up code review, and prevents the ambiguity that breeds bugs.
Casing at a Glance
| What you are naming | Convention | Example |
|---|---|---|
| Variables, refs, functions | camelCase | userName, fetchOrders, isLoading |
| Constants | SCREAMING_SNAKE_CASE | API_BASE_URL, MAX_RETRY_COUNT |
| Types, interfaces, classes, enums | PascalCase | UserDTO, CartStore |
| Vue components | PascalCase | ProfileCard.vue, CartModal.vue |
| Other files (TS, JS, JSON) | camelCase | userService.ts, config.json |
| Test files | PascalCase | CartStore.spec.ts, Auth.e2e.ts |
data-testid attributes | kebab-case | checkout-submit-button |
The sections below explain each rule in detail, with examples of what to do and what to avoid.
Casing
Rules
Variables, refs, functions: use
camelCase.- ✅
userName,fetchOrders,isLoading - ❌
User_Name,GetOrders,loading_status
- ✅
Constants: use
SCREAMING_SNAKE_CASE.- ✅
MAX_RETRY_COUNT,API_BASE_URL - ❌
maxRetryCount,ApiBaseUrl
- ✅
Enums: name the enum itself in
PascalCasewithPascalCasemembers.- ✅
export enum HttpStatus { Ok = 200, NotFound = 404 } - ❌
export enum http_status { ok = 200 }
- ✅
Types, interfaces, classes: use
PascalCase.- ✅
UserDTO,CartStore - ❌
userDto,cart_store
- ✅
Files:
- Vue components → PascalCase
- ✅
ProfileCard.vue,CartModal.vue - ❌
profile-card.vue,cart_modal.vue
- ✅
- Other files (TS, JS, JSON, etc.) → camelCase
- ✅
userService.ts,dateUtils.ts,config.json - ❌
UserService.ts,date-utils.ts,Config.JSON
- ✅
- API service modules live in
src/services/api/<module>.ts(camelCase filename matching the domain)- ✅
src/services/api/checkout.ts,src/services/api/users.ts - ❌
src/services/api/CheckoutService.ts,src/services/api/user-service.ts
- ✅
- Vue components → PascalCase
Test files: always PascalCase.
- ✅
CartStore.spec.ts,Checkout.spec.ts,Auth.e2e.ts - ❌
cart-store.spec.ts,auth.e2e.ts
- ✅
Acronyms
Acronyms like URL, HTTP, and API have their own casing rules.
Rules
In
camelCase, capitalize only the first letter of the acronym.- ✅
fetchUrl,httpClient,apiKey - ❌
fetchURL,HTTPClient,APIKey
- ✅
In
PascalCase, capitalize only the first letter of the acronym.- ✅
HttpServer,ApiClient,UrlParser - ❌
HTTPServer,APIClient,URLParser
- ✅
In
SCREAMING_SNAKE_CASE, keep the acronym fully uppercase.- ✅
API_BASE_URL,HTTP_TIMEOUT_MS - ❌
Api_Base_Url,Http_Timeout_Ms
- ✅
Booleans
Rules
Prefix with intent so the name reads as a statement:
is*→ state or condition:isOpen,isLoading,isDirtyhas*→ possession or availability:hasError,hasAccesscan*→ capability or permission:canSubmit,canEditshould*→ policy or decision:shouldPersist,shouldDebounceneeds*→ requirement:needsUpdate,needsAuth
The name plus its value must form a readable sentence.
- ✅
isLoading = true→ "is loading" - ✅
canSubmit = false→ "cannot submit"
- ✅
Avoid vague flags.
- ❌
loading,errorFlag,submitable - ✅
isLoading,hasError,canSubmit
- ❌
Refs and Computeds
Rules
Refs:
camelCase, no suffix.- ✅
count = ref(0) - ❌
count_ref,Count
- ✅
Computed values: descriptive nouns or noun phrases in
camelCase.- ✅
fullName,cartTotal,isFormValid - ❌
full_name,validForm
- ✅
Prefer meaningful names that explain the reactive value's purpose.
Collections
Rules
Use plural names for arrays, sets, and maps.
- ✅
users: User[],items: CartItem[] - ❌
userList,itemArray
- ✅
Use singular names for single entities.
- ✅
user: User,item: CartItem
- ✅
When iterating, the loop variable should be the singular of the collection.
- ✅
for (const user of users) { ... }
- ✅
Functions
Rules
Use verbNoun style, action-oriented.
- ✅
getUser,fetchOrders - ❌
userFetch,ordersList
- ✅
Keep names short but precise. Prefer domain terms over abbreviations.
- ✅
calculateDiscount,formatDate - ❌
calcDisc,fmtDt
- ✅
For async operations, prefix with
fetch,load, orupdate.- ✅
fetchUserProfile,updateCartItem
- ✅
For pure transformations, prefix with
map,to, orfrom.- ✅
mapUserDto,toUserModel,fromApiResponse
- ✅
Do not append
Asyncto function names. Theasynckeyword andPromisereturn type already signal asynchrony.- ✅
getUser(id: string): Promise<User> - ❌
getUserAsync(id: string): Promise<User>
- ✅
Constants, Types & Enums
Constants
Naming:
SCREAMING_SNAKE_CASE.- ✅
API_BASE_URL,ONE_MINUTE_MS,MAX_RETRY_COUNT - ❌
apiBaseUrl,oneMinute,MaxRetryCount
- ✅
Placement:
- Shared/cross-feature: store domain-specific constants in
src/config/<domain>.ts(e.g.,src/config/api.ts,src/config/dates.ts). - Feature-local: keep them near usage as
constants.tswithin the feature's directory (e.g.,src/pages/Checkout/constants.ts). Each feature should have only one such file.
- Shared/cross-feature: store domain-specific constants in
Exports: named exports only, no default exports.
Grouping: for related constants, prefer
as constobjects:ts// src/config/api.ts export const Api = { BASE_URL: '/api', TIMEOUT_MS: 10_000, } as const; // src/config/dates.ts export const Time = { ONE_SECOND_MS: 1_000, ONE_MINUTE_MS: 60_000, } as const;
Types & Interfaces
Naming
Domain models (application-wide):
- File names:
camelCase, single short word.- ✅
user.ts,order.ts - ❌
User.ts,userModel.ts,user-types.ts
- ✅
- Interface/type names:
PascalCase, no suffix.- ✅
export interface User { … } - ❌
export interface user_type { … }
- ✅
- File names:
API models (DTOs):
- File names:
camelCase, single word, matching the entity.- ✅
userResponse.ts,createUserRequest.ts - ❌
UserResponse.ts,user-response.ts
- ✅
- Interface/type names: must end with
ResponseorRequest.- ✅
export interface UserResponse { … } - ✅
export interface CreateUserRequest { … } - ❌
UserDTO,UserApiType
- ✅
- File names:
Nested/reused types inside DTOs:
- Use PascalCase, no suffix.
- ✅
AddressinsideuserResponse.ts - ❌
AddressResponsefor a nested field
- ✅
- Use PascalCase, no suffix.
Feature/module-local types:
- File name: always
types.tsin the module directory.- ✅
src/pages/checkout/types.ts - ❌
checkoutTypes.ts,Checkout.interfaces.ts
- ✅
- File name: always
Exports
- Always use named exports.
- ✅
export interface User { … } - ❌
export default interface User { … }
- ✅
- Do not re-export from
index.ts; consumers must import directly from the type file.
Interface vs Type
- Use
interfacefor object shapes that may be extended or merged. - Use
typefor unions, intersections, function signatures, or mapped/utility types.
Derived Types
- Prefer TypeScript utilities (
Pick,Omit,Partial,Readonly, etc.) to create variations of existing types.
Enums (and better alternatives)
Prefer union literal types for small closed sets:
tstype Status = 'idle' | 'loading' | 'success' | 'error';Use
as constobjects when you need both a map and a type:tsexport const Status = { Idle: 'idle', Loading: 'loading', Success: 'success', Error: 'error', } as const; export type Status = typeof Status[keyof typeof Status];Use
enumonly when you need numeric flags, reverse mapping, or interoperability with external enum APIs:tsexport enum HttpMethod { Get = 'GET', Post = 'POST', Put = 'PUT', Delete = 'DELETE', }Avoid
const enumin shared libraries or mixed toolchains, as it can cause build issues.
Do / Don't
| Topic | Do ✅ | Don't ❌ |
|---|---|---|
| Constants naming | ONE_MINUTE_MS, API_BASE_URL | oneMinute, ApiBaseUrl |
| Constant grouping | export const Api = { BASE_URL: … } as const | Mixed unrelated constants in a single file |
| Types/Interfaces | interface User { … }, type UserRole = … | type user = {}, interface user_role {} |
| File placement | src/models/User.ts, api/models/UserResponse.ts | Randomly defined types in components |
| Enums | Use union literals or as const objects | Use enum for simple string sets |
| Exports | Named exports only | Default exports for types/constants |
Rationale
- Consistent casing makes intent clear at a glance.
- Organized placement prevents "catch-all" files and improves discoverability.
- Named exports avoid ambiguity and support tree-shaking and refactoring.
- Unions and
as constobjects provide leaner JS output and safer typing than overusingenum. - DTO types may contain
snake_caseproperties to mirror the API response format; app domain types must usePascalCaseproperty names. This keeps UI/business logic consistent while correctly modeling the transport layer.
Environment Variables
Client-exposed variables must be prefixed with
VITE_so Vite bundles them into the client build.- ✅
VITE_API_BASE_URL,VITE_APP_TITLE - ❌
API_BASE_URL,APP_TITLE(server-only, invisible to the client)
- ✅
Server-only variables do not need the
VITE_prefix.- ✅
DATABASE_URL,SESSION_SECRET
- ✅
Use
SCREAMING_SNAKE_CASEfor all environment variable names.- ❌
viteApiUrl,api-base-url
- ❌
Access client env vars through
import.meta.env:tsconst apiUrl = import.meta.env.VITE_API_BASE_URL
Props and Emits
Props
Naming in code:
camelCase.Naming in templates: automatically converted to
kebab-case.- ✅
defineProps<{ userId: string }>()→<MyComp user-id="123" /> - ❌
<MyComp userId="123" />
- ✅
Booleans: name positively, no negations.
- ✅
isOpen,disabled - ❌
notVisible,noDisabled
- ✅
Events as props (callbacks): never prefix with
on.- ✅
submit?: (payload: FormData) => void - ❌
onSubmit?: (payload: FormData) => void
- ✅
Emits
Use simple, action-like names in lowercase.
change,update,submit,save,delete,open,close
Always declare payload type using array syntax:
tsconst emit = defineEmits<{ change: [id: number] close: [] }>()
Rationale
- Simple, predictable names make events easy to remember.
- Lowercase-only keeps consistency with Vue's event system.
- Array syntax makes payload expectations explicit.
Stores & Composables
Stores
Naming in code: always
use<Name>StoreinPascalCase.- ✅
useCartStore,useAuthStore - ❌
cartStore,auth_store
- ✅
File naming:
camelCasewith.store.tssuffix.- ✅
cart.store.ts,auth.store.ts - ❌
CartStore.ts,authStore.ts
- ✅
State keys:
camelCase, descriptive nouns.- ✅
items,userProfile,isLoading - ❌
ItemsList,profile_data,loadingFlag
- ✅
Composables
Naming in code: always
use<Name>incamelCase.- ✅
useAuth,useCart,useProfileQuery - ❌
authComposable,CartHook
- ✅
File naming: same as function,
camelCase.- ✅
useAuth.ts,useCart.ts,useProfileQuery.ts - ❌
UseAuth.ts,auth.js
- ✅
Variable assignment: the variable name must match the composable name without the
useprefix.- ✅
const auth = useAuth() - ✅
const cart = useCart() - ❌
const authComposable = useAuth() - ❌
const myCart = useCart()
- ✅
Return object keys: stable, predictable, grouped by type.
- State → nouns:
user,items,isLoading - Actions → verbs:
login,logout,addItem
- State → nouns:
Rationale
- The
use*prefix makes reactive utilities immediately recognizable. - A consistent suffix for stores avoids confusion between store files and composables.
- Matching variable names reduce cognitive load.
- A clear return contract makes composables predictable.
API / DTOs
Location
DTO interfaces live under
/api/models/.- File names:
camelCase, single short word.- ✅
user.ts,order.ts - ❌
User.ts,userModel.ts,user-types.ts
- ✅
- The main response object must end with
Response. - Nested or referenced types use regular
PascalCasenames without suffix.- ✅
api/models/UserResponse.ts,api/models/Address.ts - ❌
api/models/UserDTO.ts,api/models/AddressResponse.ts(nested type should not useResponse)
- ✅
- File names:
Domain models also live under
/api/models/.- File names:
camelCase, single short word.- ✅
api/models/User.ts - ❌
api/models/userModel.ts
- ✅
- Interface name: clean, no suffix.
- File names:
API functions live under
/api/.- File name:
camelCase, preferably a single word.- ✅
api/user.ts,api/order.ts - ❌
api/UserApi.ts,api/user-api.ts
- ✅
- File name:
Rules
DTOs
- Main object mapping to an API response → must end with
Response. - Nested objects/types inside the response → regular
PascalCasenames. - Example:ts
// api/models/UserResponse.ts export interface Address { street: string city: string zip: string } export interface UserResponse { id: string full_name: string email_address: string created_at: string address: Address }
- Main object mapping to an API response → must end with
Domain models
- Represent the cleaned, normalized shape used in the app.
- No suffix in the interface name.
- Example:ts
// api/models/User.ts export interface Address { street: string city: string zip: string } export interface User { id: string name: string email: string createdAt: Date address: Address }
API functions
- Name functions as
verbNoun, scoped to the resource. - Return domain models only.
- Mapping methods go at the end of the file, after API functions.
- Example:ts
// api/user.ts import type { UserResponse } from './models/UserResponse' import type { User } from './models/User' export async function getUser(id: string): Promise<User> { const res = await fetch(`/api/users/${id}`) if (!res.ok) throw new Error('Request failed') const data: UserResponse = await res.json() return toUser(data) } export async function createUser(user: User): Promise<User> { const res = await fetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(toUserResponse(user)), }) if (!res.ok) throw new Error('Request failed') const data: UserResponse = await res.json() return toUser(data) } // Mapping functions are defined last function toUser(response: UserResponse): User { return { id: response.id, name: response.full_name, email: response.email_address, createdAt: new Date(response.created_at), address: response.address, } } function toUserResponse(user: User): UserResponse { return { id: user.id, full_name: user.name, email_address: user.email, created_at: user.createdAt.toISOString(), address: user.address, } }
- Name functions as
Rationale
- The
Responsesuffix is reserved for top-level API response objects. - Nested types keep clean
PascalCasenaming since they may be reused. - Mapping functions at the end keep API operations easy to find at the top.
- A domain-first approach ensures UI/business logic never consumes raw API responses.
Components & Icons
Components
File names:
PascalCase.- ✅
ProfileCard.vue,CartModal.vue - ❌
profile-card.vue,cart_modal.vue
- ✅
Base components (UI primitives): use the primitive name only.
- ✅
Button.vue,Input.vue - ❌
BaseButton.vue,BaseInput.vue
- ✅
Shared components: place in
src/components/<domain>.- Group by domain:
src/components/layout→Header.vue,Footer.vue,Sidebar.vuesrc/components/navigation→NavBar.vue,Breadcrumb.vuesrc/components/forms→FormField.vue,FormError.vuesrc/components/feedback→Toast.vue,Modal.vue
- Group by domain:
Page components: place in
src/pages/<Feature>orsrc/pages/<Module>.- Use the feature/module name as the filename, in
PascalCase.- ✅
src/pages/Checkout/Checkout.vue,src/pages/UserProfile/UserProfile.vue - ❌
src/pages/Checkout/Index.vue,src/pages/UserProfile/index.vue
- ✅
- Use the feature/module name as the filename, in
Module/feature-specific components: place in
src/pages/<Feature>/components/.- One
componentsfolder per module. No nested "children" folders.- ✅
src/pages/Checkout/components/CheckoutForm.vue - ❌
src/pages/Checkout/components/forms/CheckoutForm.vue
- ✅
- Component names must include the entity and be suffixed with intent.
- ✅
UserForm.vue,UserList.vue,ProductCard.vue,OrderSummary.vue,PaymentModal.vue
- ✅
- Modal components must always end with
Modal.- ✅
DeleteConfirmationModal.vue,PaymentModal.vue - ❌
DeleteConfirmation.vue
- ✅
- One
Icons
File names:
PascalCase, suffixed withIcon.- ✅
SearchIcon.vue,CartIcon.vue - ❌
search.vue,carticon.vue
- ✅
Placement:
src/components/icons/.- ✅
src/components/icons/SearchIcon.vue - ❌
src/components/icons/search.vue
- ✅
Imports: direct file import, not barrel export.
- ✅
import SearchIcon from '@/components/icons/SearchIcon.vue' - ❌
import { SearchIcon } from '@/components/icons'
- ✅
Rationale
- PascalCase communicates that components are first-class Vue entities.
- Base components map to primitives (Button, Input), keeping names short.
- Domain grouping prevents a component "dumping ground."
- Page components reflect their feature/module and avoid
Index.vue. - Feature components are suffixed with intent (Form, List, Card, Summary, Modal) so their purpose is explicit.
- Modal suffix makes role unambiguous.
- Icons are imported directly by file path with the
Iconsuffix for clarity.
Queries/Mutations
Queries represent the single source of truth for fetching and caching server data, using the Query/Mutation layer as the data-fetching layer. It provides declarative, predictable async-state management with built-in caching, stale-data handling, background refetching, and request deduplication.
Centralizing query and mutation definitions under src/queries/ ensures consistent naming, predictable cache keys, and a clean separation between data fetching, UI logic, and state management.
Unlike a Pinia store (for client-side app state), queries are strictly for server state — data that lives on the backend and can be invalidated or refetched at any time.
Location & File Naming
- All query and mutation definitions live under
src/queries/. - File names:
camelCase, ending with.query.ts.- ✅
products.query.ts,productLines.query.ts - ❌
productsQuery.ts,Products.query.ts
- ✅
Interfaces & Types
- Response interfaces: suffixed with
Response.- ✅
ProductResponse,OrderResponse
- ✅
- Params (input) types: suffixed with
Params.- ✅
GetProductsParams,FetchOrdersParams
- ✅
- Query key args interfaces: suffixed with
QueryKeyArgs.- ✅
ProductLineRetrieveQueryKeyArgs,OrderListQueryKeyArgs
- ✅
- Hook args types: prefixed with
Use<Entity><Scope>QueryArgsorUse<Entity><Action>MutationArgs.- ✅
UseProductLineQueryArgs,UseProductLinesQueryArgs - ✅
UseCreateProductLineMutationArgs,UseUpdateProductLineMutationArgs
- ✅
- DTOs: mirror API response fields (
snake_caseif the API returns it). - Domain types:
PascalCaseproperties (camelCase inside the app).
Query Keys
- Define query keys as arrays.
- Use a query key factory per file.
- Exported const named after the entity, suffixed with
Keys. - First element: resource/entity name in
camelCase. - Additional elements: sub-scope (
list,detail, etc.) and params.
- Exported const named after the entity, suffixed with
- Pass params as part of the key for dynamic caching.
- This ensures queries (via the Query/Mutation layer) invalidate and refetch correctly when inputs change.
ts
interface ProductLineRetrieveQueryKeyArgs {
id: MaybeRefOrGetter<number>;
}
export const productLineKeys = {
all: ['productLines'] as const,
list: (params: GetProductLinesParams) => [...productLineKeys.all, 'list', params] as const,
detail: ({ id }: ProductLineRetrieveQueryKeyArgs) =>
[...productLineKeys.all, 'detail', id] as const,
};Query & Mutation Args Types
- Query args types extend
Partial<UseQueryOptions<...>>and merge with aQueryKeyArgsinterface. - Mutation args types extend
MutationObserverOptionsand merge withQueryKeyArgswhen invalidation depends on filters or other params. - Naming:
- ✅ Queries:
Use<Entity><Scope>QueryArgs - ✅ Mutations:
Use<Entity><Action>MutationArgs
- ✅ Queries:
ts
type UseProductLineQueryArgs =
Partial<UseQueryOptions<UnifiedProductLine>> & ProductLineRetrieveQueryKeyArgs;
export const useProductLineQuery = ({ id, ...options }: UseProductLineQueryArgs) =>
useQuery({
queryKey: productLineKeys.detail({ id }),
queryFn: () => getInventoryProductLine(toValue(id)),
enabled: () => !!toValue(id),
...options,
});ts
type UseCreateProductLineMutationArgs =
ProductLineQueryKeyArgs &
MutationObserverOptions<UnifiedProductLine, DefaultError, ProductLineMutationArgs>;
export const useCreateProductLinesMutation = (
{ filters, ...options }: UseCreateProductLineMutationArgs = {}
) => {
const queryClient = useQueryClient();
return useMutation({
...options,
mutationFn: async ({ data }) =>
await addInventoryProductLine({
unifiedProductLineCreateRequest: data as UnifiedProductLineCreateRequest,
}),
async onSuccess(...args) {
const rawFilters = toValue(filters);
await invalidateProductLinesQueryCache(queryClient, rawFilters);
options?.onSuccess?.(...args);
},
});
};Query Functions
- Name functions as
verb + entity.- ✅
fetchProducts,fetchProductById,fetchOrders
- ✅
- Must accept a typed
ParamsorQueryKeyArgsobject. - Return type must be a
Responseor mapped domain model. - Always export individually (no default export).
ts
export async function fetchProducts(
params: GetProductsParams
): Promise<ProductResponse[]> {
const { data } = await api.get('/products', { params })
return data
}Hook Wrappers
- Prefix with
use+ entity + scope.- ✅
useProducts,useProductDetail,useOrders
- ✅
- Align hook names with query key factory methods.
- Reuse the same
QueryKeyArgstype used by the factory. - Declare args types (
Use...QueryArgs/Use...MutationArgs).
Conventions Recap
- Files:
src/queries/*.query.ts, camelCase. - Interfaces:
*Response,*Params,*QueryKeyArgs. - Hook args types:
Use<Entity><Scope>QueryArgs/Use<Entity><Action>MutationArgs. - Query keys: factory per entity (
entityKeys). Params must be part of the key for dynamic caching. - Query functions:
fetch<Entity>[BySomething], typed withParamsorQueryKeyArgs. - Hooks:
use<Entity>[Scope], typed with the sameQueryKeyArgsorUse...Args. - Consistency: always array keys, stable factories, typed responses.
Rationale
- Centralization avoids scattering queries across features.
.query.tssuffix makes the file's purpose obvious.- Factory keys ensure stability, cache correctness, and prevent typos.
- Dynamic cache keys with params guarantee queries refresh when inputs change.
- Dedicated
QueryKeyArgsinterfaces unify typing across keys, functions, and hooks. Use...Argstypes give hooks typed extension points.- Verb + entity functions communicate intent clearly.
- Hook alignment with keys improves predictability.
- Consistent typing (
Response/Params/QueryKeyArgs/Use...Args) separates API shapes from domain logic.
Utilities
Location
- All utilities live under
src/utils/. - Feature/module-specific utilities go under
src/utils/<feature>/.
✅ Examples:
text
src/utils/date.ts
src/utils/string.ts
src/utils/httpClient.ts
src/utils/auth/token.ts
src/utils/cart/price.tsNaming
File names: intent-driven, single word if possible (
date.ts,string.ts).- If one word is not enough, use
camelCase(httpClient.ts,routeGuards.ts). - ❌ Avoid
*-utils.tsor*-helpers.ts.
- If one word is not enough, use
Functions:
verbNoun(formatCurrency,parseIsoDate,isNonEmptyString).Constants:
SCREAMING_SNAKE_CASE.Types/enums/interfaces:
PascalCase.
ts
// date.ts
export const MS_IN_MINUTE = 60_000;
export function toStartOfDay(d: Date): Date {
const out = new Date(d);
out.setHours(0, 0, 0, 0);
return out;
}Rules
- Exports: named exports only, no default exports.
- Scope: utilities must be framework-agnostic (no Vue imports).
- Flat only: do not nest deeper than one folder (
src/utils/<feature>/). - Split files: only if intent diverges (
date.tsvsdateFormat.ts).
Do / Don't
| Scope | Do ✅ | Don't ❌ |
|---|---|---|
| File | date.ts, httpClient.ts | date-utils.ts, StringHelpers.ts |
| Export | export function formatCurrency(...) | export default function helper(...) |
| Constant | MS_IN_MINUTE = 60000 | minuteMs = 60000 |
| Function | isNonEmptyString(u): u is string | stringCheck(u) |
Tests
1. Unit Tests
- Naming:
<TargetName>.spec.ts(PascalCase). - Placement: same folder as the script/component under test.
- Rule: filename must mirror the target file.
✅ Examples:
text
src/components/ProfileCard.vue
src/components/ProfileCard.spec.ts
src/stores/CartStore.ts
src/stores/CartStore.spec.ts❌ Bad:
text
src/tests/cart.test.ts
src/components/profile-card.spec.ts2. Integration Tests
- Naming:
<PageName>.spec.ts(PascalCase). - Placement: same directory as the page component.
✅ Example:
text
src/pages/Checkout.vue
src/pages/Checkout.spec.ts❌ Bad:
text
src/pages/checkout-page.spec.ts3. E2E Tests
- Naming:
<ModuleName>.e2e.ts(PascalCase). - Placement:
tests/e2e/<ModuleName>/. - Rule: never place E2E tests under
src/.
✅ Examples:
text
tests/e2e/Cart/Cart.e2e.ts
tests/e2e/Auth/Auth.e2e.ts❌ Bad:
text
src/pages/Cart.e2e.ts
tests/e2e/cart.e2e.ts4. Fixtures & Mocks
- Location: shared fixtures belong under
tests/fixtures/<module>/. Project-specific fixtures may be colocated with the test that uses them; keep shared fixtures in the canonical directory. - Directories: lowercase (
cart/,auth/). - JSON fixtures: PascalCase.
- ✅
UserCredentials.json,CartItems.json
- ✅
- TS/JS factories: camelCase.
- ✅
userFactory.ts,orderFactory.ts
- ✅
✅ Example:
text
tests/fixtures/cart/UserCredentials.json
tests/fixtures/cart/userFactory.ts❌ Bad for shared fixtures:
text
tests/fixtures/UserCredentials.json # shared fixture without a module directoryProject-specific fixtures may live beside the test that owns them. For example, this is valid when the fixture is used only by the checkout E2E flow:
text
tests/e2e/Cart/Checkout.e2e.ts
tests/e2e/Cart/fixtures/CheckoutUser.jsonDo not treat a colocated, project-specific fixture as a shared fixture. Shared fixtures belong under tests/fixtures/<module>/.
5. Identifiers
- All
data-testidattributes must use kebab-case.- ✅
data-testid="checkout-submit-button" - ❌
data-testid="checkoutSubmitBtn"
- ✅
6. Do / Don't
| Scope | Do ✅ | Don't ❌ |
|---|---|---|
| Unit | ProfileCard.spec.ts colocated with ProfileCard.vue | profile-card.spec.ts or in /tests/ |
| Integration | Checkout.spec.ts colocated with Checkout.vue | checkout.spec.ts |
| E2E | tests/e2e/Cart/Cart.e2e.ts | src/pages/Cart.e2e.ts |
| Fixtures JSON | tests/fixtures/cart/UserCredentials.json | tests/fixtures/cart/data.json (too vague) |
| Factories TS | tests/fixtures/cart/userFactory.ts | tests/fixtures/cart/UserFactory.ts (wrong case) |
| Identifiers | data-testid="checkout-submit-button" | data-testid="checkoutSubmitBtn" |