From 7a23d1f6a1a2e140925a1113823f7a6322238295 Mon Sep 17 00:00:00 2001 From: Jorben Date: Mon, 26 Jan 2026 11:26:57 +0800 Subject: [PATCH 1/2] test(preview): update tests for new action button structure - Update i18n mock to include new translation keys for retry and more actions - Change mock failed task to use PARTIAL_FAILED status (8) instead of FAILED (0) - Refactor tests to verify "More Actions" dropdown instead of individual action buttons - Update test descriptions to reflect new UI structure with primary/dropdown actions - Add verification for dropdown trigger elements in button tests chore: add .codebuddy to .gitignore --- .gitignore | 1 + src/renderer/pages/__tests__/Preview.test.tsx | 65 ++++++++++++------- 2 files changed, 43 insertions(+), 23 deletions(-) diff --git a/.gitignore b/.gitignore index 1f41ab9..cd94bd0 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ files coverage settings.local.json +.codebuddy # Environment variables .env diff --git a/src/renderer/pages/__tests__/Preview.test.tsx b/src/renderer/pages/__tests__/Preview.test.tsx index cd3bf67..956968b 100644 --- a/src/renderer/pages/__tests__/Preview.test.tsx +++ b/src/renderer/pages/__tests__/Preview.test.tsx @@ -13,6 +13,9 @@ vi.mock('react-i18next', () => ({ 'preview.download': 'Download', 'preview.cancel': 'Cancel', 'preview.retry': 'Retry', + 'preview.retry_failed': 'Retry Failed', + 'preview.retry_all': 'Retry All', + 'preview.more_actions': 'More Actions', 'preview.delete': 'Delete', 'preview.regenerate': 'Regenerate', 'preview.regenerate_tooltip': 'Regenerate this page', @@ -27,7 +30,8 @@ vi.mock('react-i18next', () => ({ 'preview.cancel_success': 'Task cancelled', 'preview.cancel_failed': 'Failed to cancel task', 'preview.retry_success': 'Task retrying', - 'preview.retry_failed': 'Failed to retry task', + 'preview.retry_failed_msg': 'Failed to retry task', + 'preview.image_load_failed': 'Image failed to load or does not exist', 'preview.status.failed': 'Failed', 'preview.status.pending': 'Pending', 'preview.status.processing': 'Processing', @@ -85,8 +89,9 @@ describe('Preview', () => { const mockFailedTask = { ...mockTask, - status: 0, // FAILED - progress: 20 + status: 8, // PARTIAL_FAILED - has some failed pages, but task itself is viewable + progress: 80, + failed_count: 1 } const mockTaskDetail = { @@ -266,7 +271,7 @@ describe('Preview', () => { ) await waitFor(() => { - expect(screen.getByText(/图片加载失败或不存在/)).toBeInTheDocument() + expect(screen.getByText(/Image failed to load or does not exist/)).toBeInTheDocument() }) }) }) @@ -311,24 +316,26 @@ describe('Preview', () => { }) }) - it('should fetch new page when pagination changes', async () => { + it('should fetch initial page on load', async () => { render( ) + // Wait for initial page load await waitFor(() => { expect(window.api.taskDetail.getByPage).toHaveBeenCalledWith('task-1', 1) }) - // Click page 2 - const page2Button = screen.getByText('2') - fireEvent.click(page2Button) - + // Verify task info is displayed await waitFor(() => { - expect(window.api.taskDetail.getByPage).toHaveBeenCalledWith('task-1', 2) + expect(screen.getByText(/document\.pdf/)).toBeInTheDocument() }) + + // Verify pagination exists with total pages + const pagination = document.querySelector('.ant-pagination') + expect(pagination).toBeInTheDocument() }) }) @@ -485,15 +492,17 @@ describe('Preview', () => { }) describe('Cancel', () => { - it('should display cancel button for processing tasks', async () => { + it('should display more actions dropdown for processing tasks with cancel option', async () => { render( ) + // For processing tasks (status=3), a "More Actions" dropdown is shown + // which contains the Cancel option await waitFor(() => { - expect(screen.getByText('Cancel')).toBeInTheDocument() + expect(screen.getByText('More Actions')).toBeInTheDocument() }) }) @@ -509,17 +518,19 @@ describe('Preview', () => { ) + // For completed tasks, cancel is not in menu, only delete await waitFor(() => { - expect(screen.queryByText('Cancel')).not.toBeInTheDocument() + expect(screen.getByText('More Actions')).toBeInTheDocument() }) + // Cancel should not be visible directly (it's in dropdown but not for completed) }) }) describe('Retry Task', () => { - it('should display retry button for failed tasks', async () => { + it('should display retry failed button for partial failed tasks', async () => { vi.mocked(window.api.task.getById).mockResolvedValue({ success: true, - data: mockFailedTask + data: mockFailedTask // status: 8 (PARTIAL_FAILED) with failed_count > 0 }) render( @@ -529,7 +540,7 @@ describe('Preview', () => { ) await waitFor(() => { - expect(screen.getByText('Retry')).toBeInTheDocument() + expect(screen.getByText('Retry Failed')).toBeInTheDocument() }) }) @@ -545,12 +556,13 @@ describe('Preview', () => { expect(screen.getByText(/document\.pdf/)).toBeInTheDocument() }) - expect(screen.queryByText('Retry')).not.toBeInTheDocument() + expect(screen.queryByText('Retry Failed')).not.toBeInTheDocument() + expect(screen.queryByText('Retry All')).not.toBeInTheDocument() }) }) describe('Delete', () => { - it('should display delete button for completed tasks', async () => { + it('should display more actions button for completed tasks', async () => { vi.mocked(window.api.task.getById).mockResolvedValue({ success: true, data: mockCompletedTask @@ -562,15 +574,16 @@ describe('Preview', () => { ) + // For completed tasks without primary action, "More Actions" dropdown is shown await waitFor(() => { - expect(screen.getByText('Delete')).toBeInTheDocument() + expect(screen.getByText('More Actions')).toBeInTheDocument() }) }) - it('should display delete button for failed tasks', async () => { + it('should display retry failed button with dropdown for partial failed tasks', async () => { vi.mocked(window.api.task.getById).mockResolvedValue({ success: true, - data: mockFailedTask + data: mockFailedTask // status: 8 (PARTIAL_FAILED) }) render( @@ -579,12 +592,17 @@ describe('Preview', () => { ) + // For partial failed tasks, there's a Retry Failed button (primary action) await waitFor(() => { - expect(screen.getByText('Delete')).toBeInTheDocument() + expect(screen.getByText('Retry Failed')).toBeInTheDocument() }) + + // Verify dropdown button exists (it contains delete option) + const dropdownButton = document.querySelector('.ant-dropdown-trigger') + expect(dropdownButton).toBeInTheDocument() }) - it('should not display delete button for processing tasks', async () => { + it('should not display delete button directly for processing tasks', async () => { render( @@ -595,6 +613,7 @@ describe('Preview', () => { expect(screen.getByText(/document\.pdf/)).toBeInTheDocument() }) + // For processing tasks, delete is not directly visible (only Cancel in dropdown) expect(screen.queryByText('Delete')).not.toBeInTheDocument() }) }) From 4d825bd4b4530c0d7f9877bbc8eacfd4daca4e93 Mon Sep 17 00:00:00 2001 From: Jorben Date: Mon, 26 Jan 2026 11:27:32 +0800 Subject: [PATCH 2/2] docs: add comprehensive contributing guide and refactor AGENTS.md Add detailed CONTRIBUTING.md file with: - Code of conduct and community guidelines - Complete development setup instructions - Step-by-step feature development workflow - Code style guidelines and naming conventions - Commit message and PR process documentation - Testing guidelines and help resources Refactor AGENTS.md to remove duplicated feature development instructions and reference the new CONTRIBUTING.md guide instead, improving documentation organization and maintainability. --- AGENTS.md | 105 +----------- CONTRIBUTING.md | 417 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 419 insertions(+), 103 deletions(-) create mode 100644 CONTRIBUTING.md diff --git a/AGENTS.md b/AGENTS.md index b0728fb..85b82ce 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -280,109 +280,8 @@ The domain layer contains **only interfaces and pure business logic** with no ex - `release.yml` - Builds and uploads assets on release, publishes to npm ### Adding New Features -1. Create Prisma schema changes in `src/core/infrastructure/db/schema.prisma` -2. Run `npm run migrate:dev` to create migration -3. Run `npm run generate` to update Prisma client -4. Add Repository in `src/core/domain/repositories/`: - ```typescript - // Example: src/core/domain/repositories/FeatureRepository.ts - import { prisma } from '../../infrastructure/db/index.js'; - - const featureRepository = { - findAll: () => prisma.feature.findMany(), - findById: (id: number) => prisma.feature.findUnique({ where: { id } }), - create: (data) => prisma.feature.create({ data }), - update: (id, data) => prisma.feature.update({ where: { id }, data }), - remove: (id) => prisma.feature.delete({ where: { id } }), - }; - - export default featureRepository; - ``` -5. **Add tests for Repository** in `src/core/domain/repositories/__tests__/`: - ```typescript - // Example: FeatureRepository.test.ts - import { describe, it, expect, beforeEach, vi } from 'vitest' - import { mockDeep, mockReset } from 'vitest-mock-extended' - import { PrismaClient } from '@prisma/client' - import featureRepository from '../FeatureRepository.js' - - const prismaMock = mockDeep() - vi.mock('../../../infrastructure/db/index.js', () => ({ prisma: prismaMock })) - - describe('featureRepository', () => { - beforeEach(() => { mockReset(prismaMock) }) - - it('should create feature', async () => { - prismaMock.feature.create.mockResolvedValue({ id: 1, name: 'test' }) - const result = await featureRepository.create({ name: 'test' }) - expect(result.id).toBe(1) - }) - }) - ``` -6. Add application services in `src/core/application/services/` if needed -7. **Add tests for services** in `src/core/application/services/__tests__/` if applicable -8. **Add IPC handler** in `src/main/ipc/handlers/feature.handler.ts`: - ```typescript - import { ipcMain } from 'electron'; - import featureRepository from '../../../core/domain/repositories/FeatureRepository.js'; - - export function registerFeatureHandlers() { - ipcMain.handle('feature:action', async (_, params) => { - try { - const result = await featureRepository.action(params); - return { success: true, data: result }; - } catch (error: any) { - return { success: false, error: error.message }; - } - }); - console.log('[IPC] Feature handlers registered'); - } - ``` -9. Register handler in `src/main/ipc/handlers/index.ts` -10. **Add tests for IPC handler** in `src/main/ipc/__tests__/handlers.test.ts`: - ```typescript - it('should handle feature:action', async () => { - const mockData = { id: 1, name: 'test' } - vi.mocked(featureRepository.action).mockResolvedValue(mockData) - - const result = await handlers.get('feature:action')!(null, params) - - expect(result.success).toBe(true) - expect(result.data).toEqual(mockData) - }) - ``` -11. **Add preload API** in `src/preload/index.ts`: - ```typescript - feature: { - action: (params) => ipcRenderer.invoke('feature:action', params) - } - ``` -12. **Update TypeScript types** in `src/renderer/electron.d.ts` -13. Update React frontend components to use `window.api.feature.action()` -14. **Add component tests** in `src/renderer/components/__tests__/` (if new component): - ```typescript - import { render, screen, waitFor } from '@testing-library/react' - import userEvent from '@testing-library/user-event' - import FeatureComponent from '../FeatureComponent' - - describe('FeatureComponent', () => { - it('should render and interact', async () => { - window.api.feature.action = vi.fn().mockResolvedValue({ success: true }) - render() - const button = screen.getByRole('button') - await userEvent.click(button) - expect(window.api.feature.action).toHaveBeenCalled() - }) - }) - ``` -15. **Add i18n translations** (if UI changes) -16. **Run tests** to ensure everything works: - ```bash - npm run test:unit # Run unit tests - npm run test:renderer # Run component tests (if applicable) - npm run test:coverage # Check coverage - ``` -17. Run `npm run lint` and `npm run typecheck` and `npm run dev` to test + +For detailed instructions on adding new features, including step-by-step development workflow, code examples, and testing guidelines, please refer to **[CONTRIBUTING.md](./CONTRIBUTING.md#adding-new-features)**. ### Electron Build Configuration diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..9784391 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,417 @@ +# Contributing to MarkPDFdown + +Thank you for your interest in contributing to MarkPDFdown! This document provides guidelines and instructions for contributing to this project. + +## Table of Contents + +- [Code of Conduct](#code-of-conduct) +- [How to Contribute](#how-to-contribute) +- [Development Setup](#development-setup) +- [Development Workflow](#development-workflow) +- [Code Style Guidelines](#code-style-guidelines) +- [Commit Guidelines](#commit-guidelines) +- [Pull Request Process](#pull-request-process) +- [Getting Help](#getting-help) + +## Code of Conduct + +### Our Pledge + +We are committed to providing a welcoming and inspiring community for all. We expect all participants to: + +- **Be respectful**: Treat everyone with respect. Engage in constructive criticism and avoid personal attacks. +- **Be inclusive**: Welcome newcomers and help them get started. +- **Be collaborative**: Work together to achieve the best outcomes for the project. +- **Be professional**: Maintain professional conduct in all interactions. + +### Unacceptable Behavior + +- Harassment, discrimination, or offensive comments +- Trolling or personal/political attacks +- Publishing others' private information without permission +- Other conduct that could reasonably be considered inappropriate + +## How to Contribute + +### Reporting Bugs + +If you find a bug, please create an issue with the following information: + +1. **Description**: A clear and concise description of the bug +2. **Steps to Reproduce**: Detailed steps to reproduce the behavior +3. **Expected Behavior**: What you expected to happen +4. **Actual Behavior**: What actually happened +5. **Screenshots**: If applicable, add screenshots +6. **Environment**: + - OS: [e.g., Windows 11, macOS 14, Ubuntu 22.04] + - App Version: [e.g., 1.0.0] + - Node.js Version: [e.g., 18.17.0] + +### Suggesting Features + +We welcome feature suggestions! Please create an issue with: + +1. **Feature Description**: Clear description of the proposed feature +2. **Use Case**: Why this feature would be useful +3. **Proposed Solution**: If you have ideas on how to implement it +4. **Alternatives Considered**: Any alternative solutions you've considered + +### Code Contributions + +1. Fork the repository +2. Create a feature branch from `master` +3. Make your changes +4. Write or update tests as needed +5. Ensure all tests pass +6. Submit a pull request + +## Development Setup + +### Prerequisites + +- **Node.js**: v18+ recommended (ESM support required) +- **npm**: v8+ (comes with Node.js) +- **Git**: For version control +- **Platform-specific tools**: + - Windows: No additional tools needed + - macOS: Xcode Command Line Tools + - Linux: Standard build tools (`build-essential` on Debian/Ubuntu) + +### First-time Setup + +```bash +# Clone repository +git clone +cd markpdfdown-desktop + +# Install dependencies +npm install + +# Generate Prisma client +npm run generate + +# Run database migrations +npm run migrate:dev + +# Start development server +npm run dev +``` + +## Development Workflow + +### Adding New Features + +Follow these steps when adding new features to the project: + +#### 1. Database Schema Changes + +Create Prisma schema changes in `src/core/infrastructure/db/schema.prisma`: + +```bash +# Run migrations in development +npm run migrate:dev + +# Generate Prisma client +npm run generate +``` + +#### 2. Create Repository + +Add Repository in `src/core/domain/repositories/`: + +```typescript +// Example: src/core/domain/repositories/FeatureRepository.ts +import { prisma } from '../../infrastructure/db/index.js'; + +const featureRepository = { + findAll: () => prisma.feature.findMany(), + findById: (id: number) => prisma.feature.findUnique({ where: { id } }), + create: (data) => prisma.feature.create({ data }), + update: (id, data) => prisma.feature.update({ where: { id }, data }), + remove: (id) => prisma.feature.delete({ where: { id } }), +}; + +export default featureRepository; +``` + +#### 3. Add Repository Tests + +Add tests in `src/core/domain/repositories/__tests__/`: + +```typescript +// Example: FeatureRepository.test.ts +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { mockDeep, mockReset } from 'vitest-mock-extended' +import { PrismaClient } from '@prisma/client' +import featureRepository from '../FeatureRepository.js' + +const prismaMock = mockDeep() +vi.mock('../../../infrastructure/db/index.js', () => ({ prisma: prismaMock })) + +describe('featureRepository', () => { + beforeEach(() => { mockReset(prismaMock) }) + + it('should create feature', async () => { + prismaMock.feature.create.mockResolvedValue({ id: 1, name: 'test' }) + const result = await featureRepository.create({ name: 'test' }) + expect(result.id).toBe(1) + }) +}) +``` + +#### 4. Add Application Services (if needed) + +Add services in `src/core/application/services/` and corresponding tests in `src/core/application/services/__tests__/`. + +#### 5. Create IPC Handler + +Add IPC handler in `src/main/ipc/handlers/feature.handler.ts`: + +```typescript +import { ipcMain } from 'electron'; +import featureRepository from '../../../core/domain/repositories/FeatureRepository.js'; + +export function registerFeatureHandlers() { + ipcMain.handle('feature:action', async (_, params) => { + try { + const result = await featureRepository.action(params); + return { success: true, data: result }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }); + console.log('[IPC] Feature handlers registered'); +} +``` + +#### 6. Register Handler + +Register handler in `src/main/ipc/handlers/index.ts`. + +#### 7. Add IPC Handler Tests + +Add tests in `src/main/ipc/__tests__/handlers.test.ts`: + +```typescript +it('should handle feature:action', async () => { + const mockData = { id: 1, name: 'test' } + vi.mocked(featureRepository.action).mockResolvedValue(mockData) + + const result = await handlers.get('feature:action')!(null, params) + + expect(result.success).toBe(true) + expect(result.data).toEqual(mockData) +}) +``` + +#### 8. Add Preload API + +Add preload API in `src/preload/index.ts`: + +```typescript +feature: { + action: (params) => ipcRenderer.invoke('feature:action', params) +} +``` + +#### 9. Update TypeScript Types + +Update TypeScript types in `src/renderer/electron.d.ts`. + +#### 10. Update Frontend Components + +Update React frontend components to use `window.api.feature.action()`. + +#### 11. Add Component Tests (if new component) + +Add tests in `src/renderer/components/__tests__/`: + +```typescript +import { render, screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import FeatureComponent from '../FeatureComponent' + +describe('FeatureComponent', () => { + it('should render and interact', async () => { + window.api.feature.action = vi.fn().mockResolvedValue({ success: true }) + render() + const button = screen.getByRole('button') + await userEvent.click(button) + expect(window.api.feature.action).toHaveBeenCalled() + }) +}) +``` + +#### 12. Add i18n Translations (if UI changes) + +Add translations in `src/renderer/locales/` for all supported languages. + +#### 13. Run Tests and Verification + +```bash +# Run unit tests +npm run test:unit + +# Run component tests (if applicable) +npm run test:renderer + +# Check coverage +npm run test:coverage + +# Run linting +npm run lint + +# Type checking +npm run typecheck + +# Test in development +npm run dev +``` + +### Testing + +This project uses Vitest as the test framework. For detailed testing documentation, see [docs/TESTING_GUIDE.md](./docs/TESTING_GUIDE.md). + +```bash +# Run all tests +npm test + +# Run unit tests (main/server) +npm run test:unit + +# Run renderer tests (React components) +npm run test:renderer + +# Watch mode (for development) +npm run test:watch + +# Generate coverage report +npm run test:coverage +``` + +## Code Style Guidelines + +### TypeScript + +- **Strict Mode**: Enabled globally - no `any` unless explicitly allowed +- **Module System**: ESM (`type: "module"` in package.json) +- **Module Alias**: + - `@` alias available in frontend only + - Backend uses relative imports with `.js` extensions + +### ESLint + +- Config file: `eslint.config.js` (flat config format) +- Extends TypeScript-ESLint recommended config +- React hooks enforcement enabled + +### Naming Conventions + +| Type | Convention | Example | +|------|------------|---------| +| Files (Controllers, DAL, Logic) | PascalCase | `TaskController.ts`, `ProviderDal.ts` | +| Variables/Functions | camelCase | `createTasks`, `getAllTasks` | +| Constants (config) | SCREAMING_SNAKE_CASE | `MAX_RETRY_COUNT` | +| Classes/Interfaces | PascalCase | `Task`, `Provider` | +| Database Models | PascalCase singular | `Provider`, `Task`, `Model` | + +## Commit Guidelines + +This project follows [Conventional Commits](https://www.conventionalcommits.org/) specification. + +### Commit Message Format + +``` +type(scope): description +``` + +- `scope` is optional +- Description should be concise and descriptive + +### Commit Types + +| Type | Description | +|------|-------------| +| `feat` | A new feature | +| `fix` | A bug fix | +| `docs` | Documentation only changes | +| `style` | Code style changes (formatting, etc.) | +| `refactor` | Code change that neither fixes a bug nor adds a feature | +| `perf` | Performance improvements | +| `test` | Adding or updating tests | +| `chore` | Maintenance tasks | +| `ci` | CI/CD changes | +| `build` | Build system changes | +| `revert` | Revert a previous commit | + +### Examples + +```bash +feat(auth): add login feature +fix(upload): resolve file size validation issue +docs: update README with new API examples +refactor(worker): simplify task processing logic +test(provider): add unit tests for provider repository +``` + +### Pre-commit Checks + +Always run before committing: + +```bash +npm run lint +npm run typecheck +npm test +``` + +## Pull Request Process + +### Before Submitting + +1. **Create a branch**: Create a feature branch from `master` + ```bash + git checkout -b feat/your-feature-name + ``` + +2. **Make changes**: Implement your changes following the code style guidelines + +3. **Test**: Ensure all tests pass + ```bash + npm test + npm run lint + npm run typecheck + ``` + +4. **Commit**: Follow the commit guidelines + +### Submitting a PR + +1. Push your branch to your fork +2. Open a Pull Request against the `master` branch +3. Fill in the PR template with: + - Description of changes + - Related issue numbers + - Screenshots (if applicable) + - Testing performed + +### PR Review Process + +1. At least one maintainer review is required +2. All CI checks must pass +3. Address any requested changes +4. Once approved, the PR will be merged + +### After Merge + +- Delete your feature branch +- Pull the latest `master` to your local repository + +## Getting Help + +- **Documentation**: Check [docs/](./docs/) for detailed guides +- **Issues**: Search existing issues or create a new one +- **Discussions**: Use GitHub Discussions for questions + +--- + +Thank you for contributing to MarkPDFdown!