mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
feat(media hosting): add imgbb provider
This commit is contained in:
@@ -29,7 +29,7 @@ export function isDirectMediaUrl(url) {
|
|||||||
/**
|
/**
|
||||||
* Run automated upload for a provider.
|
* Run automated upload for a provider.
|
||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
* @param {string} options.provider - Provider id (catbox, imgur)
|
* @param {string} options.provider - Provider id (catbox, imgur, imgbb)
|
||||||
* @param {string} options.filePath - Absolute path to the file to upload
|
* @param {string} options.filePath - Absolute path to the file to upload
|
||||||
* @returns {Promise<{ url: string; provider: string }>}
|
* @returns {Promise<{ url: string; provider: string }>}
|
||||||
* @throws {Error} On missing recipe, blocked indicators, timeout, or invalid URL
|
* @throws {Error} On missing recipe, blocked indicators, timeout, or invalid URL
|
||||||
@@ -129,6 +129,14 @@ export async function automateUploadMedia(options) {
|
|||||||
nodeId: fileInputNodeId,
|
nodeId: fileInputNodeId,
|
||||||
files: [filePath],
|
files: [filePath],
|
||||||
});
|
});
|
||||||
|
await new Promise((r) => setTimeout(r, 500));
|
||||||
|
|
||||||
|
if (recipe.prepareSubmitJs) {
|
||||||
|
await sendCommand('Runtime.evaluate', {
|
||||||
|
expression: recipe.prepareSubmitJs,
|
||||||
|
returnByValue: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let submitNodeId = null;
|
let submitNodeId = null;
|
||||||
for (const sel of recipe.submitSelectorCandidates) {
|
for (const sel of recipe.submitSelectorCandidates) {
|
||||||
@@ -173,6 +181,27 @@ export async function automateUploadMedia(options) {
|
|||||||
(function() {
|
(function() {
|
||||||
const selectors = ${JSON.stringify(selectorCandidates)};
|
const selectors = ${JSON.stringify(selectorCandidates)};
|
||||||
const attr = ${JSON.stringify(attr)};
|
const attr = ${JSON.stringify(attr)};
|
||||||
|
function normalizeUrl(value) {
|
||||||
|
if (!value) return '';
|
||||||
|
let url = String(value).trim();
|
||||||
|
if (url.startsWith('//')) url = 'https:' + url;
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
function hasMediaExtension(url) {
|
||||||
|
return /\\.(?:jpe?g|png|gif|webp|bmp|avif|mp4|webm|mov|avi|mkv|gifv)(?:[?#].*)?$/i.test(url);
|
||||||
|
}
|
||||||
|
function pickDirectMediaUrl(value) {
|
||||||
|
const text = String(value || '');
|
||||||
|
const candidates = text.match(/https?:\\/\\/[^\\s"'<>\\[\\]]+/g) || [];
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
const normalized = normalizeUrl(candidate);
|
||||||
|
if (hasMediaExtension(normalized)) return normalized;
|
||||||
|
}
|
||||||
|
const normalized = normalizeUrl(text);
|
||||||
|
if (normalized.startsWith('http') && hasMediaExtension(normalized)) return normalized;
|
||||||
|
if (normalized.startsWith('http')) return normalized;
|
||||||
|
return '';
|
||||||
|
}
|
||||||
for (const sel of selectors) {
|
for (const sel of selectors) {
|
||||||
try {
|
try {
|
||||||
const el = document.querySelector(sel);
|
const el = document.querySelector(sel);
|
||||||
@@ -185,7 +214,8 @@ export async function automateUploadMedia(options) {
|
|||||||
} else {
|
} else {
|
||||||
url = (el.getAttribute(attr) || el[attr] || '').trim();
|
url = (el.getAttribute(attr) || el[attr] || '').trim();
|
||||||
}
|
}
|
||||||
if (url && url.startsWith('http')) return url;
|
const directUrl = pickDirectMediaUrl(url);
|
||||||
|
if (directUrl) return directUrl;
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -155,6 +155,28 @@ describe('media-upload-automation', () => {
|
|||||||
expect(fakeWindow.destroy).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 () => {
|
it('fails fast when a blocked indicator is present before upload begins', async () => {
|
||||||
const recipe = MEDIA_UPLOAD_RECIPES.imgur;
|
const recipe = MEDIA_UPLOAD_RECIPES.imgur;
|
||||||
const fakeWindow = createFakeBrowserWindow({
|
const fakeWindow = createFakeBrowserWindow({
|
||||||
@@ -242,4 +264,9 @@ describe('media-upload-automation + recipes integration', () => {
|
|||||||
expect(recipe.successExtractor.selectorCandidates.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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,12 +4,14 @@
|
|||||||
* Selectors are candidate-based: try each until one matches. Fail fast on blocked indicators.
|
* Selectors are candidate-based: try each until one matches. Fail fast on blocked indicators.
|
||||||
*
|
*
|
||||||
* Android vs Electron differences (documented to prevent drift):
|
* Android vs Electron differences (documented to prevent drift):
|
||||||
* - Android (MediaUploadRecipes.java): imgur only; no catbox (uses different flow).
|
* - Android (MediaUploadRecipes.java): app uploads use native catbox, plus WebView
|
||||||
* Uses DataTransfer JS injection.
|
* automation for imgbb. The imgur runner is retained for diagnostics/tests only.
|
||||||
* - Electron: catbox/imgur. Uses CDP DOM.setFileInputFiles + submit button click.
|
* Real Uri attempts use the WebView file chooser callback; fixtures/fallback use
|
||||||
|
* DataTransfer.
|
||||||
|
* - Electron: catbox/imgur/imgbb. Uses CDP DOM.setFileInputFiles + submit button click.
|
||||||
* Catbox: Electron-only; keep behavior unchanged.
|
* Catbox: Electron-only; keep behavior unchanged.
|
||||||
* - Blocked/success selectors: reconciled with Android for imgur.
|
* - Blocked/success selectors: reconciled with Android for imgbb and imgur.
|
||||||
* - Timeouts: imgur 45s; catbox 30s (Electron-only).
|
* - Timeouts: imgbb 60s; imgur 45s; catbox 30s (Electron-only).
|
||||||
*
|
*
|
||||||
* @typedef {Object} ProviderRecipe
|
* @typedef {Object} ProviderRecipe
|
||||||
* @property {string} uploadUrl - Full URL of the provider's upload page
|
* @property {string} uploadUrl - Full URL of the provider's upload page
|
||||||
@@ -18,6 +20,7 @@
|
|||||||
* @property {Object} successExtractor - How to extract the result URL from the page
|
* @property {Object} successExtractor - How to extract the result URL from the page
|
||||||
* @property {readonly string[]} successExtractor.selectorCandidates - Selectors for element containing result URL
|
* @property {readonly string[]} successExtractor.selectorCandidates - Selectors for element containing result URL
|
||||||
* @property {'href'|'src'|'value'|'text'} successExtractor.attribute - Attribute or 'text' for textContent
|
* @property {'href'|'src'|'value'|'text'} successExtractor.attribute - Attribute or 'text' for textContent
|
||||||
|
* @property {string=} prepareSubmitJs - Optional JS to normalize provider controls before clicking submit
|
||||||
* @property {readonly string[]} blockedIndicators - Selectors that indicate captcha/login/challenge (fail immediately if present)
|
* @property {readonly string[]} blockedIndicators - Selectors that indicate captcha/login/challenge (fail immediately if present)
|
||||||
* @property {number} timeoutMs - Max time to wait for upload completion
|
* @property {number} timeoutMs - Max time to wait for upload completion
|
||||||
*/
|
*/
|
||||||
@@ -48,6 +51,26 @@ export const MEDIA_UPLOAD_RECIPES = Object.freeze({
|
|||||||
blockedIndicators: Object.freeze(['#challenge', '.captcha', '[data-captcha]', '.g-recaptcha', '#recaptcha', '.login-form', '.signin', '.login']),
|
blockedIndicators: Object.freeze(['#challenge', '.captcha', '[data-captcha]', '.g-recaptcha', '#recaptcha', '.login-form', '.signin', '.login']),
|
||||||
timeoutMs: 45_000,
|
timeoutMs: 45_000,
|
||||||
}),
|
}),
|
||||||
|
imgbb: Object.freeze({
|
||||||
|
uploadUrl: 'https://imgbb.com/',
|
||||||
|
fileInputSelectorCandidates: Object.freeze(['input[type="file"]', '#anywhere-upload-input', 'input[data-action="anywhere-upload-input"]']),
|
||||||
|
submitSelectorCandidates: Object.freeze(['button[data-action="upload"]', 'button.btn.green', 'button[type="submit"]', '[data-action="upload"]']),
|
||||||
|
successExtractor: Object.freeze({
|
||||||
|
selectorCandidates: Object.freeze([
|
||||||
|
'input[name="html-embed-medium"]',
|
||||||
|
'textarea[name="html-embed-medium"]',
|
||||||
|
'#uploaded-embed-code-1',
|
||||||
|
'input[value*="i.ibb.co"]',
|
||||||
|
'textarea',
|
||||||
|
'img[src*="i.ibb.co"]',
|
||||||
|
]),
|
||||||
|
attribute: 'value',
|
||||||
|
}),
|
||||||
|
prepareSubmitJs:
|
||||||
|
"(function(){var select=document.querySelector('#upload-expiration,select[name=\"upload-expiration\"]');if(select){select.value='';select.dispatchEvent(new Event('change',{bubbles:true}));}return true;})()",
|
||||||
|
blockedIndicators: Object.freeze(['#challenge', '.captcha', '[data-captcha]', '.g-recaptcha', '#recaptcha', '.login-form', '.signin', '.login']),
|
||||||
|
timeoutMs: 60_000,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ describe('media-upload-recipes', () => {
|
|||||||
it('exports MEDIA_UPLOAD_RECIPES with expected providers', () => {
|
it('exports MEDIA_UPLOAD_RECIPES with expected providers', () => {
|
||||||
expect(Object.keys(MEDIA_UPLOAD_RECIPES)).toContain('catbox');
|
expect(Object.keys(MEDIA_UPLOAD_RECIPES)).toContain('catbox');
|
||||||
expect(Object.keys(MEDIA_UPLOAD_RECIPES)).toContain('imgur');
|
expect(Object.keys(MEDIA_UPLOAD_RECIPES)).toContain('imgur');
|
||||||
|
expect(Object.keys(MEDIA_UPLOAD_RECIPES)).toContain('imgbb');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('validateRecipes passes for current recipes', () => {
|
it('validateRecipes passes for current recipes', () => {
|
||||||
@@ -31,6 +32,12 @@ describe('media-upload-recipes', () => {
|
|||||||
expect(MEDIA_UPLOAD_RECIPES.imgur.timeoutMs).toBe(45_000);
|
expect(MEDIA_UPLOAD_RECIPES.imgur.timeoutMs).toBe(45_000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('imgbb targets full image embed codes and has Android parity timeout', () => {
|
||||||
|
expect(MEDIA_UPLOAD_RECIPES.imgbb.timeoutMs).toBe(60_000);
|
||||||
|
expect(MEDIA_UPLOAD_RECIPES.imgbb.uploadUrl).toBe('https://imgbb.com/');
|
||||||
|
expect(MEDIA_UPLOAD_RECIPES.imgbb.successExtractor.selectorCandidates).toContain('input[name="html-embed-medium"]');
|
||||||
|
});
|
||||||
|
|
||||||
it('selector fallback order: generic file input before provider-specific', () => {
|
it('selector fallback order: generic file input before provider-specific', () => {
|
||||||
for (const [, recipe] of Object.entries(MEDIA_UPLOAD_RECIPES)) {
|
for (const [, recipe] of Object.entries(MEDIA_UPLOAD_RECIPES)) {
|
||||||
const first = recipe.fileInputSelectorCandidates[0];
|
const first = recipe.fileInputSelectorCandidates[0];
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ describe('direct-url', () => {
|
|||||||
describe('isDirectMediaUrl', () => {
|
describe('isDirectMediaUrl', () => {
|
||||||
it('returns true for image extensions', () => {
|
it('returns true for image extensions', () => {
|
||||||
expect(isDirectMediaUrl('https://example.com/photo.jpg')).toBe(true);
|
expect(isDirectMediaUrl('https://example.com/photo.jpg')).toBe(true);
|
||||||
|
expect(isDirectMediaUrl('https://i.ibb.co/example/photo.jpg')).toBe(true);
|
||||||
expect(isDirectMediaUrl('https://example.com/photo.jpeg')).toBe(true);
|
expect(isDirectMediaUrl('https://example.com/photo.jpeg')).toBe(true);
|
||||||
expect(isDirectMediaUrl('https://example.com/photo.png')).toBe(true);
|
expect(isDirectMediaUrl('https://example.com/photo.png')).toBe(true);
|
||||||
expect(isDirectMediaUrl('https://example.com/photo.gif')).toBe(true);
|
expect(isDirectMediaUrl('https://example.com/photo.gif')).toBe(true);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { orchestrateElectronUpload } from '../upload-orchestrator';
|
import { orchestrateElectronUpload } from '../upload-orchestrator';
|
||||||
import { uploadToCatbox } from '../../utils/catbox-utils';
|
import { uploadToCatbox } from '../../utils/catbox-utils';
|
||||||
|
import type { ProviderId } from '../types';
|
||||||
|
|
||||||
vi.mock('../../utils/catbox-utils', () => ({
|
vi.mock('../../utils/catbox-utils', () => ({
|
||||||
uploadToCatbox: vi.fn(),
|
uploadToCatbox: vi.fn(),
|
||||||
@@ -11,7 +12,7 @@ function createElectronApiMock() {
|
|||||||
isElectron: true,
|
isElectron: true,
|
||||||
copyToClipboard: vi.fn(async () => ({ success: true })),
|
copyToClipboard: vi.fn(async () => ({ success: true })),
|
||||||
getPlatform: vi.fn(async () => ({ platform: 'darwin' as NodeJS.Platform, arch: 'x64', version: 'v20.0.0' })),
|
getPlatform: vi.fn(async () => ({ platform: 'darwin' as NodeJS.Platform, arch: 'x64', version: 'v20.0.0' })),
|
||||||
automateUploadMedia: vi.fn(async () => ({ url: 'https://i.imgur.com/abc.png', provider: 'imgur' as const })),
|
automateUploadMedia: vi.fn(async (options: { provider: ProviderId }) => ({ url: 'https://i.imgur.com/abc.png', provider: options.provider })),
|
||||||
getPathForFile: vi.fn((): string | null => '/tmp/image.png'),
|
getPathForFile: vi.fn((): string | null => '/tmp/image.png'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -48,6 +49,21 @@ describe('orchestrateElectronUpload', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('routes ImgBB through Electron automation', async () => {
|
||||||
|
const electronApi = createElectronApiMock();
|
||||||
|
electronApi.automateUploadMedia = vi.fn(async () => ({ url: 'https://i.ibb.co/example/image.png', provider: 'imgbb' as const }));
|
||||||
|
window.electronApi = electronApi;
|
||||||
|
|
||||||
|
const file = new File(['x'], 'x.png', { type: 'image/png' });
|
||||||
|
const url = await orchestrateElectronUpload(file, ['imgbb']);
|
||||||
|
|
||||||
|
expect(url).toBe('https://i.ibb.co/example/image.png');
|
||||||
|
expect(electronApi.automateUploadMedia).toHaveBeenCalledWith({
|
||||||
|
provider: 'imgbb',
|
||||||
|
filePath: '/tmp/image.png',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('fails with provider attempt details if no file path can be resolved', async () => {
|
it('fails with provider attempt details if no file path can be resolved', async () => {
|
||||||
const electronApi = createElectronApiMock();
|
const electronApi = createElectronApiMock();
|
||||||
electronApi.getPathForFile = vi.fn((): string | null => null);
|
electronApi.getPathForFile = vi.fn((): string | null => null);
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ interface ProviderDefinition {
|
|||||||
id: ProviderId;
|
id: ProviderId;
|
||||||
label: string;
|
label: string;
|
||||||
homepageUrl: string;
|
homepageUrl: string;
|
||||||
|
/** Image URLs used to detect whether this provider is reachable from the user's network. */
|
||||||
|
availabilityProbeUrls: readonly string[];
|
||||||
/** Runtimes where automated upload is supported (non-web = no interactive fallback) */
|
/** Runtimes where automated upload is supported (non-web = no interactive fallback) */
|
||||||
supportedRuntimes: readonly MediaHostingRuntime[];
|
supportedRuntimes: readonly MediaHostingRuntime[];
|
||||||
}
|
}
|
||||||
@@ -14,12 +16,21 @@ export const MEDIA_HOSTING_PROVIDERS: readonly ProviderDefinition[] = [
|
|||||||
id: 'catbox',
|
id: 'catbox',
|
||||||
label: 'Catbox',
|
label: 'Catbox',
|
||||||
homepageUrl: 'https://catbox.moe',
|
homepageUrl: 'https://catbox.moe',
|
||||||
|
availabilityProbeUrls: ['https://catbox.moe/pictures/logo.png', 'https://files.catbox.moe/8ten4y.png'],
|
||||||
supportedRuntimes: ['web', 'electron', 'android'],
|
supportedRuntimes: ['web', 'electron', 'android'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'imgur',
|
id: 'imgur',
|
||||||
label: 'Imgur',
|
label: 'Imgur',
|
||||||
homepageUrl: 'https://imgur.com',
|
homepageUrl: 'https://imgur.com',
|
||||||
|
availabilityProbeUrls: ['https://s.imgur.com/images/favicon-32x32.png', 'https://i.imgur.com/YpB7qfa.jpg'],
|
||||||
|
supportedRuntimes: ['electron'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'imgbb',
|
||||||
|
label: 'ImgBB',
|
||||||
|
homepageUrl: 'https://imgbb.com',
|
||||||
|
availabilityProbeUrls: ['https://simgbb.com/images/logo.png', 'https://i.ibb.co/7Jsq00V5/spoiler.png'],
|
||||||
supportedRuntimes: ['electron', 'android'],
|
supportedRuntimes: ['electron', 'android'],
|
||||||
},
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/** Supported media hosting provider identifiers */
|
/** Supported media hosting provider identifiers */
|
||||||
export type ProviderId = 'catbox' | 'imgur';
|
export type ProviderId = 'catbox' | 'imgur' | 'imgbb';
|
||||||
|
|
||||||
/** User-facing upload mode */
|
/** User-facing upload mode */
|
||||||
export type UploadMode = 'random' | 'preferred' | 'none';
|
export type UploadMode = 'random' | 'preferred' | 'none';
|
||||||
|
|||||||
@@ -58,11 +58,11 @@ function resolveElectronFilePath(file: File): string | null {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Uploads a file via a single provider. Catbox uses the web API;
|
* Uploads a file via a single provider. Catbox uses the web API;
|
||||||
* imgur uses Electron automation when available.
|
* imgur/imgbb use Electron automation when available.
|
||||||
*/
|
*/
|
||||||
async function uploadViaProvider(provider: ProviderId, file: File): Promise<string> {
|
async function uploadViaProvider(provider: ProviderId, file: File): Promise<string> {
|
||||||
if (provider === 'catbox') return uploadToCatbox(file);
|
if (provider === 'catbox') return uploadToCatbox(file);
|
||||||
if (provider === 'imgur') {
|
if (provider === 'imgur' || provider === 'imgbb') {
|
||||||
const fn = typeof window !== 'undefined' && window.electronApi?.automateUploadMedia;
|
const fn = typeof window !== 'undefined' && window.electronApi?.automateUploadMedia;
|
||||||
if (fn) {
|
if (fn) {
|
||||||
const filePath = resolveElectronFilePath(file);
|
const filePath = resolveElectronFilePath(file);
|
||||||
|
|||||||
@@ -19,14 +19,23 @@ describe('useMediaHostingStore', () => {
|
|||||||
|
|
||||||
it('exports MEDIA_HOSTING_PROVIDERS with ids, labels, homepage URLs, runtime metadata', () => {
|
it('exports MEDIA_HOSTING_PROVIDERS with ids, labels, homepage URLs, runtime metadata', () => {
|
||||||
expect(MEDIA_HOSTING_PROVIDERS).toBeDefined();
|
expect(MEDIA_HOSTING_PROVIDERS).toBeDefined();
|
||||||
expect(MEDIA_HOSTING_PROVIDERS.length).toBe(2);
|
expect(MEDIA_HOSTING_PROVIDERS.length).toBe(3);
|
||||||
const catbox = MEDIA_HOSTING_PROVIDERS.find((p) => p.id === 'catbox');
|
const catbox = MEDIA_HOSTING_PROVIDERS.find((p) => p.id === 'catbox');
|
||||||
expect(catbox).toEqual({
|
expect(catbox).toEqual({
|
||||||
id: 'catbox',
|
id: 'catbox',
|
||||||
label: 'Catbox',
|
label: 'Catbox',
|
||||||
homepageUrl: 'https://catbox.moe',
|
homepageUrl: 'https://catbox.moe',
|
||||||
|
availabilityProbeUrls: ['https://catbox.moe/pictures/logo.png', 'https://files.catbox.moe/8ten4y.png'],
|
||||||
supportedRuntimes: ['web', 'electron', 'android'],
|
supportedRuntimes: ['web', 'electron', 'android'],
|
||||||
});
|
});
|
||||||
|
const imgbb = MEDIA_HOSTING_PROVIDERS.find((p) => p.id === 'imgbb');
|
||||||
|
expect(imgbb).toEqual({
|
||||||
|
id: 'imgbb',
|
||||||
|
label: 'ImgBB',
|
||||||
|
homepageUrl: 'https://imgbb.com',
|
||||||
|
availabilityProbeUrls: ['https://simgbb.com/images/logo.png', 'https://i.ibb.co/7Jsq00V5/spoiler.png'],
|
||||||
|
supportedRuntimes: ['electron', 'android'],
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('defaults uploadMode to random and preferredProvider to catbox', () => {
|
it('defaults uploadMode to random and preferredProvider to catbox', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user