Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add query builder viewer and fix bugs #12

Merged
merged 7 commits into from
Dec 16, 2024
Merged
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
70 changes: 70 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: CI

on: ['pull_request']

jobs:
test:
name: 🧪 Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: 🔧 Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'yarn'

- name: 📦 Install dependencies
run: yarn install --frozen-lockfile

- name: 🧪 Run tests
run: yarn test:ci

- name: 📊 Upload test results
if: always()
uses: actions/upload-artifact@v3
with:
name: test-results
path: |
junit.xml
coverage/

lint:
name: 🔍 Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: 🔧 Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'yarn'

- name: 📦 Install dependencies
run: yarn install --frozen-lockfile

- name: 🔍 Run ESLint
run: yarn lint

- name: 💅 Check formatting
run: yarn format:check

typecheck:
name: ʦ Type check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: 🔧 Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'yarn'

- name: 📦 Install dependencies
run: yarn install --frozen-lockfile

- name: ʦ Type check
run: yarn tsc --noEmit
17 changes: 15 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@
"lint:fix": "next lint --fix",
"format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"",
"format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,md}\"",
"fix": "npm run format && npm run lint:fix"
"fix": "npm run format && npm run lint:fix",
"test": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest run --coverage",
"test:ci": "vitest run --coverage --reporter=default --reporter=junit --outputFile=./junit.xml"
},
"dependencies": {
"@radix-ui/react-collapsible": "^1.1.1",
Expand All @@ -32,18 +36,27 @@
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.0.1",
"@testing-library/user-event": "^14.5.2",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"@typescript-eslint/eslint-plugin": "^6.19.0",
"@typescript-eslint/parser": "^6.19.0",
"@vitejs/plugin-react": "^4.3.4",
"@vitest/coverage-v8": "^2.1.8",
"@vitest/ui": "^2.1.8",
"eslint": "^8.56.0",
"eslint-config-next": "14.0.4",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.29.1",
"jsdom": "^25.0.1",
"postcss": "^8",
"prettier": "^3.2.4",
"tailwindcss": "^3.4.1",
"typescript": "^5"
"typescript": "^5",
"vitest": "^2.1.8"
}
}
4 changes: 2 additions & 2 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
import Image from 'next/image'
import { useState, useMemo } from 'react'

import DevToolsUI from '@/components/DevToolsUi'
import UploadDropzone from '@/components/UploadDropzone'
import { DevToolsUI } from '@/components/DevToolsUi'
import { UploadDropzone } from '@/components/UploadDropzone'
import { DiagnosticData } from '@/types/DiagnosticData'

