chrome-extension-test-framework
Version:
Universal testing framework for Chrome extensions - Fast static analysis without browser dependencies
463 lines (399 loc) • 17.4 kB
JavaScript
/**
* ErrorHandler - 統合エラーハンドリングシステム
* エラーの分類、詳細化、ユーザーフレンドリーなメッセージの提供
*/
const fs = require('fs').promises;
const path = require('path');
class ErrorHandler {
constructor() {
// ANSI color codes
this.colors = {
red: '\x1b[31m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
cyan: '\x1b[36m',
reset: '\x1b[0m',
bold: '\x1b[1m'
};
this.errorCategories = {
FILE_NOT_FOUND: {
level: 'error',
prefix: '📁',
color: this.colors.red,
label: 'ERROR',
suggestions: ['Check if the file exists', 'Verify the file path', 'Ensure proper file permissions']
},
INVALID_JSON: {
level: 'error',
prefix: '📄',
color: this.colors.red,
label: 'ERROR',
suggestions: ['Validate JSON syntax', 'Check for missing commas or brackets', 'Use a JSON validator']
},
PERMISSION_ERROR: {
level: 'error',
prefix: '🔒',
color: this.colors.red,
label: 'ERROR',
suggestions: ['Check file permissions', 'Run with appropriate privileges', 'Verify directory access rights']
},
VALIDATION_ERROR: {
level: 'error',
prefix: '❌',
color: this.colors.red,
label: 'ERROR',
suggestions: ['Review the validation rules', 'Check the input format', 'Refer to Chrome Extension documentation']
},
SECURITY_ERROR: {
level: 'critical',
prefix: '🚨',
color: this.colors.magenta,
label: 'CRITICAL',
suggestions: ['Review security best practices', 'Check for hardcoded secrets', 'Use secure storage methods']
},
PERFORMANCE_ERROR: {
level: 'warning',
prefix: '⚡',
color: this.colors.yellow,
label: 'WARNING',
suggestions: ['Optimize code performance', 'Use Web Workers for heavy tasks', 'Implement lazy loading']
},
CODE_QUALITY: {
level: 'warning',
prefix: '📝',
color: this.colors.yellow,
label: 'WARNING',
suggestions: ['Review code quality guidelines', 'Remove debug code for production', 'Use proper logging levels']
},
TIMEOUT_ERROR: {
level: 'error',
prefix: '⏱️',
color: this.colors.red,
label: 'ERROR',
suggestions: ['Increase timeout duration', 'Optimize test performance', 'Check for infinite loops']
},
NETWORK_ERROR: {
level: 'error',
prefix: '🌐',
color: this.colors.red,
label: 'ERROR',
suggestions: ['Check network connectivity', 'Verify API endpoints', 'Implement retry logic']
},
CONFIGURATION_ERROR: {
level: 'error',
prefix: '⚙️',
color: this.colors.red,
label: 'ERROR',
suggestions: ['Review configuration file', 'Check for required fields', 'Validate configuration syntax']
},
INTERNAL_ERROR: {
level: 'critical',
prefix: '💥',
color: this.colors.magenta,
label: 'FRAMEWORK BUG',
suggestions: ['This is an internal framework error', 'Please report this issue on GitHub with full error details', 'Include your extension structure and command used']
}
};
this.errorContext = new Map();
}
/**
* エラーを処理して詳細な情報を追加
*/
async handleError(error, context = {}) {
const enhancedError = await this.enhanceError(error, context);
// エラーコンテキストを保存
this.errorContext.set(enhancedError.id, {
timestamp: new Date().toISOString(),
context: context,
error: enhancedError
});
return enhancedError;
}
/**
* エラーを詳細化
*/
async enhanceError(error, context = {}) {
const category = this.categorizeError(error);
const errorInfo = this.errorCategories[category] || this.errorCategories.INTERNAL_ERROR;
const enhanced = {
id: this.generateErrorId(),
name: error.name || 'Error',
message: error.message,
category: category,
level: errorInfo.level,
prefix: errorInfo.prefix,
color: errorInfo.color,
label: errorInfo.label,
timestamp: new Date().toISOString(),
// コンテキスト情報
context: {
file: context.file || this.extractFileFromStack(error.stack),
line: context.line || this.extractLineFromStack(error.stack),
column: context.column || this.extractColumnFromStack(error.stack),
suite: context.suite,
test: context.test,
phase: context.phase || 'unknown',
...context
},
// スタックトレース
stack: this.cleanStackTrace(error.stack),
// 提案
suggestions: await this.generateSuggestions(error, category, context),
// 関連するコード
codeSnippet: await this.extractCodeSnippet(error, context),
// 追加のメタデータ
metadata: {
errorCode: error.code,
syscall: error.syscall,
path: error.path,
...error.metadata
}
};
// フレームワーク固有のエラー情報を追加
if (error.frameworkDetails) {
enhanced.frameworkDetails = error.frameworkDetails;
}
return enhanced;
}
/**
* エラーを分類
*/
categorizeError(error) {
const message = error.message.toLowerCase();
const code = error.code;
// 手動で設定されたカテゴリを優先
if (error.category && this.errorCategories[error.category]) {
return error.category;
}
// ファイル関連
if (code === 'ENOENT' || message.includes('file not found') || message.includes('no such file')) {
return 'FILE_NOT_FOUND';
}
// JSON関連(manifest.jsonのエラーなど)
if ((message.includes('json') || message.includes('unexpected token') || error.name === 'SyntaxError')
&& (message.includes('manifest') || message.includes('parse'))) {
return 'INVALID_JSON';
}
// 権限関連
if (code === 'EACCES' || code === 'EPERM' || message.includes('permission denied')) {
return 'PERMISSION_ERROR';
}
// セキュリティ関連(より具体的な条件)
if (message.includes('eval') || message.includes('innerhtml') || message.includes('xss') ||
message.includes('injection') || message.includes('hardcoded secret') ||
message.includes('unsafe') || message.includes('vulnerable')) {
return 'SECURITY_ERROR';
}
// パフォーマンス関連(より具体的)
if (message.includes('file size') || message.includes('bundle size') ||
message.includes('memory leak') || message.includes('optimization') ||
message.includes('slow') || message.includes('performance')) {
return 'PERFORMANCE_ERROR';
}
// タイムアウト(パフォーマンスとは別扱い)
if (message.includes('timeout') || message.includes('timed out')) {
return 'TIMEOUT_ERROR';
}
// ネットワーク関連
if (code === 'ENOTFOUND' || code === 'ECONNREFUSED' ||
message.includes('network') || message.includes('fetch')) {
return 'NETWORK_ERROR';
}
// 設定関連
if (message.includes('config') || message.includes('configuration') ||
message.includes('setting')) {
return 'CONFIGURATION_ERROR';
}
// コード品質関連(開発ファイル、console使用など)
if (message.includes('console') || message.includes('development file') ||
message.includes('debug') || message.includes('node_modules') ||
message.includes('.git') || message.includes('test file')) {
return 'CODE_QUALITY';
}
// バリデーション関連(一般的な検証エラー)
if (message.includes('invalid') || message.includes('validation') ||
message.includes('required') || message.includes('missing') ||
message.includes('must be') || message.includes('should')) {
return 'VALIDATION_ERROR';
}
// フレームワーク内部エラー(非常に限定的に使用)
if (error.isFrameworkBug ||
(error.stack && error.stack.includes('/lib/') &&
!message.includes('test') && !message.includes('validation'))) {
return 'INTERNAL_ERROR';
}
// その他のエラーはVALIDATION_ERRORとして扱う
return 'VALIDATION_ERROR';
}
/**
* 提案を生成
*/
async generateSuggestions(error, category, context) {
const baseSuggestions = this.errorCategories[category]?.suggestions || [];
const contextualSuggestions = [];
// ファイルが見つからない場合
if (category === 'FILE_NOT_FOUND' && error.path) {
const dir = path.dirname(error.path);
try {
const files = await fs.readdir(dir);
const basename = path.basename(error.path);
const similar = files.filter(f =>
f.toLowerCase().includes(basename.toLowerCase()) ||
basename.toLowerCase().includes(f.toLowerCase())
);
if (similar.length > 0) {
contextualSuggestions.push(`Did you mean: ${similar.join(', ')}?`);
}
} catch (e) {
// ディレクトリも存在しない場合
contextualSuggestions.push('The parent directory does not exist');
}
}
// JSON エラーの場合
if (category === 'INVALID_JSON' && context.file) {
contextualSuggestions.push(`Validate ${context.file} with a JSON linter`);
// 一般的なJSON エラーパターン
if (error.message.includes('Unexpected token')) {
contextualSuggestions.push('Check for trailing commas in arrays or objects');
contextualSuggestions.push('Ensure all strings are properly quoted');
}
}
// バリデーションエラーの場合
if (category === 'VALIDATION_ERROR') {
if (error.message.includes('manifest')) {
contextualSuggestions.push('Refer to: https://developer.chrome.com/docs/extensions/mv3/manifest/');
}
if (error.message.includes('permission')) {
contextualSuggestions.push('Check the permissions documentation: https://developer.chrome.com/docs/extensions/mv3/declare_permissions/');
}
}
return [...baseSuggestions, ...contextualSuggestions];
}
/**
* コードスニペットを抽出
*/
async extractCodeSnippet(error, context) {
if (!context.file) return null;
try {
const fullPath = path.isAbsolute(context.file)
? context.file
: path.join(process.cwd(), context.file);
const content = await fs.readFile(fullPath, 'utf8');
const lines = content.split('\n');
const line = context.line || this.extractLineFromStack(error.stack);
if (!line || line < 1) return null;
const startLine = Math.max(0, line - 4);
const endLine = Math.min(lines.length, line + 3);
const snippet = [];
for (let i = startLine; i < endLine; i++) {
const lineNum = i + 1;
const prefix = lineNum === line ? '>' : ' ';
snippet.push(`${prefix} ${lineNum.toString().padStart(4)} | ${lines[i]}`);
}
return {
file: context.file,
line: line,
code: snippet.join('\n')
};
} catch (e) {
return null;
}
}
/**
* スタックトレースをクリーン
*/
cleanStackTrace(stack) {
if (!stack) return '';
return stack.split('\n')
.filter(line => !line.includes('node_modules') && !line.includes('internal/'))
.map(line => line.trim())
.join('\n');
}
/**
* スタックトレースからファイル名を抽出
*/
extractFileFromStack(stack) {
if (!stack) return null;
const match = stack.match(/at\s+.*?\s+\((.+?):\d+:\d+\)/);
return match ? match[1] : null;
}
/**
* スタックトレースから行番号を抽出
*/
extractLineFromStack(stack) {
if (!stack) return null;
const match = stack.match(/at\s+.*?\s+\(.+?:(\d+):\d+\)/);
return match ? parseInt(match[1]) : null;
}
/**
* スタックトレースから列番号を抽出
*/
extractColumnFromStack(stack) {
if (!stack) return null;
const match = stack.match(/at\s+.*?\s+\(.+?:\d+:(\d+)\)/);
return match ? parseInt(match[1]) : null;
}
/**
* エラーIDを生成
*/
generateErrorId() {
return `ERR_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
/**
* エラーレポートをフォーマット
*/
formatError(enhancedError) {
const lines = [];
// ヘッダー
const useColors = process.stdout.isTTY && !process.env.NO_COLOR;
const color = useColors ? enhancedError.color : '';
const reset = useColors ? this.colors.reset : '';
const bold = useColors ? this.colors.bold : '';
lines.push(`\n${enhancedError.prefix} ${color}${bold}[${enhancedError.label}]${reset} ${enhancedError.message}`);
lines.push(` ID: ${enhancedError.id}`);
lines.push(` Level: ${enhancedError.level}`);
lines.push(` Category: ${enhancedError.category}`);
// コンテキスト
if (enhancedError.context.file) {
lines.push(` File: ${enhancedError.context.file}:${enhancedError.context.line || '?'}:${enhancedError.context.column || '?'}`);
}
if (enhancedError.context.suite) {
lines.push(` Suite: ${enhancedError.context.suite}`);
}
if (enhancedError.context.test) {
lines.push(` Test: ${enhancedError.context.test}`);
}
// コードスニペット
if (enhancedError.codeSnippet) {
lines.push('\n Code:');
lines.push(enhancedError.codeSnippet.code.split('\n').map(l => ' ' + l).join('\n'));
}
// 提案
if (enhancedError.suggestions.length > 0) {
lines.push('\n Suggestions:');
enhancedError.suggestions.forEach(s => {
lines.push(` • ${s}`);
});
}
// スタックトレース(verboseモードの場合)
if (enhancedError.stack && process.env.VERBOSE) {
lines.push('\n Stack Trace:');
lines.push(enhancedError.stack.split('\n').map(l => ' ' + l).join('\n'));
}
return lines.join('\n');
}
/**
* 保存されたエラーコンテキストを取得
*/
getErrorContext(errorId) {
return this.errorContext.get(errorId);
}
/**
* すべてのエラーコンテキストをクリア
*/
clearErrorContext() {
this.errorContext.clear();
}
}
module.exports = ErrorHandler;