feat: ajout de la fonctionnalité de test de configuration de scraper, incluant la mise à jour de l'API pour tester les configurations en temps réel, la gestion des erreurs détaillées et l'intégration des tests unitaires pour valider le bon fonctionnement de cette nouvelle fonctionnalité.

This commit is contained in:
ext.jeremy.guillot@maxicoffee.domains
2025-07-06 17:01:04 +02:00
parent ee2a9b3750
commit cbb62989d4
11 changed files with 1751 additions and 101 deletions

View File

@@ -81,4 +81,24 @@ export class ApiContentSourceRepository {
throw new Error(error.response?.data?.message || 'Erreur lors de l\'import des sources');
}
}
/**
* Teste une configuration de scraper
*/
async testConfiguration(configuration) {
try {
const response = await this.apiClient.post('/scraping/test-configuration', configuration);
return response.data;
} catch (error) {
// Gestion spécifique des erreurs de validation
if (error.response?.status === 422) {
const validationErrors = error.response.data?.violations || [];
const errorMessage = validationErrors.map(violation =>
`${violation.propertyPath}: ${violation.message}`
).join(', ') || 'Erreur de validation';
throw new Error(errorMessage);
}
throw new Error(error.response?.data?.message || 'Erreur lors du test de la configuration');
}
}
}

View File

