Add i18n, tests, and update documentation

Introduces internationalization (i18n) support with English and Turkish, adds unit tests and test setup with Vitest and React Testing Library, and updates documentation including README and changelog. Removes legacy publishing and usage guides, refactors components to use translation system, and updates build and test scripts in package.json. Also adds new utility modules for HLS and CORS, and improves PlayerContext and SettingsMenu for language support.
This commit is contained in:
hibna
2025-10-29 13:10:07 +03:00
parent e75d241421
commit bad1cc6ca0
26 changed files with 1843 additions and 1341 deletions
+225
View File
@@ -0,0 +1,225 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import {
isSameOrigin,
isBlobOrDataURL,
validateVideoURL,
getCORSErrorMessage,
isCORSError,
checkVideoCORS,
} from './corsHelper';
describe('corsHelper', () => {
describe('isSameOrigin', () => {
it('returns true for same origin URLs', () => {
const sameOriginUrl = `${window.location.origin}/video.mp4`;
expect(isSameOrigin(sameOriginUrl)).toBe(true);
});
it('returns false for different origin URLs', () => {
expect(isSameOrigin('https://example.com/video.mp4')).toBe(false);
});
it('returns true for relative URLs', () => {
expect(isSameOrigin('/videos/test.mp4')).toBe(true);
});
it('returns true for relative path-like strings', () => {
// In browsers, "not-a-url" is treated as a relative URL
expect(isSameOrigin('not-a-url')).toBe(true);
});
});
describe('isBlobOrDataURL', () => {
it('returns true for blob URLs', () => {
expect(isBlobOrDataURL('blob:http://example.com/123456')).toBe(true);
});
it('returns true for data URLs', () => {
expect(isBlobOrDataURL('data:video/mp4;base64,AAAA')).toBe(true);
});
it('returns false for regular URLs', () => {
expect(isBlobOrDataURL('https://example.com/video.mp4')).toBe(false);
});
it('returns false for empty string', () => {
expect(isBlobOrDataURL('')).toBe(false);
});
});
describe('validateVideoURL', () => {
it('returns invalid for empty URL', () => {
const result = validateVideoURL('');
expect(result.valid).toBe(false);
expect(result.error).toBe('Video URL is empty');
});
it('returns invalid for whitespace-only URL', () => {
const result = validateVideoURL(' ');
expect(result.valid).toBe(false);
expect(result.error).toBe('Video URL is empty');
});
it('returns valid for relative path strings', () => {
// Browser treats this as a relative URL
const result = validateVideoURL('not a valid url');
expect(result.valid).toBe(true);
});
it('returns valid for same origin URL without warning', () => {
const result = validateVideoURL(`${window.location.origin}/video.mp4`);
expect(result.valid).toBe(true);
expect(result.warning).toBeUndefined();
});
it('returns valid for blob URL without warning', () => {
const result = validateVideoURL('blob:http://example.com/123456');
expect(result.valid).toBe(true);
expect(result.warning).toBeUndefined();
});
it('returns valid with warning for external URL', () => {
const result = validateVideoURL('https://example.com/video.mp4');
expect(result.valid).toBe(true);
expect(result.warning).toContain('CORS');
});
it('returns valid for relative URLs', () => {
const result = validateVideoURL('/videos/test.mp4');
expect(result.valid).toBe(true);
expect(result.warning).toBeUndefined();
});
});
describe('getCORSErrorMessage', () => {
it('returns generic message for same origin', () => {
const message = getCORSErrorMessage(`${window.location.origin}/video.mp4`);
expect(message).toBe('Failed to load video. Please check the URL.');
});
it('returns generic message for blob URLs', () => {
const message = getCORSErrorMessage('blob:http://example.com/123456');
expect(message).toBe('Failed to load video. Please check the URL.');
});
it('returns CORS-specific message for external URLs', () => {
const message = getCORSErrorMessage('https://example.com/video.mp4');
expect(message).toContain('CORS Error');
expect(message).toContain('example.com');
expect(message).toContain('Access-Control-Allow-Origin');
});
});
describe('isCORSError', () => {
it('returns true for errors containing "cors"', () => {
const error = new Error('CORS policy blocked this request');
expect(isCORSError(error)).toBe(true);
});
it('returns true for errors containing "cross-origin"', () => {
const error = new Error('Cross-origin request blocked');
expect(isCORSError(error)).toBe(true);
});
it('returns true for errors containing "blocked by cors policy"', () => {
const error = new Error('Request blocked by CORS policy');
expect(isCORSError(error)).toBe(true);
});
it('returns true for errors containing "access-control-allow-origin"', () => {
const error = new Error('No \'access-control-allow-origin\' header present');
expect(isCORSError(error)).toBe(true);
});
it('returns false for non-CORS errors', () => {
const error = new Error('Network timeout');
expect(isCORSError(error)).toBe(false);
});
it('is case insensitive', () => {
const error = new Error('BLOCKED BY CORS POLICY');
expect(isCORSError(error)).toBe(true);
});
});
describe('checkVideoCORS', () => {
beforeEach(() => {
global.fetch = vi.fn();
});
afterEach(() => {
vi.restoreAllMocks();
});
it('returns supported when CORS headers are present', async () => {
(global.fetch as any).mockResolvedValue({
ok: true,
headers: new Map([
['Access-Control-Allow-Origin', '*'],
['Accept-Ranges', 'bytes'],
]),
});
const result = await checkVideoCORS('https://example.com/video.mp4');
expect(result.supported).toBe(true);
expect(result.needsProxy).toBe(false);
expect(result.supportsRange).toBe(true);
});
it('returns not supported when CORS headers are missing', async () => {
(global.fetch as any).mockResolvedValue({
ok: false,
headers: new Map(),
});
const result = await checkVideoCORS('https://example.com/video.mp4');
expect(result.supported).toBe(false);
expect(result.needsProxy).toBe(true);
expect(result.error).toContain('CORS not enabled');
});
it('detects range support', async () => {
(global.fetch as any).mockResolvedValue({
ok: true,
headers: new Map([
['Access-Control-Allow-Origin', '*'],
['Accept-Ranges', 'bytes'],
]),
});
const result = await checkVideoCORS('https://example.com/video.mp4');
expect(result.supportsRange).toBe(true);
});
it('detects no range support when header is "none"', async () => {
(global.fetch as any).mockResolvedValue({
ok: true,
headers: new Map([
['Access-Control-Allow-Origin', '*'],
['Accept-Ranges', 'none'],
]),
});
const result = await checkVideoCORS('https://example.com/video.mp4');
expect(result.supportsRange).toBe(false);
});
it('handles CORS fetch errors', async () => {
(global.fetch as any).mockRejectedValue(new TypeError('Failed to fetch (CORS)'));
const result = await checkVideoCORS('https://example.com/video.mp4');
expect(result.supported).toBe(false);
expect(result.needsProxy).toBe(true);
expect(result.error).toContain('CORS blocked');
});
it('handles general fetch errors', async () => {
(global.fetch as any).mockRejectedValue(new Error('Network error'));
const result = await checkVideoCORS('https://example.com/video.mp4');
expect(result.supported).toBe(false);
expect(result.needsProxy).toBe(true);
expect(result.error).toBe('Network error');
});
});
});