Skip to content

Commit a5399ec

Browse files
committed
added tests and fixed npm build
1 parent 4df952e commit a5399ec

File tree

11 files changed

+13145
-6004
lines changed

11 files changed

+13145
-6004
lines changed
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
2+
import Playground from '@/components/playground/index';
3+
import { executeJavaScript } from '@/lib/playground/javascript-executor';
4+
5+
// Mocks
6+
jest.mock('@/components/playground/code-editor-wrapper', () => ({
7+
CodeEditorWrapper: ({ value, onChange, onRun }: any) => (
8+
<div data-testid="code-editor">
9+
<textarea
10+
data-testid="mock-editor-textarea"
11+
value={value}
12+
onChange={(e) => onChange(e.target.value)}
13+
/>
14+
<button data-testid="editor-run-shortcut" onClick={onRun}>Run Shortcut</button>
15+
</div>
16+
),
17+
}));
18+
19+
jest.mock('@/lib/playground/javascript-executor', () => ({
20+
executeJavaScript: jest.fn(),
21+
}));
22+
23+
jest.mock('@/hooks/use-media-query', () => ({
24+
useMediaQuery: () => false,
25+
}));
26+
27+
// Mock ResizeObserver for react-resizable-panels
28+
global.ResizeObserver = jest.fn().mockImplementation(() => ({
29+
observe: jest.fn(),
30+
unobserve: jest.fn(),
31+
disconnect: jest.fn(),
32+
}));
33+
34+
describe('Playground Flow E2E (Simulated)', () => {
35+
beforeEach(() => {
36+
jest.clearAllMocks();
37+
(executeJavaScript as jest.Mock).mockResolvedValue({
38+
success: true,
39+
output: 'Hello Test Flow',
40+
executionTime: 50,
41+
});
42+
});
43+
44+
it('should run code and display output when Run button is clicked', async () => {
45+
render(<Playground />);
46+
47+
// Check if key elements are present
48+
expect(screen.getByText(/Run/i)).toBeInTheDocument();
49+
expect(screen.getByTestId('code-editor')).toBeInTheDocument();
50+
51+
// Find Run button (it might be in nav)
52+
// Assuming the button text is "Run" inside PlaygroundNav
53+
const runBtn = screen.getByRole('button', { name: /run/i });
54+
55+
// Click Run
56+
fireEvent.click(runBtn);
57+
58+
// Verify loading state (optional, might be too fast)
59+
60+
// Verify execution was called
61+
await waitFor(() => {
62+
expect(executeJavaScript).toHaveBeenCalled();
63+
});
64+
65+
// Verify output is displayed
66+
// The OutputPanel likely displays the text.
67+
await waitFor(() => {
68+
expect(screen.getByText('Hello Test Flow')).toBeInTheDocument();
69+
});
70+
});
71+
72+
it('should handle runtime errors', async () => {
73+
(executeJavaScript as jest.Mock).mockResolvedValue({
74+
success: false,
75+
error: 'Runtime Error: Boom',
76+
executionTime: 10,
77+
});
78+
79+
render(<Playground />);
80+
81+
const runBtn = screen.getByRole('button', { name: /run/i });
82+
fireEvent.click(runBtn);
83+
84+
await waitFor(() => {
85+
expect(screen.getByText('Runtime Error: Boom')).toBeInTheDocument();
86+
});
87+
});
88+
});
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { GET } from '@/app/api/comments/route';
2+
import { NextRequest } from 'next/server';
3+
4+
// Mock dependencies
5+
jest.mock('@/lib/supabase/server', () => ({
6+
createClient: jest.fn(() => ({
7+
from: jest.fn(() => ({
8+
select: jest.fn(() => ({
9+
eq: jest.fn(() => ({
10+
order: jest.fn().mockResolvedValue({
11+
data: [
12+
{ id: 1, content: 'Test Comment', profiles: { username: 'testuser' } }
13+
],
14+
error: null
15+
}),
16+
})),
17+
single: jest.fn(),
18+
})),
19+
insert: jest.fn(),
20+
})),
21+
auth: {
22+
getUser: jest.fn(),
23+
}
24+
})),
25+
}));
26+
27+
describe('API Route: /api/comments', () => {
28+
it('should fetch comments for a post', async () => {
29+
const req = new NextRequest('http://localhost:3000/api/comments?post_id=123');
30+
const response = await GET(req);
31+
32+
expect(response.status).toBe(200);
33+
const data = await response.json();
34+
expect(data.length).toBe(1);
35+
expect(data[0].content).toBe('Test Comment');
36+
});
37+
38+
it('should return 400 if post_id is missing', async () => {
39+
const req = new NextRequest('http://localhost:3000/api/comments');
40+
const response = await GET(req);
41+
expect(response.status).toBe(400);
42+
});
43+
});
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { POST } from '@/app/api/playground/execute/route';
2+
import { NextRequest } from 'next/server';
3+
4+
// Mock dependencies
5+
jest.mock('@/lib/supabase/server', () => ({
6+
createClient: jest.fn(() => ({
7+
auth: {
8+
getUser: jest.fn().mockResolvedValue({ data: { user: null } }),
9+
},
10+
from: jest.fn(() => ({
11+
insert: jest.fn().mockResolvedValue({}),
12+
})),
13+
})),
14+
}));
15+
16+
jest.mock('@/lib/playground/piston-executor', () => ({
17+
executePiston: jest.fn().mockResolvedValue({
18+
success: true,
19+
output: 'Hello World',
20+
executionTime: 100,
21+
}),
22+
}));
23+
24+
describe('API Route: /api/playground/execute', () => {
25+
it('should execute code successfully', async () => {
26+
const req = new NextRequest('http://localhost:3000/api/playground/execute', {
27+
method: 'POST',
28+
body: JSON.stringify({
29+
code: 'print("Hello World")',
30+
language: 'python',
31+
}),
32+
});
33+
34+
const response = await POST(req);
35+
expect(response.status).toBe(200);
36+
37+
const data = await response.json();
38+
expect(data.output).toBe('Hello World');
39+
expect(data.success).toBe(true);
40+
});
41+
42+
it('should return 400 for invalid language', async () => {
43+
const req = new NextRequest('http://localhost:3000/api/playground/execute', {
44+
method: 'POST',
45+
body: JSON.stringify({
46+
code: 'print("Hello")',
47+
language: 'invalid-lang',
48+
}),
49+
});
50+
51+
const response = await POST(req);
52+
expect(response.status).toBe(400);
53+
});
54+
55+
it('should return 400 if code is missing', async () => {
56+
const req = new NextRequest('http://localhost:3000/api/playground/execute', {
57+
method: 'POST',
58+
body: JSON.stringify({
59+
language: 'python',
60+
}),
61+
});
62+
63+
const response = await POST(req);
64+
expect(response.status).toBe(400);
65+
});
66+
});

