Skip to content
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
9 changes: 9 additions & 0 deletions applications/web/sources/Application.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { SignInPage } from './pages/SignInPage.tsx';
import { HomePage } from './pages/HomePage.tsx';
import { CrawlersPage } from './pages/CrawlersPage.tsx';
import { CrawlerNewPage } from './pages/CrawlerNewPage.tsx';
import { CrawlerDetailPage } from './pages/CrawlerDetailPage.tsx';
import { SchedulersPage } from './pages/SchedulersPage.tsx';
import { SchedulerDetailPage } from './pages/SchedulerDetailPage.tsx';
import { AuthenticationCallbackPage } from './pages/AuthenticationCallbackPage.tsx';
Expand Down Expand Up @@ -70,6 +71,14 @@ function ApplicationRoutes() {
</ProtectedRoute>
}
/>
<Route
path="/crawlers/:id"
element={
<ProtectedRoute>
<CrawlerDetailPage />
</ProtectedRoute>
}
/>
<Route
path="/schedulers"
element={
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,11 @@ interface CodeEditorPanelProperties {
value: string;
onChange: (value: string) => void;
disabled?: boolean;
showDefaultTemplate?: boolean;
}

export function CodeEditorPanel({ value, onChange, disabled }: CodeEditorPanelProperties) {
const displayValue = value.length > 0 ? value : DEFAULT_CODE;
export function CodeEditorPanel({ value, onChange, disabled, showDefaultTemplate = true }: CodeEditorPanelProperties) {
const displayValue = showDefaultTemplate && value.length === 0 ? DEFAULT_CODE : value;
const isOverLimit = displayValue.length > MAX_CODE_LENGTH;

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ describe('AuthenticationContext', () => {
expect(result.current.isLoading).toBe(false);
});

expect(result.current.user).toBeNull();
expect(result.current.user).toBeUndefined();
expect(result.current.isAuthenticated).toBe(false);
});

Expand Down Expand Up @@ -105,7 +105,7 @@ describe('AuthenticationContext', () => {
expect(result.current.isLoading).toBe(false);
});

expect(result.current.user).toBeNull();
expect(result.current.user).toBeUndefined();
expect(result.current.isAuthenticated).toBe(false);
expect(localStorage.getItem(storageKey)).toBeNull();
});
Expand All @@ -122,7 +122,7 @@ describe('AuthenticationContext', () => {
expect(result.current.isLoading).toBe(false);
});

expect(result.current.user).toBeNull();
expect(result.current.user).toBeUndefined();
expect(localStorage.getItem(storageKey)).toBeNull();
});

