Загрузка данных



@DefaultServiceConfiguration(DefaultPlaywrightServiceConfiguration.class)
public class PlaywrightService implements Service {
  private static final Logger LOGGER = LoggerFactory.getLogger(PlaywrightService.class);
  private DefaultPlaywrightServiceConfiguration configuration;
  private Playwright playwright;
  private Browser browser;
  private BrowserContext context;
  private Page page;
  private DevToolsManager devToolsManager;
  private Environment environment;
  private SelenoidUtils selenoidUtils = null;

  public void init(@NotNull Environment environment) {
    Preconditions.notNull(environment, "Environment must not be null");
    this.environment = environment;
    this.configuration = new DefaultPlaywrightServiceConfiguration();
  }

  public void init(@NotNull Environment environment, @NotNull ServiceConfiguration configuration) {
    Preconditions.notNull(environment, "Environment must not be null");
    Preconditions.notNull(configuration, "Service configuration must not be null");
    this.environment = environment;
    this.configuration = (DefaultPlaywrightServiceConfiguration)this.validate(configuration, DefaultPlaywrightServiceConfiguration.class);
  }

  public void beforeTest() {
    this.setup();
  }

  public void afterTest() {
    if (this.devToolsManager != null) {
      LogsUtils.attachReport(this.devToolsManager, this.context);
    } else {
      LOGGER.warn("Нет возможности сохранить логи браузера.");
    }

    if (this.page != null) {
      this.page.close();
    }

    if (this.context != null) {
      this.context.close();
    }

    if (this.browser != null) {
      this.browser.close();
    }

    if (this.playwright != null) {
      this.playwright.close();
    }

  }

  public Playwright getPlaywright() {
    return this.playwright;
  }

  public Browser getBrowser() {
    return this.browser;
  }

  public BrowserContext getBrowserContext() {
    return this.context;
  }

  public Page getPage() {
    return this.page;
  }

  public Download waitForDownload(Runnable runnable) {
    return this.waitForDownload((Page.WaitForDownloadOptions)null, runnable);
  }

  public Download waitForDownload(@Nullable Page.WaitForDownloadOptions options, Runnable runnable) {
    Download download = this.page.waitForDownload(options, runnable);
    if (this.configuration.isRemote()) {
      String fileName = download.suggestedFilename();
      byte[] bytes = this.selenoidUtils.downloadSelenoidFile(fileName);
      return new DownloadWrapper(download, bytes);
    } else {
      return download;
    }
  }

  private void setup() {
    LOGGER.info("Инициализация Playwright.");
    this.playwright = Playwright.create(this.configuration.createOptions());
    BrowserType browserType = (BrowserType)this.configuration.browserType().apply(this.playwright);
    LOGGER.info("Выбран браузер {}.", browserType.name());
    if (this.configuration.isRemote()) {
      LOGGER.info("Инициализация контекста браузера для удалённого запуска.");
      this.selenoidUtils = SelenoidUtils.createSession(this.configuration.selenoidCapabilities().toJson());
      this.browser = browserType.connectOverCDP(this.selenoidUtils.getSocketUrl(), this.configuration.connectOverCDPOptions());
      this.context = this.browser.newContext();
    } else {
      Map<String, Object> prefs = this.configuration.browserPreferences();
      LOGGER.info("Инициализация контекста браузера для локального запуска.");
      if ("chromium".equals(browserType.name())) {
        BrowserType.LaunchPersistentContextOptions options = this.configuration.launchPersistentContextOptions();
        Path profile = this.generateProfile(prefs);
        this.context = browserType.launchPersistentContext(profile, options);
        this.browser = this.context.browser();
      } else {
        BrowserType.LaunchOptions launchOptions = this.configuration.launchOptions();
        launchOptions.setArgs(this.configuration.args());
        launchOptions.setFirefoxUserPrefs(prefs);
        this.browser = browserType.launch(launchOptions);
        Browser.NewContextOptions contextOptions = this.configuration.newContextOptions();
        this.context = this.browser.newContext(contextOptions);
      }
    }

    LOGGER.info("Открытие базовой страницы.");
    if (this.context.pages().isEmpty()) {
      this.page = this.context.newPage();
    } else {
      this.page = (Page)this.context.pages().get(0);
    }

    try {
      this.devToolsManager = DevToolsManager.create(this.context, this.page);
    } catch (PlaywrightException var5) {
      LOGGER.warn("Браузер не поддерживает CDP.");
    }

    this.page.navigate(this.configuration.getBaseUrl());
  }

