Test réussi !
-
-
URL testée:
-
- {{ testResults.testedUrl }}
-
+
+
+
+
URL testée:
+
{{ testResults.testedUrl }}
+
+
+
Type de scraping:
+
{{ testResults.scrapingType }}
+
+
+
Images trouvées:
+
{{ testResults.totalImages || testResults.imageUrls?.length || 0 }}
+
+
-
-
Images trouvées ({{ testResults.images.length }}):
-
-
![]()
+
Aperçu des images trouvées :
+
+
+ class="relative group">
+
![]()
+
+
+ Page {{ index + 1 }}
+
+
+
-
- Et {{ testResults.images.length - 6 }} autres images...
+
+ Et {{ testResults.imageUrls.length - 12 }} autres images...
+
+
+
+
+
+ Le test s'est déroulé sans erreur mais aucune image n'a été trouvée.
+ Vérifiez vos sélecteurs CSS.
+
+
+
+
Test échoué
-
-
Erreur:
-
-
- {{ testResults.error }}
-
+
+
+
URL testée: {{ testResults.testedUrl || 'N/A' }}
+
Type de scraping: {{ testResults.scrapingType || 'N/A' }}
+
+
+
+
Erreurs détaillées :
+
+
+
+
+
+
+
+
+ {{ formatErrorType(error.type) }}
+
+
+ {{ error.field }}
+
+
+
+ {{ error.message }}
+
+
+
+ Suggestion : {{ error.suggestion }}
+
+
+
+
+
+
+
+
+
+
+ {{ testResults.error }}
+
+
@@ -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;
+};
diff --git a/public/api-docs.json b/public/api-docs.json
index b5dc0fb..eba4a82 100644
--- a/public/api-docs.json
+++ b/public/api-docs.json
@@ -924,66 +924,36 @@
"post": {
"operationId": "api_mangachaptersfetch_post",
"tags": [
- "Chapters"
+ "Mangadex"
],
"responses": {
"202": {
- "description": "Chapters resource created",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Chapters"
- }
- },
- "application/ld+json": {
- "schema": {
- "$ref": "#/components/schemas/Chapters.jsonld"
- }
- },
- "text/html": {
- "schema": {
- "$ref": "#/components/schemas/Chapters"
- }
- },
- "application/hal+json": {
- "schema": {
- "$ref": "#/components/schemas/Chapters.jsonhal"
- }
- }
- },
- "links": {}
- },
- "400": {
- "description": "Invalid input"
+ "description": "Demande de r\u00e9cup\u00e9ration accept\u00e9e et mise en file d'attente"
},
"422": {
- "description": "Unprocessable entity"
+ "description": "Donn\u00e9es de validation invalides"
}
},
- "summary": "Creates a Chapters resource.",
- "description": "Creates a Chapters resource.",
+ "summary": "R\u00e9cup\u00e9rer les chapitres d'un manga",
+ "description": "Lance le processus de r\u00e9cup\u00e9ration des chapitres depuis la source externe pour un manga donn\u00e9",
"parameters": [],
"requestBody": {
- "description": "The new Chapters resource",
+ "description": "Donn\u00e9es requises pour r\u00e9cup\u00e9rer les chapitres",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/Chapters"
- }
- },
- "application/ld+json": {
- "schema": {
- "$ref": "#/components/schemas/Chapters.jsonld"
- }
- },
- "text/html": {
- "schema": {
- "$ref": "#/components/schemas/Chapters"
- }
- },
- "application/hal+json": {
- "schema": {
- "$ref": "#/components/schemas/Chapters.jsonhal"
+ "type": "object",
+ "properties": {
+ "mangaId": {
+ "type": "string",
+ "format": "uuid",
+ "description": "L'identifiant unique du manga",
+ "example": "123e4567-e89b-12d3-a456-426614174000"
+ }
+ },
+ "required": [
+ "mangaId"
+ ]
}
}
},
@@ -1586,30 +1556,30 @@
"post": {
"operationId": "api_mangascreate-from-mangadex_post",
"tags": [
- "Manga"
+ "Mangadex"
],
"responses": {
"201": {
- "description": "Manga resource created",
+ "description": "Mangadex resource created",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/Manga"
+ "$ref": "#/components/schemas/Mangadex"
}
},
"application/ld+json": {
"schema": {
- "$ref": "#/components/schemas/Manga.jsonld"
+ "$ref": "#/components/schemas/Mangadex.jsonld"
}
},
"text/html": {
"schema": {
- "$ref": "#/components/schemas/Manga"
+ "$ref": "#/components/schemas/Mangadex"
}
},
"application/hal+json": {
"schema": {
- "$ref": "#/components/schemas/Manga.jsonhal"
+ "$ref": "#/components/schemas/Mangadex.jsonhal"
}
}
},
@@ -2534,6 +2504,299 @@
"deprecated": false
},
"parameters": []
+ },
+ "/api/scraping/test-configuration": {
+ "post": {
+ "operationId": "api_scrapingtest-configuration_post",
+ "tags": [
+ "Scraping",
+ "Configuration"
+ ],
+ "responses": {
+ "200": {
+ "description": "Test de configuration r\u00e9ussi",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "success": {
+ "type": "boolean",
+ "description": "Indique si le test a r\u00e9ussi"
+ },
+ "imageUrls": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "format": "uri"
+ },
+ "description": "Liste des URLs d'images trouv\u00e9es"
+ },
+ "totalImages": {
+ "type": "integer",
+ "description": "Nombre total d'images trouv\u00e9es"
+ },
+ "testedUrl": {
+ "type": "string",
+ "format": "uri",
+ "description": "URL qui a \u00e9t\u00e9 test\u00e9e"
+ },
+ "scrapingType": {
+ "type": "string",
+ "description": "Type de scraping utilis\u00e9"
+ },
+ "errors": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "Type d'erreur (selector_error, url_error, general_error)"
+ },
+ "field": {
+ "type": "string",
+ "description": "Champ concern\u00e9 par l'erreur"
+ },
+ "message": {
+ "type": "string",
+ "description": "Message d'erreur d\u00e9taill\u00e9"
+ },
+ "suggestion": {
+ "type": "string",
+ "description": "Suggestion pour corriger l'erreur"
+ }
+ }
+ },
+ "description": "Liste des erreurs d\u00e9taill\u00e9es (vide en cas de succ\u00e8s)"
+ }
+ }
+ },
+ "examples": {
+ "succes": {
+ "summary": "Test r\u00e9ussi",
+ "value": {
+ "success": true,
+ "imageUrls": [
+ "https://mangasite.example.com/images/chapter1/page1.jpg",
+ "https://mangasite.example.com/images/chapter1/page2.jpg",
+ "https://mangasite.example.com/images/chapter1/page3.jpg"
+ ],
+ "totalImages": 3,
+ "testedUrl": "https://mangasite.example.com/manga/one-piece/chapter/1",
+ "scrapingType": "html",
+ "errors": []
+ }
+ },
+ "echec_selecteur": {
+ "summary": "\u00c9chec - S\u00e9lecteur invalide",
+ "value": {
+ "success": false,
+ "imageUrls": [],
+ "totalImages": 0,
+ "testedUrl": "https://mangasite.example.com/manga/one-piece/chapter/1",
+ "scrapingType": "html",
+ "errors": [
+ {
+ "type": "selector_error",
+ "field": "imageSelector",
+ "message": "Le s\u00e9lecteur d'image '.invalid-selector' ne trouve aucun \u00e9l\u00e9ment sur la page",
+ "suggestion": "V\u00e9rifiez que le s\u00e9lecteur CSS est correct et qu'il correspond aux \u00e9l\u00e9ments d'image sur la page"
+ }
+ ]
+ }
+ },
+ "echec_url": {
+ "summary": "\u00c9chec - URL inaccessible",
+ "value": {
+ "success": false,
+ "imageUrls": [],
+ "totalImages": 0,
+ "testedUrl": "https://invalid-site.example.com/chapter/1",
+ "scrapingType": "html",
+ "errors": [
+ {
+ "type": "url_error",
+ "field": "testUrl",
+ "message": "Impossible d'acc\u00e9der \u00e0 l'URL de test: https://invalid-site.example.com/chapter/1",
+ "suggestion": "V\u00e9rifiez que l'URL est correcte et accessible, et que le chapitre existe"
+ }
+ ]
+ }
+ }
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Erreur de validation des donn\u00e9es d'entr\u00e9e",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "example": "https://tools.ietf.org/html/rfc2616#section-10"
+ },
+ "title": {
+ "type": "string",
+ "example": "An error occurred"
+ },
+ "detail": {
+ "type": "string",
+ "example": "baseUrl: L'URL de base doit \u00eatre une URL valide"
+ },
+ "violations": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "propertyPath": {
+ "type": "string",
+ "description": "Champ contenant l'erreur"
+ },
+ "message": {
+ "type": "string",
+ "description": "Message d'erreur de validation"
+ }
+ }
+ }
+ }
+ }
+ },
+ "example": {
+ "type": "https://symfony.com/errors/validation",
+ "title": "Validation Failed",
+ "detail": "baseUrl: L'URL de base doit \u00eatre une URL valide",
+ "violations": [
+ {
+ "propertyPath": "baseUrl",
+ "message": "L'URL de base doit \u00eatre une URL valide"
+ }
+ ]
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid input"
+ }
+ },
+ "summary": "Tester une configuration de scraper",
+ "description": "Teste une configuration de scraper en temps r\u00e9el sans l'enregistrer dans la base de donn\u00e9es. Cette API permet de valider les param\u00e8tres de scraping et de voir imm\u00e9diatement les URLs d'images qui seraient extraites.",
+ "parameters": [],
+ "requestBody": {
+ "description": "Configuration du scraper \u00e0 tester",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "baseUrl": {
+ "type": "string",
+ "format": "uri",
+ "description": "URL de base du site web source",
+ "example": "https://mangasite.example.com"
+ },
+ "chapterUrlFormat": {
+ "type": "string",
+ "description": "Format d'URL pour acc\u00e9der aux chapitres avec placeholders {slug} et {chapter}",
+ "example": "https://mangasite.example.com/manga/{slug}/chapter/{chapter}"
+ },
+ "scrapingType": {
+ "type": "string",
+ "enum": [
+ "html",
+ "javascript"
+ ],
+ "description": "Type de scraping \u00e0 utiliser (html pour les sites statiques, javascript pour les sites dynamiques)",
+ "example": "html"
+ },
+ "testUrl": {
+ "type": "string",
+ "format": "uri",
+ "description": "URL compl\u00e8te d'un chapitre existant \u00e0 utiliser pour le test",
+ "example": "https://mangasite.example.com/manga/one-piece/chapter/1"
+ },
+ "mangaSlug": {
+ "type": "string",
+ "description": "Slug du manga utilis\u00e9 dans les URLs (sera utilis\u00e9 pour construire les URLs futures)",
+ "example": "one-piece"
+ },
+ "chapterNumber": {
+ "type": "number",
+ "minimum": 0,
+ "description": "Num\u00e9ro du chapitre \u00e0 tester",
+ "example": 1
+ },
+ "imageSelector": {
+ "type": "string",
+ "nullable": true,
+ "description": "S\u00e9lecteur CSS pour identifier les images dans la page",
+ "example": "img.manga-page, .chapter-image img"
+ },
+ "nextPageSelector": {
+ "type": "string",
+ "nullable": true,
+ "description": "S\u00e9lecteur CSS pour le lien vers la page suivante (pour les lecteurs horizontaux)",
+ "example": "a.next-page, .navigation .next"
+ },
+ "chapterSelector": {
+ "type": "string",
+ "nullable": true,
+ "description": "S\u00e9lecteur CSS pour identifier la zone contenant le chapitre",
+ "example": ".chapter-content, #manga-reader"
+ }
+ },
+ "required": [
+ "baseUrl",
+ "chapterUrlFormat",
+ "scrapingType",
+ "testUrl",
+ "mangaSlug",
+ "chapterNumber"
+ ]
+ },
+ "examples": {
+ "lecteur_vertical": {
+ "summary": "Configuration pour un lecteur vertical",
+ "description": "Exemple de configuration pour un site avec toutes les images sur une seule page",
+ "value": {
+ "baseUrl": "https://mangasite.example.com",
+ "chapterUrlFormat": "https://mangasite.example.com/manga/{slug}/chapter/{chapter}",
+ "scrapingType": "html",
+ "testUrl": "https://mangasite.example.com/manga/one-piece/chapter/1",
+ "mangaSlug": "one-piece",
+ "chapterNumber": 1,
+ "imageSelector": "img.manga-page",
+ "nextPageSelector": null,
+ "chapterSelector": ".chapter-content"
+ }
+ },
+ "lecteur_horizontal": {
+ "summary": "Configuration pour un lecteur horizontal",
+ "description": "Exemple de configuration pour un site avec navigation page par page",
+ "value": {
+ "baseUrl": "https://mangasite.example.com",
+ "chapterUrlFormat": "https://mangasite.example.com/read/{slug}/{chapter}/1",
+ "scrapingType": "html",
+ "testUrl": "https://mangasite.example.com/read/one-piece/1/1",
+ "mangaSlug": "one-piece",
+ "chapterNumber": 1,
+ "imageSelector": "#manga-image",
+ "nextPageSelector": "a.next-page",
+ "chapterSelector": ".reader-container"
+ }
+ }
+ }
+ }
+ },
+ "required": false
+ },
+ "deprecated": false
+ },
+ "parameters": []
}
},
"components": {
@@ -3458,6 +3721,12 @@
"slug": {
"type": "string"
},
+ "alternativeSlugs": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
"description": {
"type": "string"
},
@@ -3530,6 +3799,12 @@
"slug": {
"type": "string"
},
+ "alternativeSlugs": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
"description": {
"type": "string"
},
@@ -3623,6 +3898,12 @@
"slug": {
"type": "string"
},
+ "alternativeSlugs": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
"description": {
"type": "string"
},
@@ -4348,6 +4629,19 @@
}
}
},
+ "Mangadex": {
+ "type": "object",
+ "description": "",
+ "deprecated": false,
+ "required": [
+ "externalId"
+ ],
+ "properties": {
+ "externalId": {
+ "type": "string"
+ }
+ }
+ },
"Mangadex.MangaSearchCollection": {
"type": "object",
"description": "",
@@ -4436,6 +4730,81 @@
}
}
},
+ "Mangadex.jsonhal": {
+ "type": "object",
+ "description": "",
+ "deprecated": false,
+ "required": [
+ "externalId"
+ ],
+ "properties": {
+ "_links": {
+ "type": "object",
+ "properties": {
+ "self": {
+ "type": "object",
+ "properties": {
+ "href": {
+ "type": "string",
+ "format": "iri-reference"
+ }
+ }
+ }
+ }
+ },
+ "externalId": {
+ "type": "string"
+ }
+ }
+ },
+ "Mangadex.jsonld": {
+ "type": "object",
+ "description": "",
+ "deprecated": false,
+ "required": [
+ "externalId"
+ ],
+ "properties": {
+ "@context": {
+ "readOnly": true,
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "@vocab": {
+ "type": "string"
+ },
+ "hydra": {
+ "type": "string",
+ "enum": [
+ "http://www.w3.org/ns/hydra/core#"
+ ]
+ }
+ },
+ "required": [
+ "@vocab",
+ "hydra"
+ ],
+ "additionalProperties": true
+ }
+ ]
+ },
+ "@id": {
+ "readOnly": true,
+ "type": "string"
+ },
+ "@type": {
+ "readOnly": true,
+ "type": "string"
+ },
+ "externalId": {
+ "type": "string"
+ }
+ }
+ },
"Reader": {
"type": "object",
"description": "",
@@ -4624,6 +4993,256 @@
}
}
},
+ "Scraping.TestScraperConfigurationResource": {
+ "type": "object",
+ "description": "Teste une configuration de scraper sans l'enregistrer",
+ "deprecated": false,
+ "properties": {
+ "success": {
+ "type": "boolean"
+ },
+ "imageUrls": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "format": "uri"
+ }
+ },
+ "totalImages": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "testedUrl": {
+ "type": "string",
+ "format": "uri"
+ },
+ "scrapingType": {
+ "type": "string",
+ "enum": [
+ "html",
+ "javascript"
+ ]
+ },
+ "errors": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "selector_error",
+ "url_error",
+ "general_error"
+ ],
+ "description": "Type d'erreur rencontr\u00e9e"
+ },
+ "field": {
+ "type": "string",
+ "description": "Champ de configuration concern\u00e9 par l'erreur"
+ },
+ "message": {
+ "type": "string",
+ "description": "Message d'erreur d\u00e9taill\u00e9"
+ },
+ "suggestion": {
+ "type": "string",
+ "description": "Suggestion pour corriger l'erreur"
+ }
+ },
+ "required": [
+ "type",
+ "field",
+ "message",
+ "suggestion"
+ ]
+ }
+ }
+ }
+ },
+ "Scraping.TestScraperConfigurationResource.jsonhal": {
+ "type": "object",
+ "description": "Teste une configuration de scraper sans l'enregistrer",
+ "deprecated": false,
+ "properties": {
+ "_links": {
+ "type": "object",
+ "properties": {
+ "self": {
+ "type": "object",
+ "properties": {
+ "href": {
+ "type": "string",
+ "format": "iri-reference"
+ }
+ }
+ }
+ }
+ },
+ "success": {
+ "type": "boolean"
+ },
+ "imageUrls": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "format": "uri"
+ }
+ },
+ "totalImages": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "testedUrl": {
+ "type": "string",
+ "format": "uri"
+ },
+ "scrapingType": {
+ "type": "string",
+ "enum": [
+ "html",
+ "javascript"
+ ]
+ },
+ "errors": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "selector_error",
+ "url_error",
+ "general_error"
+ ],
+ "description": "Type d'erreur rencontr\u00e9e"
+ },
+ "field": {
+ "type": "string",
+ "description": "Champ de configuration concern\u00e9 par l'erreur"
+ },
+ "message": {
+ "type": "string",
+ "description": "Message d'erreur d\u00e9taill\u00e9"
+ },
+ "suggestion": {
+ "type": "string",
+ "description": "Suggestion pour corriger l'erreur"
+ }
+ },
+ "required": [
+ "type",
+ "field",
+ "message",
+ "suggestion"
+ ]
+ }
+ }
+ }
+ },
+ "Scraping.TestScraperConfigurationResource.jsonld": {
+ "type": "object",
+ "description": "Teste une configuration de scraper sans l'enregistrer",
+ "deprecated": false,
+ "properties": {
+ "@context": {
+ "readOnly": true,
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "@vocab": {
+ "type": "string"
+ },
+ "hydra": {
+ "type": "string",
+ "enum": [
+ "http://www.w3.org/ns/hydra/core#"
+ ]
+ }
+ },
+ "required": [
+ "@vocab",
+ "hydra"
+ ],
+ "additionalProperties": true
+ }
+ ]
+ },
+ "@id": {
+ "readOnly": true,
+ "type": "string"
+ },
+ "@type": {
+ "readOnly": true,
+ "type": "string"
+ },
+ "success": {
+ "type": "boolean"
+ },
+ "imageUrls": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "format": "uri"
+ }
+ },
+ "totalImages": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "testedUrl": {
+ "type": "string",
+ "format": "uri"
+ },
+ "scrapingType": {
+ "type": "string",
+ "enum": [
+ "html",
+ "javascript"
+ ]
+ },
+ "errors": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "selector_error",
+ "url_error",
+ "general_error"
+ ],
+ "description": "Type d'erreur rencontr\u00e9e"
+ },
+ "field": {
+ "type": "string",
+ "description": "Champ de configuration concern\u00e9 par l'erreur"
+ },
+ "message": {
+ "type": "string",
+ "description": "Message d'erreur d\u00e9taill\u00e9"
+ },
+ "suggestion": {
+ "type": "string",
+ "description": "Suggestion pour corriger l'erreur"
+ }
+ },
+ "required": [
+ "type",
+ "field",
+ "message",
+ "suggestion"
+ ]
+ }
+ }
+ }
+ },
"Scraping.jsonhal": {
"type": "object",
"description": "",
diff --git a/src/Domain/Scraping/Application/Command/TestScraperConfiguration.php b/src/Domain/Scraping/Application/Command/TestScraperConfiguration.php
new file mode 100644
index 0000000..4015a1c
--- /dev/null
+++ b/src/Domain/Scraping/Application/Command/TestScraperConfiguration.php
@@ -0,0 +1,18 @@
+ $command->imageSelector,
+ 'nextPageSelector' => $command->nextPageSelector,
+ 'chapterUrlFormat' => $command->chapterUrlFormat,
+ 'scrapingType' => $command->scrapingType,
+ 'chapterSelector' => $command->chapterSelector
+ ];
+
+ // Vérification que le scraper supporte le type de scraping
+ if (!$this->scraper->supports($command->scrapingType)) {
+ return TestScraperConfigurationResponse::failure(
+ $command->testUrl,
+ $command->scrapingType,
+ ["Type de scraping '{$command->scrapingType}' non supporté"]
+ );
+ }
+
+ // Création de la requête de scraping avec l'URL de test fournie directement
+ $scrapingRequest = new ScrapingRequest(
+ $command->scrapingType,
+ $command->testUrl,
+ $scrapingParameters
+ );
+
+ // Tentative de scraping
+ $scrapingResult = $this->scraper->scrape($scrapingRequest);
+
+ // Retour du succès avec les URLs trouvées
+ return TestScraperConfigurationResponse::success(
+ $scrapingResult->getImageUrls(),
+ $command->testUrl,
+ $command->scrapingType
+ );
+
+ } catch (\Exception $e) {
+ // Analyse de l'erreur pour fournir un message plus détaillé
+ $errors = $this->analyzeError($e, $command);
+
+ return TestScraperConfigurationResponse::failure(
+ $command->testUrl,
+ $command->scrapingType,
+ $errors
+ );
+ }
+ }
+
+ private function analyzeError(\Exception $e, TestScraperConfiguration $command): array
+ {
+ $errors = [];
+ $message = $e->getMessage();
+
+ // Erreurs liées aux sélecteurs
+ if (str_contains($message, 'imageSelector') || str_contains($message, 'filter')) {
+ $errors[] = [
+ 'type' => 'selector_error',
+ 'field' => 'imageSelector',
+ 'message' => "Le sélecteur d'image '{$command->imageSelector}' ne trouve aucun élément sur la page",
+ 'suggestion' => "Vérifiez que le sélecteur CSS est correct et qu'il correspond aux éléments d'image sur la page"
+ ];
+ }
+
+ // Erreurs liées à l'URL
+ if (str_contains($message, 'Failed to fetch HTML') || str_contains($message, 'Chapter Not Found')) {
+ $errors[] = [
+ 'type' => 'url_error',
+ 'field' => 'testUrl',
+ 'message' => "Impossible d'accéder à l'URL de test: {$command->testUrl}",
+ 'suggestion' => "Vérifiez que l'URL est correcte et accessible, et que le chapitre existe"
+ ];
+ }
+
+ // Erreurs liées aux sélecteurs de navigation
+ if (str_contains($message, 'nextPageSelector')) {
+ $errors[] = [
+ 'type' => 'selector_error',
+ 'field' => 'nextPageSelector',
+ 'message' => "Le sélecteur de page suivante '{$command->nextPageSelector}' pose problème",
+ 'suggestion' => "Vérifiez le sélecteur CSS pour la navigation entre les pages"
+ ];
+ }
+
+ // Erreur générique si aucune erreur spécifique n'est détectée
+ if (empty($errors)) {
+ $errors[] = [
+ 'type' => 'general_error',
+ 'field' => 'configuration',
+ 'message' => $message,
+ 'suggestion' => "Vérifiez l'ensemble de la configuration et l'accessibilité du site web"
+ ];
+ }
+
+ return $errors;
+ }
+}
diff --git a/src/Domain/Scraping/Application/Response/TestScraperConfigurationResponse.php b/src/Domain/Scraping/Application/Response/TestScraperConfigurationResponse.php
new file mode 100644
index 0000000..265d7ba
--- /dev/null
+++ b/src/Domain/Scraping/Application/Response/TestScraperConfigurationResponse.php
@@ -0,0 +1,39 @@
+ [
+ 'schema' => [
+ 'type' => 'object',
+ 'properties' => [
+ 'baseUrl' => [
+ 'type' => 'string',
+ 'format' => 'uri',
+ 'description' => 'URL de base du site web source',
+ 'example' => 'https://mangasite.example.com'
+ ],
+ 'chapterUrlFormat' => [
+ 'type' => 'string',
+ 'description' => 'Format d\'URL pour accéder aux chapitres avec placeholders {slug} et {chapter}',
+ 'example' => 'https://mangasite.example.com/manga/{slug}/chapter/{chapter}'
+ ],
+ 'scrapingType' => [
+ 'type' => 'string',
+ 'enum' => ['html', 'javascript'],
+ 'description' => 'Type de scraping à utiliser (html pour les sites statiques, javascript pour les sites dynamiques)',
+ 'example' => 'html'
+ ],
+ 'testUrl' => [
+ 'type' => 'string',
+ 'format' => 'uri',
+ 'description' => 'URL complète d\'un chapitre existant à utiliser pour le test',
+ 'example' => 'https://mangasite.example.com/manga/one-piece/chapter/1'
+ ],
+ 'mangaSlug' => [
+ 'type' => 'string',
+ 'description' => 'Slug du manga utilisé dans les URLs (sera utilisé pour construire les URLs futures)',
+ 'example' => 'one-piece'
+ ],
+ 'chapterNumber' => [
+ 'type' => 'number',
+ 'minimum' => 0,
+ 'description' => 'Numéro du chapitre à tester',
+ 'example' => 1.0
+ ],
+ 'imageSelector' => [
+ 'type' => 'string',
+ 'nullable' => true,
+ 'description' => 'Sélecteur CSS pour identifier les images dans la page',
+ 'example' => 'img.manga-page, .chapter-image img'
+ ],
+ 'nextPageSelector' => [
+ 'type' => 'string',
+ 'nullable' => true,
+ 'description' => 'Sélecteur CSS pour le lien vers la page suivante (pour les lecteurs horizontaux)',
+ 'example' => 'a.next-page, .navigation .next'
+ ],
+ 'chapterSelector' => [
+ 'type' => 'string',
+ 'nullable' => true,
+ 'description' => 'Sélecteur CSS pour identifier la zone contenant le chapitre',
+ 'example' => '.chapter-content, #manga-reader'
+ ]
+ ],
+ 'required' => ['baseUrl', 'chapterUrlFormat', 'scrapingType', 'testUrl', 'mangaSlug', 'chapterNumber']
+ ],
+ 'examples' => [
+ 'lecteur_vertical' => [
+ 'summary' => 'Configuration pour un lecteur vertical',
+ 'description' => 'Exemple de configuration pour un site avec toutes les images sur une seule page',
+ 'value' => [
+ 'baseUrl' => 'https://mangasite.example.com',
+ 'chapterUrlFormat' => 'https://mangasite.example.com/manga/{slug}/chapter/{chapter}',
+ 'scrapingType' => 'html',
+ 'testUrl' => 'https://mangasite.example.com/manga/one-piece/chapter/1',
+ 'mangaSlug' => 'one-piece',
+ 'chapterNumber' => 1.0,
+ 'imageSelector' => 'img.manga-page',
+ 'nextPageSelector' => null,
+ 'chapterSelector' => '.chapter-content'
+ ]
+ ],
+ 'lecteur_horizontal' => [
+ 'summary' => 'Configuration pour un lecteur horizontal',
+ 'description' => 'Exemple de configuration pour un site avec navigation page par page',
+ 'value' => [
+ 'baseUrl' => 'https://mangasite.example.com',
+ 'chapterUrlFormat' => 'https://mangasite.example.com/read/{slug}/{chapter}/1',
+ 'scrapingType' => 'html',
+ 'testUrl' => 'https://mangasite.example.com/read/one-piece/1/1',
+ 'mangaSlug' => 'one-piece',
+ 'chapterNumber' => 1.0,
+ 'imageSelector' => '#manga-image',
+ 'nextPageSelector' => 'a.next-page',
+ 'chapterSelector' => '.reader-container'
+ ]
+ ]
+ ]
+ ]
+ ])
+ ),
+ responses: [
+ '200' => new OpenApiResponse(
+ description: 'Test de configuration réussi',
+ content: new \ArrayObject([
+ 'application/json' => [
+ 'schema' => [
+ 'type' => 'object',
+ 'properties' => [
+ 'success' => [
+ 'type' => 'boolean',
+ 'description' => 'Indique si le test a réussi'
+ ],
+ 'imageUrls' => [
+ 'type' => 'array',
+ 'items' => [
+ 'type' => 'string',
+ 'format' => 'uri'
+ ],
+ 'description' => 'Liste des URLs d\'images trouvées'
+ ],
+ 'totalImages' => [
+ 'type' => 'integer',
+ 'description' => 'Nombre total d\'images trouvées'
+ ],
+ 'testedUrl' => [
+ 'type' => 'string',
+ 'format' => 'uri',
+ 'description' => 'URL qui a été testée'
+ ],
+ 'scrapingType' => [
+ 'type' => 'string',
+ 'description' => 'Type de scraping utilisé'
+ ],
+ 'errors' => [
+ 'type' => 'array',
+ 'items' => [
+ 'type' => 'object',
+ 'properties' => [
+ 'type' => [
+ 'type' => 'string',
+ 'description' => 'Type d\'erreur (selector_error, url_error, general_error)'
+ ],
+ 'field' => [
+ 'type' => 'string',
+ 'description' => 'Champ concerné par l\'erreur'
+ ],
+ 'message' => [
+ 'type' => 'string',
+ 'description' => 'Message d\'erreur détaillé'
+ ],
+ 'suggestion' => [
+ 'type' => 'string',
+ 'description' => 'Suggestion pour corriger l\'erreur'
+ ]
+ ]
+ ],
+ 'description' => 'Liste des erreurs détaillées (vide en cas de succès)'
+ ]
+ ]
+ ],
+ 'examples' => [
+ 'succes' => [
+ 'summary' => 'Test réussi',
+ 'value' => [
+ 'success' => true,
+ 'imageUrls' => [
+ 'https://mangasite.example.com/images/chapter1/page1.jpg',
+ 'https://mangasite.example.com/images/chapter1/page2.jpg',
+ 'https://mangasite.example.com/images/chapter1/page3.jpg'
+ ],
+ 'totalImages' => 3,
+ 'testedUrl' => 'https://mangasite.example.com/manga/one-piece/chapter/1',
+ 'scrapingType' => 'html',
+ 'errors' => []
+ ]
+ ],
+ 'echec_selecteur' => [
+ 'summary' => 'Échec - Sélecteur invalide',
+ 'value' => [
+ 'success' => false,
+ 'imageUrls' => [],
+ 'totalImages' => 0,
+ 'testedUrl' => 'https://mangasite.example.com/manga/one-piece/chapter/1',
+ 'scrapingType' => 'html',
+ 'errors' => [
+ [
+ 'type' => 'selector_error',
+ 'field' => 'imageSelector',
+ 'message' => 'Le sélecteur d\'image \'.invalid-selector\' ne trouve aucun élément sur la page',
+ 'suggestion' => 'Vérifiez que le sélecteur CSS est correct et qu\'il correspond aux éléments d\'image sur la page'
+ ]
+ ]
+ ]
+ ],
+ 'echec_url' => [
+ 'summary' => 'Échec - URL inaccessible',
+ 'value' => [
+ 'success' => false,
+ 'imageUrls' => [],
+ 'totalImages' => 0,
+ 'testedUrl' => 'https://invalid-site.example.com/chapter/1',
+ 'scrapingType' => 'html',
+ 'errors' => [
+ [
+ 'type' => 'url_error',
+ 'field' => 'testUrl',
+ 'message' => 'Impossible d\'accéder à l\'URL de test: https://invalid-site.example.com/chapter/1',
+ 'suggestion' => 'Vérifiez que l\'URL est correcte et accessible, et que le chapitre existe'
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ])
+ ),
+ '422' => new OpenApiResponse(
+ description: 'Erreur de validation des données d\'entrée',
+ content: new \ArrayObject([
+ 'application/json' => [
+ 'schema' => [
+ 'type' => 'object',
+ 'properties' => [
+ 'type' => [
+ 'type' => 'string',
+ 'example' => 'https://tools.ietf.org/html/rfc2616#section-10'
+ ],
+ 'title' => [
+ 'type' => 'string',
+ 'example' => 'An error occurred'
+ ],
+ 'detail' => [
+ 'type' => 'string',
+ 'example' => 'baseUrl: L\'URL de base doit être une URL valide'
+ ],
+ 'violations' => [
+ 'type' => 'array',
+ 'items' => [
+ 'type' => 'object',
+ 'properties' => [
+ 'propertyPath' => [
+ 'type' => 'string',
+ 'description' => 'Champ contenant l\'erreur'
+ ],
+ 'message' => [
+ 'type' => 'string',
+ 'description' => 'Message d\'erreur de validation'
+ ]
+ ]
+ ]
+ ]
+ ]
+ ],
+ 'example' => [
+ 'type' => 'https://symfony.com/errors/validation',
+ 'title' => 'Validation Failed',
+ 'detail' => 'baseUrl: L\'URL de base doit être une URL valide',
+ 'violations' => [
+ [
+ 'propertyPath' => 'baseUrl',
+ 'message' => 'L\'URL de base doit être une URL valide'
+ ]
+ ]
+ ]
+ ]
+ ])
+ )
+ ]
+ )
+ ),
+ ]
+)]
+readonly class TestScraperConfigurationRequest
+{
+ public function __construct(
+ #[ApiProperty(
+ description: 'URL de base du site web source de mangas',
+ example: 'https://mangasite.example.com',
+ schema: ['type' => 'string', 'format' => 'uri']
+ )]
+ #[Assert\NotBlank(message: 'L\'URL de base est obligatoire')]
+ #[Assert\Url(message: 'L\'URL de base doit être une URL valide')]
+ public string $baseUrl,
+
+ #[ApiProperty(
+ description: 'Format d\'URL pour accéder aux chapitres. Utilisez {slug} pour le nom du manga et {chapter} pour le numéro de chapitre',
+ example: 'https://mangasite.example.com/manga/{slug}/chapter/{chapter}',
+ schema: ['type' => 'string', 'pattern' => '.*\{slug\}.*\{chapter\}.*']
+ )]
+ #[Assert\NotBlank(message: 'Le format d\'URL de chapitre est obligatoire')]
+ public string $chapterUrlFormat,
+
+ #[ApiProperty(
+ description: 'Type de scraping à utiliser. "html" pour les sites statiques, "javascript" pour les sites avec contenu dynamique',
+ example: 'html',
+ schema: ['type' => 'string', 'enum' => ['html', 'javascript']]
+ )]
+ #[Assert\NotBlank(message: 'Le type de scraping est obligatoire')]
+ #[Assert\Choice(choices: ['html', 'javascript'], message: 'Le type de scraping doit être html ou javascript')]
+ public string $scrapingType,
+
+ #[ApiProperty(
+ description: 'URL complète d\'un chapitre existant à utiliser pour tester la configuration. Cette URL doit être accessible et contenir des images',
+ example: 'https://mangasite.example.com/manga/one-piece/chapter/1',
+ schema: ['type' => 'string', 'format' => 'uri']
+ )]
+ #[Assert\NotBlank(message: 'L\'URL de test est obligatoire')]
+ #[Assert\Url(message: 'L\'URL de test doit être une URL valide')]
+ public string $testUrl,
+
+ #[ApiProperty(
+ description: 'Identifiant du manga utilisé dans les URLs (slug). Sera utilisé pour remplacer {slug} dans le format d\'URL',
+ example: 'one-piece',
+ schema: ['type' => 'string', 'pattern' => '^[a-z0-9-_]+$']
+ )]
+ #[Assert\NotBlank(message: 'Le slug du manga est obligatoire')]
+ public string $mangaSlug,
+
+ #[ApiProperty(
+ description: 'Numéro du chapitre à tester. Peut être décimal pour les chapitres spéciaux (ex: 1.5)',
+ example: 1.0,
+ schema: ['type' => 'number', 'minimum' => 0]
+ )]
+ #[Assert\NotBlank(message: 'Le numéro de chapitre est obligatoire')]
+ #[Assert\Type(type: 'numeric', message: 'Le numéro de chapitre doit être numérique')]
+ #[Assert\Positive(message: 'Le numéro de chapitre doit être positif')]
+ public float $chapterNumber,
+
+ #[ApiProperty(
+ description: 'Sélecteur CSS pour identifier les images du manga sur la page. Exemples: "img.manga-page", ".chapter img", "#reader img"',
+ example: 'img.manga-page, .chapter-image img',
+ schema: ['type' => 'string', 'nullable' => true, 'maxLength' => 500]
+ )]
+ #[Assert\Length(min: 1, max: 500, minMessage: 'Le sélecteur d\'image ne peut pas être vide', maxMessage: 'Le sélecteur d\'image est trop long')]
+ public ?string $imageSelector = null,
+
+ #[ApiProperty(
+ description: 'Sélecteur CSS pour le lien vers la page suivante (requis pour les lecteurs horizontaux). Exemples: "a.next", ".navigation .next-page"',
+ example: 'a.next-page, .navigation .next',
+ schema: ['type' => 'string', 'nullable' => true, 'maxLength' => 500]
+ )]
+ #[Assert\Length(max: 500, maxMessage: 'Le sélecteur de page suivante est trop long')]
+ public ?string $nextPageSelector = null,
+
+ #[ApiProperty(
+ description: 'Sélecteur CSS pour délimiter la zone contenant le chapitre (optionnel). Utile pour cibler une zone spécifique de la page',
+ example: '.chapter-content, #manga-reader',
+ schema: ['type' => 'string', 'nullable' => true, 'maxLength' => 500]
+ )]
+ #[Assert\Length(max: 500, maxMessage: 'Le sélecteur de chapitre est trop long')]
+ public ?string $chapterSelector = null,
+ ) {}
+}
diff --git a/src/Domain/Scraping/Infrastructure/ApiPlatform/Resource/TestScraperConfigurationResource.php b/src/Domain/Scraping/Infrastructure/ApiPlatform/Resource/TestScraperConfigurationResource.php
new file mode 100644
index 0000000..738a83d
--- /dev/null
+++ b/src/Domain/Scraping/Infrastructure/ApiPlatform/Resource/TestScraperConfigurationResource.php
@@ -0,0 +1,93 @@
+ 'boolean']
+ )]
+ public bool $success,
+
+ #[ApiProperty(
+ description: 'Liste des URLs d\'images trouvées lors du scraping. Vide en cas d\'échec.',
+ example: [
+ 'https://mangasite.example.com/images/chapter1/page1.jpg',
+ 'https://mangasite.example.com/images/chapter1/page2.jpg'
+ ],
+ schema: [
+ 'type' => 'array',
+ 'items' => [
+ 'type' => 'string',
+ 'format' => 'uri'
+ ]
+ ]
+ )]
+ public array $imageUrls,
+
+ #[ApiProperty(
+ description: 'Nombre total d\'images trouvées',
+ example: 2,
+ schema: ['type' => 'integer', 'minimum' => 0]
+ )]
+ public int $totalImages,
+
+ #[ApiProperty(
+ description: 'URL qui a été testée',
+ example: 'https://mangasite.example.com/manga/one-piece/chapter/1',
+ schema: ['type' => 'string', 'format' => 'uri']
+ )]
+ public string $testedUrl,
+
+ #[ApiProperty(
+ description: 'Type de scraping qui a été utilisé pour le test',
+ example: 'html',
+ schema: ['type' => 'string', 'enum' => ['html', 'javascript']]
+ )]
+ public string $scrapingType,
+
+ #[ApiProperty(
+ description: 'Liste des erreurs détaillées en cas d\'échec. Vide en cas de succès. Chaque erreur contient un type, le champ concerné, un message et une suggestion.',
+ example: [
+ [
+ 'type' => 'selector_error',
+ 'field' => 'imageSelector',
+ 'message' => 'Le sélecteur d\'image ne trouve aucun élément',
+ 'suggestion' => 'Vérifiez le sélecteur CSS'
+ ]
+ ],
+ schema: [
+ 'type' => 'array',
+ 'items' => [
+ 'type' => 'object',
+ 'properties' => [
+ 'type' => [
+ 'type' => 'string',
+ 'enum' => ['selector_error', 'url_error', 'general_error'],
+ 'description' => 'Type d\'erreur rencontrée'
+ ],
+ 'field' => [
+ 'type' => 'string',
+ 'description' => 'Champ de configuration concerné par l\'erreur'
+ ],
+ 'message' => [
+ 'type' => 'string',
+ 'description' => 'Message d\'erreur détaillé'
+ ],
+ 'suggestion' => [
+ 'type' => 'string',
+ 'description' => 'Suggestion pour corriger l\'erreur'
+ ]
+ ],
+ 'required' => ['type', 'field', 'message', 'suggestion']
+ ]
+ ]
+ )]
+ public array $errors = [],
+ ) {}
+}
diff --git a/src/Domain/Scraping/Infrastructure/ApiPlatform/State/Processor/TestScraperConfigurationStateProcessor.php b/src/Domain/Scraping/Infrastructure/ApiPlatform/State/Processor/TestScraperConfigurationStateProcessor.php
new file mode 100644
index 0000000..2fd99c0
--- /dev/null
+++ b/src/Domain/Scraping/Infrastructure/ApiPlatform/State/Processor/TestScraperConfigurationStateProcessor.php
@@ -0,0 +1,48 @@
+baseUrl,
+ chapterUrlFormat: $data->chapterUrlFormat,
+ scrapingType: $data->scrapingType,
+ testUrl: $data->testUrl,
+ mangaSlug: $data->mangaSlug,
+ chapterNumber: $data->chapterNumber,
+ imageSelector: $data->imageSelector,
+ nextPageSelector: $data->nextPageSelector,
+ chapterSelector: $data->chapterSelector,
+ );
+
+ // Exécution du CommandHandler
+ $response = $this->handler->handle($command);
+
+ // Conversion de la Response en Resource
+ return new TestScraperConfigurationResource(
+ success: $response->success,
+ imageUrls: $response->imageUrls,
+ totalImages: $response->totalImages,
+ testedUrl: $response->testedUrl,
+ scrapingType: $response->scrapingType,
+ errors: $response->errors,
+ );
+ }
+}
diff --git a/tests/Feature/Scraping/TestScraperConfigurationTest.php b/tests/Feature/Scraping/TestScraperConfigurationTest.php
new file mode 100644
index 0000000..7c292f8
--- /dev/null
+++ b/tests/Feature/Scraping/TestScraperConfigurationTest.php
@@ -0,0 +1,198 @@
+ 'https://example.com',
+ 'chapterUrlFormat' => 'https://example.com/manga/{slug}/chapter/{chapter}',
+ 'scrapingType' => 'html',
+ 'testUrl' => 'https://httpbin.org/html',
+ 'mangaSlug' => 'test-manga',
+ 'chapterNumber' => 1.0,
+ 'imageSelector' => 'img',
+ 'nextPageSelector' => null,
+ 'chapterSelector' => null,
+ ];
+
+ // When
+ $response = static::createClient()->request('POST', '/api/scraping/test-configuration', [
+ 'json' => $payload,
+ 'headers' => ['Accept' => 'application/json'],
+ ]);
+
+ // Then
+ $this->assertResponseStatusCodeSame(200);
+ $responseData = $response->toArray();
+
+ $this->assertArrayHasKey('success', $responseData);
+ $this->assertArrayHasKey('imageUrls', $responseData);
+ $this->assertArrayHasKey('totalImages', $responseData);
+ $this->assertArrayHasKey('testedUrl', $responseData);
+ $this->assertArrayHasKey('scrapingType', $responseData);
+ $this->assertArrayHasKey('errors', $responseData);
+
+ $this->assertEquals('https://httpbin.org/html', $responseData['testedUrl']);
+ $this->assertEquals('html', $responseData['scrapingType']);
+ }
+
+ public function testTestScraperConfigurationWithInvalidUrl(): void
+ {
+ // Given - Configuration avec une URL invalide
+ $payload = [
+ 'baseUrl' => 'https://example.com',
+ 'chapterUrlFormat' => 'https://example.com/manga/{slug}/chapter/{chapter}',
+ 'scrapingType' => 'html',
+ 'testUrl' => 'https://invalid-url-that-does-not-exist-12345.com',
+ 'mangaSlug' => 'test-manga',
+ 'chapterNumber' => 1.0,
+ 'imageSelector' => 'img',
+ 'nextPageSelector' => null,
+ 'chapterSelector' => null,
+ ];
+
+ // When
+ $response = static::createClient()->request('POST', '/api/scraping/test-configuration', [
+ 'json' => $payload,
+ 'headers' => ['Accept' => 'application/json'],
+ ]);
+
+ // Then
+ $this->assertResponseStatusCodeSame(200);
+ $responseData = $response->toArray();
+
+ $this->assertFalse($responseData['success']);
+ $this->assertEquals(0, $responseData['totalImages']);
+ $this->assertEmpty($responseData['imageUrls']);
+ $this->assertNotEmpty($responseData['errors']);
+
+ // Vérifier qu'on a une erreur détaillée
+ $this->assertIsArray($responseData['errors']);
+ $this->assertGreaterThan(0, count($responseData['errors']));
+ }
+
+ public function testTestScraperConfigurationWithInvalidSelector(): void
+ {
+ // Given - Configuration avec un sélecteur CSS qui ne trouvera rien
+ $payload = [
+ 'baseUrl' => 'https://example.com',
+ 'chapterUrlFormat' => 'https://example.com/manga/{slug}/chapter/{chapter}',
+ 'scrapingType' => 'html',
+ 'testUrl' => 'https://httpbin.org/html',
+ 'mangaSlug' => 'test-manga',
+ 'chapterNumber' => 1.0,
+ 'imageSelector' => '.non-existent-class-selector-12345',
+ 'nextPageSelector' => null,
+ 'chapterSelector' => null,
+ ];
+
+ // When
+ $response = static::createClient()->request('POST', '/api/scraping/test-configuration', [
+ 'json' => $payload,
+ 'headers' => ['Accept' => 'application/json'],
+ ]);
+
+ // Then
+ $this->assertResponseStatusCodeSame(200);
+ $responseData = $response->toArray();
+
+ // Le sélecteur invalide peut ne pas retourner d'images, ou juste retourner un tableau vide
+ // C'est un comportement acceptable, le test vérifie juste que ça ne plante pas
+ $this->assertArrayHasKey('success', $responseData);
+ $this->assertArrayHasKey('imageUrls', $responseData);
+ $this->assertArrayHasKey('totalImages', $responseData);
+ $this->assertArrayHasKey('errors', $responseData);
+ }
+
+ public function testTestScraperConfigurationWithInvalidValidation(): void
+ {
+ // Given - Payload avec des données invalides
+ $payload = [
+ 'baseUrl' => 'invalid-url',
+ 'chapterUrlFormat' => '',
+ 'scrapingType' => 'invalid-type',
+ 'testUrl' => 'not-a-url',
+ 'mangaSlug' => '',
+ 'chapterNumber' => -1,
+ 'imageSelector' => str_repeat('x', 501), // Trop long
+ ];
+
+ // When
+ $response = static::createClient()->request('POST', '/api/scraping/test-configuration', [
+ 'json' => $payload,
+ 'headers' => ['Accept' => 'application/json'],
+ ]);
+
+ // Then
+ $this->assertResponseStatusCodeSame(422);
+ $this->assertJsonContains([
+ 'violations' => [
+ [
+ 'propertyPath' => 'baseUrl',
+ 'message' => 'L\'URL de base doit être une URL valide',
+ ],
+ [
+ 'propertyPath' => 'chapterUrlFormat',
+ 'message' => 'Le format d\'URL de chapitre est obligatoire',
+ ],
+ [
+ 'propertyPath' => 'scrapingType',
+ 'message' => 'Le type de scraping doit être html ou javascript',
+ ],
+ [
+ 'propertyPath' => 'testUrl',
+ 'message' => 'L\'URL de test doit être une URL valide',
+ ],
+ [
+ 'propertyPath' => 'mangaSlug',
+ 'message' => 'Le slug du manga est obligatoire',
+ ],
+ [
+ 'propertyPath' => 'chapterNumber',
+ 'message' => 'Le numéro de chapitre doit être positif',
+ ],
+ [
+ 'propertyPath' => 'imageSelector',
+ 'message' => 'Le sélecteur d\'image est trop long',
+ ],
+ ],
+ ]);
+ }
+
+ public function testTestScraperConfigurationWithUnsupportedScrapingType(): void
+ {
+ // Given - Type de scraping non supporté
+ $payload = [
+ 'baseUrl' => 'https://example.com',
+ 'chapterUrlFormat' => 'https://example.com/manga/{slug}/chapter/{chapter}',
+ 'scrapingType' => 'unsupported-type', // Type vraiment non supporté
+ 'testUrl' => 'https://httpbin.org/html',
+ 'mangaSlug' => 'test-manga',
+ 'chapterNumber' => 1.0,
+ 'imageSelector' => 'img',
+ ];
+
+ // When
+ $response = static::createClient()->request('POST', '/api/scraping/test-configuration', [
+ 'json' => $payload,
+ 'headers' => ['Accept' => 'application/json'],
+ ]);
+
+ // Then
+ $this->assertResponseStatusCodeSame(422); // Validation error
+ $this->assertJsonContains([
+ 'violations' => [
+ [
+ 'propertyPath' => 'scrapingType',
+ 'message' => 'Le type de scraping doit être html ou javascript',
+ ],
+ ],
+ ]);
+ }
+}