mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-02-25 11:27:09 -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:
@@ -21,10 +21,12 @@ On Compass, it can be any browser (Chrome, Firefox, etc.).
|
||||
|
||||
Any remote infrastructure, i.e., not running in the user's environment / OS. The back-end is the code that runs in that environment.
|
||||
On Compass, there are two servers:
|
||||
|
||||
- Web server: hosted on Vercel at `compassmeet.com`, which mostly provides the web pages to the client. That's the server we are talking about in the rest of the document.
|
||||
- Core server: hosted on Google Cloud at `api.compassmeet.com`, a server with more resources and permissions to update the database. It's in charge of any operation related to non-web data (i.e., no HTML or CSS) such as accounts, profiles, messages, and votes.
|
||||
|
||||
---
|
||||
|
||||
### React
|
||||
|
||||
React is a client-side UI library.
|
||||
@@ -38,6 +40,7 @@ React itself **does not** define routing, data fetching conventions, or server r
|
||||
- React uses a Virtual DOM to compute minimal changes, then applies them to the real DOM.
|
||||
|
||||
---
|
||||
|
||||
### Hydration
|
||||
|
||||
When a framework pre-renders HTML on the server, the browser receives static markup (HTML, JS and CSS). React then runs on the client and attaches event listeners and internal state to that markup.
|
||||
@@ -52,21 +55,17 @@ React re-renders a component **whenever its state or props change**. Hooks don
|
||||
### React re-renders when:
|
||||
|
||||
1. **A state updater runs**
|
||||
- `setState(...)` from `useState`
|
||||
- `dispatch(...)` from `useReducer`
|
||||
|
||||
- `setState(...)` from `useState`
|
||||
- `dispatch(...)` from `useReducer`
|
||||
2. **Parent props change**
|
||||
- Any parent re-render that produces new props for the child
|
||||
|
||||
- Any parent re-render that produces new props for the child
|
||||
3. **Context value changes**
|
||||
- When a context provider updates its value, all consumers re-render
|
||||
|
||||
- When a context provider updates its value, all consumers re-render
|
||||
4. **External stores change** (in React 18+ “use sync external store” pattern)
|
||||
- `useSyncExternalStore`
|
||||
- Custom store hooks subscribing to something (auth store, Zustand, Redux, etc.)
|
||||
|
||||
- `useSyncExternalStore`
|
||||
- Custom store hooks subscribing to something (auth store, Zustand, Redux, etc.)
|
||||
5. **Server → Client hydration mismatch forces a re-render**
|
||||
- Rare; usually an error condition
|
||||
- Rare; usually an error condition
|
||||
|
||||
Re-rendering **does NOT happen** simply because:
|
||||
|
||||
@@ -98,8 +97,8 @@ Hydration is **startup initialization**.
|
||||
- Variables inside components do **not** persist across renders.
|
||||
- Only state, context, memoized values, refs, and hooks preserve information.
|
||||
|
||||
|
||||
---
|
||||
|
||||
### Next.js: What it adds
|
||||
|
||||
Next.js is a React framework that controls **where** code runs (server vs client), **when** it runs (build vs request time), and how HTML is generated. It adds routing, rendering strategies, data fetching conventions, and server infrastructure.
|
||||
@@ -162,6 +161,7 @@ Notes:
|
||||
- Runs server logic each time a user requests the page.
|
||||
|
||||
Can be dynamic or edge.
|
||||
|
||||
###### λ (Dynamic)
|
||||
|
||||
- **Server-rendered on demand using Node.js**
|
||||
@@ -278,10 +278,10 @@ export async function getServerSideProps(context) { ... }
|
||||
- Runs **on the server for every request**.
|
||||
- Provides props to the page component.
|
||||
- Has access to:
|
||||
- database
|
||||
- filesystem
|
||||
- environment variables
|
||||
- cookies, headers, auth context
|
||||
- database
|
||||
- filesystem
|
||||
- environment variables
|
||||
- cookies, headers, auth context
|
||||
|
||||
### What it implies:
|
||||
|
||||
@@ -417,20 +417,15 @@ Fallback generation effectively behaves like **ISR** for pages not pre-rendered.
|
||||
### How to Think About It When Architecting
|
||||
|
||||
1. **Default to Server Components** whenever no browser interactivity is needed.
|
||||
Reduces bundle size and avoids unnecessary hydration.
|
||||
|
||||
Reduces bundle size and avoids unnecessary hydration.
|
||||
2. **Use Client Components** only where interaction happens (buttons, forms, animations, local state).
|
||||
|
||||
3. **Choose a rendering model based on data volatility**:
|
||||
- Rarely changing: SSG
|
||||
- Somewhat changing and OK with slightly stale: ISR
|
||||
- Must always be fresh or personalized: SSR
|
||||
|
||||
- Rarely changing: SSG
|
||||
- Somewhat changing and OK with slightly stale: ISR
|
||||
- Must always be fresh or personalized: SSR
|
||||
4. **Remember:** Hydration cost scales with the amount of Client Components. Keep them narrow.
|
||||
|
||||
5. **Consider caching**:
|
||||
Next.js can automatically cache server component results; knowing what is cached impacts performance heavily.
|
||||
|
||||
Next.js can automatically cache server component results; knowing what is cached impacts performance heavily.
|
||||
|
||||
### Backend vs Frontend on Next.js
|
||||
|
||||
@@ -441,4 +436,4 @@ Fallback generation effectively behaves like **ISR** for pages not pre-rendered.
|
||||
|
||||
### Downtime
|
||||
|
||||
To simulate downtime **you need the error to happen at runtime, not at build time**. That means the page must be **server-rendered**, not statically generated.
|
||||
To simulate downtime **you need the error to happen at runtime, not at build time**. That means the page must be **server-rendered**, not statically generated.
|
||||
|
||||
328
docs/TESTING.md
328
docs/TESTING.md
@@ -25,28 +25,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
|
||||
|
||||
@@ -116,23 +116,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
|
||||
|
||||
@@ -141,13 +141,14 @@ standards ensures consistency, maintainability, and comprehensive test coverage.
|
||||
|
||||
#### Best Practices
|
||||
|
||||
1. Isolate a function route - Each test should focus on one thing that can affect the function outcome
|
||||
1. Isolate a function route - Each test should focus on one thing that can affect the function outcome
|
||||
2. Keep tests independent - Tests should not rely on the execution order
|
||||
3. Use meaningful assertions - Assert that functions are called, what they are called with and the results
|
||||
4. Avoid testing implementation details - Focus on behavior and outputs
|
||||
5. Mock external dependencies - Isolate the unit being tested
|
||||
|
||||
#### Running Tests
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
yarn test
|
||||
@@ -155,16 +156,18 @@ yarn test
|
||||
# Run specific test file
|
||||
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
|
||||
|
||||
@@ -207,11 +210,17 @@ When writing mocks, assert both outcome and interaction:
|
||||
- Interaction: that dependencies were called the expected number of times and with the right arguments.
|
||||
|
||||
Why mocking is important?
|
||||
- *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 without waiting.
|
||||
- *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 inputs = same results, every time.
|
||||
- *Focus* - Verify your function's logic and how it uses its dependencies, without requiring those dependencies to actually work yet.
|
||||
|
||||
- _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
|
||||
without waiting.
|
||||
- _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
|
||||
inputs = same results, every time.
|
||||
- _Focus_ - Verify your function's logic and how it uses its dependencies, without requiring those dependencies to
|
||||
actually work yet.
|
||||
|
||||
###### Use `jest.mock()`
|
||||
|
||||
@@ -223,40 +232,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
|
||||
@@ -265,26 +274,26 @@ When mocking modules it's important to verify what was returned if applicable, t
|
||||
|
||||
```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";
|
||||
jest.mock('path/to/module');
|
||||
import {functionUnderTest} from 'path/to/function'
|
||||
import {module as mockedDep} from '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
|
||||
@@ -292,15 +301,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.
|
||||
@@ -308,172 +317,173 @@ Use namespace imports when you want to import everything a module exports under
|
||||
```tsx
|
||||
//moduleFile.ts
|
||||
export const module = async (param) => {
|
||||
const value = "module"
|
||||
return value
|
||||
};
|
||||
const value = 'module'
|
||||
return value
|
||||
}
|
||||
|
||||
export const moduleTwo = async (param) => {
|
||||
const value = "moduleTwo"
|
||||
return value
|
||||
};
|
||||
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;
|
||||
};
|
||||
const mockValue = await moduleTwo(param)
|
||||
const returnValue = await module(mockValue)
|
||||
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()`.
|
||||
|
||||
- `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
|
||||
|
||||
Mocking dependencies allows you to test `your code’s` logic in isolation, without relying on third-party services or 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;
|
||||
beforeEach(() => {
|
||||
mockDependency = {
|
||||
module: jest.fn(),
|
||||
};
|
||||
jest.resetAllMocks(); // Resets any mocks from previous tests
|
||||
});
|
||||
afterEach(() => {
|
||||
//Run after each test
|
||||
jest.restoreAllMocks(); // Cleans up between tests
|
||||
});
|
||||
/**
|
||||
* 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
|
||||
beforeEach(() => {
|
||||
mockDependency = {
|
||||
module: jest.fn(),
|
||||
}
|
||||
jest.resetAllMocks() // Resets any mocks from previous tests
|
||||
})
|
||||
afterEach(() => {
|
||||
//Run after each test
|
||||
jest.restoreAllMocks() // Cleans up between tests
|
||||
})
|
||||
|
||||
//Inside the test case
|
||||
(mockDependency.module as jest.Mock).mockResolvedValue(mockReturnValue);
|
||||
//Inside the test case
|
||||
;(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
|
||||
);
|
||||
|
||||
'Error message',
|
||||
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) {
|
||||
jest.spyOn(Array.prototype, 'includes').mockImplementation(function (value) {
|
||||
if (value === 2) {
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return false
|
||||
})
|
||||
```
|
||||
|
||||
# Playwright (E2E) Testing Guide
|
||||
@@ -605,7 +615,7 @@ yarn test:db:reset # Drops and re-applies all migrations + seed
|
||||
## What Needs a Restart vs Not
|
||||
|
||||
| Change | Action needed |
|
||||
|-------------------------------|------------------------------------------|
|
||||
| ----------------------------- | ---------------------------------------- |
|
||||
| Edit test file | Just re-run in Playwright UI |
|
||||
| Edit app code (Next.js) | Next.js hot reloads automatically |
|
||||
| Edit API code | API dev server hot reloads automatically |
|
||||
@@ -621,7 +631,7 @@ yarn test:db:reset # Drops and re-applies all migrations + seed
|
||||
E2E tests run against local emulators with these defaults (set in `.env.test`):
|
||||
|
||||
| Service | Local URL |
|
||||
|------------------|-----------------------------------------------------------|
|
||||
| ---------------- | --------------------------------------------------------- |
|
||||
| Supabase API | `http://127.0.0.1:54321` |
|
||||
| Supabase DB | `postgresql://postgres:postgres@127.0.0.1:54322/postgres` |
|
||||
| Supabase Studio | `http://127.0.0.1:54323` (browse data visually) |
|
||||
@@ -667,23 +677,26 @@ tests/
|
||||
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.
|
||||
@@ -691,15 +704,15 @@ This hierarchy mirrors how users actually interact with your application, making
|
||||
### Example test
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '@playwright/test'
|
||||
import {test, expect} from '@playwright/test'
|
||||
|
||||
test.describe('Authentication', () => {
|
||||
test('should login successfully', async ({ page }) => {
|
||||
test('should login successfully', async ({page}) => {
|
||||
await page.goto('/')
|
||||
await page.getByRole('button', { name: 'Sign In' }).click()
|
||||
await page.getByRole('button', {name: 'Sign In'}).click()
|
||||
await page.getByLabel('Email').fill('test@example.com')
|
||||
await page.getByLabel('Password').fill('password123')
|
||||
await page.getByRole('button', { name: 'Login' }).click()
|
||||
await page.getByRole('button', {name: 'Login'}).click()
|
||||
|
||||
await expect(page.getByText('Welcome')).toBeVisible()
|
||||
})
|
||||
@@ -711,7 +724,7 @@ test.describe('Authentication', () => {
|
||||
These are seeded automatically by `yarn test:db:seed`:
|
||||
|
||||
| Email | Password |
|
||||
|---------------------|---------------|
|
||||
| ------------------- | ------------- |
|
||||
| `test1@example.com` | `password123` |
|
||||
| `test2@example.com` | `password123` |
|
||||
|
||||
@@ -799,4 +812,3 @@ To download the Playwright report from a failed CI run:
|
||||
2. Click **Artifacts** at the bottom
|
||||
3. Download `playwright-report`
|
||||
4. Open `index.html` in your browser
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
> TODO: This document is a work in progress. Please help us improve it!
|
||||
|
||||
See those other useful documents as well:
|
||||
|
||||
- [knowledge.md](knowledge.md) for high-level architecture and design decisions.
|
||||
- [README.md](../backend/api/README.md) for the backend API
|
||||
- [README.md](../backend/email/README.md) for the email routines and how to set up a local server for quick email rendering
|
||||
@@ -14,19 +15,28 @@ See those other useful documents as well:
|
||||
|
||||
A profile field is any variable associated with a user profile, such as age, politics, diet, etc. You may want to add a new profile field if it helps people find better matches.
|
||||
|
||||
To do so, you can add code in a similar way as in [this commit](https://github.com/CompassConnections/Compass/commit/940c1f5692f63bf72ddccd4ec3b00b1443801682) for the `religion` field. If you also want people to filter by that profile field, you'll also need to add it to the search filters, as done in [this commit](https://github.com/CompassConnections/Compass/commit/a4bb184e95553184a4c8773d7896e4b570508fe5) (for the `religion` field as well).
|
||||
To do so, you can add code in a similar way as
|
||||
in [this commit](https://github.com/CompassConnections/Compass/commit/940c1f5692f63bf72ddccd4ec3b00b1443801682) for the
|
||||
`religion` field. If you also want people to filter by that profile field, you'll also need to add it to the search
|
||||
filters, as done
|
||||
in [this commit](https://github.com/CompassConnections/Compass/commit/a4bb184e95553184a4c8773d7896e4b570508fe5) (for the
|
||||
`religion` field as well).
|
||||
|
||||
Note that you will also need to add a column to the `profiles` table in the dev database before running the code; you can do so via this SQL command (change the type if not `TEXT`):
|
||||
|
||||
```sql
|
||||
ALTER TABLE profiles ADD COLUMN profile_field TEXT;
|
||||
ALTER TABLE profiles
|
||||
ADD COLUMN profile_field TEXT;
|
||||
```
|
||||
|
||||
Store it in `add_profile_field.sql` in the [migrations](../backend/supabase/migrations) folder and run [migrate.sh](../scripts/migrate.sh) from the root folder:
|
||||
|
||||
```bash
|
||||
./scripts/migrate.sh backend/supabase/migrations/add_profile_field.sql
|
||||
```
|
||||
|
||||
Then sync the database types from supabase to the local files (which assist Typescript in typing):
|
||||
|
||||
```bash
|
||||
yarn regen-types dev
|
||||
```
|
||||
@@ -41,4 +51,4 @@ Adding a new language is very easy, especially with translating tools like large
|
||||
- Duplicate [fr.json](../web/messages/fr.json) and rename it to the locale code (e.g., `de.json` for German). Translate all the strings in the new file (keep the keys identical). LLMs like ChatGPT may not be able to translate the whole file in one go; try to copy-paste by batch of 300 lines and ask the LLM to `translate the values of the json above to <new language> (keep the keys unchanged)`. In order to fit the bottom navigation bar on mobile, make sure the values for those keys are less than 10 characters: "nav.home", "nav.messages", "nav.more", "nav.notifs", "nav.people".
|
||||
- Duplicate the [fr](../web/public/md/fr) folder and rename it to the locale code (e.g., `de` for German). Translate all the markdown files in the new folder. To do so, you can copy-paste each file into an LLM and ask it to `translate the markdown above to <new language>`.
|
||||
|
||||
That's all, no code needed!
|
||||
That's all, no code needed!
|
||||
|
||||
@@ -36,14 +36,14 @@ Here's an example component from web in our style:
|
||||
import clsx from 'clsx'
|
||||
import Link from 'next/link'
|
||||
|
||||
import { isAdminId, isModId } from 'common/envs/constants'
|
||||
import { type Headline } from 'common/news'
|
||||
import { EditNewsButton } from 'web/components/news/edit-news-button'
|
||||
import { Carousel } from 'web/components/widgets/carousel'
|
||||
import { useUser } from 'web/hooks/use-user'
|
||||
import { track } from 'web/lib/service/analytics'
|
||||
import { DashboardEndpoints } from 'web/components/dashboard/dashboard-page'
|
||||
import { removeEmojis } from 'common/util/string'
|
||||
import {isAdminId, isModId} from 'common/envs/constants'
|
||||
import {type Headline} from 'common/news'
|
||||
import {EditNewsButton} from 'web/components/news/edit-news-button'
|
||||
import {Carousel} from 'web/components/widgets/carousel'
|
||||
import {useUser} from 'web/hooks/use-user'
|
||||
import {track} from 'web/lib/service/analytics'
|
||||
import {DashboardEndpoints} from 'web/components/dashboard/dashboard-page'
|
||||
import {removeEmojis} from 'common/util/string'
|
||||
|
||||
export function HeadlineTabs(props: {
|
||||
headlines: Headline[]
|
||||
@@ -53,20 +53,13 @@ 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 }) => (
|
||||
{headlines.map(({id, slug, title}) => (
|
||||
<Tab
|
||||
key={id}
|
||||
label={hideEmoji ? removeEmojis(title) : title}
|
||||
@@ -140,9 +133,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
|
||||
@@ -186,25 +177,19 @@ 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 } = {
|
||||
const {enabled = true, ...apiOptions} = {
|
||||
contractId,
|
||||
...opts,
|
||||
}
|
||||
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))
|
||||
})
|
||||
}
|
||||
@@ -239,12 +224,12 @@ export function broadcastUpdatedPrivateUser(userId: string) {
|
||||
broadcast(`private-user/${userId}`, {})
|
||||
}
|
||||
|
||||
export function broadcastUpdatedUser(user: Partial<User> & { id: string }) {
|
||||
broadcast(`user/${user.id}`, { user })
|
||||
export function broadcastUpdatedUser(user: Partial<User> & {id: string}) {
|
||||
broadcast(`user/${user.id}`, {user})
|
||||
}
|
||||
|
||||
export function broadcastUpdatedComment(comment: Comment) {
|
||||
broadcast(`user/${comment.onUserId}/comment`, { comment })
|
||||
broadcast(`user/${comment.onUserId}/comment`, {comment})
|
||||
}
|
||||
```
|
||||
|
||||
@@ -313,7 +298,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],
|
||||
)
|
||||
}
|
||||
```
|
||||
@@ -335,7 +320,7 @@ const handlers = {
|
||||
We have two ways to access our postgres database.
|
||||
|
||||
```ts
|
||||
import { db } from 'web/lib/supabase/db'
|
||||
import {db} from 'web/lib/supabase/db'
|
||||
|
||||
db.from('profiles').select('*').eq('user_id', userId)
|
||||
```
|
||||
@@ -343,7 +328,7 @@ db.from('profiles').select('*').eq('user_id', userId)
|
||||
and
|
||||
|
||||
```ts
|
||||
import { createSupabaseDirectClient } from 'shared/supabase/init'
|
||||
import {createSupabaseDirectClient} from 'shared/supabase/init'
|
||||
|
||||
const pg = createSupabaseDirectClient()
|
||||
pg.oneOrNone<Row<'profiles'>>('select * from profiles where user_id = $1', [userId])
|
||||
@@ -356,13 +341,10 @@ The supabase client just uses the supabase client library, which is a wrapper ar
|
||||
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)
|
||||
}
|
||||
@@ -414,9 +396,9 @@ Example usage:
|
||||
const query = renderSql(
|
||||
select('distinct user_id'),
|
||||
from('contract_bets'),
|
||||
where('contract_id = ${id}', { id }),
|
||||
where('contract_id = ${id}', {id}),
|
||||
orderBy('created_time desc'),
|
||||
limitValue != null && limit(limitValue)
|
||||
limitValue != null && limit(limitValue),
|
||||
)
|
||||
|
||||
const res = await pg.manyOrNone(query)
|
||||
@@ -427,10 +409,10 @@ Use these functions instead of string concatenation.
|
||||
### Translations
|
||||
|
||||
```typescript
|
||||
import {useT} from "web/lib/locale";
|
||||
import {useT} from 'web/lib/locale'
|
||||
|
||||
const t = useT()
|
||||
t("common.key", "English translations")
|
||||
t('common.key', 'English translations')
|
||||
```
|
||||
|
||||
Translations should go to the JSON files in `web/messages` (`de.json` and `fr.json`, as of now).
|
||||
@@ -444,7 +426,7 @@ We have many useful hooks that should be reused rather than rewriting them again
|
||||
We prefer using lodash functions instead of reimplementing them with for loops:
|
||||
|
||||
```ts
|
||||
import { keyBy, uniq } from 'lodash'
|
||||
import {keyBy, uniq} from 'lodash'
|
||||
|
||||
const betsByUserId = keyBy(bets, 'userId')
|
||||
const betIds = uniq(bets, (b) => b.id)
|
||||
|
||||
Reference in New Issue
Block a user