Initial project setup
Some checks failed
Build and Test YT2AI Bookmarklet / build-and-test (16.x) (push) Has been cancelled
Build and Test YT2AI Bookmarklet / build-and-test (18.x) (push) Has been cancelled
Build and Test YT2AI Bookmarklet / build-and-test (20.x) (push) Has been cancelled
Build and Test YT2AI Bookmarklet / release (push) Has been cancelled
Build and Test YT2AI Bookmarklet / security-scan (push) Has been cancelled
Some checks failed
Build and Test YT2AI Bookmarklet / build-and-test (16.x) (push) Has been cancelled
Build and Test YT2AI Bookmarklet / build-and-test (18.x) (push) Has been cancelled
Build and Test YT2AI Bookmarklet / build-and-test (20.x) (push) Has been cancelled
Build and Test YT2AI Bookmarklet / release (push) Has been cancelled
Build and Test YT2AI Bookmarklet / security-scan (push) Has been cancelled
Add project structure with package.json, source code, tests, documentation, and GitHub workflows. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
421
tests/unit/core/bookmarklet.test.js
Normal file
421
tests/unit/core/bookmarklet.test.js
Normal file
@@ -0,0 +1,421 @@
|
||||
/**
|
||||
* Unit Tests for YT2AI Bookmarklet Core Functionality
|
||||
* Testing Framework: Jest + jsdom for DOM simulation
|
||||
* Mobile environment mocking included
|
||||
*/
|
||||
|
||||
// Mock mobile environment
|
||||
const mockMobileEnvironment = () => {
|
||||
// Mock mobile user agent
|
||||
Object.defineProperty(navigator, 'userAgent', {
|
||||
writable: true,
|
||||
value: 'Mozilla/5.0 (Linux; Android 10; SM-G975F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.120 Mobile Safari/537.36'
|
||||
});
|
||||
|
||||
// Mock mobile viewport
|
||||
Object.defineProperty(window, 'innerWidth', { writable: true, value: 375 });
|
||||
Object.defineProperty(window, 'innerHeight', { writable: true, value: 667 });
|
||||
|
||||
};
|
||||
|
||||
// Mock YouTube environment
|
||||
const mockYouTubeEnvironment = (videoId = 'dQw4w9WgXcQ') => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: {
|
||||
href: `https://www.youtube.com/watch?v=${videoId}`,
|
||||
hostname: 'youtube.com',
|
||||
search: ''
|
||||
},
|
||||
writable: true
|
||||
});
|
||||
};
|
||||
|
||||
// Clean environment before each test
|
||||
const cleanEnvironment = () => {
|
||||
// Reset globals
|
||||
delete window.__YT2AI_INITIALIZED__;
|
||||
delete window.__YT2AI_VERSION__;
|
||||
delete window.__YT2AI__;
|
||||
|
||||
// Clean DOM
|
||||
document.head.innerHTML = '';
|
||||
document.body.innerHTML = '';
|
||||
document.body.style.overflow = '';
|
||||
|
||||
// Clear console spies
|
||||
jest.clearAllMocks();
|
||||
};
|
||||
|
||||
describe('YT2AI Bookmarklet Core', () => {
|
||||
let consoleLogSpy, consoleInfoSpy, consoleWarnSpy, consoleErrorSpy;
|
||||
|
||||
// Helper function to load bookmarklet
|
||||
const loadBookmarklet = () => {
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const bookmarkletCode = fs.readFileSync(path.join(__dirname, '../../../src/bookmarklet.js'), 'utf8');
|
||||
eval(bookmarkletCode);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
cleanEnvironment();
|
||||
mockMobileEnvironment();
|
||||
mockYouTubeEnvironment();
|
||||
|
||||
// Spy on console methods
|
||||
consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
|
||||
consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation();
|
||||
consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
|
||||
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanEnvironment();
|
||||
// Restore console methods if they exist
|
||||
if (consoleLogSpy && consoleLogSpy.mockRestore) consoleLogSpy.mockRestore();
|
||||
if (consoleInfoSpy && consoleInfoSpy.mockRestore) consoleInfoSpy.mockRestore();
|
||||
if (consoleWarnSpy && consoleWarnSpy.mockRestore) consoleWarnSpy.mockRestore();
|
||||
if (consoleErrorSpy && consoleErrorSpy.mockRestore) consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
||||
describe('Initialization', () => {
|
||||
test('should initialize bookmarklet without errors on YouTube page', () => {
|
||||
console.log('Before load - location:', window.location);
|
||||
console.log('Before load - initialized:', window.__YT2AI_INITIALIZED__);
|
||||
|
||||
expect(() => {
|
||||
loadBookmarklet();
|
||||
}).not.toThrow();
|
||||
|
||||
console.log('After load - initialized:', window.__YT2AI_INITIALIZED__);
|
||||
console.log('After load - version:', window.__YT2AI_VERSION__);
|
||||
|
||||
expect(window.__YT2AI_INITIALIZED__).toBe(true);
|
||||
expect(window.__YT2AI_VERSION__).toBeDefined();
|
||||
expect(consoleInfoSpy).toHaveBeenCalledWith(
|
||||
'[YT2AI:INFO]',
|
||||
'Bookmarklet initialized',
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
test('should prevent double initialization', () => {
|
||||
// First initialization
|
||||
loadBookmarklet();
|
||||
|
||||
// Try to initialize again
|
||||
loadBookmarklet();
|
||||
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
||||
'[YT2AI] Bookmarklet already initialized'
|
||||
);
|
||||
});
|
||||
|
||||
test('should detect mobile environment correctly', () => {
|
||||
loadBookmarklet();
|
||||
|
||||
if (window.__YT2AI__) {
|
||||
expect(window.__YT2AI__.environment.isMobile).toBe(true);
|
||||
expect(window.__YT2AI__.environment.isAndroid).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test('should set development mode correctly', () => {
|
||||
// Mock development environment
|
||||
window.location.search = '?debug=true';
|
||||
|
||||
loadBookmarklet();
|
||||
|
||||
if (window.__YT2AI__) {
|
||||
expect(window.__YT2AI__.environment.isDevelopment).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('YouTube Context Detection', () => {
|
||||
test('should extract video ID from standard YouTube URLs', () => {
|
||||
const testUrls = [
|
||||
{ url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', expected: 'dQw4w9WgXcQ' },
|
||||
{ url: 'https://youtu.be/dQw4w9WgXcQ', expected: 'dQw4w9WgXcQ' },
|
||||
{ url: 'https://www.youtube.com/shorts/dQw4w9WgXcQ', expected: 'dQw4w9WgXcQ' },
|
||||
{ url: 'https://www.youtube.com/embed/dQw4w9WgXcQ', expected: 'dQw4w9WgXcQ' }
|
||||
];
|
||||
|
||||
loadBookmarklet();
|
||||
|
||||
if (window.__YT2AI__) {
|
||||
testUrls.forEach(({ url, expected }) => {
|
||||
const videoId = window.__YT2AI__.extractVideoId(url);
|
||||
expect(videoId).toBe(expected);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('should return null for invalid URLs', () => {
|
||||
loadBookmarklet();
|
||||
|
||||
if (window.__YT2AI__) {
|
||||
const invalidUrls = [
|
||||
'https://www.google.com',
|
||||
'https://www.youtube.com/playlist?list=123',
|
||||
'https://www.youtube.com',
|
||||
'not-a-url'
|
||||
];
|
||||
|
||||
invalidUrls.forEach(url => {
|
||||
const videoId = window.__YT2AI__.extractVideoId(url);
|
||||
expect(videoId).toBeNull();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('should validate YouTube context successfully', () => {
|
||||
loadBookmarklet();
|
||||
|
||||
if (window.__YT2AI__) {
|
||||
const context = window.__YT2AI__.validateYouTubeContext();
|
||||
expect(context.videoId).toBe('dQw4w9WgXcQ');
|
||||
expect(context.url).toContain('youtube.com');
|
||||
}
|
||||
});
|
||||
|
||||
test('should throw error for non-YouTube pages', () => {
|
||||
// Mock non-YouTube page
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: {
|
||||
href: 'https://www.google.com',
|
||||
hostname: 'google.com'
|
||||
}
|
||||
});
|
||||
|
||||
expect(() => {
|
||||
loadBookmarklet();
|
||||
}).not.toThrow(); // Initialization shouldn't throw, but validation should
|
||||
|
||||
if (window.__YT2AI__) {
|
||||
expect(() => {
|
||||
window.__YT2AI__.validateYouTubeContext();
|
||||
}).toThrow('This bookmarklet only works on YouTube pages');
|
||||
}
|
||||
});
|
||||
|
||||
test('should throw error for YouTube pages without video', () => {
|
||||
// Mock YouTube homepage
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: {
|
||||
href: 'https://www.youtube.com',
|
||||
hostname: 'youtube.com'
|
||||
}
|
||||
});
|
||||
|
||||
if (window.__YT2AI__) {
|
||||
expect(() => {
|
||||
window.__YT2AI__.validateYouTubeContext();
|
||||
}).toThrow('Please navigate to a specific YouTube video page');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Mobile Overlay System', () => {
|
||||
test('should create overlay with mobile-optimized styling', () => {
|
||||
loadBookmarklet();
|
||||
|
||||
if (window.__YT2AI__) {
|
||||
const overlay = new window.__YT2AI__.MobileOverlay({
|
||||
id: 'test-overlay',
|
||||
type: 'info',
|
||||
content: 'Test content'
|
||||
});
|
||||
|
||||
overlay.show();
|
||||
|
||||
// Check if overlay exists in DOM
|
||||
const overlayElement = document.querySelector('.yt2ai-overlay');
|
||||
expect(overlayElement).toBeTruthy();
|
||||
expect(overlayElement.classList.contains('yt2ai-info')).toBe(true);
|
||||
|
||||
// Check mobile-specific styling
|
||||
const styles = document.getElementById('yt2ai-overlay-styles');
|
||||
expect(styles).toBeTruthy();
|
||||
expect(styles.textContent).toContain('95vw'); // Mobile max-width
|
||||
expect(styles.textContent).toContain('44px'); // Touch target size
|
||||
|
||||
overlay.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
test('should handle touch events correctly', () => {
|
||||
loadBookmarklet();
|
||||
|
||||
if (window.__YT2AI__) {
|
||||
const overlay = new window.__YT2AI__.MobileOverlay({
|
||||
id: 'touch-test',
|
||||
type: 'success',
|
||||
content: 'Touch test'
|
||||
});
|
||||
|
||||
overlay.show();
|
||||
|
||||
const closeBtn = document.querySelector('.yt2ai-close-btn');
|
||||
expect(closeBtn).toBeTruthy();
|
||||
|
||||
// Simulate touch event
|
||||
const touchEvent = new Event('touchend', { bubbles: true });
|
||||
closeBtn.dispatchEvent(touchEvent);
|
||||
|
||||
// Should close overlay
|
||||
expect(overlay.isVisible).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
test('should prevent body scroll when shown', () => {
|
||||
loadBookmarklet();
|
||||
|
||||
if (window.__YT2AI__) {
|
||||
const overlay = new window.__YT2AI__.MobileOverlay({
|
||||
id: 'scroll-test',
|
||||
content: 'Scroll test'
|
||||
});
|
||||
|
||||
overlay.show();
|
||||
expect(document.body.style.overflow).toBe('hidden');
|
||||
|
||||
overlay.hide();
|
||||
expect(document.body.style.overflow).toBe('');
|
||||
|
||||
overlay.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
test('should auto-close when configured', (done) => {
|
||||
loadBookmarklet();
|
||||
|
||||
if (window.__YT2AI__) {
|
||||
const overlay = new window.__YT2AI__.MobileOverlay({
|
||||
id: 'autoclose-test',
|
||||
content: 'Auto close test',
|
||||
autoClose: 100
|
||||
});
|
||||
|
||||
overlay.show();
|
||||
expect(overlay.isVisible).toBe(true);
|
||||
|
||||
setTimeout(() => {
|
||||
expect(overlay.isVisible).toBe(false);
|
||||
done();
|
||||
}, 150);
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
test('should handle and display errors with mobile-friendly UI', () => {
|
||||
// Mock error scenario
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: {
|
||||
href: 'https://www.google.com',
|
||||
hostname: 'google.com'
|
||||
}
|
||||
});
|
||||
|
||||
loadBookmarklet();
|
||||
|
||||
// Error should be logged
|
||||
expect(consoleErrorSpy).toHaveBeenCalled();
|
||||
|
||||
// Error overlay should appear
|
||||
setTimeout(() => {
|
||||
const errorOverlay = document.querySelector('.yt2ai-error');
|
||||
expect(errorOverlay).toBeTruthy();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
test('should cleanup properly on error', () => {
|
||||
// Mock error scenario and test cleanup
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: {
|
||||
href: 'https://www.google.com',
|
||||
hostname: 'google.com'
|
||||
}
|
||||
});
|
||||
|
||||
loadBookmarklet();
|
||||
|
||||
if (window.__YT2AI__) {
|
||||
window.__YT2AI__.cleanup();
|
||||
|
||||
// Check cleanup
|
||||
expect(document.querySelectorAll('.yt2ai-overlay')).toHaveLength(0);
|
||||
expect(document.body.style.overflow).toBe('');
|
||||
expect(window.__YT2AI__.stateManager.getState().initialized).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('State Management', () => {
|
||||
test('should manage state correctly', () => {
|
||||
loadBookmarklet();
|
||||
|
||||
if (window.__YT2AI__) {
|
||||
const stateManager = window.__YT2AI__.stateManager;
|
||||
|
||||
// Initial state
|
||||
const initialState = stateManager.getState();
|
||||
expect(initialState.initialized).toBe(true);
|
||||
expect(initialState.videoId).toBe('dQw4w9WgXcQ');
|
||||
|
||||
// Update state
|
||||
stateManager.updateState({ processing: true, currentStep: 'test' });
|
||||
const updatedState = stateManager.getState();
|
||||
expect(updatedState.processing).toBe(true);
|
||||
expect(updatedState.currentStep).toBe('test');
|
||||
|
||||
// Reset state
|
||||
stateManager.reset();
|
||||
const resetState = stateManager.getState();
|
||||
expect(resetState.initialized).toBe(false);
|
||||
expect(resetState.videoId).toBeNull();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Performance Tracking', () => {
|
||||
test('should track performance in development mode', () => {
|
||||
// Mock development environment
|
||||
window.location.search = '?debug=true';
|
||||
|
||||
loadBookmarklet();
|
||||
|
||||
// In test environment, performance API is not available in eval context
|
||||
// so trackPerformance should gracefully handle this and continue execution
|
||||
expect(window.__YT2AI_INITIALIZED__).toBe(true);
|
||||
expect(window.__YT2AI__.environment.isDevelopment).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Cleanup', () => {
|
||||
test('should cleanup all resources properly', () => {
|
||||
loadBookmarklet();
|
||||
|
||||
if (window.__YT2AI__) {
|
||||
// Create some overlays
|
||||
const overlay1 = new window.__YT2AI__.MobileOverlay({ id: 'test1', content: 'Test 1' });
|
||||
const overlay2 = new window.__YT2AI__.MobileOverlay({ id: 'test2', content: 'Test 2' });
|
||||
|
||||
overlay1.show();
|
||||
overlay2.show();
|
||||
|
||||
// Perform cleanup
|
||||
window.__YT2AI__.cleanup();
|
||||
|
||||
// Verify cleanup
|
||||
expect(document.querySelectorAll('.yt2ai-overlay')).toHaveLength(0);
|
||||
expect(document.getElementById('yt2ai-overlay-styles')).toBeNull();
|
||||
expect(document.body.style.overflow).toBe('');
|
||||
expect(window.__YT2AI_INITIALIZED__).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user