  private Path generateProfile(Map<String, Object> prefs) {
    try {
      LOGGER.info("Создание профиля для Chromium браузера");
      Path tempDirectory = Files.createTempDirectory("playwright-");
      Path path = Files.createDirectories(tempDirectory.resolve("Default"));
      Path prefsPath = path.resolve("Preferences");
      Files.writeString(prefsPath, BrowserPreferenceUnwrapper.unwrap(prefs));
      Runtime.getRuntime().addShutdownHook(new Thread(() -> {
        try {
          FileUtils.deleteDirectory(tempDirectory.toFile());
        } catch (IOException e) {
          LOGGER.warn("Ошибка при удалении временной директории", e);
        }

      }));
      return tempDirectory;
    } catch (IOException var5) {
      throw ChromeProfileInitialization.exception("Не удалось создать профиль для браузера.");
    }
  }
}

public interface Service {

    /**
     * TODO: JavaDoc
     * Environment instance недоступен из метода Environment.getForCurrentThread().
     */
    void init(@NotNull Environment environment);

    /**
     * TODO: JavaDoc
     * Environment instance недоступен из метода Environment.getForCurrentThread().
     */
    void init(@NotNull Environment environment, @NotNull ServiceConfiguration configuration);

    /**
     * TODO: JavaDoc
     * Возможно, сделать вызов этого метода прямо перед тестов (после метода beforeEach() в тестовом классе
     * Environment instance доступен из метода Environment.getForCurrentThread().
     */
    default void beforeTest() {
        // do nothing
    }

    /**
     * TODO: JavaDoc
     * Тут работает линтер, например
     * Environment instance доступен из метода Environment.getForCurrentThread().
     */
    default void afterTest() {
        // do nothing
    }

    default <T extends ServiceConfiguration> T validate(ServiceConfiguration configuration, Class<T> classToValidate) {
        if (CastUtils.isSubtypeOf(configuration, classToValidate)) {
            return CastUtils.castObject(configuration, classToValidate);
        }
        throw IncorrectServiceConfiguration.exception(
                SERVICE_CONFIGURATION_NOT_VALID.getMessage(configuration.getClass().getCanonicalName(), this.getClass().getCanonicalName(), classToValidate.getCanonicalName()));
    }

}

