Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/check-pr-size.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ on:

jobs:
check_pr_size:
if: false
runs-on: ubuntu-latest
timeout-minutes: 1

Expand Down
88 changes: 66 additions & 22 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,37 +1,51 @@
# Contributing

## Table of Contents

- [Architecture](#architecture)
- [Code](#code)
- [File template](#file-template)
- [Testing](#testing)
- [Imports](#imports)
- [Use function instead of arrow functions by default](#use-function-instead-of-arrow-functions-by-default)
- [Use object props instead of multiple props](#use-object-props-instead-of-multiple-props)
- [Never use return types if the function infers the type correctly](#never-use-return-types-if-the-function-infers-the-type-correctly)
- [Logger](#logger)
- [Comments](#comments)
- [Frontend](#frontend)

## Architecture

```
backend
| core
| logger
| utils
| infra
| drive-server-wip
| sqlite
sqlite.module.ts
| services
function1.ts
function2.ts
| features
| backups
| sync
sync.module.ts
| services
function1.ts
function2.ts
📁 backend
📁 core
📁 logger
📁 utils
📁 infra
📁 drive-server-wip
📁 sqlite
📄 sqlite.module.ts
📁 services
📄 function1.ts
📄 function2.ts
📁 features
📁 backups
📁 sync
📄 sync.module.ts
📁 services
📄 function1.ts
📄 function2.ts
📁 frontend
📁 core
📁 api
```

## Code

### File template

```ts
type Props = {
prop1: A;
prop2: B;
};
type Props = { prop1: A; prop2: B };

export function fn({ prop1, prop2 }: Props) {}
```
Expand Down Expand Up @@ -136,3 +150,33 @@ logger.debug({
* Whenever we change something, we should retain the comments from the previous version to see the history of the decision.
*/
```

### Frontend

We'll follow a structure similar to Angular's with services and components. The service will be a hook that manages all the logic. Both the service and the component will be stored in the same file.

```ts
export function useComponent() {
const { t } = useI18n();
const { data, status } = useCustomHook();

const value = useMemo(() => {
switch (status) {
case 'loading':
return t('loading');
case 'error':
return '';
case 'success': {
return data;
}
}
}, [status]);

return { value };
}

export function Component() {
const { value } = useComponent();
return <p>{value}</p>;
}
```
67 changes: 57 additions & 10 deletions docs/TESTING.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Testing

## Table of Contents

- [Types](#types)
- [Describe](#describe)
- [Mocks](#mocks)
- [Assert](#assert)
- [Structure](#structure)
- [Frontend](#frontend)

## Types

- Unit tests (`service.test.ts`). It just tests the service function inside the file and mocks all other functions.
- Infra tests (`anything.infra.test.ts`). It tests multiple functions and not only a service function or it is a long test. We want to keep these tests in a separate runner so as not to block the main runner with slow tests.

## Describe

Use `name-of-file` in describe. Why?
Expand All @@ -22,7 +36,7 @@ describe('name-of-file', () => {
const depMock = partialSpyOn(depModule, 'dep');

beforeEach(() => {
depMock.mockReturnValue('first');
depMock.mockReturnValue('value');
});
});
```
Expand All @@ -36,12 +50,12 @@ describe('name-of-file', () => {
const depMock = deepMocked(dep);

beforeEach(() => {
depMock.mockReturnValue('first');
depMock.mockReturnValue('value');
});
});
```

## Expect
## Assert

To check the calls of a depMock we use `call` (just one call) or `calls` (0 or more calls). We use only `toHaveLength`, `toBe`, `toMatchObject` and `toStrictEqual` for assertions.

Expand All @@ -59,9 +73,14 @@ describe('name-of-file', () => {
expect(res).toBe();
expect(res).toMatchObject();
expect(res).toStrictEqual();

call(depMock).toBe();
call(depMock).toMatchObject();
call(depMock).toStrictEqual();

calls(depMock).toHaveLength();
calls(depMock).toMatchObject();
calls(depMock).toStrictEqual();
});
});
```
Expand All @@ -79,21 +98,49 @@ describe('name-of-file', () => {
let props: Parameters<typeof fn>[0];

beforeEach(() => {
dep1Mock.mockReturnValue('first');
dep2Mock.mockReturnValue('first');
dep1Mock.mockReturnValue('value1');
dep2Mock.mockReturnValue('value1');

props = mockProps<typeof fn>({ prop: 'first' });
props = mockProps<typeof fn>({ prop: 'prop1' });
});

it('should do x when y', () => {
// Given
dep1Mock.mockReturnValue('second');
props.prop = 'second';
dep1Mock.mockReturnValue('value2');
props.prop = 'prop2';
// When
const res = fn(props);
// Then
expect(res).toMatchObject({ prop: 'result' });
calls(dep1Mock).toMatchObject([{ prop: 'input' }]);
expect(res).toMatchObject([{ res: 'value2' }, { res: 'value1' }]);
call(dep1Mock).toStrictEqual([{ prop: 'prop2' }]);
});
});
```

## Frontend

Testing the frontend is more complicated due to all the possible interactions we have. Therefore, to keep it as testable as possible and with as little code as possible, we'll only test the component logic (service).

```ts
import { renderHook } from '@testing-library/react-hooks';
import * as depModule from 'module';

describe('name-of-file', () => {
const depMock = partialSpyOn(depModule, 'dep');

it('should do x when y', () => {
// Given
depMock.mockReturnValue('value1');
// When
const { result, rerender } = renderHook(() => useComponent());
// Then
expect(result.current.value).toBe('value1');
// Given
depMock.mockReturnValue('value2');
// When
rerender();
// Then
expect(result.current.value).toBe('value2');
});
});
```
Loading