@@ -152,12 +152,21 @@
<input
id="testChapterNumber"
v-model="testData.chapterNumber"
type="text"
type="number"
step="0.1"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="1" />
</div>
</div>
<!-- Preview de l'URL qui sera testée -->
<div v-if="generatedTestUrl" class="mb-4 p-3 bg-blue-50 dark:bg-blue-900 border border-blue-200 dark:border-blue-700 rounded-md">
<div class="text-sm text-blue-800 dark:text-blue-200">
<strong>URL qui sera testée :</strong>
<div class="mt-1 font-mono text-xs break-all">{{ generatedTestUrl }}</div>
</div>
</div>
<button
type="button"
@click="testConfiguration"
@@ -206,7 +215,7 @@ const form = ref({
chapterUrlFormat: '',
nextPageSelector: '',
chapterSelector: '',
scrapingType: 'HTML',
scrapingType: 'html',
token: ''
});
@@ -224,6 +233,15 @@ const canTest = computed(() => {
testData.value.chapterNumber;
});
const generatedTestUrl = computed(() => {
if (!form.value.chapterUrlFormat || !testData.value.mangaSlug || !testData.value.chapterNumber) {
return '';
}
return form.value.chapterUrlFormat
.replace('{slug}', testData.value.mangaSlug)
.replace('{chapterNumber}', testData.value.chapterNumber);
});
// Initialize form with source data if editing, clear if creating new
watch(() => props.source, (newSource) => {
if (newSource) {
@@ -233,7 +251,7 @@ watch(() => props.source, (newSource) => {
chapterUrlFormat: newSource.chapterUrlFormat || '',
nextPageSelector: newSource.nextPageSelector || '',
chapterSelector: newSource.chapterSelector || '',
scrapingType: newSource.scrapingType || 'HTML',
scrapingType: (newSource.scrapingType || 'html').toLowerCase(),
token: newSource.token || ''
};
} else {
@@ -244,7 +262,7 @@ watch(() => props.source, (newSource) => {
chapterUrlFormat: '',
nextPageSelector: '',
chapterSelector: '',
scrapingType: 'HTML',
scrapingType: 'html',
token: ''
};
}
@@ -259,7 +277,10 @@ const testConfiguration = async () => {
try {
await emit('test', {
configuration: { ...form.value },
testData: { ...testData.value }
testData: {
...testData.value,
testUrl: generatedTestUrl.value
}
});
} finally {
testing.value = false;

View File

@@ -38,7 +38,7 @@
<!-- Test Results Modal -->
<div v-if="showTestResults" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-2xl max-h-[80vh] overflow-hidden">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-4xl max-h-[80vh] overflow-hidden">
<div class="p-6 border-b border-gray-200 dark:border-gray-600">
<div class="flex justify-between items-center">
<h3 class="text-lg font-semibold">Résultats du test</h3>
@@ -51,50 +51,125 @@
</div>
<div class="p-6 overflow-y-auto">
<div v-if="testResults.success" class="space-y-4">
<!-- Loading state during test -->
<div v-if="testingConfiguration" class="flex items-center justify-center py-8">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mr-3"></div>
<span class="text-gray-600">Test en cours...</span>
</div>
<!-- Success Results -->
<div v-else-if="testResults.success" class="space-y-4">
<div class="flex items-center text-green-600 mb-4">
<CheckCircleIcon class="w-5 h-5 mr-2" />
<span class="font-medium">Test réussi !</span>
</div>
<div>
<h4 class="font-medium mb-2">URL testée:</h4>
<code class="block bg-gray-100 dark:bg-gray-700 p-2 rounded text-sm">
{{ testResults.testedUrl }}
</code>
<div class="bg-green-50 dark:bg-green-900 border border-green-200 dark:border-green-700 rounded-lg p-4">
<div class="grid grid-cols-2 gap-4 text-sm">
<div>
<span class="font-medium text-green-800 dark:text-green-200">URL testée:</span>
<div class="text-green-700 dark:text-green-300 break-all">{{ testResults.testedUrl }}</div>
</div>
<div>
<span class="font-medium text-green-800 dark:text-green-200">Type de scraping:</span>
<div class="text-green-700 dark:text-green-300">{{ testResults.scrapingType }}</div>
</div>
<div>
<span class="font-medium text-green-800 dark:text-green-200">Images trouvées:</span>
<div class="text-green-700 dark:text-green-300">{{ testResults.totalImages || testResults.imageUrls?.length || 0 }}</div>
</div>
</div>
</div>
<div v-if="testResults.images && testResults.images.length > 0">
<h4 class="font-medium mb-2">Images trouvées ({{ testResults.images.length }}):</h4>
<div class="grid grid-cols-2 gap-2 max-h-64 overflow-y-auto">
<img
v-for="(image, index) in testResults.images.slice(0, 6)"
<div v-if="testResults.imageUrls && testResults.imageUrls.length > 0">
<h4 class="font-medium mb-3">Aperçu des images trouvées :</h4>
<div class="grid grid-cols-3 gap-3 max-h-96 overflow-y-auto">
<div
v-for="(imageUrl, index) in testResults.imageUrls.slice(0, 12)"
:key="index"
:src="image"
:alt="`Image ${index + 1}`"
class="w-full h-32 object-cover rounded border"
@error="handleImageError" />
class="relative group">
<img
:src="imageUrl"
:alt="`Image ${index + 1}`"
class="w-full h-32 object-cover rounded border border-gray-200 dark:border-gray-600"
@error="handleImageError"
@load="handleImageLoad" />
<div class="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-30 transition-opacity rounded flex items-center justify-center">
<span class="text-white opacity-0 group-hover:opacity-100 text-sm font-medium">
Page {{ index + 1 }}
</span>
</div>
</div>
</div>
<p v-if="testResults.images.length > 6" class="text-sm text-gray-500 mt-2">
Et {{ testResults.images.length - 6 }} autres images...
<p v-if="testResults.imageUrls.length > 12" class="text-sm text-gray-500 mt-3 text-center">
Et {{ testResults.imageUrls.length - 12 }} autres images...
</p>
</div>
<div v-else class="bg-yellow-50 dark:bg-yellow-900 border border-yellow-200 dark:border-yellow-700 rounded-lg p-4">
<div class="flex items-center">
<ExclamationTriangleIcon class="w-5 h-5 text-yellow-400 mr-2" />
<p class="text-yellow-800 dark:text-yellow-200">
Le test s'est déroulé sans erreur mais aucune image n'a été trouvée.
Vérifiez vos sélecteurs CSS.
</p>
</div>
</div>
</div>
<!-- Error Results -->
<div v-else class="space-y-4">
<div class="flex items-center text-red-600 mb-4">
<XCircleIcon class="w-5 h-5 mr-2" />
<span class="font-medium">Test échoué</span>
</div>
<div>
<h4 class="font-medium mb-2">Erreur:</h4>
<div class="bg-red-50 dark:bg-red-900 border border-red-200 dark:border-red-700 rounded p-3">
<code class="text-sm text-red-800 dark:text-red-200">
{{ testResults.error }}
</code>
<div class="bg-red-50 dark:bg-red-900 border border-red-200 dark:border-red-700 rounded-lg p-4 mb-4">
<div class="text-sm text-red-800 dark:text-red-200">
<div><strong>URL testée:</strong> {{ testResults.testedUrl || 'N/A' }}</div>
<div><strong>Type de scraping:</strong> {{ testResults.scrapingType || 'N/A' }}</div>
</div>
</div>
<!-- Detailed Errors -->
<div v-if="testResults.errors && testResults.errors.length > 0" class="space-y-3">
<h4 class="font-medium text-red-800 dark:text-red-200">Erreurs détaillées :</h4>
<div
v-for="(error, index) in testResults.errors"
:key="index"
class="bg-red-100 dark:bg-red-800 border-l-4 border-red-400 p-4 rounded">
<div class="flex items-start">
<div class="flex-shrink-0">
<ExclamationTriangleIcon class="w-5 h-5 text-red-400" />
</div>
<div class="ml-3 flex-1">
<div class="flex items-center mb-1">
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-red-200 text-red-800 dark:bg-red-700 dark:text-red-200 mr-2">
{{ formatErrorType(error.type) }}
</span>
<span class="text-sm font-medium text-red-800 dark:text-red-200">
{{ error.field }}
</span>
</div>
<p class="text-sm text-red-700 dark:text-red-300 mb-2">
{{ error.message }}
</p>
<div class="bg-red-50 dark:bg-red-900 rounded p-2">
<p class="text-xs text-red-600 dark:text-red-400">
<strong>Suggestion :</strong> {{ error.suggestion }}
</p>
</div>
</div>
</div>
</div>
</div>
<!-- Generic Error -->
<div v-else-if="testResults.error" class="bg-red-50 dark:bg-red-900 border border-red-200 dark:border-red-700 rounded p-3">
<code class="text-sm text-red-800 dark:text-red-200">
{{ testResults.error }}
</code>
</div>
</div>
</div>
</div>
@@ -121,11 +196,13 @@ import { computed, onMounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import Toolbar from '../../../../shared/components/ui/Toolbar.vue';
import { useContentSourceStore } from '../../application/store/contentSourceStore';
import { ApiContentSourceRepository } from '../../infrastructure/api/apiContentSourceRepository';
import ContentSourceForm from '../components/ContentSourceForm.vue';
const route = useRoute();
const router = useRouter();
const contentSourceStore = useContentSourceStore();
const contentSourceRepository = new ApiContentSourceRepository();
const {
currentSource,
@@ -139,6 +216,7 @@ const {
const showTestResults = ref(false);
const showSuccessMessage = ref(false);
const testResults = ref({});
const testingConfiguration = ref(false);
const isEditing = computed(() => !!route.params.id);
@@ -194,35 +272,61 @@ const handleSubmit = async (formData) => {
};
const handleTest = async ({ configuration, testData }) => {
try {
// Simulate test API call - You'll need to implement this endpoint
const testUrl = configuration.chapterUrlFormat
.replace('{slug}', testData.mangaSlug)
.replace('{chapterNumber}', testData.chapterNumber);
testingConfiguration.value = true;
showTestResults.value = true;
testResults.value = {};
// Mock test results for now
testResults.value = {
success: true,
testedUrl: testUrl,
images: [
'https://via.placeholder.com/400x600/008000/FFFFFF?text=Page+1',
'https://via.placeholder.com/400x600/FF0000/FFFFFF?text=Page+2',
'https://via.placeholder.com/400x600/0000FF/FFFFFF?text=Page+3',
'https://via.placeholder.com/400x600/FFA500/FFFFFF?text=Page+4'
]
try {
// Préparer les données selon le format de l'API
const testConfiguration = {
baseUrl: configuration.baseUrl,
chapterUrlFormat: configuration.chapterUrlFormat,
scrapingType: configuration.scrapingType?.toLowerCase() || 'html',
testUrl: testData.testUrl,
mangaSlug: testData.mangaSlug,
chapterNumber: parseFloat(testData.chapterNumber),
imageSelector: configuration.imageSelector || null,
nextPageSelector: configuration.nextPageSelector || null,
chapterSelector: configuration.chapterSelector || null
};
showTestResults.value = true;
console.log('Envoi de la configuration de test:', testConfiguration);
const response = await contentSourceRepository.testConfiguration(testConfiguration);
testResults.value = response;
console.log('Résultats du test:', response);
} catch (error) {
console.error('Erreur lors du test:', error);
testResults.value = {
success: false,
error: error.message
error: error.message,
testedUrl: testData.testUrl,
scrapingType: configuration.scrapingType?.toLowerCase() || 'html',
errors: []
};
showTestResults.value = true;
} finally {
testingConfiguration.value = false;
}
};
const handleImageError = (event) => {
// Hide broken images
event.target.style.display = 'none';
};
const handleImageLoad = (event) => {
// Ensure loaded images are visible
event.target.style.display = 'block';
};
const formatErrorType = (type) => {
const typeMap = {
'selector_error': 'Erreur sélecteur',
'url_error': 'Erreur URL',
'general_error': 'Erreur générale'
};
return typeMap[type] || type;
};
</script>