public class PerfeccionistaExtension implements ParameterResolver, TestInstancePostProcessor,
        BeforeAllCallback, BeforeEachCallback, AfterEachCallback, AfterAllCallback,
        TestTemplateInvocationContextProvider, TestExecutionExceptionHandler, TestWatcher, ExtensionContext.Store.CloseableResource {
    private static final Logger logger = LoggerFactory.getLogger(PerfeccionistaExtension.class);

    private static boolean started = false;

    // Test Case by UniqueId
    protected Map<String, Environment> testCaseEnvironment = new HashMap<>();
    protected Map<String, Deque<TestExecutionResult>> testCaseResults = new HashMap<>();

    PerfeccionistaExtension() {}

    // Lifecycle methods

    @Override
    public void postProcessTestInstance(Object testInstance, ExtensionContext context) {
        // do nothing
    }


    @Override
    public void beforeAll(ExtensionContext context) {
        if (!started) {
            started = true;
            context.getRoot().getStore(GLOBAL).put("PerfeccionistaExtensionInstance", this);
        }
        // TODO: Тут нужна 2-х фазная подготовка.
        //  первым шагом находим конфигурацию для класса в котором выполняется BeforeAll, но не создаем Environment
        //  вторым шагом в параметр-резолвере проверяем, что в метод BeforeAll передается параметр Environment или сервис или стеш
        //  и тогда инициализируем Environment, если он есть и прокидываем в метод
//        Class<?> testClass = context.getTestClass()
//                .orElseThrow(() -> TestClassNotFound.exception(UNEXPECTED_TEST_CLASS_NOT_FOUND.getMessage()));
//        EnvironmentConfiguration environmentConfiguration = resolveEnvironmentConfiguration(testClass);

        // TODO: Сделать возможность использования Environment в BeforeAll/AfterAll методах
        //  с конфигурацией уровня тестового класса


    }

    @Override
    public void afterAll(ExtensionContext context) {

        // TODO: Сделать возможность использования Environment в BeforeAll/AfterAll методах
        //  с конфигурацией уровня тестового класса

    }

    /**
     * Устанавливает для теста сконфигурированный для него экземпляр Environment
     * Если требуемый экземпляр Environment был создан ранее и сконфигурирован как shared(),
     * то он устанавливается в качестве активного, иначе создается новый экземпляр.
     * Выполняем beforeEach() для заданного теста
     */
    @Override
    public void beforeEach(ExtensionContext context) {
        Class<?> testClass = context.getTestClass()
                .orElseThrow(() -> TestClassNotFound.exception(UNEXPECTED_TEST_CLASS_NOT_FOUND.getMessage()));
        Method testMethod = context.getTestMethod()
                .orElseThrow(() -> TestMethodNotFound.exception(UNEXPECTED_TEST_METHOD_NOT_FOUND.getMessage()));
        String testName = testClass.getCanonicalName() + "#" + testMethod.getName();

        EnvironmentConfiguration environmentConfiguration = resolveEnvironmentConfiguration(testMethod, testClass);
        Set<ConfiguredServiceHolder> externalServiceConfigurations = resolveExternalServiceConfigurations(testMethod, testClass);
        Environment environment = resolveActiveEnvironment(environmentConfiguration, externalServiceConfigurations, testName)
                .addRelatedObject("Context", context)
                .init();
        Environment.setForCurrentThread(environment);
        testCaseEnvironment.put(context.getUniqueId(), environment);
        environment.beforeTest();
    }

    @Override
    public void afterEach(ExtensionContext context) {
        Environment environment = testCaseEnvironment.get(context.getUniqueId());
        // TODO: Возможно, здесь нужен более изящный механизм вывода, который можно настраивать
        context.getExecutionException()
                .flatMap(throwable -> environment.getEnvironmentAttachment()
                        .getContent())
                .ifPresent(logger::info);
        environment.afterTest();
        // TODO: Работа в многопоточке может быть некорректной
        environment.removeForCurrentThread();
        environment.shutdown();
        testCaseEnvironment.remove(context.getUniqueId());
    }

    // Методы для передачи в тестовый метод экземпляра Environment

    @Override
    public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
        // TODO: Сделать более изящную проверку по типам: Environment, Service, DataSource, DataConverter
        if (Environment.class.isAssignableFrom(parameterContext.getParameter().getType())) {
            return true;
        }
        Environment environment = testCaseEnvironment.get(extensionContext.getUniqueId());
        Class<?> parameterType = parameterContext.getParameter().getType();
        return environment.getServiceClasses().stream()
                .anyMatch(serviceClass -> serviceClass.getCanonicalName().equals(parameterType.getCanonicalName()));
    }

    /**
     * @return Экземпляр {@link Environment} для текущего потока. Никогда не может быть {@code null}
     *
     *
     * TODO: Добавить возможность пробрасывать датасорсы
     *
     */
    @Override
    public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
        // TODO: Сделать более изящную проверку по типам: Environment, Service, DataSource, DataConverter
        // TODO: Возможны проблемы, если попробуют передать имплементацию Environment отличную по типу от типа параметра
        //  Нужно написать на это тесты
        Environment environment = testCaseEnvironment.get(extensionContext.getUniqueId());
        if (Environment.class.isAssignableFrom(parameterContext.getParameter().getType())) {
            return environment;
        }
        Class<?> parameterType = parameterContext.getParameter().getType();
        logger.debug("Test method parameter with type '" + parameterType.getCanonicalName() + "' resolved");
        // Здесь не может быть параметра с отличным от Service типом, так как он проверяется в supportsParameter
        return environment.getService((Class<? extends Service>) parameterType);
    }

    // Repeater methods

    /**
     * Check that test method contain {@link TestRepeatedOnCondition} annotation
     * @param extensionContext - encapsulates the context in which the current test or container is being executed
     * @return true/false
     */
    @Override
    public boolean supportsTestTemplate(ExtensionContext extensionContext) {
        return isAnnotated(extensionContext.getTestMethod(), TestRepeatedOnCondition.class);
    }

    /**
     * Context call TestTemplateInvocationContext
     * @param context - Test Class Context
     * @return Stream of TestTemplateInvocationContext
     * TODO: Test this
     */
    @Override
    public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContexts(ExtensionContext context) {
        RepeatPolicy repeatPolicy = new NoRepeatPolicy();
        Environment environment = testCaseEnvironment.get(context.getUniqueId());
        Optional<RepeatPolicyService> optionalRepeatPolicyService = environment.getOptionalService(RepeatPolicyService.class);
        if (optionalRepeatPolicyService.isPresent()) {
            repeatPolicy = optionalRepeatPolicyService.get().getRepeatPolicy();
        }
        Optional<TestRepeatedOnCondition> optionalAnnotation = context.getTestMethod()
                .flatMap(testMethods -> findAnnotation(testMethods, TestRepeatedOnCondition.class));
        if (optionalAnnotation.isPresent()) {
            Class<? extends RepeatPolicy> repeatPolicyClass = optionalAnnotation.get().value();
            if (!RepeatPolicy.class.equals(repeatPolicyClass)) {
                if (org.junit.platform.commons.util.ReflectionUtils.isAbstract(repeatPolicyClass) || repeatPolicyClass.isInterface()) {
                    throw RepeatPolicyInitialization.exception(CREATE_REPEAT_POLICY_INSTANCE_EXCEPTION.getMessage(repeatPolicyClass));
                }
                repeatPolicy = newInstance(repeatPolicyClass);
            }
        }

        Preconditions.condition(repeatPolicy.minAttempt() > 0, "Total repeats must be higher than 0");
        Preconditions.condition(repeatPolicy.maxAttempt() > 0, "Total minimum success must be higher than 0");

        Iterator<TestTemplateInvocationContext> iterator;

        switch (repeatPolicy.getRepeatMode()) {
            case REPEAT_IF: {
                iterator = new RepeatIfTestTemplateIterator(this, repeatPolicy, context);
                break;
            }
            case REPEAT_BEFORE: {
                iterator = new RepeatWhileTestTemplateIterator(this, repeatPolicy, context);
                break;
            }
            default: {
                iterator = new NoRepeatTestTemplateIterator(context.getDisplayName());
            }
        }

        return stream(spliteratorUnknownSize(iterator, Spliterator.NONNULL), false);
    }

    // Test result methods

    @Override
    public void testDisabled(ExtensionContext context, Optional<String> reason) {
        // do nothing, no need calculate condition for disabled test
    }

    @Override
    public void testSuccessful(ExtensionContext context) {
        getTestResults(context).addLast(TestExecutionResult.successful());
    }

    @Override
    public void testAborted(ExtensionContext context, Throwable cause) {
        getTestResults(context).addLast(TestExecutionResult.aborted(cause));
    }

    @Override
    public void testFailed(ExtensionContext context, Throwable cause) {
        getTestResults(context).addLast(TestExecutionResult.failed(cause));
    }

    @Override
    public void handleTestExecutionException(ExtensionContext context, Throwable throwable) throws Throwable {
        if (throwable instanceof PerfeccionistaException) {
            ((PerfeccionistaException) throwable).getAttachment().ifPresent(attachment -> {
                if (!attachment.isProcessed()) {
                    Environment environment = testCaseEnvironment.get(context.getUniqueId());
                    environment.getOptionalService(InvocationService.class).ifPresent(invocationService -> {
                        invocationService.getAttachmentProcessors()
                                .forEach(attachmentProcessor -> attachmentProcessor.processAttachment(attachment));
                    });
                }
                attachment.setProcessed(true);
            });
        }
        throw throwable;
    }

    // Методы необходимые для конфигурирования Environment

    /**
     * Создаем экземпляр {@link Environment} используя полученную конфигурацию и тестовый класс
     * @param environmentConfiguration экземпляр конфигурации {@link Environment}
     * @param <T> тип {@link Environment}
     * @return экземпляр {@link Environment}
     */
    @SuppressWarnings("WeakerAccess")
    protected <T extends Environment> T createEnvironment(@NotNull EnvironmentConfiguration environmentConfiguration,
                                                          @NotNull String testName) {
        Constructor<? extends Environment> constructor = ReflectionUtilsForClasses
                .getConstructor(environmentConfiguration.getEnvironmentClass(), EnvironmentConfiguration.class, String.class);
        // noinspection unchecked
        return (T) newInstance(constructor, environmentConfiguration, testName);
    }

    /**
     */
    protected Environment resolveActiveEnvironment(EnvironmentConfiguration environmentConfiguration,
                                            Set<ConfiguredServiceHolder> externalServiceConfigurations,
                                            String testName) {
        externalServiceConfigurations.forEach(environmentConfiguration::addOrOverrideServiceConfiguration);
        return createEnvironment(environmentConfiguration, testName);
    }

    public static Set<AttachmentProcessor> resolveAttachmentProcessors(Method testMethod, Class<?> testClass) {
        Set<Class<? extends AttachmentProcessor>> attachmentProcessors = new HashSet<>();

        findAnnotation(testMethod, SetAttachmentProcessor.class)
                .ifPresent(attachmentProcessorAnnotation ->
                        attachmentProcessors.addAll(Arrays.asList(attachmentProcessorAnnotation.value())));

        Class<?> processedClass = testClass;
        while (!Object.class.equals(processedClass)) {
            findAnnotation(processedClass, SetAttachmentProcessor.class)
                    .ifPresent(attachmentProcessorAnnotation ->
                            attachmentProcessors.addAll(Arrays.asList(attachmentProcessorAnnotation.value())));
            processedClass = processedClass.getSuperclass();
        }

        Set<AttachmentProcessor> result = new HashSet<>();
        attachmentProcessors.forEach(attachmentProcessorClass -> result.add(newInstance(attachmentProcessorClass)));
        return result;
    }

    /**
     * TODO: Test and JavaDoc
     */
    public @NotNull Deque<TestExecutionResult> getTestResults(ExtensionContext context) {
        Deque<TestExecutionResult> testResults = testCaseResults.get(context.getUniqueId());
        if (null == testResults) {
            Deque<TestExecutionResult> newTestResults = new ArrayDeque<>();
            testCaseResults.put(context.getUniqueId(), newTestResults);
            return newTestResults;
        }
        return testResults;
    }

    @Override
    public void close() {
        Environment.executeAfterAllHooks();
    }

}