mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
278 lines
11 KiB
JavaScript
278 lines
11 KiB
JavaScript
/**
|
|
* Unit tests for media-upload-automation (Electron).
|
|
* Covers direct URL detection plus BrowserWindow/CDP automation flows.
|
|
*/
|
|
import { EventEmitter } from 'node:events';
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
const electronState = vi.hoisted(() => ({
|
|
createWindow: null,
|
|
}));
|
|
|
|
vi.mock('electron', () => ({
|
|
BrowserWindow: vi.fn(function BrowserWindow(options) {
|
|
if (!electronState.createWindow) {
|
|
throw new Error(`Missing BrowserWindow mock for ${JSON.stringify(options)}`);
|
|
}
|
|
return electronState.createWindow(options);
|
|
}),
|
|
}));
|
|
|
|
import { automateUploadMedia, isDirectMediaUrl } from './media-upload-automation.js';
|
|
import { MEDIA_UPLOAD_RECIPES } from './media-upload-recipes.js';
|
|
|
|
const createFakeBrowserWindow = ({ attachError = null, loadError = null, getNodeId = () => 0, runtimeValues = [], missingBoxModel = false } = {}) => {
|
|
const emitter = new EventEmitter();
|
|
let attached = false;
|
|
let destroyed = false;
|
|
|
|
const sendCommand = vi.fn(async (method, params = {}) => {
|
|
switch (method) {
|
|
case 'DOM.enable':
|
|
case 'Page.enable':
|
|
case 'Input.enable':
|
|
return {};
|
|
case 'DOM.getDocument':
|
|
return { root: { nodeId: 1 } };
|
|
case 'DOM.querySelector':
|
|
return { nodeId: getNodeId(params.selector) || 0 };
|
|
case 'DOM.setFileInputFiles':
|
|
return {};
|
|
case 'DOM.getBoxModel':
|
|
if (missingBoxModel) {
|
|
return { model: null };
|
|
}
|
|
return { model: { content: [0, 0, 20, 0, 20, 20, 0, 20] } };
|
|
case 'Runtime.evaluate':
|
|
return { result: { value: runtimeValues.length > 0 ? runtimeValues.shift() : null } };
|
|
case 'Input.dispatchMouseEvent':
|
|
return {};
|
|
default:
|
|
throw new Error(`Unhandled CDP command in test: ${method}`);
|
|
}
|
|
});
|
|
|
|
const fakeWindow = {
|
|
loadURL: vi.fn((url) => {
|
|
queueMicrotask(() => {
|
|
if (loadError) {
|
|
emitter.emit('did-fail-load', {}, loadError.code ?? -1, loadError.description ?? 'load failed');
|
|
return;
|
|
}
|
|
emitter.emit('did-finish-load');
|
|
});
|
|
return url;
|
|
}),
|
|
destroy: vi.fn(() => {
|
|
destroyed = true;
|
|
}),
|
|
isDestroyed: vi.fn(() => destroyed),
|
|
webContents: Object.assign(emitter, {
|
|
debugger: {
|
|
attach: vi.fn(() => {
|
|
if (attachError) {
|
|
throw attachError;
|
|
}
|
|
attached = true;
|
|
}),
|
|
sendCommand,
|
|
detach: vi.fn(() => {
|
|
attached = false;
|
|
}),
|
|
isAttached: vi.fn(() => attached),
|
|
},
|
|
}),
|
|
};
|
|
|
|
return fakeWindow;
|
|
};
|
|
|
|
describe('media-upload-automation', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
electronState.createWindow = null;
|
|
});
|
|
|
|
describe('isDirectMediaUrl', () => {
|
|
it('returns true for image extensions', () => {
|
|
expect(isDirectMediaUrl('https://example.com/photo.jpg')).toBe(true);
|
|
expect(isDirectMediaUrl('https://i.imgur.com/abc.png')).toBe(true);
|
|
expect(isDirectMediaUrl('https://i.postimg.cc/xyz.webp')).toBe(true);
|
|
});
|
|
|
|
it('returns true for video extensions', () => {
|
|
expect(isDirectMediaUrl('https://example.com/video.webm')).toBe(true);
|
|
expect(isDirectMediaUrl('https://example.com/video.mp4')).toBe(true);
|
|
});
|
|
|
|
it('returns true for Flash movie extensions', () => {
|
|
expect(isDirectMediaUrl('https://example.com/movie.swf')).toBe(true);
|
|
expect(isDirectMediaUrl('https://example.com/movie.SWF?download=1')).toBe(true);
|
|
});
|
|
|
|
it('strips query strings and fragments before checking', () => {
|
|
expect(isDirectMediaUrl('https://i.imgur.com/abc.png?size=large')).toBe(true);
|
|
expect(isDirectMediaUrl('https://imgur.com/page.html?img=photo.jpg')).toBe(false);
|
|
expect(isDirectMediaUrl('https://i.imgur.com/abc.png#fragment')).toBe(true);
|
|
expect(isDirectMediaUrl('https://i.postimg.cc/xyz.webp#')).toBe(true);
|
|
});
|
|
|
|
it('returns false for non-direct URLs (guards against non-media pages)', () => {
|
|
expect(isDirectMediaUrl('https://imgur.com/abc123')).toBe(false);
|
|
expect(isDirectMediaUrl('https://imgur.com/upload')).toBe(false);
|
|
expect(isDirectMediaUrl('')).toBe(false);
|
|
});
|
|
|
|
it('returns false for invalid input', () => {
|
|
expect(isDirectMediaUrl(null)).toBe(false);
|
|
expect(isDirectMediaUrl(undefined)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('automateUploadMedia', () => {
|
|
it('rejects unknown providers before opening a window', async () => {
|
|
await expect(automateUploadMedia({ provider: 'unknown', filePath: '/tmp/file.png' })).rejects.toThrow('No automation recipe for provider: unknown');
|
|
});
|
|
|
|
it('uploads through a hidden BrowserWindow and returns a direct media URL', async () => {
|
|
const recipe = MEDIA_UPLOAD_RECIPES.imgur;
|
|
const fakeWindow = createFakeBrowserWindow({
|
|
getNodeId: (selector) => {
|
|
if (selector === recipe.fileInputSelectorCandidates[0]) return 10;
|
|
if (selector === recipe.submitSelectorCandidates[0]) return 20;
|
|
return 0;
|
|
},
|
|
runtimeValues: ['https://i.imgur.com/uploaded.png'],
|
|
});
|
|
electronState.createWindow = () => fakeWindow;
|
|
|
|
const result = await automateUploadMedia({ provider: 'imgur', filePath: '/tmp/upload.png' });
|
|
|
|
expect(result).toEqual({ url: 'https://i.imgur.com/uploaded.png', provider: 'imgur' });
|
|
expect(fakeWindow.loadURL).toHaveBeenCalledWith(recipe.uploadUrl);
|
|
expect(fakeWindow.webContents.debugger.attach).toHaveBeenCalledWith('1.3');
|
|
expect(fakeWindow.webContents.debugger.sendCommand).toHaveBeenCalledWith('DOM.setFileInputFiles', {
|
|
nodeId: 10,
|
|
files: ['/tmp/upload.png'],
|
|
});
|
|
expect(fakeWindow.webContents.debugger.sendCommand.mock.calls.filter(([method]) => method === 'Input.dispatchMouseEvent')).toHaveLength(2);
|
|
expect(fakeWindow.webContents.debugger.detach).toHaveBeenCalledOnce();
|
|
expect(fakeWindow.destroy).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('uploads to ImgBB through the hidden BrowserWindow recipe', async () => {
|
|
const recipe = MEDIA_UPLOAD_RECIPES.imgbb;
|
|
const fakeWindow = createFakeBrowserWindow({
|
|
getNodeId: (selector) => {
|
|
if (selector === recipe.fileInputSelectorCandidates[0]) return 10;
|
|
if (selector === recipe.submitSelectorCandidates[0]) return 20;
|
|
return 0;
|
|
},
|
|
runtimeValues: [true, 'https://i.ibb.co/example/uploaded.png'],
|
|
});
|
|
electronState.createWindow = () => fakeWindow;
|
|
|
|
const result = await automateUploadMedia({ provider: 'imgbb', filePath: '/tmp/upload.png' });
|
|
|
|
expect(result).toEqual({ url: 'https://i.ibb.co/example/uploaded.png', provider: 'imgbb' });
|
|
expect(fakeWindow.loadURL).toHaveBeenCalledWith(recipe.uploadUrl);
|
|
expect(fakeWindow.webContents.debugger.sendCommand).toHaveBeenCalledWith('DOM.setFileInputFiles', {
|
|
nodeId: 10,
|
|
files: ['/tmp/upload.png'],
|
|
});
|
|
});
|
|
|
|
it('fails fast when a blocked indicator is present before upload begins', async () => {
|
|
const recipe = MEDIA_UPLOAD_RECIPES.imgur;
|
|
const fakeWindow = createFakeBrowserWindow({
|
|
getNodeId: (selector) => (selector === recipe.blockedIndicators[0] ? 99 : 0),
|
|
});
|
|
electronState.createWindow = () => fakeWindow;
|
|
|
|
await expect(automateUploadMedia({ provider: 'imgur', filePath: '/tmp/upload.png' })).rejects.toThrow(
|
|
`Provider blocked: captcha, login, or challenge detected (imgur), selector: ${recipe.blockedIndicators[0]}`,
|
|
);
|
|
|
|
expect(fakeWindow.webContents.debugger.detach).toHaveBeenCalledOnce();
|
|
expect(fakeWindow.destroy).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('errors when no file input can be found', async () => {
|
|
const recipe = MEDIA_UPLOAD_RECIPES.imgur;
|
|
const fakeWindow = createFakeBrowserWindow({
|
|
getNodeId: (selector) => (selector === recipe.submitSelectorCandidates[0] ? 20 : 0),
|
|
});
|
|
electronState.createWindow = () => fakeWindow;
|
|
|
|
await expect(automateUploadMedia({ provider: 'imgur', filePath: '/tmp/upload.png' })).rejects.toThrow(
|
|
`No file input found for imgur. Tried: ${recipe.fileInputSelectorCandidates.join(', ')}`,
|
|
);
|
|
|
|
expect(fakeWindow.webContents.debugger.detach).toHaveBeenCalledOnce();
|
|
expect(fakeWindow.destroy).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('bubbles page-load failures and still destroys the hidden window', async () => {
|
|
const fakeWindow = createFakeBrowserWindow({
|
|
loadError: { code: -3, description: 'aborted' },
|
|
});
|
|
electronState.createWindow = () => fakeWindow;
|
|
|
|
await expect(automateUploadMedia({ provider: 'imgur', filePath: '/tmp/upload.png' })).rejects.toThrow('Page load failed: -3 aborted');
|
|
|
|
expect(fakeWindow.destroy).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('surfaces debugger attach failures and still destroys the hidden window', async () => {
|
|
const fakeWindow = createFakeBrowserWindow({
|
|
attachError: new Error('attach failed'),
|
|
});
|
|
electronState.createWindow = () => fakeWindow;
|
|
|
|
await expect(automateUploadMedia({ provider: 'imgur', filePath: '/tmp/upload.png' })).rejects.toThrow('attach failed');
|
|
|
|
expect(fakeWindow.destroy).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('surfaces click failures when the submit button box model is unavailable', async () => {
|
|
const recipe = MEDIA_UPLOAD_RECIPES.imgur;
|
|
const fakeWindow = createFakeBrowserWindow({
|
|
getNodeId: (selector) => {
|
|
if (selector === recipe.fileInputSelectorCandidates[0]) return 10;
|
|
if (selector === recipe.submitSelectorCandidates[0]) return 20;
|
|
return 0;
|
|
},
|
|
missingBoxModel: true,
|
|
});
|
|
electronState.createWindow = () => fakeWindow;
|
|
|
|
await expect(automateUploadMedia({ provider: 'imgur', filePath: '/tmp/upload.png' })).rejects.toThrow(
|
|
'Cannot click node: box model unavailable (element may be hidden or zero-size)',
|
|
);
|
|
|
|
expect(fakeWindow.webContents.debugger.detach).toHaveBeenCalledOnce();
|
|
expect(fakeWindow.destroy).toHaveBeenCalledOnce();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('media-upload-automation + recipes integration', () => {
|
|
it('imgur success extractor targets direct-media domain', () => {
|
|
const imgurSelectors = MEDIA_UPLOAD_RECIPES.imgur.successExtractor.selectorCandidates;
|
|
expect(imgurSelectors.some((selector) => selector.includes('i.imgur.com'))).toBe(true);
|
|
});
|
|
|
|
it('all providers have fallback selector chains for file input and submit', () => {
|
|
for (const recipe of Object.values(MEDIA_UPLOAD_RECIPES)) {
|
|
expect(recipe.fileInputSelectorCandidates.length).toBeGreaterThanOrEqual(1);
|
|
expect(recipe.submitSelectorCandidates.length).toBeGreaterThanOrEqual(1);
|
|
expect(recipe.successExtractor.selectorCandidates.length).toBeGreaterThanOrEqual(1);
|
|
}
|
|
});
|
|
|
|
it('imgbb success extractor targets direct-media domain', () => {
|
|
const imgbbSelectors = MEDIA_UPLOAD_RECIPES.imgbb.successExtractor.selectorCandidates;
|
|
expect(imgbbSelectors.some((selector) => selector.includes('i.ibb.co') || selector.includes('html-embed-medium'))).toBe(true);
|
|
});
|
|
});
|