/
koskv
/
TeamSync-Tests
Обзор
Документация
Войти
/
koskv
/
TeamSync-Tests
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
src/main/java/TeamSyncTest/config/AdvancedAshotConfig.java
457 строк
19 KB
Koskv
Переделал тесты - поправил
30 окт 2025, 15:28
30 окт 2025, 15:28
380cae1
Код
Авторство
О чём код?
package TeamSyncTest.config; import io.qameta.allure.Allure; import io.qameta.allure.Step; import com.codeborne.selenide.Selenide; import com.codeborne.selenide.Configuration; import org.openqa.selenium.OutputType; import ru.yandex.qatools.ashot.comparison.ImageDiff; import ru.yandex.qatools.ashot.comparison.ImageDiffer; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.TestInfo; import org.opentest4j.AssertionFailedError; import javax.imageio.ImageIO; import java.awt.*; import java.awt.image.BufferedImage; import java.io.*; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; /** * Конфигурируемый класс для сравнения скриншотов с использованием AShot * Поддерживает различные стратегии сравнения и расширенную отчетность */ @DisplayName("Visual Testing Framework") public class AdvancedAshotConfig { private static final String DEFAULT_SCREENSHOTS_DIR = "src/main/resources/screenshots/"; private static final String DEFAULT_OUTPUT_DIR = "target/visual-testing/"; private static final double DEFAULT_DIFF_THRESHOLD = 0.1; // 10% различий /** * Конфигурация для сравнения изображений */ public static class ComparisonConfig { private double diffThreshold = DEFAULT_DIFF_THRESHOLD; private boolean ignoreAntialiasing = true; private boolean resizeImages = true; private Color diffColor = Color.RED; private int diffSize = 5; public ComparisonConfig withDiffThreshold(double threshold) { this.diffThreshold = threshold; return this; } public ComparisonConfig withIgnoreAntialiasing(boolean ignore) { this.ignoreAntialiasing = ignore; return this; } public ComparisonConfig withResizeImages(boolean resize) { this.resizeImages = resize; return this; } public ComparisonConfig withDiffColor(Color color) { this.diffColor = color; return this; } // Getters public double getDiffThreshold() { return diffThreshold; } public boolean isIgnoreAntialiasing() { return ignoreAntialiasing; } public boolean isResizeImages() { return resizeImages; } public Color getDiffColor() { return diffColor; } public int getDiffSize() { return diffSize; } } /** * Результат сравнения изображений */ public static class ComparisonResult { private final boolean passed; private final double similarity; private final int diffPixels; private final int totalPixels; private final File diffImage; private final File actualImage; private final String message; public ComparisonResult(boolean passed, double similarity, int diffPixels, int totalPixels, File diffImage, File actualImage, String message) { this.passed = passed; this.similarity = similarity; this.diffPixels = diffPixels; this.totalPixels = totalPixels; this.diffImage = diffImage; this.actualImage = actualImage; this.message = message; } // Getters public boolean isPassed() { return passed; } public double getSimilarity() { return similarity; } public int getDiffPixels() { return diffPixels; } public int getTotalPixels() { return totalPixels; } public File getDiffImage() { return diffImage; } public File getActualImage() { return actualImage; } public String getMessage() { return message; } } /** * Основной метод сравнения скриншотов с конфигурацией */ @Step("Compare screenshot with reference: {methodName}") @DisplayName("Visual Regression Test") public static ComparisonResult assertScreen(TestInfo info) { return assertScreen(info, new ComparisonConfig()); } @Step("Compare screenshot with reference: {methodName}") public static ComparisonResult assertScreen(TestInfo info, ComparisonConfig config) { String methodName = info.getTestMethod().map(method -> method.getName()).orElse("unknown"); String expectedFileName = methodName + ".png"; Path expectedScreenshotPath = Paths.get(DEFAULT_SCREENSHOTS_DIR, expectedFileName); Path outputDir = Paths.get(DEFAULT_OUTPUT_DIR); try { // Создаем директории если не существуют Files.createDirectories(outputDir); Files.createDirectories(Paths.get(DEFAULT_SCREENSHOTS_DIR)); // Делаем скриншот byte[] actualBytes = captureScreenshot(methodName); BufferedImage actualImage = convertToBufferedImage(actualBytes); // Проверяем существование эталона if (!Files.exists(expectedScreenshotPath)) { return handleMissingBaseline(expectedScreenshotPath, actualBytes, methodName); } // Читаем эталонное изображение BufferedImage expectedImage = ImageIO.read(expectedScreenshotPath.toFile()); if (expectedImage == null) { throw new RuntimeException("Cannot read expected image: " + expectedScreenshotPath); } // Подготавливаем изображения для сравнения BufferedImage preparedActual = prepareImageForComparison(actualImage, expectedImage, config); // Сравниваем изображения return compareImages(expectedImage, preparedActual, config, methodName, outputDir); } catch (Exception e) { handleVisualTestError(methodName, e); throw new RuntimeException("Visual comparison failed for: " + methodName, e); } } @Step("Visual test failed with error") private static void handleVisualTestError(String methodName, Exception e) { Allure.addAttachment("Error Details", "text/plain", "Method: " + methodName + "\nError: " + e.getMessage()); } /** * Захват скриншота с обработкой ошибок */ @Step("Capture screenshot") private static byte[] captureScreenshot(String methodName) { try { // Убедимся, что Selenide настроен правильно if (Configuration.reportsFolder == null) { Configuration.reportsFolder = "target/selenide-reports"; } byte[] screenshotBytes = Selenide.screenshot(OutputType.BYTES); if (screenshotBytes == null || screenshotBytes.length == 0) { throw new RuntimeException("Screenshot capture returned empty result"); } // Исправленный вызов addAttachment для изображений Allure.getLifecycle().addAttachment( "Captured Screenshot - " + methodName, "image/png", "png", screenshotBytes ); return screenshotBytes; } catch (Exception e) { throw new RuntimeException("Failed to capture screenshot for: " + methodName, e); } } /** * Обработка отсутствия эталонного изображения */ @Step("Handle missing baseline screenshot") private static ComparisonResult handleMissingBaseline(Path expectedPath, byte[] actualBytes, String methodName) { try { // Сохраняем текущий скриншот как новый эталон Files.write(expectedPath, actualBytes); String message = "New baseline created: " + expectedPath; Allure.addAttachment("Baseline Created", "text/plain", message); return new ComparisonResult(true, 1.0, 0, 0, null, null, message); } catch (IOException e) { throw new RuntimeException("Failed to create baseline for: " + methodName, e); } } /** * Подготовка изображений для сравнения */ @Step("Prepare images for comparison") private static BufferedImage prepareImageForComparison(BufferedImage actual, BufferedImage expected, ComparisonConfig config) { if (!config.isResizeImages()) { return actual; } // Проверяем необходимость изменения размера if (actual.getWidth() != expected.getWidth() || actual.getHeight() != expected.getHeight()) { Allure.addAttachment("Image Resize Info", "text/plain", String.format("Resizing actual image from %dx%d to %dx%d", actual.getWidth(), actual.getHeight(), expected.getWidth(), expected.getHeight()) ); return resizeImage(actual, expected.getWidth(), expected.getHeight()); } return actual; } /** * Сравнение изображений с расширенной отчетностью */ @Step("Compare images") private static ComparisonResult compareImages(BufferedImage expected, BufferedImage actual, ComparisonConfig config, String methodName, Path outputDir) throws IOException { // Исправленный ImageDiffer - убираем несуществующие методы ImageDiffer differ = new ImageDiffer(); ImageDiff diff = differ.makeDiff(expected, actual); // Расчет метрик int totalPixels = expected.getWidth() * expected.getHeight(); int diffPixels = calculateDiffPixels(diff, expected, actual); double similarity = 1.0 - ((double) diffPixels / totalPixels); boolean passed = similarity >= (1 - config.getDiffThreshold()); // Сохраняем результаты File diffImage = saveDiffImage(diff.getMarkedImage(), methodName, outputDir); File actualImage = saveActualImage(actual, methodName, outputDir); // Формируем отчет String message = String.format( "Similarity: %.2f%%, Different pixels: %d/%d (Threshold: %.1f%%)", similarity * 100, diffPixels, totalPixels, config.getDiffThreshold() * 100 ); // Прикрепляем все к Allure attachComparisonResults(expected, actual, diffImage, message, passed, methodName); if (!passed) { throw new AssertionFailedError("Visual comparison failed: " + message, String.valueOf(similarity), String.valueOf(1 - config.getDiffThreshold())); } return new ComparisonResult(passed, similarity, diffPixels, totalPixels, diffImage, actualImage, message); } /** * Расчет количества различных пикселей */ private static int calculateDiffPixels(ImageDiff diff, BufferedImage expected, BufferedImage actual) { try { // Получаем изображение с различиями BufferedImage diffImage = diff.getMarkedImage(); int diffPixels = 0; // Простой подсчет красных пикселей (цвет различий) for (int y = 0; y < diffImage.getHeight(); y++) { for (int x = 0; x < diffImage.getWidth(); x++) { int rgb = diffImage.getRGB(x, y); Color color = new Color(rgb); // Проверяем, является ли пиксель красным (цвет различий) if (color.getRed() > 200 && color.getGreen() < 100 && color.getBlue() < 100) { diffPixels++; } } } return diffPixels; } catch (Exception e) { // Альтернативный метод подсчета - сравниваем пиксели напрямую return calculatePixelDifference(expected, actual); } } /** * Альтернативный метод подсчета различий пикселей */ private static int calculatePixelDifference(BufferedImage img1, BufferedImage img2) { int diffPixels = 0; int width = Math.min(img1.getWidth(), img2.getWidth()); int height = Math.min(img1.getHeight(), img2.getHeight()); for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { int rgb1 = img1.getRGB(x, y); int rgb2 = img2.getRGB(x, y); // Сравниваем RGB значения if (rgb1 != rgb2) { diffPixels++; } } } // Добавляем разницу в размерах как дополнительные различия diffPixels += Math.abs(img1.getWidth() - img2.getWidth()) * height; diffPixels += Math.abs(img1.getHeight() - img2.getHeight()) * width; return diffPixels; } /** * Сохранение diff изображения */ private static File saveDiffImage(BufferedImage diffImage, String methodName, Path outputDir) throws IOException { File diffFile = outputDir.resolve("diff_" + methodName + ".png").toFile(); ImageIO.write(diffImage, "png", diffFile); return diffFile; } /** * Сохранение актуального изображения */ private static File saveActualImage(BufferedImage actualImage, String methodName, Path outputDir) throws IOException { File actualFile = outputDir.resolve("actual_" + methodName + ".png").toFile(); ImageIO.write(actualImage, "png", actualFile); return actualFile; } /** * Прикрепление результатов к Allure */ @Step("Attach comparison results to report") private static void attachComparisonResults(BufferedImage expected, BufferedImage actual, File diffImage, String message, boolean passed, String methodName) { try { // Прикрепляем эталонное изображение ByteArrayOutputStream expectedStream = new ByteArrayOutputStream(); ImageIO.write(expected, "png", expectedStream); Allure.getLifecycle().addAttachment( "Expected Screenshot - " + methodName, "image/png", "png", expectedStream.toByteArray() ); // Прикрепляем актуальное изображение ByteArrayOutputStream actualStream = new ByteArrayOutputStream(); ImageIO.write(actual, "png", actualStream); Allure.getLifecycle().addAttachment( "Actual Screenshot - " + methodName, "image/png", "png", actualStream.toByteArray() ); // Прикрепляем diff изображение if (diffImage != null && diffImage.exists()) { byte[] diffBytes = Files.readAllBytes(diffImage.toPath()); Allure.getLifecycle().addAttachment( "Difference Highlighted - " + methodName, "image/png", "png", diffBytes ); } // Прикрепляем текстовый отчет Allure.addAttachment("Comparison Results - " + methodName, "text/plain", message); // Статус теста String status = passed ? "✅ PASSED" : "❌ FAILED"; Allure.addAttachment("Test Status - " + methodName, "text/plain", status); } catch (IOException e) { Allure.addAttachment("Attachment Error - " + methodName, "text/plain", "Failed to attach comparison results: " + e.getMessage()); } } /** * Изменение размера изображения */ @Step("Resize image to {targetWidth}x{targetHeight}") private static BufferedImage resizeImage(BufferedImage originalImage, int targetWidth, int targetHeight) { BufferedImage resizedImage = new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_INT_ARGB); Graphics2D g2d = resizedImage.createGraphics(); // Настройка качества рендеринга g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2d.drawImage(originalImage, 0, 0, targetWidth, targetHeight, null); g2d.dispose(); return resizedImage; } private static BufferedImage convertToBufferedImage(byte[] imageBytes) throws IOException { return ImageIO.read(new ByteArrayInputStream(imageBytes)); } /** * Вспомогательный метод для простого использования в тестах */ public static void compareScreenshot(TestInfo testInfo) { assertScreen(testInfo); } public static void compareScreenshot(TestInfo testInfo, ComparisonConfig config) { assertScreen(testInfo, config); } /** * Метод для создания baseline скриншота */ @Step("Create baseline screenshot") public static void createBaseline(TestInfo testInfo) { String methodName = testInfo.getTestMethod().map(method -> method.getName()).orElse("unknown"); String expectedFileName = methodName + ".png"; Path expectedScreenshotPath = Paths.get(DEFAULT_SCREENSHOTS_DIR, expectedFileName); try { Files.createDirectories(Paths.get(DEFAULT_SCREENSHOTS_DIR)); byte[] actualBytes = captureScreenshot(methodName); Files.write(expectedScreenshotPath, actualBytes); Allure.addAttachment("Baseline Created", "text/plain", "Baseline screenshot created at: " + expectedScreenshotPath); } catch (IOException e) { throw new RuntimeException("Failed to create baseline for: " + methodName, e); } } }