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
56 changes: 50 additions & 6 deletions electron/main.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { app, BrowserWindow } from 'electron'
import { app, BrowserWindow, ipcMain } from 'electron'
import path from 'node:path'
import { ApiResponse } from '../src/api/types'

// The built directory structure
//
Expand All @@ -13,17 +14,18 @@ import path from 'node:path'
process.env.DIST = path.join(__dirname, '../dist')
process.env.VITE_PUBLIC = app.isPackaged ? process.env.DIST : path.join(process.env.DIST, '../public')


let win: BrowserWindow | null
// 🚧 Use ['ENV_NAME'] avoid vite:define plugin - Vite@2.x
const VITE_DEV_SERVER_URL = process.env['VITE_DEV_SERVER_URL']

function createWindow() {
win = new BrowserWindow({
const win = new BrowserWindow({
icon: path.join(process.env.VITE_PUBLIC, 'reqio.svg'),
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: false,
contextIsolation: true
},

width: 1450,
height: 900,
})
Expand All @@ -47,7 +49,6 @@ function createWindow() {
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
win = null
}
})

Expand All @@ -57,6 +58,49 @@ app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
});

ipcMain.handle("rest", async (_event: any, url: string, options?: RequestInit): Promise<ApiResponse> => {
try {
const response = await fetch(url, options);

const headers: Record<string, string> = {};
response.headers.forEach((value, key) => {
headers[key] = value;
});

const contentType = response.headers.get('content-type') || '';
let data: any;

if (contentType.includes('application/json')) {
data = await response.json();
} else if (contentType.includes('application/xml') || contentType.includes('text/xml')) {
data = await response.text();
} else if (contentType.includes('text/html')) {
data = await response.text();
} else if (contentType.includes('text/')) {
data = await response.text();
} else if (contentType.includes('application/octet-stream') ||
contentType.includes('image/') ||
contentType.includes('application/pdf')) {
const buffer = await response.arrayBuffer();
data = Buffer.from(buffer).toString('base64');
} else {
data = await response.text();
}

return {
ok: response.ok,
data,
status: response.status,
statusText: response.statusText,
headers
};
} catch (err) {
const error = err instanceof Error ? err.message : String(err);
console.error('IPC Handler - Error:', error);
return { ok: false, error };
}
});

app.whenReady().then(createWindow)
12 changes: 12 additions & 0 deletions electron/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@ import { contextBridge, ipcRenderer } from 'electron'
// --------- Expose some API to the Renderer process ---------
contextBridge.exposeInMainWorld('ipcRenderer', withPrototype(ipcRenderer))

contextBridge.exposeInMainWorld("api", {
fetch: <T = unknown>(url: string, options?: RequestInit) =>
ipcRenderer.invoke("rest", url, options) as Promise<{
ok: boolean;
data?: T;
error?: string;
status?: number;
statusText?: string;
headers?: Record<string, string>;
}>,
});

