feat: Best Practices (#102)

* feat: best practices

* feat: add tests workflow

* feat: add best-practices workflow

* fix: wrong indentation in workflow
This commit is contained in:
doprz
2024-02-21 15:54:21 -06:00
committed by GitHub
parent f01cb070b3
commit d5a04c745f
13 changed files with 1739 additions and 1756 deletions

107
.eslintrc
View File

@@ -4,12 +4,9 @@
"browser": true, "browser": true,
"es6": true, "es6": true,
"node": true, "node": true,
"webextensions": true "webextensions": true,
}, },
"ignorePatterns": [ "ignorePatterns": ["*.html", "tsconfig.json"],
"*.html",
"tsconfig.json"
],
"extends": [ "extends": [
"eslint:recommended", "eslint:recommended",
"plugin:react/recommended", "plugin:react/recommended",
@@ -21,18 +18,14 @@
"@unocss", "@unocss",
"prettier", "prettier",
], ],
"plugins": [ "plugins": ["import", "jsdoc", "react-prefer-function-component", "@typescript-eslint", "simple-import-sort"],
"import",
"jsdoc",
"react-prefer-function-component"
],
"globals": { "globals": {
"Atomics": "readonly", "Atomics": "readonly",
"SharedArrayBuffer": "readonly", "SharedArrayBuffer": "readonly",
"debugger": true, "debugger": true,
"browser": true, "browser": true,
"context": true, "context": true,
"JSX": true "JSX": true,
}, },
"parser": "@typescript-eslint/parser", "parser": "@typescript-eslint/parser",
"parserOptions": { "parserOptions": {
@@ -42,36 +35,33 @@
"ecmaFeatures": { "ecmaFeatures": {
"jsx": true, "jsx": true,
"modules": true, "modules": true,
"experimentalObjectRestSpread": true "experimentalObjectRestSpread": true,
} },
}, },
"settings": { "settings": {
"react": { "react": {
"version": "detect" "version": "detect",
}, },
"jsdoc": { "jsdoc": {
"mode": "typescript" "mode": "typescript",
}, },
"import/parsers": { "import/parsers": {
"@typescript-eslint/parser": [ "@typescript-eslint/parser": [".ts", ".tsx"],
".ts",
".tsx"
]
}, },
"import/resolver": { "import/resolver": {
"typescript": { "typescript": {
"alwaysTryTypes": true, "alwaysTryTypes": true,
"project": "./tsconfig.json" "project": "./tsconfig.json",
} },
} },
}, },
"rules": { "rules": {
"prefer-const": [ "prefer-const": [
"off", "off",
{ {
"destructuring": "any", "destructuring": "any",
"ignoreReadBeforeAssign": false "ignoreReadBeforeAssign": false,
} },
], ],
"no-plusplus": "off", "no-plusplus": "off",
"no-inner-declarations": "off", "no-inner-declarations": "off",
@@ -83,20 +73,16 @@
"no-undef": "off", "no-undef": "off",
"no-return-await": "off", "no-return-await": "off",
"@typescript-eslint/return-await": "off", "@typescript-eslint/return-await": "off",
"@typescript-eslint/no-shadow": [ "@typescript-eslint/no-shadow": ["off"],
"off" "@typescript-eslint/no-use-before-define": ["off"],
],
"@typescript-eslint/no-use-before-define": [
"off"
],
"class-methods-use-this": "off", "class-methods-use-this": "off",
"react-hooks/exhaustive-deps": "warn", "react-hooks/exhaustive-deps": "warn",
"@typescript-eslint/lines-between-class-members": "off", "@typescript-eslint/lines-between-class-members": "off",
"no-param-reassign": [ "no-param-reassign": [
"error", "error",
{ {
"props": false "props": false,
} },
], ],
"no-console": "off", "no-console": "off",
"consistent-return": "off", "consistent-return": "off",
@@ -110,8 +96,8 @@
"error", "error",
{ {
"before": true, "before": true,
"after": true "after": true,
} },
], ],
"no-continue": "off", "no-continue": "off",
"space-before-blocks": [ "space-before-blocks": [
@@ -119,24 +105,22 @@
{ {
"functions": "always", "functions": "always",
"keywords": "always", "keywords": "always",
"classes": "always" "classes": "always",
} },
], ],
"react/jsx-filename-extension": [ "react/jsx-filename-extension": [
1, 1,
{ {
"extensions": [ "extensions": [".tsx"],
".tsx" },
]
}
], ],
"react/no-deprecated": "warn", "react/no-deprecated": "warn",
"react/prop-types": "off", "react/prop-types": "off",
"react-prefer-function-component/react-prefer-function-component": [ "react-prefer-function-component/react-prefer-function-component": [
"warn", "warn",
{ {
"allowComponentDidCatch": false "allowComponentDidCatch": false,
} },
], ],
"react/function-component-definition": "off", "react/function-component-definition": "off",
"react/button-has-type": "off", "react/button-has-type": "off",
@@ -154,7 +138,7 @@
"ArrowFunctionExpression": true, "ArrowFunctionExpression": true,
"ClassDeclaration": true, "ClassDeclaration": true,
"ClassExpression": true, "ClassExpression": true,
"FunctionExpression": true "FunctionExpression": true,
}, },
"contexts": [ "contexts": [
"MethodDefinition:not([key.name=\"componentDidMount\"]):not([key.name=\"render\"])", "MethodDefinition:not([key.name=\"componentDidMount\"]):not([key.name=\"render\"])",
@@ -169,9 +153,9 @@
"TSInterfaceDeclaration", "TSInterfaceDeclaration",
"TSMethodSignature", "TSMethodSignature",
"TSModuleDeclaration", "TSModuleDeclaration",
"TSTypeAliasDeclaration" "TSTypeAliasDeclaration",
] ],
} },
], ],
"@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": "warn", "@typescript-eslint/no-unused-vars": "warn",
@@ -186,31 +170,36 @@
{ {
"target": "./src/background", "target": "./src/background",
"from": "./src/views", "from": "./src/views",
"message": "You cannot import into the `background` directory from the `views` directory (i.e. content script files) because it will break the build!" "message": "You cannot import into the `background` directory from the `views` directory (i.e. content script files) because it will break the build!",
}, },
{ {
"target": "./src/views", "target": "./src/views",
"from": "./src/background", "from": "./src/background",
"message": "You cannot import into the `views` directory from the `background` directory (i.e. background script files) because it will break the build!" "message": "You cannot import into the `views` directory from the `background` directory (i.e. background script files) because it will break the build!",
}, },
{ {
"target": "./src/shared", "target": "./src/shared",
"from": "./", "from": "./",
"except": [ "except": ["./src/shared", "./node_modules"],
"./src/shared", "message": "You cannot import into `shared` from an external directory.",
"./node_modules" },
], ],
"message": "You cannot import into `shared` from an external directory." },
}
]
}
], ],
"import/extensions": "off", "import/extensions": "off",
"no-restricted-syntax": [ "no-restricted-syntax": [
"error", "error",
"ForInStatement", "ForInStatement",
"LabeledStatement", "LabeledStatement",
"WithStatement" "WithStatement",
] {
} "selector": "TSEnumDeclaration",
"message": "Don't declare enums",
},
],
"@typescript-eslint/consistent-type-exports": "error",
"@typescript-eslint/consistent-type-imports": "error",
"simple-import-sort/imports": "error",
"simple-import-sort/exports": "error",
},
} }

