Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,48 @@ Please take a look at [this demo](https://www.youtube.com/watch?v=prKgvy8d9-c).

Visit [Chrome Web Store](https://chrome.google.com/webstore/detail/copy-for-scrapbox/kalhokahkhkmbkiliieonfdmdeajlnog) and Click `Add to Chrome` button.

## Development

### Running Tests

```bash
# Run all tests (utils + components)
npm test

# Run only utility tests
npm run test:utils

# Run only component tests
npm run test:components

# Run tests with coverage
npm run test:coverage

# Run tests in watch mode
npm run test:watch
```

### Type Checking

```bash
npm run compile
```

### Building

```bash
# Development mode with hot reload
npm run dev

# Production build
npm run build

# Create distributable ZIP
npm run zip
```

For more information about development, see [CLAUDE.md](./CLAUDE.md).

## License

This extension is available as open source under the terms of [MIT License](https://github.com/satoryu/copy-for-scrapbox/blob/main/LICENSE).
45 changes: 33 additions & 12 deletions docs/development-guideline.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,17 @@ This project follows **Test-Driven Development (TDD)** practices. The standard w
3. Refactor while keeping tests green (Refactor)
4. Type check with TypeScript
4. Verify CI checks locally (REQUIRED)
- Run: npm run compile
- Run: npm run test:coverage
5. Manual testing in browser
6. Commit changes
```

**⚠️ Step 4 is CRITICAL**: Always run the same checks that CI runs (`npm run compile` and `npm run test:coverage`) before committing. This prevents CI failures and ensures code quality.

### Daily Development Commands

```bash
Expand Down Expand Up @@ -635,33 +639,50 @@ git commit -m "docs: update architecture documentation"

### Pre-Commit Checklist

Before committing, ensure:
**⚠️ CRITICAL: Always verify CI checks locally before committing**

Before committing, you **MUST** run the same checks that CI runs and ensure they all pass:

```bash
# Run CI verification locally (REQUIRED before every commit)
npm run compile # TypeScript type checking (CI step 1)
npm run test:coverage # Run tests with coverage (CI step 2)
```

Complete checklist:

1. ✅ All tests pass: `npm test`
2. ✅ TypeScript compiles: `npm run compile`
3. ✅ Manual testing in browser completed
4. ✅ i18n messages updated (if UI text changed)
5. ✅ Commit message is descriptive
1. ✅ **CI Verification**: Run `npm run compile` and `npm run test:coverage` - both must succeed
2. ✅ Manual testing in browser completed
3. ✅ i18n messages updated (if UI text changed)
4. ✅ Commit message is descriptive

**Why this is important**:
- Prevents CI failures after pushing
- Catches type errors and test failures early
- Ensures code quality before review
- Saves time for reviewers and maintainers

### Pull Request Guidelines

#### Before Creating PR

**⚠️ CRITICAL: Run CI checks locally before creating PR**

```bash
# Ensure branch is up to date
git fetch origin
git rebase origin/main

# Run full test suite
npm test

# Type check
npm run compile
# Run CI verification (REQUIRED)
npm run compile # TypeScript type checking (CI step 1)
npm run test:coverage # Run tests with coverage (CI step 2)

# Build to verify no errors
npm run build
```

All CI checks must pass locally before creating the pull request.

#### PR Description Template

```markdown
Expand Down
22 changes: 11 additions & 11 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,26 +227,26 @@ Consider using Husky to run tests before commits, though this may slow down deve

## Implementation Roadmap

### Phase 1: Foundation (Weeks 1-2)
### Phase 1: Foundation (Weeks 1-2) ✅ **COMPLETE**
**Goal**: Set up testing infrastructure

- [ ] Install React Testing Library and dependencies
- [ ] Configure jsdom environment in Vitest
- [ ] Create test setup file
- [ ] Configure coverage reporting
- [ ] Update CI workflow
- [x] Install React Testing Library and dependencies
- [x] Configure jsdom environment in Vitest
- [x] Create test setup file
- [x] Configure coverage reporting
- [x] Update CI workflow

**Outcome**: Ready to write component tests

---

### Phase 2: Unit Test Expansion (Weeks 3-4)
### Phase 2: Unit Test Expansion (Weeks 3-4) ✅ **COMPLETE**
**Goal**: Achieve 100% coverage for utilities

- [ ] Expand existing utility tests to cover all edge cases
- [ ] Add tests for clipboard operations
- [ ] Add tests for history management edge cases
- [ ] Ensure all tab query functions are tested
- [x] Expand existing utility tests to cover all edge cases
- [x] Add tests for clipboard operations
- [x] Add tests for history management edge cases
- [x] Ensure all tab query functions are tested

**Outcome**: Complete utils layer testing

Expand Down
104 changes: 104 additions & 0 deletions entrypoints/popup/App.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import App from './App';
import * as clipboard from '@/utils/clipboard';
import * as tabs from '@/utils/tabs';
import * as link from '@/utils/link';

// Mock the utility modules
vi.mock('@/utils/clipboard');
vi.mock('@/utils/tabs');
vi.mock('@/utils/link');

describe('Popup App', () => {
// Simplified mock data without full Browser.tabs.Tab type for test readability
const mockCurrentTab = { id: 1, url: 'https://example.com', title: 'Example' };
const mockSelectedTabs = [
{ id: 1, url: 'https://example.com', title: 'Example' },
{ id: 2, url: 'https://test.com', title: 'Test' },
];
const mockAllTabs = [
{ id: 1, url: 'https://example.com', title: 'Example' },
{ id: 2, url: 'https://test.com', title: 'Test' },
{ id: 3, url: 'https://demo.com', title: 'Demo' },
];
const mockLink = '[https://example.com Example]';
const mockLinks = '[https://example.com Example]\n[https://test.com Test]';

beforeEach(() => {
vi.clearAllMocks();

// Set up i18n mock
browser.i18n.getMessage = vi.fn((key: string) => {
const messages: Record<string, string> = {
copyCurrentTabButtonName: 'Copy Current Tab',
copySelectedTabsButtonName: 'Copy Selected Tabs',
copyAllTabsButtonName: 'Copy All Tabs',
copyCurrentTabMessage: 'Copied current tab!',
copySelectedTabsMessage: 'Copied selected tabs!',
copyAllTabsMessage: 'Copied all tabs!',
};
return messages[key] || key;
});

// Set up successful mock implementations (using 'as any' for simplified mock Tab objects)
vi.mocked(tabs.getCurrentTab).mockResolvedValue([mockCurrentTab] as any);
vi.mocked(tabs.getSelectedTabs).mockResolvedValue(mockSelectedTabs as any);
vi.mocked(tabs.getAllTabsOnCurrentWindow).mockResolvedValue(mockAllTabs as any);
vi.mocked(link.createLinkForTab).mockResolvedValue(mockLink);
vi.mocked(link.createLinksForTabs).mockResolvedValue(mockLinks);
vi.mocked(clipboard.writeTextToClipboard).mockResolvedValue(undefined);
});

it('should render all three copy buttons', async () => {
render(<App />);

// Wait for async renders to complete
await waitFor(() => {
expect(screen.getByRole('button', { name: /Copy Current Tab/ })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Copy Selected Tabs/ })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Copy All Tabs/ })).toBeInTheDocument();
});
});

it('should render container with correct CSS class', async () => {
const { container } = render(<App />);

// Wait for async renders to complete
await waitFor(() => {
const containerDiv = container.querySelector('.container');
expect(containerDiv).toBeInTheDocument();
});
});

it('should render message box', async () => {
const { container } = render(<App />);

// Wait for async renders to complete
await waitFor(() => {
const messageBox = container.querySelector('#message-box');
expect(messageBox).toBeInTheDocument();
});
});

it('should have empty message box initially', async () => {
const { container } = render(<App />);

// Wait for async renders to complete
await waitFor(() => {
const messageBox = container.querySelector('#message-box');
expect(messageBox).toBeInTheDocument();
expect(messageBox).toBeEmptyDOMElement();
});
});

it('should render exactly three button components', async () => {
render(<App />);

await waitFor(() => {
const buttons = screen.getAllByRole('button');
expect(buttons).toHaveLength(3);
});
});
});
Loading