ChatGPT-Next-Web/test/stream-fetch.test.ts
Mihail Klimin 8fa7c14f18 feat(tauri): Migrate from Tauri v1 to v2
# Summary
This commit completes the migration from Tauri v1 to v2, resolves configuration issues, upgrades Next.js, and adds test coverage for critical components to ensure stability during the transition.

# Details
## Tauri v2 Migration
- Updated Tauri dependencies to v2.3.0 series in package.json
- Restructured build configuration in `/app/config/build.ts` to align with Tauri v2 requirements
- Fixed imports and API usage patterns across the codebase
- Added compatibility layer for window.__TAURI__ references to maintain backward compatibility

## Next.js Issues
- Upgraded Next.js from 14.1.1 to 14.2.24
- Resolved caching problems with Server Actions
- Updated eslint-config-next to match the new version
- Cleared Next.js cache and temporary files to address build issues

## Testing & Stability
- Added comprehensive tests for `stream.ts` to verify streaming functionality
- Created mocks for Tauri API to support test environment
- Verified that critical functionality continues to work correctly
- Translated all comments to English for consistency

## Infrastructure
- Fixed peer dependency warnings during installation
- Ensured proper integration with Tauri v2 plugins (clipboard-manager, dialog, fs, http, notification, shell, updater, window-state)

# Approach
Prioritized stability by:
1. Making minimal necessary changes to configuration files
2. Preserving most `window.__TAURI__` calls as they still function in v2
3. Planning gradual migration to new APIs with test coverage for critical components
4. Documenting areas that will require future attention

# Testing
- Created unit tests for critical streaming functionality
- Performed manual testing of key application features
- Verified successful build and launch with Tauri v2

# Future Work
- Future PRs will gradually replace deprecated Tauri v1 API calls with v2 equivalents
- Additional test coverage will be added for other critical components
2025-03-16 02:14:47 +03:00

167 lines
4.3 KiB
TypeScript

import { jest } from '@jest/globals';
// Create mocks
const mockInvoke = jest.fn();
const mockListen = jest.fn();
// Mock Tauri modules before import
jest.mock('@tauri-apps/api/core', () => ({
invoke: (...args: any[]) => mockInvoke(...args)
}));
jest.mock('@tauri-apps/api/event', () => ({
listen: (...args: any[]) => mockListen(...args)
}));
// Import function after mocking
import { fetch as streamFetch } from '../app/utils/stream';
import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';
// Mock global objects
// @ts-ignore
global.TransformStream = class TransformStream {
writable = {
getWriter: () => ({
ready: Promise.resolve(),
// @ts-ignore
write: jest.fn().mockResolvedValue(undefined as any),
// @ts-ignore
close: jest.fn().mockResolvedValue(undefined as any)
})
};
readable = {} as any;
} as any;
// Add Response to global context
global.Response = class Response {
constructor(public body: any, public init: any) {
Object.assign(this, init);
}
status: number = 200;
statusText: string = 'OK';
headers: any = {};
} as any;
describe('stream-fetch', () => {
let originalFetch: any;
let originalWindow: any;
beforeAll(() => {
originalFetch = global.fetch;
originalWindow = global.window;
// Mock window object
Object.defineProperty(global, 'window', {
value: {
__TAURI__: true,
__TAURI_INTERNALS__: {
transformCallback: (callback: Function) => callback,
invoke: mockInvoke
},
fetch: jest.fn(),
Headers: class Headers {
constructor() {}
entries() { return []; }
},
TextEncoder: class TextEncoder {
encode(text: string) {
return new Uint8Array(Array.from(text).map(c => c.charCodeAt(0)));
}
},
navigator: {
userAgent: 'test-agent'
},
Response: class Response {
constructor(public body: any, public init: any) {}
status: number = 200;
statusText: string = 'OK';
headers: any = {};
}
},
writable: true,
});
});
afterAll(() => {
Object.defineProperty(global, 'window', {
value: originalWindow,
writable: true,
});
global.fetch = originalFetch;
});
beforeEach(() => {
jest.clearAllMocks();
// Set up default behavior for listen
mockListen.mockImplementation(() => Promise.resolve(() => {}));
});
test('should use native fetch when Tauri is unavailable', async () => {
// Temporarily remove __TAURI__
const tempWindow = { ...window };
delete (tempWindow as any).__TAURI__;
Object.defineProperty(global, 'window', {
value: tempWindow,
writable: true,
});
await streamFetch('https://example.com');
// Check that native fetch was called
expect(window.fetch).toHaveBeenCalledWith('https://example.com', undefined);
// Restore __TAURI__
Object.defineProperty(global, 'window', {
value: { ...tempWindow, __TAURI__: true },
writable: true,
});
});
test('should use Tauri API when Tauri is available', async () => {
// Mock successful response from Tauri
// @ts-ignore
mockInvoke.mockResolvedValue({
request_id: 123,
status: 200,
status_text: 'OK',
headers: {}
} as any);
// Call fetch function
await streamFetch('https://example.com');
// Check that Tauri invoke was called with correct parameters
expect(mockInvoke).toHaveBeenCalledWith(
'stream_fetch',
expect.objectContaining({
url: 'https://example.com'
}),
undefined
);
});
test('should add abort signal to request', async () => {
// Mock successful response from Tauri
// @ts-ignore
mockInvoke.mockResolvedValue({
request_id: 123,
status: 200,
status_text: 'OK',
headers: {}
} as any);
// Create AbortController
const controller = new AbortController();
const addEventListenerSpy = jest.spyOn(controller.signal, 'addEventListener');
// Call fetch with signal
await streamFetch('https://example.com', {
signal: controller.signal
});
// Check that signal was added
expect(addEventListenerSpy).toHaveBeenCalledWith('abort', expect.any(Function));
});
});