// `exposeInMainWorld` can't detect attributes and methods of `prototype`, manually patching it.
function withPrototype(obj: Record<string, any>) {
const protos = Object.getPrototypeOf(obj)
Expand Down
53 changes: 42 additions & 11 deletions src/api/rest.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import axios from 'axios';
import { Header } from '../components/RequestPanel/types.ts';
import { HeadersMapping, JSONValue } from './types.ts';

Expand All @@ -10,31 +9,63 @@ const restructureHeaders = (headers: Header[]) => {
return formattedHeaders;
};

declare global {
interface Window {
api: {
fetch<T = unknown>(
url: string,
options?: RequestInit
): Promise<{
ok: boolean;
data?: T;
error?: string;
status?: number;
statusText?: string;
headers?: Record<string, string>;
}>;
};
}
}

export const get = async (url: string, headers: Header[] = []) => {
return axios.get(url, {
const options: RequestInit = {
headers: restructureHeaders(headers),
});
method: 'GET',
};
return window.api.fetch(url, options);
};

export const post = async (url: string, body: JSONValue, headers: Header[] = []) => {
return axios.post(url, body, {
const options: RequestInit = {
headers: restructureHeaders(headers),
});
method: 'POST',
body: JSON.stringify(body),
};
return window.api.fetch(url, options);
};

export const patch = async (url: string, body: JSONValue, headers: Header[] = []) => {
return axios.patch(url, body, {
const options: RequestInit = {
headers: restructureHeaders(headers),
});
method: 'PATCH',
body: JSON.stringify(body),
};
return window.api.fetch(url, options);
};

export const put = async (url: string, body: JSONValue, headers: Header[] = []) => {
return axios.put(url, body, {
const options: RequestInit = {
headers: restructureHeaders(headers),
});
method: 'PUT',
body: JSON.stringify(body),
};
return window.api.fetch(url, options);
};

export const delete_req = async (url: string, headers: Header[] = []) => {
return axios.delete(url, {
const options: RequestInit = {
headers: restructureHeaders(headers),
});
method: 'DELETE',
};
return window.api.fetch(url, options);
};
10 changes: 10 additions & 0 deletions src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ export type HttpStatusCodes = {
[key: number]: string;
};

export interface ApiResponse<T = unknown> {
ok: boolean;
data?: T;
error?: string;
status?: number;
statusText?: string;
headers?: Record<string, string>;
}

// Deprecated - no longer using axios
export type AxiosErrorCodes = {
[key: string]: string;
};
39 changes: 23 additions & 16 deletions src/components/AppBody/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@ import './index.scss';
import { Method } from './types.ts';

import { delete_req, get, patch, post, put } from '../../api/rest.ts';
import { getErrorCode, getHttpStatusText } from '../../api/statusCodes.ts';
import { getHttpStatusText } from '../../api/statusCodes.ts';
import { Header, QueryParam } from '../RequestPanel/types.ts';
import { ApiResponse } from '../../api/types.ts';

import RequestPanel from '../RequestPanel';
import ResponsePanel from '../ResponsePanel';
import PaneSplitter from '../PaneSplitter';
import UrlPanel from '../UrlPanel';

import { useState } from 'react';
import { AxiosError, AxiosResponse, AxiosResponseHeaders, RawAxiosResponseHeaders } from 'axios';
import { AuthType, Credentials } from '../RequestAuthPanel/types.ts';

const AppBody = () => {
Expand All @@ -31,35 +31,42 @@ const AppBody = () => {
const [statusText, setStatusText] = useState('');
const [timeTaken, setTimeTaken] = useState(0);

function parseResponseHeader(header: RawAxiosResponseHeaders | AxiosResponseHeaders) {
return Object.entries(header || {}).map(([key, value]) => ({
function parseResponseHeader(header: Record<string, string> = {}) {
return Object.entries(header).map(([key, value]) => ({
key,
value: Array.isArray(value) ? value.join(', ') : value,
value: Array.isArray(value) ? value.join(', ') : String(value),
}));
}

const onSuccessResponse = (response: AxiosResponse, startTime: number) => {
const onSuccessResponse = (response: ApiResponse, startTime: number) => {
const endTime = performance.now();
const duration = endTime - startTime;
setTimeTaken(duration);

setIsLoading(false);
setResponse(JSON.stringify(response.data, null, 2));
setResponseHeaders(parseResponseHeader(response.headers));
setStatusCode(response.status);
setStatusText(response.statusText || getHttpStatusText(response.status));
setStatusCode(response.status || 0);
setStatusText(response.statusText || getHttpStatusText(response.status || 0));
};

const onFailureResponse = (error: AxiosError) => {
const onFailureResponse = (error: ApiResponse | Error) => {
setIsLoading(false);
if (error.response) {
setResponse(JSON.stringify(error.response?.data || {}, null, 2));
setResponseHeaders(parseResponseHeader(error.response.headers));
setStatusCode(error.response?.status || 0);
setStatusText(error.response?.statusText || getHttpStatusText(statusCode));
} else {
if ('status' in error && error.status) {
// ApiResponse with error
setResponse(error.error || JSON.stringify(error.data || {}, null, 2));
setResponseHeaders(parseResponseHeader(error.headers));
setStatusCode(error.status);
setStatusText(error.statusText || getHttpStatusText(error.status));
} else if (error instanceof Error) {
// JavaScript Error (network error, etc.)
setResponse(`Error: ${error.message}`);
setStatusText(`ERROR: ${getErrorCode(error?.code || '')}`);
setStatusText('ERROR: NETWORK_FAILURE');
setStatusCode(0);
} else {
// Generic error
setResponse('Unknown error occurred');
setStatusText('ERROR: UNKNOWN');
setStatusCode(0);
}
};
Expand Down
95 changes: 95 additions & 0 deletions tests/api/integration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { get, post } from '../../src/api/rest';

describe('API Integration Tests', () => {
beforeEach(() => {
vi.clearAllMocks();
});

describe('Full Request Flow', () => {
it('should handle a complete GET request with headers and response', async () => {
const mockResponse = {
ok: true,
data: { users: [{ id: 1, name: 'John' }] },
status: 200,
statusText: 'OK',
headers: {
'content-type': 'application/json',
'x-ratelimit-remaining': '99',
},
};

global.window = {
...global.window,
api: {
fetch: vi.fn().mockResolvedValue(mockResponse),
},
} as any;

const result = await get('https://api.example.com/users', [
{ key: 'Authorization', value: 'Bearer token' },
]);

expect(result.ok).toBe(true);
expect(result.status).toBe(200);
expect(result.data).toEqual({ users: [{ id: 1, name: 'John' }] });
expect(result.headers).toBeDefined();
expect(result.headers?.['content-type']).toBe('application/json');
});

it('should handle a complete POST request with body', async () => {
const mockResponse = {
ok: true,
data: { id: 123, created: true },
status: 201,
statusText: 'Created',
headers: {
'content-type': 'application/json',
location: '/api/users/123',
},
};

global.window = {
...global.window,
api: {
fetch: vi.fn().mockResolvedValue(mockResponse),
},
} as any;

const body = { name: 'Jane Doe', email: 'jane@example.com' };
const result = await post('https://api.example.com/users', body, [
{ key: 'Content-Type', value: 'application/json' },
]);

expect(result.ok).toBe(true);
expect(result.status).toBe(201);
expect(result.data).toEqual({ id: 123, created: true });
expect(result.headers?.location).toBe('/api/users/123');
});

it('should handle error responses', async () => {
const mockErrorResponse = {
ok: false,
error: 'HTTP 404: Not Found',
status: 404,
statusText: 'Not Found',
headers: {
'content-type': 'application/json',
},
};

global.window = {
...global.window,
api: {
fetch: vi.fn().mockResolvedValue(mockErrorResponse),
},
} as any;

const result = await get('https://api.example.com/notfound');

expect(result.ok).toBe(false);
expect(result.status).toBe(404);
expect(result.error).toBe('HTTP 404: Not Found');
});
});
});
Loading