__tests__/unit/executor.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { executeJavaScript } from '@/lib/playground/javascript-executor';
2+
3+
// Mock Worker
4+
class MockWorker {
5+
onmessage: ((e: MessageEvent) => void) | null = null;
6+
onerror: ((e: ErrorEvent) => void) | null = null;
7+
8+
postMessage(data: any) {
9+
// Simulate successful execution for specific code
10+
if (data.code === "console.log('hello')") {
11+
setTimeout(() => {
12+
this.onmessage?.({ data: { success: true, output: 'hello' } } as MessageEvent);
13+
}, 10);
14+
}
15+
// Simulate error
16+
else if (data.code === "throw error") {
17+
setTimeout(() => {
18+
this.onerror?.({ message: 'Runtime Error' } as ErrorEvent);
19+
}, 10);
20+
}
21+
// Simulate timeout (we just don't reply in time, but the test timeout handles mocking)
22+
}
23+
24+
terminate() { }
25+
}
26+
27+
// @ts-ignore
28+
global.Worker = MockWorker;
29+
30+
describe('JavaScript Executor', () => {
31+
it('should execute valid code', async () => {
32+
const result = await executeJavaScript("console.log('hello')");
33+
expect(result.success).toBe(true);
34+
expect(result.output).toBe('hello');
35+
expect(result.executionTime).toBeGreaterThanOrEqual(0);
36+
});
37+
38+
it('should handle errors', async () => {
39+
const result = await executeJavaScript("throw error");
40+
expect(result.success).toBe(false);
41+
expect(result.error).toBe('Runtime Error');
42+
});
43+
44+
// Note: Testing timeout requires fake timers which interacts complexly with the Promise race
45+
// Skipping timeout test for now to keep it simple
46+
});
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { checkRateLimit } from '@/lib/playground/rate-limiter';
2+
3+
describe('Rate Limiter', () => {
4+
// Mock Date.now to control time
5+
const originalDateNow = Date.now;
6+
7+
beforeEach(() => {
8+
// Reset time to 0 for consistent testing
9+
Date.now = jest.fn(() => 0);
10+
});
11+
12+
afterAll(() => {
13+
Date.now = originalDateNow;
14+
});
15+
16+
it('should allow first request', () => {
17+
const result = checkRateLimit('user-1');
18+
expect(result.allowed).toBe(true);
19+
expect(result.remaining).toBe(9);
20+
});
21+
22+
it('should decrease remaining count on subsequent requests', () => {
23+
checkRateLimit('user-2');
24+
const result = checkRateLimit('user-2');
25+
expect(result.allowed).toBe(true);
26+
expect(result.remaining).toBe(8);
27+
});
28+
29+
it('should block requests after limit is reached', () => {
30+
const userId = 'user-limit';
31+
// Consume 10 requests
32+
for (let i = 0; i < 10; i++) {
33+
checkRateLimit(userId);
34+
}
35+
36+
const result = checkRateLimit(userId);
37+
expect(result.allowed).toBe(false);
38+
expect(result.remaining).toBe(0);
39+
expect(result.resetIn).toBeGreaterThan(0);
40+
});
41+
42+
it('should reset after window expires', () => {
43+
const userId = 'user-reset';
44+
checkRateLimit(userId); // 1 request at time 0
45+
46+
// Advance time by 61 seconds (61000ms)
47+
Date.now = jest.fn(() => 61000);
48+
49+
const result = checkRateLimit(userId);
50+
expect(result.allowed).toBe(true);
51+
expect(result.remaining).toBe(9); // New window, first request counts as 1
52+
});
53+
});

