Development Guidelines
Development Guidelines
Table of Contents
- Getting Started
- Development Workflow
- Test-Driven Development (TDD)
- Coding Standards
- Testing Guidelines
- Git Workflow
- Common Development Tasks
- Debugging
- Code Review Guidelines
- Release Process
- Troubleshooting
Getting Started
Prerequisites
- Node.js 20+ - Runtime environment
- npm - Package manager (comes with Node.js)
- Git - Version control
- Chrome or Firefox - For testing the extension
Initial Setup
# Clone the repository
git clone https://github.com/satoryu/copy-for-scrapbox.git
cd copy-for-scrapbox
# Install dependencies
npm install
# Run tests to verify setup
npm test
# Start development server
npm run dev
Loading the Extension in Chrome
- Open Chrome and navigate to
chrome://extensions - Enable “Developer mode” (toggle in top right)
- Click “Load unpacked”
- Select the
.output/chrome-mv3/directory - The extension should now appear in your browser
Loading the Extension in Firefox
# Start Firefox development mode
npm run dev:firefox
Firefox will automatically open with the extension loaded.
Development Workflow
Standard Development Cycle
This project follows Test-Driven Development (TDD) practices. The standard workflow is:
1. Write failing test (Red)
↓
2. Write minimal code to pass test (Green)
↓
3. Refactor while keeping tests green (Refactor)
↓
4. Type check with TypeScript
↓
5. Manual testing in browser
↓
6. Commit changes
Daily Development Commands
# Development with hot reload
npm run dev # Chrome
npm run dev:firefox # Firefox
# Testing
npm test # Run all tests once
npm run test:watch # Watch mode (recommended during development)
# Type checking
npm run compile # Check TypeScript types
# Build for production
npm run build # Chrome
npm run build:firefox # Firefox
# Create distribution package
npm run zip # Chrome Web Store
npm run zip:firefox # Firefox Add-ons
Test-Driven Development (TDD)
Core Principle
⚠️ CRITICAL: Always write tests BEFORE implementation
This project strictly follows TDD practices. Writing tests first:
- Defines the API contract before implementation
- Ensures testability
- Provides living documentation
- Catches regressions immediately
- Enables confident refactoring
TDD Cycle: Red-Green-Refactor
🔴 Red - Write a Failing Test
Before writing any production code, write a test that fails:
// Example: Adding a new function to utils/link.ts
// Step 1: Write the test FIRST in utils/link.test.js
import { describe, it, expect } from 'vitest';
import { removeEmoji } from './link';
describe('removeEmoji', () => {
it('should remove emoji from title', () => {
expect(removeEmoji('Hello 👋 World')).toBe('Hello World');
});
});
Run the test to confirm it fails:
npm test
# ❌ FAIL: removeEmoji is not defined
🟢 Green - Make the Test Pass
Write minimal code to make the test pass:
// Step 2: Implement in utils/link.ts
export function removeEmoji(title: string): string {
return title.replace(/[\u{1F000}-\u{1F9FF}]/gu, '');
}
Run tests again:
npm test
# ✅ PASS: All tests passing
♻️ Refactor - Improve the Code
Clean up the code while keeping tests green:
// Step 3: Improve implementation
export function removeEmoji(title: string): string {
// More comprehensive emoji regex
const emojiRegex = /[\u{1F000}-\u{1F9FF}\u{2600}-\u{26FF}]/gu;
return title.replace(emojiRegex, '');
}
Run tests after each change:
npm test
# ✅ PASS: All tests still passing
TDD Workflow Examples
Example 1: Adding a Utility Function
# ❌ WRONG: Don't do this
vim utils/newfeature.ts # Writing code first
# ✅ CORRECT: Do this
vim utils/newfeature.test.ts # 1. Write test first
npm test # 2. See it fail (Red)
vim utils/newfeature.ts # 3. Write implementation
npm test # 4. See it pass (Green)
vim utils/newfeature.ts # 5. Refactor if needed
npm test # 6. Verify still passing
Example 2: Fixing a Bug
# 1. Write a test that reproduces the bug
vim utils/link.test.js
# Add test case that currently fails
# 2. Verify the test fails
npm test
# ❌ Test should fail, confirming the bug
# 3. Fix the bug
vim utils/link.ts
# 4. Verify the fix
npm test
# ✅ Test should now pass
Example 3: Adding a React Component
# 1. Test the underlying logic first
vim entrypoints/popup/components/MyButton.test.tsx
npm test # Should fail
# 2. Implement component with minimal logic
vim entrypoints/popup/components/MyButton.tsx
# 3. Extract complex logic to utils/ with tests
vim utils/myfeature.test.ts
vim utils/myfeature.ts
# 4. Verify all tests pass
npm test
TDD Best Practices
- One Test Per Behavior
- Each test should verify one specific behavior
- Use descriptive test names:
it('should remove square brackets from title')
- Test File Location
- Place tests next to source files
link.ts→link.test.jsorlink.test.ts- Same directory structure
- Use Watch Mode
npm run test:watchTests re-run automatically on file changes
- Mock Browser APIs
import { browser } from 'wxt/testing'; // Mock is automatically provided by WxtVitest browser.tabs.query.mockResolvedValue([{ id: 1, title: 'Test' }]); - Test Edge Cases
- Empty strings
- Special characters
- Unicode
- Null/undefined
- Maximum values
When TDD Is Challenging
In rare cases, TDD might be difficult:
- Initial UI layout exploration
- Extension permissions setup
- Manifest configuration
However, even in these cases:
- Add tests after implementation
- Extract and test any logic from UI
- Test integration points
Coding Standards
TypeScript
Type Safety
- Always use TypeScript for new code
- Enable strict mode (inherited from WXT config)
- Avoid
anytype - useunknownif truly dynamic
// ❌ Bad
function process(data: any) { }
// ✅ Good
function process(data: Browser.tabs.Tab) { }
Type Imports
// Use type imports when importing only types
import type { ClipboardHistoryItem } from '../../utils/history';
// Regular import for values
import { getHistory } from '../../utils/history';
Browser API Types
WXT provides built-in types:
// Tabs
globalThis.Browser.tabs.Tab
globalThis.Browser.tabs.query
// Context Menus
globalThis.Browser.contextMenus.OnClickData
globalThis.Browser.contextMenus.ContextType
React
Functional Components
Always use functional components with hooks:
// ✅ Good
const MyComponent: React.FC<Props> = ({ onCopied }) => {
const [state, setState] = useState<string>('');
return <div>{state}</div>;
};
// ❌ Bad - Don't use class components
class MyComponent extends React.Component { }
Props Pattern
interface Props {
onCopied: (message: string) => void;
}
const MyButton: React.FC<Props> = ({ onCopied }) => {
const handleClick = async () => {
// Logic here
onCopied('Success!');
};
return <button onClick={handleClick}>Copy</button>;
};
Component File Structure
// 1. Imports
import React, { useState } from 'react';
// 2. Type definitions
interface Props {
// ...
}
// 3. Component
const MyComponent: React.FC<Props> = (props) => {
// Hooks
const [state, setState] = useState();
// Event handlers
const handleClick = () => { };
// Render
return <div>...</div>;
};
// 4. Export
export default MyComponent;
Async/Await
Always use async/await instead of raw promises:
// ✅ Good
async function copyTab() {
const tabs = await browser.tabs.query({ active: true });
const link = await createLinkForTab(tabs[0]);
await writeTextToClipboard(link);
}
// ❌ Bad
function copyTab() {
browser.tabs.query({ active: true })
.then(tabs => createLinkForTab(tabs[0]))
.then(link => writeTextToClipboard(link));
}
Naming Conventions
Files
- React components: PascalCase.tsx (
CopyCurrentTabButton.tsx) - Utilities: camelCase.ts (
clipboard.ts,link.ts) - Tests: Match source with
.test.jsor.test.ts(link.test.js) - Config files: lowercase with dots (
wxt.config.ts,vitest.config.ts)
Variables and Functions
- camelCase for variables and functions
- PascalCase for React components and classes
- UPPER_SNAKE_CASE for constants
// Variables
const clipboardHistory = [];
const MAX_HISTORY_ITEMS = 100;
// Functions
async function writeTextToClipboard(text: string) { }
// Components
const CopyButton: React.FC = () => { };
// Classes
class HandlerRepository { }
Code Organization
Function Length
- Keep functions small (ideally < 20 lines)
- Single responsibility principle
- Extract complex logic to separate functions
File Length
- Keep files focused (< 200 lines ideal)
- Split large components into smaller ones
- Extract shared logic to utils
Import Order
// 1. External libraries
import React, { useState } from 'react';
// 2. WXT/Browser
import { browser } from 'wxt/browser';
// 3. Internal utilities (absolute paths)
import { createLinkForTab } from '@/utils/link';
// 4. Components (relative paths)
import CopyButton from './components/CopyButton';
// 5. Types
import type { Props } from './types';
Comments
When to Comment
- Complex algorithms
- Non-obvious business logic
- Workarounds for browser bugs
When NOT to Comment
- Self-explanatory code
- What the code does (code should be self-documenting)
- Redundant information
// ❌ Bad - Obvious comment
// Get the current tab
const tab = await getCurrentTab();
// ✅ Good - Explains "why"
// Remove square brackets because they break Scrapbox's link syntax
const sanitizedTitle = title.replaceAll(/[\[\]]/g, '');
Testing Guidelines
📖 For comprehensive testing strategy including coverage goals, test layers, tools, and implementation roadmap, see testing.md
This section covers practical testing guidelines for writing tests.
Test Structure
import { describe, it, expect, beforeEach } from 'vitest';
describe('ModuleName', () => {
describe('functionName', () => {
it('should handle normal case', () => {
const result = functionName('input');
expect(result).toBe('expected');
});
it('should handle edge case', () => {
const result = functionName('');
expect(result).toBe('');
});
it('should throw error for invalid input', () => {
expect(() => functionName(null)).toThrow();
});
});
});
Testing Browser APIs
Use WxtVitest plugin for automatic mocking:
import { browser } from 'wxt/testing';
describe('tabs module', () => {
it('should query current tab', async () => {
// Mock the browser API
browser.tabs.query.mockResolvedValue([
{ id: 1, title: 'Test Tab', url: 'https://example.com' }
]);
const tab = await getCurrentTab();
expect(tab.title).toBe('Test Tab');
expect(browser.tabs.query).toHaveBeenCalledWith({
active: true,
currentWindow: true
});
});
});
Testing i18n
import { browser } from 'wxt/testing';
describe('i18n', () => {
beforeEach(() => {
browser.i18n.getMessage.mockImplementation((key) => {
const messages = {
'extensionName': 'Copy for Scrapbox',
'copySuccess': 'Copied!'
};
return messages[key] || key;
});
});
it('should get message', () => {
const message = browser.i18n.getMessage('copySuccess');
expect(message).toBe('Copied!');
});
});
Test Coverage Goals
- Utility functions: 100% coverage
- Edge cases: All critical paths
- Browser API interactions: Mock and verify
- Error handling: Test failure scenarios
Running Tests
# Run all tests once
npm test
# Watch mode (recommended)
npm run test:watch
# Run specific test file
npm test -- link.test.js
# Run with coverage
npm test -- --coverage
Git Workflow
Branch Strategy
main- Protected branch, production-ready code- Feature branches -
feature/descriptionorfix/description - All changes via Pull Requests
Creating a Feature Branch
# Create and switch to new branch
git checkout -b feature/add-emoji-filter
# Make changes with TDD workflow
# (Write tests, implement, commit)
# Push to remote
git push -u origin feature/add-emoji-filter
# Create pull request on GitHub
Commit Message Guidelines
Follow conventional commits style:
<type>: <description>
[optional body]
[optional footer]
Types:
feat: New featurefix: Bug fixdocs: Documentation changestest: Adding or updating testsrefactor: Code refactoringchore: Maintenance tasks
Examples:
git commit -m "feat: add emoji filtering to link titles"
git commit -m "fix: handle undefined tab titles correctly"
git commit -m "test: add edge cases for link formatting"
git commit -m "docs: update architecture documentation"
Pre-Commit Checklist
Before committing, ensure:
- ✅ All tests pass:
npm test - ✅ TypeScript compiles:
npm run compile - ✅ Manual testing in browser completed
- ✅ i18n messages updated (if UI text changed)
- ✅ Commit message is descriptive
Pull Request Guidelines
Before Creating PR
# Ensure branch is up to date
git fetch origin
git rebase origin/main
# Run full test suite
npm test
# Type check
npm run compile
# Build to verify no errors
npm run build
PR Description Template
## Description
Brief description of changes
## Type of Change
- [ ] Bug fix
- [ ] New feature
- [ ] Breaking change
- [ ] Documentation update
## Testing
- [ ] Unit tests added/updated
- [ ] Manual testing completed
- [ ] All tests passing
## Checklist
- [ ] Tests pass (`npm test`)
- [ ] TypeScript compiles (`npm run compile`)
- [ ] i18n messages updated (if applicable)
- [ ] Documentation updated (if applicable)
Common Development Tasks
Adding a New Popup Button
Follow TDD workflow:
- Write test for underlying logic (if testable)
vim utils/myfeature.test.ts npm test # Should fail - Implement utility function
vim utils/myfeature.ts npm test # Should pass - Create React component
vim entrypoints/popup/components/MyButton.tsximport React from 'react'; interface Props { onCopied: (message: string) => void; } const MyButton: React.FC<Props> = ({ onCopied }) => { const handleClick = async () => { // Use utility function const result = await myFeature(); await writeTextToClipboard(result); onCopied(browser.i18n.getMessage('myFeatureSuccess')); }; return ( <button onClick={handleClick}> {browser.i18n.getMessage('myFeatureButton')} </button> ); }; export default MyButton; - Add i18n messages
# Update all 4 locale files vim public/_locales/en/messages.json vim public/_locales/ja/messages.json vim public/_locales/zh_CN/messages.json vim public/_locales/zh_TW/messages.json{ "myFeatureButton": { "message": "My Feature" }, "myFeatureSuccess": { "message": "Success!" } } - Add to App.tsx
import MyButton from './components/MyButton'; <MyButton onCopied={appendMessage} /> - Test manually
npm run dev # Load extension and test button
Adding a Context Menu Item
- Write test for handler logic
vim utils/myhandler.test.ts npm test # Should fail - Implement handler
vim utils/myhandler.ts npm test # Should pass - Register in context menu
// entrypoints/context_menu/index.ts repository.registerHandler({ id: 'my-menu-item', title: '__MSG_myMenuItem__', contexts: ['page'], // or ['selection'], ['link'], etc. handler: async (info, tab) => { const result = await myHandler(info, tab); const tabId = tab.id; if (tabId === undefined) return; await browser.scripting.executeScript({ target: { tabId }, func: writeTextToClipboard, args: [result] }); } }); - Add i18n messages
{ "myMenuItem": { "message": "My Menu Item" } } - Test
npm run dev # Right-click on page and verify menu item appears
Modifying Link Format
⚠️ CRITICAL: Always use TDD for link formatting changes
- Write failing test FIRST
// utils/link.test.js it('should handle new format requirement', () => { const result = createLinkForTab({ url: 'https://example.com', title: 'Test [Brackets]' }); expect(result).toBe('[https://example.com Test Brackets]'); });npm test # Should fail - Implement in utils/link.ts
async function createLinkForTab(tab: Browser.tabs.Tab): Promise<string> { const title = (tab.title || '') .replaceAll(/[\[\]]/g, '') // Remove brackets .replaceAll(/`(.*)`/g, '$1'); // Remove backticks return `[${tab.url} ${title}]`; } - Verify tests pass
npm test # Should pass - Add more edge cases
it('should handle unicode characters', () => { }); it('should handle very long titles', () => { }); it('should handle empty titles', () => { }); - Manual testing
npm run dev # Test with various URL/title combinations
Adding a New Locale
- Create locale directory
mkdir -p public/_locales/fr - Copy and translate messages
cp public/_locales/en/messages.json public/_locales/fr/messages.json vim public/_locales/fr/messages.json # Translate all messages - Test locale
- Change Chrome language to French
- Reload extension
- Verify all text appears in French
Debugging
Popup Debugging
- Open popup
- Right-click on popup → “Inspect”
- DevTools opens with popup context
- Use Console, Network, Elements tabs
// Add console.log for debugging
const handleClick = async () => {
console.log('Button clicked');
const tabs = await browser.tabs.query({ active: true });
console.log('Tabs:', tabs);
};
Background Script Debugging
- Navigate to
chrome://extensions - Find “Copy for Scrapbox”
- Click “Inspect service worker”
- DevTools opens with background context
// background.ts
export default defineBackground(() => {
console.log('Background script loaded');
browser.contextMenus.onClicked.addListener((info, tab) => {
console.log('Menu clicked:', info.menuItemId);
console.log('Tab:', tab);
});
});
Content Script Debugging
- Open page where content script runs
- F12 to open DevTools (page context)
- Content script logs appear in Console
Side Panel Debugging
- Open side panel
- Right-click in side panel → “Inspect”
- DevTools opens with side panel context
Common Issues
“Extension context invalidated”
- Cause: Extension reloaded while DevTools open
- Solution: Close and reopen DevTools
Clipboard not working
- Cause: Script injection failed or permissions missing
- Solution: Check console for errors, verify
scriptingpermission
i18n messages not showing
- Cause: Message key typo or locale file missing
- Solution: Check all locale files have the message key
Tests failing in CI but passing locally
- Cause: Browser API mocks not configured
- Solution: Ensure
WxtVitest()plugin is invitest.config.ts
Code Review Guidelines
For Authors
Before requesting review:
- ✅ Self-review your own changes
- ✅ All tests passing
- ✅ TypeScript compiling without errors
- ✅ Manual testing completed
- ✅ PR description is complete
- ✅ i18n messages added (if applicable)
For Reviewers
Review checklist:
- Tests: Are there tests? Do they follow TDD?
- Type Safety: Any
anytypes? Proper TypeScript usage? - Code Quality: Clean, readable, follows standards?
- Performance: Any unnecessary re-renders or loops?
- Security: Input validation? XSS risks?
- i18n: All user-facing text internationalized?
- Documentation: Complex logic documented?
Review Comments
Use constructive language:
❌ "This is wrong"
✅ "Consider using async/await here for better readability"
❌ "Bad code"
✅ "This could be simplified by extracting a helper function"
❌ "You forgot tests"
✅ "Could you add a test case for the empty string scenario?"
Release Process
Version Bumping
Use semantic versioning:
- Major (1.x.x): Breaking changes
- Minor (x.1.x): New features
- Patch (x.x.1): Bug fixes
# Update version in package.json
vim package.json
# Change "version": "1.11.0" → "1.12.0"
# Commit version bump
git add package.json
git commit -m "chore: bump version to 1.12.0"
Creating a Release
- Ensure all tests pass
npm test npm run compile - Build production version
npm run build npm run zip - Tag the release
git tag v1.12.0 git push origin v1.12.0 - GitHub Actions automatically:
- Runs tests
- Builds extension
- Creates GitHub release
- Uploads ZIP artifact
- Manual upload to Chrome Web Store:
- Download ZIP from GitHub release
- Upload to Chrome Web Store Developer Dashboard
- Submit for review
Release Checklist
- Version bumped in
package.json - All tests passing
- TypeScript compiles
- Manual testing completed
- CHANGELOG updated (if exists)
- Git tag created
- GitHub release published
- Chrome Web Store updated
Troubleshooting
Build Issues
npm install fails
# Clear cache and reinstall
rm -rf node_modules package-lock.json
npm cache clean --force
npm install
TypeScript errors after update
# Regenerate WXT types
npm run postinstall
npm run compile
Testing Issues
Tests not running
# Verify Vitest is installed
npm list vitest
# Check test files are named correctly
# Must end with .test.js or .test.ts
Browser API mocks not working
// Ensure WxtVitest plugin is configured
// vitest.config.ts
import { WxtVitest } from 'wxt/testing';
export default defineConfig({
plugins: [WxtVitest()],
});
Extension Issues
Extension not loading
- Check for errors in
chrome://extensions - Verify build completed successfully
- Try removing and re-adding extension
Hot reload not working
- Restart
npm run dev - Manually reload extension
- Check console for WXT errors
Context menus not appearing
- Check
wxt.config.tshascontextMenuspermission - Verify background script loaded (check service worker)
- Check browser console for errors
Runtime Issues
Clipboard not working
- Ensure page is HTTPS (required for clipboard API)
- Check
scriptingpermission in manifest - Verify script injection succeeded
Storage not persisting
- Check
storagepermission in manifest - Verify using correct storage API (
localvssync) - Check quota limits (5MB for local storage)
Resources
Official Documentation
Project Resources
Useful Links
Getting Help
Internal Resources
- Check this documentation first
- Review
CLAUDE.mdfor AI development guidelines - Search existing GitHub issues
Asking Questions
When asking for help:
- Describe the problem clearly
- Share code snippets (not screenshots of code)
- Include error messages in full
- Describe what you’ve tried
- Mention your environment (OS, Node version, browser)
Reporting Bugs
Use the GitHub issue template:
**Describe the bug**
A clear description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior.
**Expected behavior**
What you expected to happen.
**Screenshots**
If applicable, add screenshots.
**Environment**
- OS: [e.g. macOS, Windows, Linux]
- Browser: [e.g. Chrome 120]
- Extension Version: [e.g. 1.11.0]
Quick Reference
Most Common Commands
# Development
npm run dev # Start dev server
npm run test:watch # Run tests in watch mode
# Before committing
npm test # Run all tests
npm run compile # Type check
# Release
npm run build # Production build
npm run zip # Create distribution package
File Locations
- Tests: Next to source files (e.g.,
link.test.js) - Components:
entrypoints/popup/components/orentrypoints/sidepanel/components/ - Utilities:
utils/ - i18n:
public/_locales/{locale}/messages.json - Config: Root directory (
wxt.config.ts,vitest.config.ts)
Key Principles
- Test-Driven Development - Always write tests first
- Type Safety - Use TypeScript strictly
- Minimal Dependencies - Keep bundle size small
- Browser Compatibility - Use
browserAPI, notchrome - Internationalization - Always use i18n for user-facing text
Document Version: 1.0 Last Updated: 2026-01-11 For Codebase Version: 1.11.0