diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 8305af83..cd957a43 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -28,6 +28,9 @@ jobs: FRONT: niffler-ng-client ALLURE_DOCKER_API: ${{ secrets.ALLURE_DOCKER_API }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BUILD_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + HEAD_COMMIT_MESSAGE: ${{ github.event.pull_request.head.sha || github.sha }} + EXECUTION_TYPE: github run: | : # build backends with profile `docker`, only for testing bash ./gradlew jibDockerBuild -x :niffler-e-2-e-tests:test || exit 1 diff --git a/README.md b/README.md index 59664708..c97a55fd 100644 --- a/README.md +++ b/README.md @@ -387,7 +387,7 @@ User-MacBook-Pro niffler % bash docker-compose-e2e.sh gql #### 3. Selenoid UI доступен по адресу: http://localhost:9090/ -#### 4. Allure доступен по адресу: http://localhost:5050/allure-docker-service/projects/niffler/reports/latest/index.html +#### 4. Allure доступен по адресу: http://localhost:5050/allure-docker-service/projects/niffler-ng/reports/latest/index.html #### 5. OpenAPI (Swagger) доступен по адресу: http://localhost:8090/swagger-ui/index.html diff --git a/docker-compose-e2e.sh b/docker-compose-e2e.sh index e12a93de..af71d41e 100644 --- a/docker-compose-e2e.sh +++ b/docker-compose-e2e.sh @@ -3,6 +3,7 @@ source ./docker.properties export PROFILE=docker export PREFIX="${IMAGE_PREFIX}" export ALLURE_DOCKER_API=http://allure:5050/ +export HEAD_COMMIT_MESSAGE="local build" export ARCH=$(uname -m) echo '### Java version ###' diff --git a/docker-compose.test.yml b/docker-compose.test.yml index a38ab454..c8e07706 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -163,9 +163,15 @@ services: DOCKER: eclipse-temurin:21-jdk ALLURE_DOCKER_API: ${ALLURE_DOCKER_API} GITHUB_TOKEN: ${GITHUB_TOKEN} + BUILD_URL: ${BUILD_URL} + HEAD_COMMIT_MESSAGE: ${HEAD_COMMIT_MESSAGE} + EXECUTION_TYPE: ${EXECUTION_TYPE} environment: - ALLURE_DOCKER_API=${ALLURE_DOCKER_API} - GITHUB_TOKEN=${GITHUB_TOKEN} + - BUILD_URL=${BUILD_URL} + - HEAD_COMMIT_MESSAGE=${HEAD_COMMIT_MESSAGE} + - EXECUTION_TYPE=${EXECUTION_TYPE} depends_on: frontend.niffler.dc: condition: service_started @@ -178,7 +184,7 @@ services: depends_on: - niffler-e-2-e environment: - CHECK_RESULTS_EVERY_SECONDS: 3 + CHECK_RESULTS_EVERY_SECONDS: NONE KEEP_HISTORY: 1 ports: - 5050:5050 diff --git a/niffler-e-2-e-tests/Dockerfile b/niffler-e-2-e-tests/Dockerfile index 753035c3..066eb105 100644 --- a/niffler-e-2-e-tests/Dockerfile +++ b/niffler-e-2-e-tests/Dockerfile @@ -1,11 +1,17 @@ ARG DOCKER ARG ALLURE_DOCKER_API ARG GITHUB_TOKEN +ARG BUILD_URL +ARG HEAD_COMMIT_MESSAGE +ARG EXECUTION_TYPE FROM ${DOCKER} ENV ALLURE_DOCKER_API=${ALLURE_DOCKER_API} ENV GITHUB_TOKEN=${GITHUB_TOKEN} +ENV BUILD_URL=${BUILD_URL} +ENV HEAD_COMMIT_MESSAGE=${HEAD_COMMIT_MESSAGE} +ENV EXECUTION_TYPE=${EXECUTION_TYPE} WORKDIR /niffler COPY ./gradle ./gradle diff --git a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/api/AllureDockerApi.java b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/api/AllureDockerApi.java index 8ab4df04..b77a3655 100644 --- a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/api/AllureDockerApi.java +++ b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/api/AllureDockerApi.java @@ -19,6 +19,15 @@ Call uploadResults(@Query("project_id") String projectId, @GET("allure-docker-service/projects/{project_id}") Call project(@Path("project_id") String projectId); + @GET("allure-docker-service/clean-results") + Call cleanResults(@Query("project_id") String projectId); + + @GET("allure-docker-service/generate-report") + Call generateReport(@Query("project_id") String projectId, + @Query("execution_name") String executionName, + @Query(value = "execution_from", encoded = true) String executionFrom, + @Query("execution_type") String executionType); + @POST("allure-docker-service/projects") Call createProject(@Body AllureProject project); } diff --git a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/api/AllureDockerApiClient.java b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/api/AllureDockerApiClient.java index ea7b1617..a949bf4c 100644 --- a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/api/AllureDockerApiClient.java +++ b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/api/AllureDockerApiClient.java @@ -20,6 +20,17 @@ public AllureDockerApiClient() { this.allureDockerApi = retrofit.create(AllureDockerApi.class); } + public void clean(String projectId) throws IOException { + allureDockerApi.cleanResults(projectId).execute(); + } + + public void generateReport(String projectId, + String executionName, + String executionFrom, + String executionType) throws IOException { + allureDockerApi.generateReport(projectId, executionName, executionFrom, executionType).execute(); + } + public void sendResultsToAllure(String projectId, AllureResults allureResults) throws IOException { int code = allureDockerApi.uploadResults( projectId, diff --git a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/annotation/meta/GqlTest.java b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/annotation/meta/GqlTest.java index abbbed9d..32c65890 100644 --- a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/annotation/meta/GqlTest.java +++ b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/annotation/meta/GqlTest.java @@ -1,6 +1,5 @@ package guru.qa.niffler.jupiter.annotation.meta; -import guru.qa.niffler.jupiter.extension.ClearCookiesExtension; import guru.qa.niffler.jupiter.extension.DatabaseCreateUserExtension; import guru.qa.niffler.jupiter.extension.GqlReqResolver; import io.qameta.allure.junit5.AllureJunit5; @@ -16,7 +15,6 @@ @ExtendWith({ DatabaseCreateUserExtension.class, GqlReqResolver.class, - ClearCookiesExtension.class, AllureJunit5.class}) public @interface GqlTest { diff --git a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/annotation/meta/KafkaTest.java b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/annotation/meta/KafkaTest.java index ea54c319..61820c92 100644 --- a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/annotation/meta/KafkaTest.java +++ b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/annotation/meta/KafkaTest.java @@ -1,6 +1,5 @@ package guru.qa.niffler.jupiter.annotation.meta; -import guru.qa.niffler.jupiter.extension.ClearCookiesExtension; import guru.qa.niffler.jupiter.extension.DatabaseCreateUserExtension; import guru.qa.niffler.jupiter.extension.KafkaExtension; import io.qameta.allure.junit5.AllureJunit5; @@ -16,7 +15,6 @@ @ExtendWith({ KafkaExtension.class, DatabaseCreateUserExtension.class, - ClearCookiesExtension.class, AllureJunit5.class }) public @interface KafkaTest { diff --git a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/annotation/meta/RestTest.java b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/annotation/meta/RestTest.java index 9deceb81..34706cd8 100644 --- a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/annotation/meta/RestTest.java +++ b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/annotation/meta/RestTest.java @@ -1,6 +1,5 @@ package guru.qa.niffler.jupiter.annotation.meta; -import guru.qa.niffler.jupiter.extension.ClearCookiesExtension; import guru.qa.niffler.jupiter.extension.DatabaseCreateUserExtension; import io.qameta.allure.junit5.AllureJunit5; import org.junit.jupiter.api.extension.ExtendWith; @@ -14,7 +13,6 @@ @Target(ElementType.TYPE) @ExtendWith({ DatabaseCreateUserExtension.class, - ClearCookiesExtension.class, AllureJunit5.class }) public @interface RestTest { diff --git a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/annotation/meta/SoapTest.java b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/annotation/meta/SoapTest.java index a9702af9..a5126fa4 100644 --- a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/annotation/meta/SoapTest.java +++ b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/annotation/meta/SoapTest.java @@ -1,6 +1,5 @@ package guru.qa.niffler.jupiter.annotation.meta; -import guru.qa.niffler.jupiter.extension.ClearCookiesExtension; import guru.qa.niffler.jupiter.extension.DatabaseCreateUserExtension; import io.qameta.allure.junit5.AllureJunit5; import org.junit.jupiter.api.extension.ExtendWith; @@ -14,7 +13,6 @@ @Target(ElementType.TYPE) @ExtendWith({ DatabaseCreateUserExtension.class, - ClearCookiesExtension.class, AllureJunit5.class }) public @interface SoapTest { diff --git a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/annotation/meta/WebTest.java b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/annotation/meta/WebTest.java index ba08cb9c..4e558153 100644 --- a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/annotation/meta/WebTest.java +++ b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/annotation/meta/WebTest.java @@ -2,7 +2,6 @@ import guru.qa.niffler.jupiter.extension.ApiLoginExtension; import guru.qa.niffler.jupiter.extension.BrowserExtension; -import guru.qa.niffler.jupiter.extension.ClearCookiesExtension; import guru.qa.niffler.jupiter.extension.DatabaseCreateUserExtension; import io.qameta.allure.junit5.AllureJunit5; import org.junit.jupiter.api.extension.ExtendWith; @@ -18,7 +17,6 @@ BrowserExtension.class, DatabaseCreateUserExtension.class, ApiLoginExtension.class, - ClearCookiesExtension.class, AllureJunit5.class }) public @interface WebTest { diff --git a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/extension/AllureDockerExtension.java b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/extension/AllureDockerExtension.java index e6123622..17cdbfc3 100644 --- a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/extension/AllureDockerExtension.java +++ b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/extension/AllureDockerExtension.java @@ -3,6 +3,8 @@ import guru.qa.niffler.api.AllureDockerApiClient; import guru.qa.niffler.model.allure.AllureResults; import guru.qa.niffler.model.allure.DecodedAllureFile; +import lombok.SneakyThrows; +import org.junit.jupiter.api.extension.ExtensionContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -25,6 +27,15 @@ public class AllureDockerExtension implements SuiteExtension { private static final AllureDockerApiClient allureDockerApiClient = new AllureDockerApiClient(); + @Override + @SneakyThrows + public void beforeSuite(ExtensionContext context) { + if ("docker".equals(System.getProperty("test.env"))) { + allureDockerApiClient.createProjectIfNotExist(projectId); + allureDockerApiClient.clean(projectId); + } + } + @Override public void afterSuite() { if ("docker".equals(System.getProperty("test.env"))) { @@ -48,6 +59,12 @@ public void afterSuite() { filesToSend ) ); + allureDockerApiClient.generateReport( + projectId, + System.getenv("HEAD_COMMIT_MESSAGE"), + System.getenv("BUILD_URL"), + System.getenv("EXECUTION_TYPE") + ); } catch (IOException e) { throw new RuntimeException(e); } diff --git a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/extension/ApiLoginExtension.java b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/extension/ApiLoginExtension.java index 9dcd5fec..44283e43 100644 --- a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/extension/ApiLoginExtension.java +++ b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/extension/ApiLoginExtension.java @@ -78,13 +78,20 @@ public void beforeEach(ExtensionContext context) throws Exception { } setCodeVerifier(context, OauthUtils.codeVerifier()); setCodeChallenge(context, OauthUtils.codeChallenge(getCodeVerifier(context))); - authClient.login(context, userToLogin.username(), userToLogin.testData().password()); - - if (setUpBrowser) { - Selenide.open(CFG.frontUrl()); - Selenide.localStorage().setItem("id_token", getToken(context)); - WebDriverRunner.getWebDriver().manage().addCookie(getJsessionIdCookie()); - Selenide.open(CFG.frontUrl(), MainPage.class).waitForPageLoaded(); + try { + authClient.login(context, userToLogin.username(), userToLogin.testData().password()); + if (setUpBrowser) { + Selenide.open(CFG.frontUrl()); + Selenide.localStorage().setItem("id_token", getToken(context)); + WebDriverRunner.getWebDriver().manage().addCookie(getJsessionIdCookie()); + Selenide.open(MainPage.URL, MainPage.class) + .waitForPageLoaded(); + } + } catch (Exception e) { + BrowserExtension.doScreen(); + throw e; + } finally { + ThreadLocalCookieStore.INSTANCE.removeAll(); } } } diff --git a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/extension/BrowserExtension.java b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/extension/BrowserExtension.java index 693a041f..04b604c3 100644 --- a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/extension/BrowserExtension.java +++ b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/extension/BrowserExtension.java @@ -19,25 +19,19 @@ public class BrowserExtension implements @Override public void handleTestExecutionException(ExtensionContext context, Throwable throwable) throws Throwable { - if (WebDriverRunner.hasWebDriverStarted()) { - doScreen(); - } + doScreen(); throw throwable; } @Override public void handleBeforeEachMethodExecutionException(ExtensionContext context, Throwable throwable) throws Throwable { - if (WebDriverRunner.hasWebDriverStarted()) { - doScreen(); - } + doScreen(); throw throwable; } @Override public void handleAfterEachMethodExecutionException(ExtensionContext context, Throwable throwable) throws Throwable { - if (WebDriverRunner.hasWebDriverStarted()) { - doScreen(); - } + doScreen(); throw throwable; } @@ -48,9 +42,11 @@ public void afterEach(ExtensionContext context) { } } - private void doScreen() { - Allure.addAttachment("Screenshot on fail", - new ByteArrayInputStream(((TakesScreenshot) WebDriverRunner.getWebDriver()) - .getScreenshotAs(OutputType.BYTES))); + static void doScreen() { + if (WebDriverRunner.hasWebDriverStarted()) { + Allure.addAttachment("Screenshot on fail", + new ByteArrayInputStream(((TakesScreenshot) WebDriverRunner.getWebDriver()) + .getScreenshotAs(OutputType.BYTES))); + } } } diff --git a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/extension/ClearCookiesExtension.java b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/extension/CookiesExtension.java similarity index 52% rename from niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/extension/ClearCookiesExtension.java rename to niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/extension/CookiesExtension.java index 5c3e952a..04480948 100644 --- a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/extension/ClearCookiesExtension.java +++ b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/extension/CookiesExtension.java @@ -1,13 +1,12 @@ package guru.qa.niffler.jupiter.extension; import guru.qa.niffler.api.service.ThreadLocalCookieStore; -import org.junit.jupiter.api.extension.AfterTestExecutionCallback; +import org.junit.jupiter.api.extension.AfterEachCallback; import org.junit.jupiter.api.extension.ExtensionContext; -public class ClearCookiesExtension implements AfterTestExecutionCallback { - +public class CookiesExtension implements AfterEachCallback { @Override - public void afterTestExecution(ExtensionContext context) { + public void afterEach(ExtensionContext context) throws Exception { ThreadLocalCookieStore.INSTANCE.removeAll(); } } diff --git a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/extension/RestCreateUserExtension.java b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/extension/RestCreateUserExtension.java index ddc2ea58..fb963592 100644 --- a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/extension/RestCreateUserExtension.java +++ b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/extension/RestCreateUserExtension.java @@ -37,8 +37,11 @@ public class RestCreateUserExtension extends AbstractCreateUserExtension { @Nonnull protected UserJson createUser(@Nonnull String username, @Nonnull String password) throws Exception { - authClient.register(username, password); - ThreadLocalCookieStore.INSTANCE.removeAll(); + try { + authClient.register(username, password); + } finally { + ThreadLocalCookieStore.INSTANCE.removeAll(); + } UserJson currentUser = waitWhileUserToBeConsumed(username, 10000L); return currentUser.addTestData(new TestData(password)); } diff --git a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/page/FriendsPage.java b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/page/FriendsPage.java index 614d7cf3..f87f136a 100644 --- a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/page/FriendsPage.java +++ b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/page/FriendsPage.java @@ -1,6 +1,5 @@ package guru.qa.niffler.page; -import com.codeborne.selenide.ClickOptions; import com.codeborne.selenide.Condition; import com.codeborne.selenide.SelenideElement; import guru.qa.niffler.page.component.SearchField; diff --git a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/page/component/SpendingTable.java b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/page/component/SpendingTable.java index ce9a5d56..8b80c86c 100644 --- a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/page/component/SpendingTable.java +++ b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/page/component/SpendingTable.java @@ -1,6 +1,5 @@ package guru.qa.niffler.page.component; -import com.codeborne.selenide.ClickOptions; import com.codeborne.selenide.ElementsCollection; import com.codeborne.selenide.SelenideElement; import guru.qa.niffler.model.rest.DataFilterValues; diff --git a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/test/kafka/AuthRegistrationKafkaTest.java b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/test/kafka/AuthRegistrationKafkaTest.java index a8d4ed1e..ee595423 100644 --- a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/test/kafka/AuthRegistrationKafkaTest.java +++ b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/test/kafka/AuthRegistrationKafkaTest.java @@ -1,6 +1,7 @@ package guru.qa.niffler.test.kafka; import guru.qa.niffler.api.AuthApiClient; +import guru.qa.niffler.api.service.ThreadLocalCookieStore; import guru.qa.niffler.kafka.KafkaConsumer; import guru.qa.niffler.model.rest.UserJson; import guru.qa.niffler.utils.DataUtils; @@ -28,7 +29,12 @@ void messageShouldBeProducedToKafkaAfterSuccessfulRegistration() throws Exceptio final String username = DataUtils.generateRandomUsername(); final String password = DataUtils.generateRandomPassword(); - authClient.register(username, password); + try { + authClient.register(username, password); + } finally { + ThreadLocalCookieStore.INSTANCE.removeAll(); + } + final UserJson messageFromKafka = KafkaConsumer.getMessage(username, 10000L); step("Check that message from kafka exist", () -> diff --git a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/test/web/FriendsTest.java b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/test/web/FriendsTest.java index a2b68dfb..66616d1f 100644 --- a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/test/web/FriendsTest.java +++ b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/test/web/FriendsTest.java @@ -11,7 +11,6 @@ import io.qameta.allure.AllureId; import io.qameta.allure.Epic; import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; diff --git a/niffler-e-2-e-tests/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension b/niffler-e-2-e-tests/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension index 5d11c58c..65b0b1b7 100644 --- a/niffler-e-2-e-tests/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension +++ b/niffler-e-2-e-tests/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension @@ -1,3 +1,4 @@ guru.qa.niffler.jupiter.extension.ContextHolderExtension +guru.qa.niffler.jupiter.extension.CookiesExtension guru.qa.niffler.jupiter.extension.JpaExtension guru.qa.niffler.jupiter.extension.AllureDockerExtension diff --git a/niffler-ng-client/README.md b/niffler-ng-client/README.md index f5033d01..43381a1b 100644 --- a/niffler-ng-client/README.md +++ b/niffler-ng-client/README.md @@ -17,13 +17,13 @@ If you are developing a production application, we recommend updating the config ```js export default { - // other rules... - parserOptions: { - ecmaVersion: 'latest', - sourceType: 'module', - project: ['./tsconfig.json', './tsconfig.node.json'], - tsconfigRootDir: __dirname, - }, + // other rules... + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + project: ['./tsconfig.json', './tsconfig.node.json'], + tsconfigRootDir: __dirname, + }, } ```