diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000..112af537 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,34 @@ +module.exports = { + extends: 'erb', + plugins: ['@typescript-eslint'], + rules: { + '@typescript-eslint/no-explicit-any': 'off', + 'react/react-in-jsx-scope': 'off', + 'react-hooks/exhaustive-deps': 'warn', + 'import/prefer-default-export': 'off', + 'no-shadow': 'off', + '@typescript-eslint/no-shadow': 'error', + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': 'warn', + 'import/extensions': 'off', + 'import/no-extraneous-dependencies': 'off', + }, + parserOptions: { + ecmaVersion: 2022, + sourceType: 'module', + }, + settings: { + 'import/resolver': { + node: {}, + webpack: { + config: require.resolve( + './scripts/configs/webpack.config.eslint.ts', + ), + }, + typescript: {}, + }, + 'import/parsers': { + '@typescript-eslint/parser': ['.ts', '.tsx'], + }, + }, +}; diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..93419a73 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,61 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + continue-on-error: true + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 18 + cache: npm + - name: Install dependencies + run: npm ci --ignore-scripts + - name: Run linter + run: npm run lint + + typecheck: + name: Type Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 18 + cache: npm + - name: Install dependencies + run: npm ci --ignore-scripts + - name: Run TypeScript type check + run: npx tsc --noEmit + + test: + name: Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 18 + cache: npm + - name: Install dependencies + run: npm ci --ignore-scripts + - name: Run tests + run: npm test + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results + path: | + coverage/ + if-no-files-found: ignore diff --git a/package.json b/package.json index 5bb0d725..0f867469 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ "start:main": "cross-env NODE_ENV=development electronmon -r ts-node/register/transpile-only .", "start:preload": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./scripts/configs/webpack.config.preload.dev.ts", "start:renderer": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack serve --config ./scripts/configs/webpack.config.renderer.dev.ts", - "format": "prettier '**/*.{js,jsx,ts,tsx}' --write" + "format": "prettier '**/*.{js,jsx,ts,tsx}' --write", + "test": "jest" }, "config": { "forge": "./forge.config.js" @@ -210,8 +211,14 @@ } }, "devEngines": { - "node": ">=18.x", - "npm": ">=7.x" + "runtime": { + "name": "node", + "version": ">=18.x" + }, + "packageManager": { + "name": "npm", + "version": ">=7.x" + } }, "electronmon": { "patterns": [ @@ -219,5 +226,41 @@ "src/main/**" ], "logLevel": "verbose" + }, + "jest": { + "testEnvironment": "jsdom", + "testMatch": [ + "**/__tests__/**/*.test.[jt]s?(x)", + "**/?(*.)+(spec|test).[jt]s?(x)" + ], + "transform": { + "\\.(ts|tsx|js|jsx)$": [ + "ts-jest", + { + "tsconfig": { + "jsx": "react-jsx", + "module": "commonjs" + }, + "diagnostics": false + } + ] + }, + "moduleNameMapper": { + "\\.(jpg|jpeg|png|gif|svg|ico|eot|ttf|woff|woff2)$": "/src/__tests__/__mocks__/fileMock.js", + "tailwindcss/tailwind\\.css": "identity-obj-proxy", + "\\.(css|less|sass|scss)$": "identity-obj-proxy", + "^/lib/(.*)$": "/src/lib/$1" + }, + "setupFilesAfterEnv": [ + "/src/__tests__/setupTests.ts" + ], + "moduleDirectories": [ + "node_modules", + "src" + ], + "testPathIgnorePatterns": [ + "/node_modules/", + "/release/" + ] } } diff --git a/src/__tests__/__mocks__/fileMock.js b/src/__tests__/__mocks__/fileMock.js new file mode 100644 index 00000000..86059f36 --- /dev/null +++ b/src/__tests__/__mocks__/fileMock.js @@ -0,0 +1 @@ +module.exports = 'test-file-stub'; diff --git a/src/__tests__/setupTests.ts b/src/__tests__/setupTests.ts new file mode 100644 index 00000000..913c9e37 --- /dev/null +++ b/src/__tests__/setupTests.ts @@ -0,0 +1,42 @@ +import '@testing-library/jest-dom'; + +// Mock window.electron to simulate Electron IPC bridge in tests +Object.defineProperty(window, 'electron', { + value: { + ipcRenderer: { + sendMessage: jest.fn(), + on: jest.fn().mockReturnValue(() => {}), + once: jest.fn(), + }, + electronStore: { + get: jest + .fn() + .mockImplementation( + (_key: string, defaultValue: unknown) => defaultValue, + ), + set: jest.fn(), + }, + browserWindow: { + reload: jest.fn(), + getAlwaysOnTop: jest.fn().mockReturnValue(false), + setAlwaysOnTop: jest.fn(), + promptHiddenChat: jest.fn(), + enableOpenAtLogin: jest.fn(), + disableOpenAtLogin: jest.fn(), + }, + }, + writable: true, +}); + +// Mock window.settings to simulate settings IPC bridge in tests +Object.defineProperty(window, 'settings', { + value: { + getGlobalShortcut: jest.fn().mockResolvedValue('CommandOrControl+Shift+G'), + setGlobalShortcut: jest.fn().mockResolvedValue(undefined), + getFocusSuperprompt: jest.fn().mockResolvedValue(false), + setFocusSuperprompt: jest.fn().mockResolvedValue(undefined), + getPlatform: jest.fn().mockResolvedValue('darwin'), + getOpenAtLogin: jest.fn().mockResolvedValue(false), + }, + writable: true, +});