mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-02-24 02:46:11 -05:00
Add pretty formatting (#29)
* Test * Add pretty formatting * Fix Tests * Fix Tests * Fix Tests * Fix * Add pretty formatting fix * Fix * Test * Fix tests * Clean typeckech * Add prettier check * Fix api tsconfig * Fix api tsconfig * Fix tsconfig * Fix * Fix * Prettier
This commit is contained in:
6
.github/ISSUE_TEMPLATE/other.yml
vendored
6
.github/ISSUE_TEMPLATE/other.yml
vendored
@@ -12,9 +12,9 @@ body:
|
||||
attributes:
|
||||
label: Info
|
||||
description: |
|
||||
- Browser: [e.g. chrome, safari]
|
||||
- Device (if mobile): [e.g. iPhone6]
|
||||
- Build info
|
||||
- Browser: [e.g. chrome, safari]
|
||||
- Device (if mobile): [e.g. iPhone6]
|
||||
- Build info
|
||||
placeholder: |
|
||||
Build info from `Settings` -> `About`
|
||||
validations:
|
||||
|
||||
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -4,5 +4,5 @@
|
||||
- [ ] Tests added and passed if fixing a bug or adding a new feature.
|
||||
|
||||
### Description
|
||||
<!-- Describe your changes in detail -->
|
||||
|
||||
<!-- Describe your changes in detail -->
|
||||
|
||||
323
.github/copilot-instructions.md
vendored
323
.github/copilot-instructions.md
vendored
@@ -7,17 +7,17 @@ globs:
|
||||
## Project Structure
|
||||
|
||||
- next.js react tailwind frontend `/web`
|
||||
- broken down into pages, components, hooks, lib
|
||||
- broken down into pages, components, hooks, lib
|
||||
- express node api server `/backend/api`
|
||||
- one off scripts, like migrations `/backend/scripts`
|
||||
- supabase postgres. schema in `/backend/supabase`
|
||||
- supabase-generated types in `/backend/supabase/schema.ts`
|
||||
- supabase-generated types in `/backend/supabase/schema.ts`
|
||||
- files shared between backend directories `/backend/shared`
|
||||
- anything in `/backend` can import from `shared`, but not vice versa
|
||||
- anything in `/backend` can import from `shared`, but not vice versa
|
||||
- files shared between the frontend and backend in `/common`
|
||||
- `/common` has lots of type definitions for our data structures, like User. It also contains many useful utility
|
||||
functions. We try not to add package dependencies to common. `/web` and `/backend` are allowed to import from
|
||||
`/common`, but not vice versa.
|
||||
- `/common` has lots of type definitions for our data structures, like User. It also contains many useful utility
|
||||
functions. We try not to add package dependencies to common. `/web` and `/backend` are allowed to import from
|
||||
`/common`, but not vice versa.
|
||||
|
||||
## Deployment
|
||||
|
||||
@@ -52,18 +52,11 @@ export function HeadlineTabs(props: {
|
||||
notSticky?: boolean
|
||||
className?: string
|
||||
}) {
|
||||
const {headlines, endpoint, currentSlug, hideEmoji, notSticky, className} =
|
||||
props
|
||||
const {headlines, endpoint, currentSlug, hideEmoji, notSticky, className} = props
|
||||
const user = useUser()
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
className,
|
||||
'bg-canvas-50 w-full',
|
||||
!notSticky && 'sticky top-0 z-50'
|
||||
)}
|
||||
>
|
||||
<div className={clsx(className, 'bg-canvas-50 w-full', !notSticky && 'sticky top-0 z-50')}>
|
||||
<Carousel labelsParentClassName="gap-px">
|
||||
{headlines.map(({id, slug, title}) => (
|
||||
<Tab
|
||||
@@ -73,9 +66,9 @@ export function HeadlineTabs(props: {
|
||||
active={slug === currentSlug}
|
||||
/>
|
||||
))}
|
||||
{user && <Tab label="More" href="/dashboard"/>}
|
||||
{user && <Tab label="More" href="/dashboard" />}
|
||||
{user && (isAdminId(user.id) || isModId(user.id)) && (
|
||||
<EditNewsButton endpoint={endpoint} defaultDashboards={headlines}/>
|
||||
<EditNewsButton endpoint={endpoint} defaultDashboards={headlines} />
|
||||
)}
|
||||
</Carousel>
|
||||
</div>
|
||||
@@ -146,9 +139,7 @@ Here's the definition of usePersistentInMemoryState:
|
||||
|
||||
```ts
|
||||
export const usePersistentInMemoryState = <T>(initialValue: T, key: string) => {
|
||||
const [state, setState] = useStateCheckEquality<T>(
|
||||
safeJsonParse(store[key]) ?? initialValue
|
||||
)
|
||||
const [state, setState] = useStateCheckEquality<T>(safeJsonParse(store[key]) ?? initialValue)
|
||||
|
||||
useEffect(() => {
|
||||
const storedValue = safeJsonParse(store[key]) ?? initialValue
|
||||
@@ -192,7 +183,7 @@ In `use-bets`, we have this hook to get live updates with useApiSubscription:
|
||||
```ts
|
||||
export const useContractBets = (
|
||||
contractId: string,
|
||||
opts?: APIParams<'bets'> & { enabled?: boolean }
|
||||
opts?: APIParams<'bets'> & {enabled?: boolean},
|
||||
) => {
|
||||
const {enabled = true, ...apiOptions} = {
|
||||
contractId,
|
||||
@@ -200,17 +191,11 @@ export const useContractBets = (
|
||||
}
|
||||
const optionsKey = JSON.stringify(apiOptions)
|
||||
|
||||
const [newBets, setNewBets] = usePersistentInMemoryState<Bet[]>(
|
||||
[],
|
||||
`${optionsKey}-bets`
|
||||
)
|
||||
const [newBets, setNewBets] = usePersistentInMemoryState<Bet[]>([], `${optionsKey}-bets`)
|
||||
|
||||
const addBets = (bets: Bet[]) => {
|
||||
setNewBets((currentBets) => {
|
||||
const uniqueBets = sortBy(
|
||||
uniqBy([...currentBets, ...bets], 'id'),
|
||||
'createdTime'
|
||||
)
|
||||
const uniqueBets = sortBy(uniqBy([...currentBets, ...bets], 'id'), 'createdTime')
|
||||
return uniqueBets.filter((b) => !betShouldBeFiltered(b, apiOptions))
|
||||
})
|
||||
}
|
||||
@@ -245,7 +230,7 @@ export function broadcastUpdatedPrivateUser(userId: string) {
|
||||
broadcast(`private-user/${userId}`, {})
|
||||
}
|
||||
|
||||
export function broadcastUpdatedUser(user: Partial<User> & { id: string }) {
|
||||
export function broadcastUpdatedUser(user: Partial<User> & {id: string}) {
|
||||
broadcast(`user/${user.id}`, {user})
|
||||
}
|
||||
|
||||
@@ -330,7 +315,7 @@ export const placeBet: APIHandler<'bet'> = async (props, auth) => {
|
||||
const isApi = auth.creds.kind === 'key'
|
||||
return await betsQueue.enqueueFn(
|
||||
() => placeBetMain(props, auth.uid, isApi),
|
||||
[props.contractId, auth.uid]
|
||||
[props.contractId, auth.uid],
|
||||
)
|
||||
}
|
||||
```
|
||||
@@ -376,13 +361,10 @@ using the pg-promise library. The client (code in web) does not have permission
|
||||
Another example using the direct client:
|
||||
|
||||
```ts
|
||||
export const getUniqueBettorIds = async (
|
||||
contractId: string,
|
||||
pg: SupabaseDirectClient
|
||||
) => {
|
||||
export const getUniqueBettorIds = async (contractId: string, pg: SupabaseDirectClient) => {
|
||||
const res = await pg.manyOrNone(
|
||||
'select distinct user_id from contract_bets where contract_id = $1',
|
||||
[contractId]
|
||||
[contractId],
|
||||
)
|
||||
return res.map((r) => r.user_id as string)
|
||||
}
|
||||
@@ -437,7 +419,7 @@ const query = renderSql(
|
||||
from('contract_bets'),
|
||||
where('contract_id = ${id}', {id}),
|
||||
orderBy('created_time desc'),
|
||||
limitValue != null && limit(limitValue)
|
||||
limitValue != null && limit(limitValue),
|
||||
)
|
||||
|
||||
const res = await pg.manyOrNone(query)
|
||||
@@ -476,7 +458,7 @@ can do so via this SQL command (change the type if not `TEXT`):
|
||||
|
||||
```sql
|
||||
ALTER TABLE profiles
|
||||
ADD COLUMN profile_field TEXT;
|
||||
ADD COLUMN profile_field TEXT;
|
||||
```
|
||||
|
||||
Store it in `add_profile_field.sql` in the [migrations](../backend/supabase/migrations) folder and
|
||||
@@ -540,28 +522,28 @@ How we apply it here
|
||||
This project uses three complementary test types. Use the right level for the job:
|
||||
|
||||
- Unit tests
|
||||
- Purpose: Verify a single function/module in isolation; fast, deterministic.
|
||||
- Where: Each package under `tests/unit` (e.g., `backend/api/tests/unit`, `web/tests/unit`, `common/tests/unit`,
|
||||
etc.).
|
||||
- Runner: Jest (configured via root `jest.config.js`).
|
||||
- Naming: `*.unit.test.ts` (or `.tsx` for React in `web`).
|
||||
- When to use: Pure logic, utilities, hooks, reducers, small components with mocked dependencies.
|
||||
- Purpose: Verify a single function/module in isolation; fast, deterministic.
|
||||
- Where: Each package under `tests/unit` (e.g., `backend/api/tests/unit`, `web/tests/unit`, `common/tests/unit`,
|
||||
etc.).
|
||||
- Runner: Jest (configured via root `jest.config.js`).
|
||||
- Naming: `*.unit.test.ts` (or `.tsx` for React in `web`).
|
||||
- When to use: Pure logic, utilities, hooks, reducers, small components with mocked dependencies.
|
||||
|
||||
- Integration tests
|
||||
- Purpose: Verify multiple units working together (e.g., function + DB/client, component + context/provider) without
|
||||
spinning up the full app.
|
||||
- Where: Each package under `tests/integration` (e.g., `backend/shared/tests/integration`, `web/tests/integration`).
|
||||
- Runner: Jest (configured via root `jest.config.js`).
|
||||
- Naming: `*.integration.test.ts` (or `.tsx` for React in `web`).
|
||||
- When to use: Boundaries between modules, real serialization/parsing, API handlers with mocked network/DB,
|
||||
component trees with providers.
|
||||
- Purpose: Verify multiple units working together (e.g., function + DB/client, component + context/provider) without
|
||||
spinning up the full app.
|
||||
- Where: Each package under `tests/integration` (e.g., `backend/shared/tests/integration`, `web/tests/integration`).
|
||||
- Runner: Jest (configured via root `jest.config.js`).
|
||||
- Naming: `*.integration.test.ts` (or `.tsx` for React in `web`).
|
||||
- When to use: Boundaries between modules, real serialization/parsing, API handlers with mocked network/DB,
|
||||
component trees with providers.
|
||||
|
||||
- End-to-End (E2E) tests
|
||||
- Purpose: Validate real user flows across the full stack.
|
||||
- Where: Top-level `tests/e2e` with separate areas for `web` and `backend`.
|
||||
- Runner: Playwright (see root `playwright.config.ts`, `testDir: ./tests/e2e`).
|
||||
- Naming: `*.e2e.spec.ts`.
|
||||
- When to use: Critical journeys (signup, login, checkout), cross-service interactions, smoke tests for deployments.
|
||||
- Purpose: Validate real user flows across the full stack.
|
||||
- Where: Top-level `tests/e2e` with separate areas for `web` and `backend`.
|
||||
- Runner: Playwright (see root `playwright.config.ts`, `testDir: ./tests/e2e`).
|
||||
- Naming: `*.e2e.spec.ts`.
|
||||
- When to use: Critical journeys (signup, login, checkout), cross-service interactions, smoke tests for deployments.
|
||||
|
||||
Quick commands
|
||||
|
||||
@@ -631,23 +613,23 @@ web/
|
||||
- Unit and integration tests live in each package’s `tests` folder and are executed by Jest via the root
|
||||
`jest.config.js` projects array.
|
||||
- Naming:
|
||||
- Unit: `*.unit.test.ts` (or `.tsx` for React in `web`)
|
||||
- Integration: `*.integration.test.ts`
|
||||
- E2E (Playwright): `*.e2e.spec.ts`
|
||||
- Unit: `*.unit.test.ts` (or `.tsx` for React in `web`)
|
||||
- Integration: `*.integration.test.ts`
|
||||
- E2E (Playwright): `*.e2e.spec.ts`
|
||||
|
||||
### Best Practices
|
||||
|
||||
* Test Behavior, Not Implementation. Don’t test internal state or function calls unless you’re testing utilities or very
|
||||
- Test Behavior, Not Implementation. Don’t test internal state or function calls unless you’re testing utilities or very
|
||||
critical behavior.
|
||||
* Use msw to Mock APIs. Don't manually mock fetch—use msw to simulate realistic behavior, including network delays and
|
||||
- Use msw to Mock APIs. Don't manually mock fetch—use msw to simulate realistic behavior, including network delays and
|
||||
errors.
|
||||
* Don’t Overuse Snapshots. Snapshots are fragile and often meaningless unless used sparingly (e.g., for JSON response
|
||||
- Don’t Overuse Snapshots. Snapshots are fragile and often meaningless unless used sparingly (e.g., for JSON response
|
||||
schemas).
|
||||
* Prefer userEvent Over fireEvent. It simulates real user interactions more accurately.
|
||||
* Avoid Testing Next.js Internals . You don’t need to test getStaticProps, getServerSideProps themselves-test what they
|
||||
- Prefer userEvent Over fireEvent. It simulates real user interactions more accurately.
|
||||
- Avoid Testing Next.js Internals . You don’t need to test getStaticProps, getServerSideProps themselves-test what they
|
||||
render.
|
||||
* Don't test just for coverage. Test to prevent regressions, document intent, and handle edge cases.
|
||||
* Don't write end-to-end tests for features that change frequently unless absolutely necessary.
|
||||
- Don't test just for coverage. Test to prevent regressions, document intent, and handle edge cases.
|
||||
- Don't write end-to-end tests for features that change frequently unless absolutely necessary.
|
||||
|
||||
### Jest Unit Testing Guide
|
||||
|
||||
@@ -675,14 +657,14 @@ yarn test path/to/test.unit.test.ts
|
||||
#### Test Standards
|
||||
|
||||
- Test file names should convey what to expect
|
||||
- Follow the pattern: `<exact-filename>.[unit,integration].test.ts`. Examples:
|
||||
- filename.unit.test.ts
|
||||
- filename.integration.test.ts
|
||||
- Follow the pattern: `<exact-filename>.[unit,integration].test.ts`. Examples:
|
||||
- filename.unit.test.ts
|
||||
- filename.integration.test.ts
|
||||
- Group related tests using describe blocks
|
||||
- Use descriptive test names that explain the expected behavior.
|
||||
- Follow the pattern: "should `expected behavior` [relevant modifier]". Examples:
|
||||
- should `ban user` [with matching user id]
|
||||
- should `ban user` [with matching user name]
|
||||
- Follow the pattern: "should `expected behavior` [relevant modifier]". Examples:
|
||||
- should `ban user` [with matching user id]
|
||||
- should `ban user` [with matching user name]
|
||||
|
||||
#### Mocking
|
||||
|
||||
@@ -726,15 +708,15 @@ When writing mocks, assert both outcome and interaction:
|
||||
|
||||
Why mocking is important?
|
||||
|
||||
- *Isolation* - Test your code independently of databases, APIs, and external systems. Tests only fail when your code
|
||||
- _Isolation_ - Test your code independently of databases, APIs, and external systems. Tests only fail when your code
|
||||
breaks, not when a server is down.
|
||||
- *Speed* - Mocked tests run in milliseconds vs. seconds for real network/database calls. Run your suite constantly
|
||||
- _Speed_ - Mocked tests run in milliseconds vs. seconds for real network/database calls. Run your suite constantly
|
||||
without waiting.
|
||||
- *Control* - Easily simulate edge cases like API errors, timeouts, or rare conditions that are difficult to reproduce
|
||||
- _Control_ - Easily simulate edge cases like API errors, timeouts, or rare conditions that are difficult to reproduce
|
||||
with real systems.
|
||||
- *Reliability* - Eliminate unpredictable failures from network issues, rate limits, or changing external data. Same
|
||||
- _Reliability_ - Eliminate unpredictable failures from network issues, rate limits, or changing external data. Same
|
||||
inputs = same results, every time.
|
||||
- *Focus* - Verify your function's logic and how it uses its dependencies, without requiring those dependencies to
|
||||
- _Focus_ - Verify your function's logic and how it uses its dependencies, without requiring those dependencies to
|
||||
actually work yet.
|
||||
|
||||
###### Use `jest.mock()`
|
||||
@@ -747,40 +729,40 @@ function’s return value isn’t used, there’s no need to mock it further.
|
||||
|
||||
```tsx
|
||||
//Function and module mocks
|
||||
jest.mock('path/to/module');
|
||||
jest.mock('path/to/module')
|
||||
|
||||
//Function and module imports
|
||||
import {functionUnderTest} from "path/to/function"
|
||||
import {module} from "path/to/module"
|
||||
import {functionUnderTest} from 'path/to/function'
|
||||
import {module} from 'path/to/module'
|
||||
|
||||
describe('functionUnderTest', () => {
|
||||
//Setup
|
||||
beforeEach(() => {
|
||||
//Run before each test
|
||||
jest.resetAllMocks(); // Resets any mocks from previous tests
|
||||
});
|
||||
jest.resetAllMocks() // Resets any mocks from previous tests
|
||||
})
|
||||
afterEach(() => {
|
||||
//Run after each test
|
||||
jest.restoreAllMocks(); // Cleans up between tests
|
||||
});
|
||||
jest.restoreAllMocks() // Cleans up between tests
|
||||
})
|
||||
|
||||
describe('when given valid input', () => {
|
||||
it('should describe what is being tested', async () => {
|
||||
//Arrange: Setup test data
|
||||
const mockData = 'test';
|
||||
const mockData = 'test'
|
||||
|
||||
//Act: Execute the function under test
|
||||
const result = myFunction(mockData);
|
||||
const result = myFunction(mockData)
|
||||
|
||||
//Assert: Verify the result
|
||||
expect(result).toBe('expected');
|
||||
});
|
||||
});
|
||||
expect(result).toBe('expected')
|
||||
})
|
||||
})
|
||||
|
||||
describe('when an error occurs', () => {
|
||||
//Test cases for errors
|
||||
});
|
||||
});
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
###### Modules
|
||||
@@ -790,27 +772,27 @@ called and what it was called with.
|
||||
|
||||
```tsx
|
||||
//functionFile.ts
|
||||
import {module as mockedDep} from "path/to/module"
|
||||
import {module as mockedDep} from 'path/to/module'
|
||||
|
||||
export const functionUnderTest = async (param) => {
|
||||
return await mockedDep(param);
|
||||
};
|
||||
return await mockedDep(param)
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
//testFile.unit.test.ts
|
||||
import {functionUnderTest} from "path/to/function";
|
||||
import {module as mockedDep} from "path/to/module";
|
||||
import {functionUnderTest} from 'path/to/function'
|
||||
import {module as mockedDep} from 'path/to/module'
|
||||
|
||||
jest.mock('path/to/module');
|
||||
jest.mock('path/to/module')
|
||||
|
||||
/**
|
||||
* Inside the test case
|
||||
* We create a mock for any information passed into the function that is being tested
|
||||
* and if the function returns a result we create a mock to test the result
|
||||
*/
|
||||
const mockParam = "mockParam";
|
||||
const mockReturnValue = "mockModuleValue";
|
||||
const mockParam = 'mockParam'
|
||||
const mockReturnValue = 'mockModuleValue'
|
||||
|
||||
/**
|
||||
* use .mockResolvedValue when handling async/await modules that return values
|
||||
@@ -818,15 +800,15 @@ const mockReturnValue = "mockModuleValue";
|
||||
*/
|
||||
describe('functionUnderTest', () => {
|
||||
it('returns mocked module value and calls dependency correctly', async () => {
|
||||
(mockedDep as jest.Mock).mockResolvedValue(mockReturnValue);
|
||||
;(mockedDep as jest.Mock).mockResolvedValue(mockReturnValue)
|
||||
|
||||
const result = await functionUnderTest(mockParam);
|
||||
const result = await functionUnderTest(mockParam)
|
||||
|
||||
expect(result).toBe(mockReturnValue);
|
||||
expect(mockedDep).toHaveBeenCalledTimes(1);
|
||||
expect(mockedDep).toHaveBeenCalledWith(mockParam);
|
||||
});
|
||||
});
|
||||
expect(result).toBe(mockReturnValue)
|
||||
expect(mockedDep).toHaveBeenCalledTimes(1)
|
||||
expect(mockedDep).toHaveBeenCalledWith(mockParam)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
Use namespace imports when you want to import everything a module exports under a single name.
|
||||
@@ -834,37 +816,36 @@ Use namespace imports when you want to import everything a module exports under
|
||||
```tsx
|
||||
//moduleFile.ts
|
||||
export const module = async (param) => {
|
||||
const value = "module"
|
||||
const value = 'module'
|
||||
return value
|
||||
};
|
||||
}
|
||||
|
||||
export const moduleTwo = async (param) => {
|
||||
const value = "moduleTwo"
|
||||
const value = 'moduleTwo'
|
||||
return value
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
//functionFile.ts
|
||||
import {module, moduleTwo} from "path/to/module"
|
||||
import {module, moduleTwo} from 'path/to/module'
|
||||
|
||||
export const functionUnderTest = async (param) => {
|
||||
const mockValue = await moduleTwo(param)
|
||||
const returnValue = await module(mockValue)
|
||||
return returnValue;
|
||||
};
|
||||
return returnValue
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
//testFile.unit.test.ts
|
||||
jest.mock('path/to/module');
|
||||
jest.mock('path/to/module')
|
||||
|
||||
/**
|
||||
* This creates an object containing all named exports from ./path/to/module
|
||||
*/
|
||||
import * as mockModule from "path/to/module"
|
||||
|
||||
(mockModule.module as jest.Mock).mockResolvedValue(mockReturnValue);
|
||||
import * as mockModule from 'path/to/module'
|
||||
;(mockModule.module as jest.Mock).mockResolvedValue(mockReturnValue)
|
||||
```
|
||||
|
||||
When mocking modules, you can use `jest.spyOn()` instead of `jest.mock()`.
|
||||
@@ -872,21 +853,21 @@ When mocking modules, you can use `jest.spyOn()` instead of `jest.mock()`.
|
||||
- `jest.mock()` mocks the entire module, which is ideal for external dependencies like Axios or database clients.
|
||||
- `jest.spyOn()` mocks specific methods while keeping the real implementation for others. It can also be used to observe
|
||||
how a real method is called without changing its behavior.
|
||||
- also replaces the need to have `jest.mock()` at the top of the file.
|
||||
- also replaces the need to have `jest.mock()` at the top of the file.
|
||||
|
||||
```tsx
|
||||
//testFile.unit.test.ts
|
||||
import * as mockModule from "path/to/module"
|
||||
import * as mockModule from 'path/to/module'
|
||||
|
||||
//Mocking the return value of the module
|
||||
jest.spyOn(mockModule, 'module').mockResolvedValue(mockReturnValue);
|
||||
jest.spyOn(mockModule, 'module').mockResolvedValue(mockReturnValue)
|
||||
|
||||
//Spying on the module to check functionality
|
||||
jest.spyOn(mockModule, 'module');
|
||||
jest.spyOn(mockModule, 'module')
|
||||
|
||||
//You can assert the module functionality with both of the above exactly like you would if you used jest.mock()
|
||||
expect(mockModule.module).toBeCalledTimes(1);
|
||||
expect(mockModule.module).toBeCalledWith(mockParam);
|
||||
expect(mockModule.module).toBeCalledTimes(1)
|
||||
expect(mockModule.module).toBeCalledWith(mockParam)
|
||||
```
|
||||
|
||||
###### Dependencies
|
||||
@@ -896,119 +877,114 @@ external functionality.
|
||||
|
||||
```tsx
|
||||
//functionFile.ts
|
||||
import {dependency} from "path/to/dependency"
|
||||
import {dependency} from 'path/to/dependency'
|
||||
|
||||
export const functionUnderTest = async (param) => {
|
||||
const depen = await dependency();
|
||||
const value = depen.module();
|
||||
const depen = await dependency()
|
||||
const value = depen.module()
|
||||
|
||||
return value;
|
||||
};
|
||||
return value
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
//testFile.unit.test.ts
|
||||
jest.mock('path/to/dependency');
|
||||
jest.mock('path/to/dependency')
|
||||
|
||||
import {dependency} from "path/to/dependency"
|
||||
import {dependency} from 'path/to/dependency'
|
||||
|
||||
describe('functionUnderTest', () => {
|
||||
/**
|
||||
* Because the dependency has modules that are used we need to
|
||||
* create a variable outside of scope that can be asserted on
|
||||
*/
|
||||
let mockDependency = {} as any;
|
||||
let mockDependency = {} as any
|
||||
beforeEach(() => {
|
||||
mockDependency = {
|
||||
module: jest.fn(),
|
||||
};
|
||||
jest.resetAllMocks(); // Resets any mocks from previous tests
|
||||
});
|
||||
}
|
||||
jest.resetAllMocks() // Resets any mocks from previous tests
|
||||
})
|
||||
afterEach(() => {
|
||||
//Run after each test
|
||||
jest.restoreAllMocks(); // Cleans up between tests
|
||||
});
|
||||
jest.restoreAllMocks() // Cleans up between tests
|
||||
})
|
||||
|
||||
//Inside the test case
|
||||
(mockDependency.module as jest.Mock).mockResolvedValue(mockReturnValue);
|
||||
;(mockDependency.module as jest.Mock).mockResolvedValue(mockReturnValue)
|
||||
|
||||
expect(mockDependency.module).toBeCalledTimes(1);
|
||||
expect(mockDependency.module).toBeCalledWith(mockParam);
|
||||
});
|
||||
expect(mockDependency.module).toBeCalledTimes(1)
|
||||
expect(mockDependency.module).toBeCalledWith(mockParam)
|
||||
})
|
||||
```
|
||||
|
||||
###### Error checking
|
||||
|
||||
```tsx
|
||||
//function.ts
|
||||
const result = await functionName(param);
|
||||
const result = await functionName(param)
|
||||
|
||||
if (!result) {
|
||||
throw new Error(403, 'Error text', error);
|
||||
throw new Error(403, 'Error text', error)
|
||||
}
|
||||
;
|
||||
```
|
||||
|
||||
```tsx
|
||||
//testFile.unit.test.ts
|
||||
const mockParam = {} as any;
|
||||
const mockParam = {} as any
|
||||
|
||||
//This will check only the error message
|
||||
expect(functionName(mockParam))
|
||||
.rejects
|
||||
.toThrowError('Error text');
|
||||
expect(functionName(mockParam)).rejects.toThrowError('Error text')
|
||||
|
||||
//This will check the complete error
|
||||
try {
|
||||
await functionName(mockParam);
|
||||
fail('Should have thrown');
|
||||
await functionName(mockParam)
|
||||
fail('Should have thrown')
|
||||
} catch (error) {
|
||||
const functionError = error as Error;
|
||||
expect(functionError.code).toBe(403);
|
||||
expect(functionError.message).toBe('Error text');
|
||||
expect(functionError.details).toBe(mockParam);
|
||||
expect(functionError.name).toBe('Error');
|
||||
const functionError = error as Error
|
||||
expect(functionError.code).toBe(403)
|
||||
expect(functionError.message).toBe('Error text')
|
||||
expect(functionError.details).toBe(mockParam)
|
||||
expect(functionError.name).toBe('Error')
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
//For console.error types
|
||||
console.error('Error message', error);
|
||||
console.error('Error message', error)
|
||||
|
||||
//Use spyOn to mock
|
||||
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {
|
||||
});
|
||||
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
|
||||
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
'Error message',
|
||||
expect.objectContaining({name: 'Error'}) //The error 'name' refers to the error type
|
||||
);
|
||||
|
||||
expect.objectContaining({name: 'Error'}), //The error 'name' refers to the error type
|
||||
)
|
||||
```
|
||||
|
||||
###### Mocking array return value
|
||||
|
||||
```tsx
|
||||
//arrayFile.ts
|
||||
const exampleArray = [1, 2, 3, 4, 5];
|
||||
const exampleArray = [1, 2, 3, 4, 5]
|
||||
|
||||
const arrayResult = exampleArray.includes(2);
|
||||
const arrayResult = exampleArray.includes(2)
|
||||
```
|
||||
|
||||
```tsx
|
||||
//testFile.unit.test.ts
|
||||
|
||||
//This will mock 'includes' for all arrays and force the return value to be true
|
||||
jest.spyOn(Array.prototype, 'includes').mockReturnValue(true);
|
||||
jest.spyOn(Array.prototype, 'includes').mockReturnValue(true)
|
||||
|
||||
// ---
|
||||
//This will specify which 'includes' array to mock based on the args passed into the .includes()
|
||||
jest.spyOn(Array.prototype, 'includes').mockImplementation(function (value) {
|
||||
if (value === 2) {
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return false
|
||||
})
|
||||
```
|
||||
|
||||
### Playwright (E2E) Testing Guide
|
||||
@@ -1034,23 +1010,26 @@ yarn test:db:reset
|
||||
Use this priority order for selecting elements in Playwright tests:
|
||||
|
||||
1. Prefer `getByRole()` — use semantic roles that reflect how users interact
|
||||
|
||||
```typescript
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
await page.getByRole('button', {name: 'Submit'}).click()
|
||||
```
|
||||
|
||||
If a meaningful ARIA role is not available, fall back to accessible text selectors (next point).
|
||||
|
||||
2. Use accessible text selectors — when roles don't apply, target user-facing text
|
||||
|
||||
```typescript
|
||||
await page.getByLabel('Email').fill('user@example.com');
|
||||
await page.getByPlaceholder('Enter your name').fill('John');
|
||||
await page.getByText('Welcome back').isVisible();
|
||||
await page.getByLabel('Email').fill('user@example.com')
|
||||
await page.getByPlaceholder('Enter your name').fill('John')
|
||||
await page.getByText('Welcome back').isVisible()
|
||||
```
|
||||
|
||||
3. Only use `data-testid` — when elements have no stable user-facing text
|
||||
```typescript
|
||||
// For icons, toggles, or dynamic content without text
|
||||
await page.getByTestId('menu-toggle').click();
|
||||
await page.getByTestId('loading-spinner').isVisible();
|
||||
await page.getByTestId('menu-toggle').click()
|
||||
await page.getByTestId('loading-spinner').isVisible()
|
||||
```
|
||||
|
||||
This hierarchy mirrors how users actually interact with your application, making tests more reliable and meaningful.
|
||||
|
||||
8
.github/workflows/cd-android-live-update.yml
vendored
8
.github/workflows/cd-android-live-update.yml
vendored
@@ -1,10 +1,10 @@
|
||||
name: CD Android Live Update
|
||||
on:
|
||||
push:
|
||||
branches: [ main, master ]
|
||||
branches: [main, master]
|
||||
paths:
|
||||
- "android/capawesome.json"
|
||||
- ".github/workflows/cd-android-live-update.yml"
|
||||
- 'android/capawesome.json'
|
||||
- '.github/workflows/cd-android-live-update.yml'
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # we need full history for git log
|
||||
fetch-depth: 0 # we need full history for git log
|
||||
|
||||
- name: Install jq
|
||||
run: sudo apt-get install -y jq
|
||||
|
||||
8
.github/workflows/cd-api.yml
vendored
8
.github/workflows/cd-api.yml
vendored
@@ -1,10 +1,10 @@
|
||||
name: CD API
|
||||
on:
|
||||
push:
|
||||
branches: [ main, master ]
|
||||
branches: [main, master]
|
||||
paths:
|
||||
- "backend/api/package.json"
|
||||
- ".github/workflows/cd-api.yml"
|
||||
- 'backend/api/package.json'
|
||||
- '.github/workflows/cd-api.yml'
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # we need full history for git log
|
||||
fetch-depth: 0 # we need full history for git log
|
||||
|
||||
- name: Install jq
|
||||
run: sudo apt-get install -y jq
|
||||
|
||||
11
.github/workflows/cd.yml
vendored
11
.github/workflows/cd.yml
vendored
@@ -2,13 +2,12 @@ name: CD
|
||||
|
||||
# Must select "Read and write permissions" in GitHub → Repo → Settings → Actions → General → Workflow permissions
|
||||
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, master ]
|
||||
branches: [main, master]
|
||||
paths:
|
||||
- "package.json"
|
||||
- ".github/workflows/cd.yml"
|
||||
- 'package.json'
|
||||
- '.github/workflows/cd.yml'
|
||||
|
||||
jobs:
|
||||
release:
|
||||
@@ -18,7 +17,7 @@ jobs:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@master
|
||||
with:
|
||||
fetch-depth: 0 # To fetch all history for tags
|
||||
fetch-depth: 0 # To fetch all history for tags
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
@@ -32,4 +31,4 @@ jobs:
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}
|
||||
./scripts/release.sh
|
||||
./scripts/release.sh
|
||||
|
||||
12
.github/workflows/ci-e2e.yml
vendored
12
.github/workflows/ci-e2e.yml
vendored
@@ -2,9 +2,9 @@ name: E2E Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
e2e:
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '21' # Required for firebase-tools@15+
|
||||
java-version: '21' # Required for firebase-tools@15+
|
||||
|
||||
- name: Setup Supabase CLI
|
||||
uses: supabase/setup-cli@v1
|
||||
@@ -41,8 +41,8 @@ jobs:
|
||||
|
||||
- name: Run E2E tests
|
||||
env:
|
||||
SKIP_DB_CLEANUP: true # Don't try to stop Docker in CI
|
||||
FIREBASE_TOKEN: "dummy" # Suppresses auth warning
|
||||
SKIP_DB_CLEANUP: true # Don't try to stop Docker in CI
|
||||
FIREBASE_TOKEN: 'dummy' # Suppresses auth warning
|
||||
# or
|
||||
run: |
|
||||
yarn test:e2e
|
||||
@@ -61,4 +61,4 @@ jobs:
|
||||
with:
|
||||
name: test-results
|
||||
path: test-results/
|
||||
retention-days: 7
|
||||
retention-days: 7
|
||||
|
||||
24
.github/workflows/ci.yml
vendored
24
.github/workflows/ci.yml
vendored
@@ -2,9 +2,9 @@ name: Jest Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
@@ -29,22 +29,22 @@ jobs:
|
||||
run: yarn lint
|
||||
|
||||
- name: Type check
|
||||
run: npx tsc --noEmit
|
||||
run: yarn typecheck
|
||||
|
||||
- name: Run Jest tests
|
||||
env:
|
||||
NEXT_PUBLIC_FIREBASE_ENV: DEV
|
||||
run: |
|
||||
yarn test:coverage
|
||||
# npm install -g lcov-result-merger
|
||||
# mkdir coverage
|
||||
# lcov-result-merger \
|
||||
# "backend/api/coverage/lcov.info" \
|
||||
# "backend/shared/coverage/lcov.info" \
|
||||
# "backend/email/coverage/lcov.info" \
|
||||
# "common/coverage/lcov.info" \
|
||||
# "web/coverage/lcov.info" \
|
||||
# > coverage/lcov.info
|
||||
# npm install -g lcov-result-merger
|
||||
# mkdir coverage
|
||||
# lcov-result-merger \
|
||||
# "backend/api/coverage/lcov.info" \
|
||||
# "backend/shared/coverage/lcov.info" \
|
||||
# "backend/email/coverage/lcov.info" \
|
||||
# "common/coverage/lcov.info" \
|
||||
# "web/coverage/lcov.info" \
|
||||
# > coverage/lcov.info
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
|
||||
Reference in New Issue
Block a user