__tests__/unit/utils.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { cn } from '@/lib/utils';
2+
3+
describe('cn utility', () => {
4+
it('should merge class names correctly', () => {
5+
expect(cn('bg-red-500', 'text-white')).toBe('bg-red-500 text-white');
6+
});
7+
8+
it('should handle conditionals', () => {
9+
expect(cn('bg-red-500', false && 'text-white')).toBe('bg-red-500');
10+
expect(cn('bg-red-500', true && 'text-white')).toBe('bg-red-500 text-white');
11+
});
12+
13+
it('should resolve conflicting tailwind classes', () => {
14+
// tailwind-merge should keep the last one
15+
expect(cn('bg-red-500', 'bg-blue-500')).toBe('bg-blue-500');
16+
expect(cn('p-4', 'p-2')).toBe('p-2');
17+
});
18+
});

build_output.txt

26.3 KB
Binary file not shown.

jest.config.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type { Config } from 'jest';
2+
import nextJest from 'next/jest';
3+
4+
const createJestConfig = nextJest({
5+
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
6+
dir: './',
7+
});
8+
9+
// Add any custom config to be passed to Jest
10+
const config: Config = {
11+
coverageProvider: 'v8',
12+
testEnvironment: 'jsdom',
13+
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
14+
moduleNameMapper: {
15+
// Handle module aliases (this will be automatically configured for you soon)
16+
'^@/(.*)$': '<rootDir>/$1',
17+
},
18+
};
19+
20+
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
21+
export default createJestConfig(config);

jest.setup.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import '@testing-library/jest-dom';

0 commit comments

Comments
 (0)