43
.github/workflows/best-practices.yml vendored Normal file
View File

@@ -0,0 +1,43 @@
name: Best Practices
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 8
- name: Install dependencies
run: pnpm install
- name: Run ESLint
run: pnpm run lint
format:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 8
- name: Install dependencies
run: pnpm install
- name: Run Prettier
run: pnpm run prettier

24
.github/workflows/tests.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 8
- name: Install dependencies
run: pnpm install
- name: Run tests
run: pnpm test

View File

@@ -9,7 +9,13 @@
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc && vite build", "build": "tsc && vite build",
"prettier": "prettier src --check",
"prettier:fix": "prettier src --write",
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"lint:fix": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0 --fix",
"test": "vitest",
"test:ui": "vitest --ui",
"coverage": "vitest run --coverage",
"preview": "vite preview", "preview": "vite preview",
"devtools": "react-devtools", "devtools": "react-devtools",
"preinstall": "npx only-allow pnpm", "preinstall": "npx only-allow pnpm",
@@ -31,9 +37,7 @@
"react": "^18.2.0", "react": "^18.2.0",
"react-devtools-core": "^5.0.0", "react-devtools-core": "^5.0.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-icons": "^5.0.1", "sass": "^1.71.1",
"react-window": "^1.8.10",
"sass": "^1.70.0",
"sql.js": "1.10.2", "sql.js": "1.10.2",
"styled-components": "^6.1.8", "styled-components": "^6.1.8",
"uuid": "^9.0.1" "uuid": "^9.0.1"
@@ -42,22 +46,22 @@
"@commitlint/cli": "^18.6.1", "@commitlint/cli": "^18.6.1",
"@commitlint/config-conventional": "^18.6.2", "@commitlint/config-conventional": "^18.6.2",
"@crxjs/vite-plugin": "2.0.0-beta.21", "@crxjs/vite-plugin": "2.0.0-beta.21",
"@iconify-json/material-symbols": "^1.1.72", "@iconify-json/material-symbols": "^1.1.73",
"@storybook/addon-designs": "^7.0.9", "@storybook/addon-designs": "^7.0.9",
"@storybook/addon-essentials": "^7.6.13", "@storybook/addon-essentials": "^7.6.17",
"@storybook/addon-links": "^7.6.13", "@storybook/addon-links": "^7.6.17",
"@storybook/blocks": "^7.6.13", "@storybook/blocks": "^7.6.17",
"@storybook/react": "^7.6.13", "@storybook/react": "^7.6.17",
"@storybook/react-vite": "^7.6.13", "@storybook/react-vite": "^7.6.17",
"@storybook/test": "^7.6.13", "@storybook/test": "^7.6.17",
"@svgr/core": "^8.1.0", "@svgr/core": "^8.1.0",
"@svgr/plugin-jsx": "^8.1.0", "@svgr/plugin-jsx": "^8.1.0",
"@types/chrome": "^0.0.260", "@types/chrome": "^0.0.260",
"@types/node": "^20.11.17", "@types/node": "^20.11.19",
"@types/prompts": "^2.4.9", "@types/prompts": "^2.4.9",
"@types/react": "^18.2.55", "@types/react": "^18.2.57",
"@types/react-dom": "^18.2.19", "@types/react-dom": "^18.2.19",
"@types/semver": "^7.5.6", "@types/semver": "^7.5.7",
"@types/uuid": "^9.0.8", "@types/uuid": "^9.0.8",
"@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0", "@typescript-eslint/parser": "^6.21.0",
@@ -69,10 +73,12 @@
"@unocss/transformer-directives": "^0.58.5", "@unocss/transformer-directives": "^0.58.5",
"@unocss/transformer-variant-group": "^0.58.5", "@unocss/transformer-variant-group": "^0.58.5",
"@vitejs/plugin-react-swc": "^3.6.0", "@vitejs/plugin-react-swc": "^3.6.0",
"chromatic": "^10.9.1", "@vitest/coverage-v8": "^1.3.1",
"@vitest/ui": "^1.3.1",
"chromatic": "^10.9.6",
"cssnano": "^6.0.3", "cssnano": "^6.0.3",
"cssnano-preset-advanced": "^6.0.3", "cssnano-preset-advanced": "^6.0.3",
"dotenv": "^16.4.1", "dotenv": "^16.4.5",
"es-module-lexer": "^1.4.1", "es-module-lexer": "^1.4.1",
"eslint": "^8.56.0", "eslint": "^8.56.0",
"eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb": "^19.0.4",
@@ -81,12 +87,13 @@
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.6.1", "eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.29.1", "eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsdoc": "^48.0.6", "eslint-plugin-jsdoc": "^48.1.0",
"eslint-plugin-prettier": "^5.1.3", "eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-react": "^7.33.2", "eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-prefer-function-component": "^3.3.0", "eslint-plugin-react-prefer-function-component": "^3.3.0",
"eslint-plugin-react-refresh": "^0.4.5", "eslint-plugin-react-refresh": "^0.4.5",
"eslint-plugin-simple-import-sort": "^12.0.0",
"eslint-plugin-storybook": "^0.6.15", "eslint-plugin-storybook": "^0.6.15",
"husky": "^9.0.11", "husky": "^9.0.11",
"path": "^0.12.7", "path": "^0.12.7",
@@ -94,12 +101,13 @@
"prettier": "^3.2.5", "prettier": "^3.2.5",
"react-dev-utils": "^12.0.1", "react-dev-utils": "^12.0.1",
"react-devtools": "^5.0.0", "react-devtools": "^5.0.0",
"storybook": "^7.6.13", "storybook": "^7.6.17",
"typescript": "^5.3.3", "typescript": "^5.3.3",
"unocss": "^0.58.5", "unocss": "^0.58.5",
"unplugin-icons": "^0.18.5", "unplugin-icons": "^0.18.5",
"vite": "^5.1.1", "vite": "^5.1.4",
"vite-plugin-inspect": "^0.8.3" "vite-plugin-inspect": "^0.8.3",
"vitest": "^1.3.1"
}, },
"pnpm": { "pnpm": {
"patchedDependencies": { "patchedDependencies": {

3146
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@ export interface CourseColors {
} }
// calculates luminance of a hex string // calculates luminance of a hex string
function getLuminance(hex: string): number { export function getLuminance(hex: string): number {
let r = parseInt(hex.substring(1, 3), 16); let r = parseInt(hex.substring(1, 3), 16);
let g = parseInt(hex.substring(3, 5), 16); let g = parseInt(hex.substring(3, 5), 16);
let b = parseInt(hex.substring(5, 7), 16); let b = parseInt(hex.substring(5, 7), 16);

View File

@@ -0,0 +1,22 @@
import { describe, expect, it } from 'vitest';
import { getLuminance } from '../colors';
describe('getLuminance', () => {
it('should return the correct luminance value for a given hex color', () => {
// Test case 1: Hex color #FFFFFF (white)
expect(getLuminance('#FFFFFF')).toBeCloseTo(1);
// Test case 2: Hex color #000000 (black)
expect(getLuminance('#000000')).toBeCloseTo(0);
// Test case 3: Hex color #FF0000 (red)
expect(getLuminance('#FF0000')).toBeCloseTo(0.2126);
// Test case 4: Hex color #00FF00 (green)
expect(getLuminance('#00FF00')).toBeCloseTo(0.7152);
// Test case 5: Hex color #0000FF (blue)
expect(getLuminance('#0000FF')).toBeCloseTo(0.0722);
});
});

View File

@@ -0,0 +1,26 @@
import { describe, expect, it } from 'vitest';
import { generateRandomId } from '../random';
describe('generateRandomId', () => {
it('should generate a random ID with the specified length', () => {
// Test case 1: Length 5
expect(generateRandomId(5)).toHaveLength(5);
// Test case 2: Length 10
expect(generateRandomId(10)).toHaveLength(10);
// Test case 3: Length 15
expect(generateRandomId(15)).toHaveLength(15);
});
it('should generate a unique ID each time', () => {
// Generate 100 IDs and check if they are all unique
const ids = new Set<string>();
for (let i = 0; i < 100; i += 1) {
const id = generateRandomId();
expect(ids.has(id)).toBe(false);
ids.add(id);
}
});
});

View File

@@ -0,0 +1,39 @@
import { describe, expect, it } from 'vitest';
import { capitalize } from '../string';
// TODO: Fix `string.ts` and `string.test.ts` to make the tests pass
// `capitalize` is adding an extra space at the end of the word.
describe('capitalize', () => {
it('should capitalize the first letter of each word', () => {
// Debug
const word = 'hello world';
const capitalized = capitalize(word);
console.log(capitalize(word));
console.log(capitalized.length);
console.log(capitalized.split(''));
// Test case 1: Single word
expect(capitalize('hello')).toBe('Hello');
// Test case 2: Multiple words
expect(capitalize('hello world')).toBe('Hello World');
// Test case 3: Words with hyphens
expect(capitalize('hello-world')).toBe('Hello-World');
// Test case 4: Words with hyphens and spaces
expect(capitalize('hello-world test')).toBe('Hello-World Test');
});
it('should not change the capitalization of the remaining letters', () => {
// Test case 1: All lowercase
expect(capitalize('hello')).toBe('Hello');
// Test case 2: All uppercase
expect(capitalize('WORLD')).toBe('WORLD');
// Test case 3: Mixed case
expect(capitalize('HeLLo WoRLd')).toBe('Hello World');
});
});

View File

@@ -0,0 +1,14 @@
import { describe, expect, it } from 'vitest';
import { sleep } from '../time';
describe('sleep', () => {
it('should resolve after the specified number of milliseconds', async () => {
const start = Date.now();
const milliseconds = 1000;
await sleep(milliseconds);
const end = Date.now();
const elapsed = end - start;
expect(elapsed).toBeGreaterThanOrEqual(milliseconds);
});
});

View File

@@ -0,0 +1,12 @@
import { describe, expect, it } from 'vitest';
import { convertMinutesToIndex } from '../useFlattenedCourseSchedule';
describe('useFlattenedCourseSchedule', () => {
it('should convert minutes to index correctly', () => {
const minutes = 480; // 8:00 AM
const expectedIndex = 2; // (480 - 420) / 30 = 2
const result = convertMinutesToIndex(minutes);
expect(result).toBe(expectedIndex);
});
});

View File

@@ -1,4 +1,5 @@
import { CalendarCourseCellProps } from 'src/views/components/calendar/CalendarCourseCell/CalendarCourseCell'; import type { CalendarCourseCellProps } from 'src/views/components/calendar/CalendarCourseCell/CalendarCourseCell';
import useSchedules from './useSchedules'; import useSchedules from './useSchedules';
const dayToNumber: { [day: string]: number } = { const dayToNumber: { [day: string]: number } = {
@@ -26,7 +27,7 @@ export interface CalendarGridCourse {
totalColumns?: number; totalColumns?: number;
} }
const convertMinutesToIndex = (minutes: number): number => Math.floor(minutes - 420 / 30); export const convertMinutesToIndex = (minutes: number): number => Math.floor(minutes - 420 / 30);
/** /**
* Get the active schedule, and convert it to be render-able into a calendar. * Get the active schedule, and convert it to be render-able into a calendar.

9
vitest.config.ts Normal file
View File

@@ -0,0 +1,9 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
coverage: {
provider: 'v8',
},
},
});