diff --git a/.github/workflows/check-pr-size.yml b/.github/workflows/check-pr-size.yml index 5b62fb2..8b730e0 100644 --- a/.github/workflows/check-pr-size.yml +++ b/.github/workflows/check-pr-size.yml @@ -6,7 +6,6 @@ on: jobs: check_pr_size: - if: false runs-on: ubuntu-latest timeout-minutes: 1 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bff7d9b..ea8d238 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,26 +1,43 @@ # 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 @@ -28,10 +45,7 @@ backend ### File template ```ts -type Props = { - prop1: A; - prop2: B; -}; +type Props = { prop1: A; prop2: B }; export function fn({ prop1, prop2 }: Props) {} ``` @@ -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

{value}

; +} +``` diff --git a/docs/TESTING.md b/docs/TESTING.md index 06b818f..a3bc98e 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -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? @@ -22,7 +36,7 @@ describe('name-of-file', () => { const depMock = partialSpyOn(depModule, 'dep'); beforeEach(() => { - depMock.mockReturnValue('first'); + depMock.mockReturnValue('value'); }); }); ``` @@ -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. @@ -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(); }); }); ``` @@ -79,21 +98,49 @@ describe('name-of-file', () => { let props: Parameters[0]; beforeEach(() => { - dep1Mock.mockReturnValue('first'); - dep2Mock.mockReturnValue('first'); + dep1Mock.mockReturnValue('value1'); + dep2Mock.mockReturnValue('value1'); - props = mockProps({ prop: 'first' }); + props = mockProps({ 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'); }); }); ```