export default function Home({
Expand Down
91 changes: 91 additions & 0 deletions src/components/ConsoleOutput.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { describe, it, expect } from 'vitest'

import { ConsoleOutput } from './ConsoleOutput'
import { fireEvent, render, screen } from '../test/test-utils'

const sampleErrors = [
'"[webpack-dev-server] ERROR in ./components/ErrorPages/utils.ts\\n × Module not found: Error message 1"',
'"Warning: Something went wrong\\nStack trace for warning"',
'"[webpack-dev-server] Another error occurred\\nStack trace for error"',
'{"level": "info", "message": "Server started successfully", "stack": "Info stack trace"}',
'"Regular log message without specific level"',
]

describe('ConsoleOutput', () => {
it('renders without crashing', () => {
expect(() => render(<ConsoleOutput errors={[]} />)).not.toThrow()
})

it('displays errors correctly', () => {
render(<ConsoleOutput errors={sampleErrors} />)

// Using more precise text matching
expect(
screen.getByText('[webpack-dev-server] ERROR in ./components/ErrorPages/utils.ts')
).toBeInTheDocument()
expect(screen.getByText('Warning: Something went wrong')).toBeInTheDocument()
expect(screen.getByText('[webpack-dev-server] Another error occurred')).toBeInTheDocument()
})

it('filters errors based on search input', () => {
render(<ConsoleOutput errors={sampleErrors} />)

const searchInput = screen.getByPlaceholderText('Search logs...')
fireEvent.change(searchInput, { target: { value: 'message 1' } })

expect(
screen.getByText('[webpack-dev-server] ERROR in ./components/ErrorPages/utils.ts')
).toBeInTheDocument()
expect(screen.queryByText('Warning: Something went wrong')).not.toBeInTheDocument()
expect(
screen.queryByText('[webpack-dev-server] Another error occurred')
).not.toBeInTheDocument()
})

it('toggles error details when clicked', () => {
render(<ConsoleOutput errors={sampleErrors} />)

// Find and click the first error
const firstError = screen.getByText(
'[webpack-dev-server] ERROR in ./components/ErrorPages/utils.ts'
)
fireEvent.click(firstError)

// Check if stack trace is visible
expect(screen.getByText('× Module not found: Error message 1')).toBeInTheDocument()
})

it('applies correct styling based on error level', () => {
render(<ConsoleOutput errors={sampleErrors} />)

// Error should have red styling
const errorElement = screen
.getByText('[webpack-dev-server] ERROR in ./components/ErrorPages/utils.ts')
.closest('.bg-red-100')
expect(errorElement).toBeInTheDocument()

// Warning should have yellow styling
const warningElement = screen
.getByText('Warning: Something went wrong')
.closest('.bg-yellow-100')
expect(warningElement).toBeInTheDocument()
})

it('handles empty error list', () => {
render(<ConsoleOutput errors={[]} />)
const searchInput = screen.getByPlaceholderText('Search logs...')
expect(searchInput).toBeInTheDocument()
expect(screen.queryByRole('button')).not.toBeInTheDocument()
})

it('expands info logs to show stack trace', () => {
render(<ConsoleOutput errors={[sampleErrors[3]]} />)

// Find and click the info message
const infoMessage = screen.getByText('Server started successfully')
fireEvent.click(infoMessage)

// Check if stack trace is visible
expect(screen.getByText('Info stack trace')).toBeInTheDocument()
})
})
49 changes: 39 additions & 10 deletions src/components/ConsoleOutput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,24 +36,53 @@ export const ConsoleOutput: React.FC<ConsoleOutputProps> = ({ errors, onErrorCou
const parsedErrors = useMemo(() => {
const parsed = errors.map((error, index) => {
const cleanError = error.replace(/^"|"$/g, '').replace(/\\n/g, '\n')
const parts = cleanError.split('"').filter(Boolean)
const format = parts[0]
const args = parts.slice(1)
let message = cleanError
let jsonData = null
let stack = ''
let level = 'error'

let message = format
args.forEach((arg) => {
message = message.replace('%s', arg)
})
try {
// Try to parse as JSON first
const parsedJson = JSON.parse(cleanError)
if (parsedJson.level && parsedJson.message) {
jsonData = parsedJson
message = parsedJson.message
stack = parsedJson.stack || ''
level = parsedJson.level
return {
id: index,
level,
message,
stack,
details: jsonData,
}
}
} catch (e) {
// If JSON parsing fails, try the other formats
try {
const jsonMatch = cleanError.match(/(?:Error:|Warning:)({.*})/i)
if (jsonMatch && jsonMatch[1]) {
jsonData = JSON.parse(jsonMatch[1])
message = cleanError.split('{')[0].trim()
if (jsonData.status) message += ` Status: ${jsonData.status}`
if (jsonData.data) message += ` ${jsonData.data}`
}
} catch (innerE) {
// If all JSON parsing fails, fall back to original parsing logic
const parts = cleanError.split('"').filter(Boolean)
message = parts[0] || cleanError
}
}

return {
id: index,
level: message.startsWith('Warning:') ? 'warn' : 'error',
message: message,
message,
stack: message.split('\n').slice(1).join('\n'),
details: jsonData,
}
})

// Count actual errors and notify parent
const errorCount = parsed.filter((error) => error.level === 'error').length
onErrorCountChange?.(errorCount)

Expand All @@ -72,7 +101,7 @@ export const ConsoleOutput: React.FC<ConsoleOutputProps> = ({ errors, onErrorCou
<>
<Input
type="text"
placeholder="Search errors..."
placeholder="Search logs..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="mb-4"
Expand Down
Loading
Loading