Expand Down Expand Up @@ -173,7 +173,7 @@ describe('AuthenticationContext', () => {
result.current.logout();

await vi.waitFor(() => {
expect(result.current.user).toBeNull();
expect(result.current.user).toBeUndefined();
expect(result.current.isAuthenticated).toBe(false);
});
expect(localStorage.getItem(storageKey)).toBeNull();
Expand Down
4 changes: 2 additions & 2 deletions applications/web/sources/hooks/use-authentication.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ describe('useAuthentication', () => {
});

test('isAuthenticated reflects user presence', async () => {
const noUserContext = { ...mockContextValue, user: null, isAuthenticated: false };
const noUserContext = { ...mockContextValue, user: undefined, isAuthenticated: false };
const wrapper = ({ children }: { children: React.ReactNode }) => (
<AuthenticationContext.Provider value={noUserContext}>
{children}
Expand All @@ -39,7 +39,7 @@ describe('useAuthentication', () => {

const { result } = await renderHook(() => useAuthentication(), { wrapper });
expect(result.current.isAuthenticated).toBe(false);
expect(result.current.user).toBeNull();
expect(result.current.user).toBeUndefined();
});

test('provides login and logout functions', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ describe('useCrawlerCodeRunner', () => {
});

expect(result.current.status).toBe('idle');
expect(result.current.result).toBeNull();
expect(result.current.error).toBeNull();
expect(result.current.result).toBeUndefined();
expect(result.current.error).toBeUndefined();
});

test('logs info entries when starting execution', async () => {
Expand Down Expand Up @@ -196,13 +196,13 @@ describe('useCrawlerCodeRunner', () => {
result.current.runTest('https://example.com', 'return {}');

await vi.waitFor(() => {
expect(result.current.result).not.toBeNull();
expect(result.current.result).not.toBeUndefined();
});

result.current.reset();

await vi.waitFor(() => {
expect(result.current.result).toBeNull();
expect(result.current.result).toBeUndefined();
expect(result.current.status).toBe('idle');
});
});
Expand Down
231 changes: 230 additions & 1 deletion applications/web/sources/hooks/use-crawler-manager.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { renderHook } from 'vitest-browser-react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { http, HttpResponse } from 'msw';
import { test, expect } from '../tests/extensions.ts';
import { useCreateCrawler, useListCrawlers, useDeleteCrawler } from './use-crawler-manager.ts';
import { useCreateCrawler, useListCrawlers, useDeleteCrawler, useGetCrawler, useUpdateCrawler } from './use-crawler-manager.ts';
import { worker } from '../tests/mocks/browser.ts';
import type { ReactNode } from 'react';

Expand Down Expand Up @@ -214,3 +214,232 @@ describe('useDeleteCrawler', () => {
expect(capturedAuthorization).toBe('Bearer test-access-token');
});
});

describe('useGetCrawler', () => {
test('fetches a single crawler by id', async () => {
const mockCrawler = {
id: 'c1',
user_uuid: 'u1',
name: 'Detail Crawler',
type: 'web',
url_pattern: '^https://example\\.com',
code: '(body) => body.length',
input_schema: { body: 'string' },
output_schema: {},
created_at: '2024-01-01T00:00:00Z',
updated_at: '2024-01-02T00:00:00Z',
};
worker.use(
http.get(`${MANAGER_URL}/crawlers/c1`, async () => {
return HttpResponse.json(mockCrawler);
}),
);

const { result } = await renderHook(() => useGetCrawler('c1'), {
wrapper: createWrapper(),
});

await vi.waitFor(() => {
expect(result.current.crawler).toBeDefined();
});

expect(result.current.crawler).toEqual(mockCrawler);
});

test('does not fetch when id is undefined', async () => {
let requestCount = 0;
worker.use(
http.get(`${MANAGER_URL}/crawlers/:id`, async () => {
requestCount += 1;
return HttpResponse.json({});
}),
);

const { result } = await renderHook(() => useGetCrawler(undefined), {
wrapper: createWrapper(),
});

await vi.waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
expect(result.current.crawler).toBeUndefined();
expect(requestCount).toBe(0);
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

test('throws on server error', async () => {
worker.use(
http.get(`${MANAGER_URL}/crawlers/c1`, async () => {
return HttpResponse.json(
{ error: 'not_found', error_description: 'Crawler not found' },
{ status: 404 },
);
}),
);

const { result } = await renderHook(() => useGetCrawler('c1'), {
wrapper: createWrapper(),
});

await vi.waitFor(() => {
expect(result.current.error).toBeDefined();
});

expect(result.current.error?.message).toBe('Crawler not found');
});

test('includes Authorization header in GET request', async () => {
let capturedAuthorization: string | null = null;
worker.use(
http.get(`${MANAGER_URL}/crawlers/c1`, async ({ request }) => {
capturedAuthorization = request.headers.get('Authorization');
return HttpResponse.json({ id: 'c1' });
}),
);

const { result } = await renderHook(() => useGetCrawler('c1'), {
wrapper: createWrapper(),
});

await vi.waitFor(() => {
expect(result.current.crawler).toBeDefined();
});

expect(capturedAuthorization).toBe('Bearer test-access-token');
});
});

describe('useUpdateCrawler', () => {
test('updates a crawler and returns the new row', async () => {
const updated = {
id: 'c1',
user_uuid: 'u1',
name: 'Renamed',
type: 'web',
url_pattern: '^https://example\\.com',
code: '(body) => 1',
input_schema: { body: 'string' },
output_schema: {},
created_at: '2024-01-01T00:00:00Z',
updated_at: '2024-01-03T00:00:00Z',
};
worker.use(
http.put(`${MANAGER_URL}/crawlers/c1`, async () => {
return HttpResponse.json(updated);
}),
);

const { result } = await renderHook(() => useUpdateCrawler(), {
wrapper: createWrapper(),
});

const response = await result.current.updateCrawler({
id: 'c1',
name: 'Renamed',
type: 'web',
url_pattern: '^https://example\\.com',
code: '(body) => 1',
output_schema: {},
});

expect(response).toEqual(updated);
});

test('sends PUT body without id field', async () => {
let capturedBody: Record<string, unknown> | undefined;
worker.use(
http.put(`${MANAGER_URL}/crawlers/c1`, async ({ request }) => {
capturedBody = (await request.json()) as Record<string, unknown>;
return HttpResponse.json({
id: 'c1',
user_uuid: 'u1',
name: 'X',
type: 'web',
url_pattern: '',
code: '',
input_schema: {},
output_schema: {},
created_at: '',
updated_at: '',
});
}),
);

const { result } = await renderHook(() => useUpdateCrawler(), {
wrapper: createWrapper(),
});

await result.current.updateCrawler({
id: 'c1',
name: 'X',
type: 'web',
url_pattern: '',
code: '',
output_schema: {},
});

expect(capturedBody).toBeDefined();
expect(capturedBody && 'id' in capturedBody).toBe(false);
expect(capturedBody?.name).toBe('X');
});

test('throws on update error', async () => {
worker.use(
http.put(`${MANAGER_URL}/crawlers/c1`, async () => {
return HttpResponse.json(
{ error: 'invalid_request', error_description: 'Name too long' },
{ status: 400 },
);
}),
);

const { result } = await renderHook(() => useUpdateCrawler(), {
wrapper: createWrapper(),
});

await expect(
result.current.updateCrawler({
id: 'c1',
name: 'X',
type: 'web',
url_pattern: '',
code: '',
output_schema: {},
}),
).rejects.toThrow('Name too long');
});

test('includes Authorization header in PUT request', async () => {
let capturedAuthorization: string | null = null;
worker.use(
http.put(`${MANAGER_URL}/crawlers/c1`, async ({ request }) => {
capturedAuthorization = request.headers.get('Authorization');
return HttpResponse.json({
id: 'c1',
user_uuid: 'u1',
name: 'X',
type: 'web',
url_pattern: '^.*$',
code: '(b)=>b',
input_schema: { body: 'string' },
output_schema: {},
created_at: '',
updated_at: '',
});
}),
);

const { result } = await renderHook(() => useUpdateCrawler(), {
wrapper: createWrapper(),
});

await result.current.updateCrawler({
id: 'c1',
name: 'X',
type: 'web',
url_pattern: '^.*$',
code: '(b)=>b',
});

expect(capturedAuthorization).toBe('Bearer test-access-token');
});
});
Loading
Loading