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:
105
.eslintrc
105
.eslintrc
@@ -4,12 +4,9 @@
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"node": true,
|
||||
"webextensions": true
|
||||
"webextensions": true,
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"*.html",
|
||||
"tsconfig.json"
|
||||
],
|
||||
"ignorePatterns": ["*.html", "tsconfig.json"],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended",
|
||||
@@ -21,18 +18,14 @@
|
||||
"@unocss",
|
||||
"prettier",
|
||||
],
|
||||
"plugins": [
|
||||
"import",
|
||||
"jsdoc",
|
||||
"react-prefer-function-component"
|
||||
],
|
||||
"plugins": ["import", "jsdoc", "react-prefer-function-component", "@typescript-eslint", "simple-import-sort"],
|
||||
"globals": {
|
||||
"Atomics": "readonly",
|
||||
"SharedArrayBuffer": "readonly",
|
||||
"debugger": true,
|
||||
"browser": true,
|
||||
"context": true,
|
||||
"JSX": true
|
||||
"JSX": true,
|
||||
},
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
@@ -42,36 +35,33 @@
|
||||
"ecmaFeatures": {
|
||||
"jsx": true,
|
||||
"modules": true,
|
||||
"experimentalObjectRestSpread": true
|
||||
}
|
||||
"experimentalObjectRestSpread": true,
|
||||
},
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect"
|
||||
"version": "detect",
|
||||
},
|
||||
"jsdoc": {
|
||||
"mode": "typescript"
|
||||
"mode": "typescript",
|
||||
},
|
||||
"import/parsers": {
|
||||
"@typescript-eslint/parser": [
|
||||
".ts",
|
||||
".tsx"
|
||||
]
|
||||
"@typescript-eslint/parser": [".ts", ".tsx"],
|
||||
},
|
||||
"import/resolver": {
|
||||
"typescript": {
|
||||
"alwaysTryTypes": true,
|
||||
"project": "./tsconfig.json"
|
||||
}
|
||||
}
|
||||
"project": "./tsconfig.json",
|
||||
},
|
||||
},
|
||||
},
|
||||
"rules": {
|
||||
"prefer-const": [
|
||||
"off",
|
||||
{
|
||||
"destructuring": "any",
|
||||
"ignoreReadBeforeAssign": false
|
||||
}
|
||||
"ignoreReadBeforeAssign": false,
|
||||
},
|
||||
],
|
||||
"no-plusplus": "off",
|
||||
"no-inner-declarations": "off",
|
||||
@@ -83,20 +73,16 @@
|
||||
"no-undef": "off",
|
||||
"no-return-await": "off",
|
||||
"@typescript-eslint/return-await": "off",
|
||||
"@typescript-eslint/no-shadow": [
|
||||
"off"
|
||||
],
|
||||
"@typescript-eslint/no-use-before-define": [
|
||||
"off"
|
||||
],
|
||||
"@typescript-eslint/no-shadow": ["off"],
|
||||
"@typescript-eslint/no-use-before-define": ["off"],
|
||||
"class-methods-use-this": "off",
|
||||
"react-hooks/exhaustive-deps": "warn",
|
||||
"@typescript-eslint/lines-between-class-members": "off",
|
||||
"no-param-reassign": [
|
||||
"error",
|
||||
{
|
||||
"props": false
|
||||
}
|
||||
"props": false,
|
||||
},
|
||||
],
|
||||
"no-console": "off",
|
||||
"consistent-return": "off",
|
||||
@@ -110,8 +96,8 @@
|
||||
"error",
|
||||
{
|
||||
"before": true,
|
||||
"after": true
|
||||
}
|
||||
"after": true,
|
||||
},
|
||||
],
|
||||
"no-continue": "off",
|
||||
"space-before-blocks": [
|
||||
@@ -119,24 +105,22 @@
|
||||
{
|
||||
"functions": "always",
|
||||
"keywords": "always",
|
||||
"classes": "always"
|
||||
}
|
||||
"classes": "always",
|
||||
},
|
||||
],
|
||||
"react/jsx-filename-extension": [
|
||||
1,
|
||||
{
|
||||
"extensions": [
|
||||
".tsx"
|
||||
]
|
||||
}
|
||||
"extensions": [".tsx"],
|
||||
},
|
||||
],
|
||||
"react/no-deprecated": "warn",
|
||||
"react/prop-types": "off",
|
||||
"react-prefer-function-component/react-prefer-function-component": [
|
||||
"warn",
|
||||
{
|
||||
"allowComponentDidCatch": false
|
||||
}
|
||||
"allowComponentDidCatch": false,
|
||||
},
|
||||
],
|
||||
"react/function-component-definition": "off",
|
||||
"react/button-has-type": "off",
|
||||
@@ -154,7 +138,7 @@
|
||||
"ArrowFunctionExpression": true,
|
||||
"ClassDeclaration": true,
|
||||
"ClassExpression": true,
|
||||
"FunctionExpression": true
|
||||
"FunctionExpression": true,
|
||||
},
|
||||
"contexts": [
|
||||
"MethodDefinition:not([key.name=\"componentDidMount\"]):not([key.name=\"render\"])",
|
||||
@@ -169,9 +153,9 @@
|
||||
"TSInterfaceDeclaration",
|
||||
"TSMethodSignature",
|
||||
"TSModuleDeclaration",
|
||||
"TSTypeAliasDeclaration"
|
||||
]
|
||||
}
|
||||
"TSTypeAliasDeclaration",
|
||||
],
|
||||
},
|
||||
],
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-unused-vars": "warn",
|
||||
@@ -186,31 +170,36 @@
|
||||
{
|
||||
"target": "./src/background",
|
||||
"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",
|
||||
"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",
|
||||
"from": "./",
|
||||
"except": [
|
||||
"./src/shared",
|
||||
"./node_modules"
|
||||
"except": ["./src/shared", "./node_modules"],
|
||||
"message": "You cannot import into `shared` from an external directory.",
|
||||
},
|
||||
],
|
||||
"message": "You cannot import into `shared` from an external directory."
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
],
|
||||
"import/extensions": "off",
|
||||
"no-restricted-syntax": [
|
||||
"error",
|
||||
"ForInStatement",
|
||||
"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
43
.github/workflows/best-practices.yml
vendored
Normal 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
24
.github/workflows/tests.yml
vendored
Normal 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
|
||||
46
package.json
46
package.json
@@ -9,7 +9,13 @@
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"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: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",
|
||||
"devtools": "react-devtools",
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
@@ -32,9 +38,7 @@
|
||||
"react": "^18.2.0",
|
||||
"react-devtools-core": "^5.0.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-icons": "^5.0.1",
|
||||
"react-window": "^1.8.10",
|
||||
"sass": "^1.70.0",
|
||||
"sass": "^1.71.1",
|
||||
"sql.js": "1.10.2",
|
||||
"styled-components": "^6.1.8",
|
||||
"uuid": "^9.0.1"
|
||||
@@ -43,22 +47,22 @@
|
||||
"@commitlint/cli": "^18.6.1",
|
||||
"@commitlint/config-conventional": "^18.6.2",
|
||||
"@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-essentials": "^7.6.13",
|
||||
"@storybook/addon-links": "^7.6.13",
|
||||
"@storybook/blocks": "^7.6.13",
|
||||
"@storybook/react": "^7.6.13",
|
||||
"@storybook/react-vite": "^7.6.13",
|
||||
"@storybook/test": "^7.6.13",
|
||||
"@storybook/addon-essentials": "^7.6.17",
|
||||
"@storybook/addon-links": "^7.6.17",
|
||||
"@storybook/blocks": "^7.6.17",
|
||||
"@storybook/react": "^7.6.17",
|
||||
"@storybook/react-vite": "^7.6.17",
|
||||
"@storybook/test": "^7.6.17",
|
||||
"@svgr/core": "^8.1.0",
|
||||
"@svgr/plugin-jsx": "^8.1.0",
|
||||
"@types/chrome": "^0.0.260",
|
||||
"@types/node": "^20.11.17",
|
||||
"@types/node": "^20.11.19",
|
||||
"@types/prompts": "^2.4.9",
|
||||
"@types/react": "^18.2.55",
|
||||
"@types/react": "^18.2.57",
|
||||
"@types/react-dom": "^18.2.19",
|
||||
"@types/semver": "^7.5.6",
|
||||
"@types/semver": "^7.5.7",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
@@ -70,10 +74,12 @@
|
||||
"@unocss/transformer-directives": "^0.58.5",
|
||||
"@unocss/transformer-variant-group": "^0.58.5",
|
||||
"@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-preset-advanced": "^6.0.3",
|
||||
"dotenv": "^16.4.1",
|
||||
"dotenv": "^16.4.5",
|
||||
"es-module-lexer": "^1.4.1",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
@@ -82,12 +88,13 @@
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-import-resolver-typescript": "^3.6.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-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-prefer-function-component": "^3.3.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.5",
|
||||
"eslint-plugin-simple-import-sort": "^12.0.0",
|
||||
"eslint-plugin-storybook": "^0.6.15",
|
||||
"husky": "^9.0.11",
|
||||
"path": "^0.12.7",
|
||||
@@ -95,12 +102,13 @@
|
||||
"prettier": "^3.2.5",
|
||||
"react-dev-utils": "^12.0.1",
|
||||
"react-devtools": "^5.0.0",
|
||||
"storybook": "^7.6.13",
|
||||
"storybook": "^7.6.17",
|
||||
"typescript": "^5.3.3",
|
||||
"unocss": "^0.58.5",
|
||||
"unplugin-icons": "^0.18.5",
|
||||
"vite": "^5.1.1",
|
||||
"vite-plugin-inspect": "^0.8.3"
|
||||
"vite": "^5.1.4",
|
||||
"vite-plugin-inspect": "^0.8.3",
|
||||
"vitest": "^1.3.1"
|
||||
},
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
|
||||
3146
pnpm-lock.yaml
generated
3146
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@ export interface CourseColors {
|
||||
}
|
||||
|
||||
// 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 g = parseInt(hex.substring(3, 5), 16);
|
||||
let b = parseInt(hex.substring(5, 7), 16);
|
||||
|
||||
22
src/shared/util/tests/colors.test.ts
Normal file
22
src/shared/util/tests/colors.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
26
src/shared/util/tests/random.test.ts
Normal file
26
src/shared/util/tests/random.test.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
39
src/shared/util/tests/string.test.ts
Normal file
39
src/shared/util/tests/string.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
14
src/shared/util/tests/time.test.ts
Normal file
14
src/shared/util/tests/time.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
12
src/views/hooks/tests/useFlattenedCourseSchedule.test.ts
Normal file
12
src/views/hooks/tests/useFlattenedCourseSchedule.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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';
|
||||
|
||||
const dayToNumber: { [day: string]: number } = {
|
||||
@@ -26,7 +27,7 @@ export interface CalendarGridCourse {
|
||||
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.
|
||||
|
||||
9
vitest.config.ts
Normal file
9
vitest.config.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user