diff --git a/components/api/api-modules-javascript/pom.xml b/components/api/api-modules-javascript/pom.xml index 207eda6058a..c749d1879ff 100644 --- a/components/api/api-modules-javascript/pom.xml +++ b/components/api/api-modules-javascript/pom.xml @@ -1,6 +1,6 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 @@ -13,7 +13,7 @@ Components - API - Modules dirigible-components-api-modules-javascript jar - + org.webjars @@ -26,6 +26,10 @@ stomp__stompjs ${stompjs.version} + + org.junit.jupiter + junit-jupiter + @@ -79,4 +83,4 @@ ../../../ - + \ No newline at end of file diff --git a/components/api/api-modules-javascript/src/main/resources/META-INF/dirigible/modules/src/junit/junit.ts b/components/api/api-modules-javascript/src/main/resources/META-INF/dirigible/modules/src/junit/junit.ts index 3c4eef5d109..812356a0897 100644 --- a/components/api/api-modules-javascript/src/main/resources/META-INF/dirigible/modules/src/junit/junit.ts +++ b/components/api/api-modules-javascript/src/main/resources/META-INF/dirigible/modules/src/junit/junit.ts @@ -9,6 +9,7 @@ * ### Key Features: * - **Test Definition**: The `test` function allows developers to define individual test cases with a descriptive name and a function containing the test logic. * - **Assertions**: Functions such as `assertEquals`, `assertNotEquals`, `assertTrue`, `assertFalse`, and `fail` provide a variety of assertion methods to validate conditions and compare values within tests. + * - **Result Tracking**: Test results are automatically captured and can be retrieved via the results API. * * ### Use Cases: * - **Unit Testing**: These utilities are primarily used for writing unit tests to verify the functionality of individual components or functions in isolation. @@ -16,21 +17,112 @@ * * ### Example Usage: * ```ts - * import { test, assertEquals, assertTrue } from "@aerokit/sdk/junit"; + * import { test, assertEquals, assertTrue, storeResults } from "@aerokit/sdk/junit"; * * test("should add two numbers correctly", () => { - * const result = add(2, 3); + * const result = 2 + 3; * assertEquals(5, result); * }); * * test("should return true for valid input", () => { - * const isValid = validateInput("valid input"); + * const isValid = false; // Replace with actual validation logic * assertTrue(isValid); * }); + * + * // After running tests, store results for later retrieval + * storeResults(); * ``` */ -const Assert = Java.type('org.junit.Assert'); +import { Response } from '@aerokit/sdk/http'; + +const Assertions = Java.type('org.junit.jupiter.api.Assertions'); +const TestResultsService = Java.type('org.eclipse.dirigible.components.ide.junit.service.TestResultsService'); +const ArrayList = Java.type('java.util.ArrayList'); +const TestResult = Java.type('org.eclipse.dirigible.components.ide.junit.domain.TestResult'); + +// Test Result Tracking +interface TestResult { + name: string; + status: 'passed' | 'failed' | 'skipped'; + duration: number; + error?: string; + stackTrace?: string; + timestamp: number; +} + +class TestResultCollector { + private results: TestResult[] = []; + private currentTest: { name: string; startTime: number } | null = null; + private haveFailedTests: boolean = false; + + public hasFailedTests(): boolean { + return this.haveFailedTests; + } + + public recordTestStart(name: string): void { + this.currentTest = { name, startTime: new Date().getTime() }; + } + + public recordTestComplete(status: 'passed' | 'failed' | 'skipped', error?: { message: string; stackTrace?: string }): void { + if (!this.currentTest) return; + + const result: TestResult = { + name: this.currentTest.name, + status, + duration: new Date().getTime() - this.currentTest.startTime, + timestamp: new Date().getTime(), + }; + + if (error) { + result.error = error.message; + result.stackTrace = error.stackTrace; + } + + this.results.push(result); + this.currentTest = null; + + if (status === 'failed' || error) { + this.haveFailedTests = true; + } + } + + public getResults(): TestResult[] { + return [...this.results]; + } + + public storeResults(): void { + const resultsList = new ArrayList(); + this.results.forEach(e => resultsList.add(new TestResult(e.name, e.status, e.duration, e.error, e.stackTrace, e.timestamp))); + TestResultsService.get().storeResults(resultsList); + Response.sendRedirect('/services/web/ide-junit-results/junit-results'); + } + + public getSummary(): { + total: number; + passed: number; + failed: number; + skipped: number; + duration: number; + } { + const results = this.results; + const total = results.length; + const passed = results.filter(r => r.status === 'passed').length; + const failed = results.filter(r => r.status === 'failed').length; + const skipped = results.filter(r => r.status === 'skipped').length; + const duration = results.reduce((sum, r) => sum + r.duration, 0); + + return { total, passed, failed, skipped, duration }; + } + + public clearResults(): void { + this.results = []; + this.currentTest = null; + this.haveFailedTests = false; + } +} + +const resultCollector = new TestResultCollector(); /** * Defines a test case. @@ -39,8 +131,21 @@ const Assert = Java.type('org.junit.Assert'); * @param testFn The function containing the test logic and assertions. */ export function test(name: string, testFn: () => void) { - // Calls the global test runner function provided by the SDK environment. - (globalThis as any).test(name, testFn); + resultCollector.recordTestStart(name); + if (resultCollector.hasFailedTests()) { + resultCollector.recordTestComplete('skipped'); + console.log(`- Skipping test "${name}" due to previous failures`); + } else { + try { + testFn(); + resultCollector.recordTestComplete('passed'); + } catch (error: any) { + resultCollector.recordTestComplete('failed', { + message: error?.message || String(error), + stackTrace: error?.stack, + }); + } + } } /** @@ -55,9 +160,9 @@ export function assertEquals(expected: T, actual: T): void export function assertEquals(message: string, expected: T, actual: T): void export function assertEquals(messageOrExpected?: string | T, expectedOrActual?: T, actualOrUndefined?: T): void { if (arguments.length === 3) { - Assert.assertEquals(messageOrExpected, expectedOrActual, actualOrUndefined); + Assertions.assertEquals(messageOrExpected, expectedOrActual, actualOrUndefined); } else { - Assert.assertEquals(messageOrExpected, expectedOrActual); + Assertions.assertEquals(messageOrExpected, expectedOrActual); } } @@ -73,9 +178,9 @@ export function assertNotEquals(unexpected: T, actual: T): void export function assertNotEquals(message: string, unexpected: T, actual: T): void export function assertNotEquals(messageOrUnexpected?: string | T, unexpectedOrActual?: T, actualOrUndefined?: T): void { if (arguments.length === 3) { - Assert.assertNotEquals(messageOrUnexpected, unexpectedOrActual, actualOrUndefined); + Assertions.assertNotEquals(messageOrUnexpected, unexpectedOrActual, actualOrUndefined); } else { - Assert.assertNotEquals(messageOrUnexpected, unexpectedOrActual); + Assertions.assertNotEquals(messageOrUnexpected, unexpectedOrActual); } } @@ -89,9 +194,9 @@ export function assertTrue(condition: boolean): void export function assertTrue(message: string, condition: boolean): void export function assertTrue(messageOrCondition?: string | boolean, conditionOrUndefined?: boolean): void { if (arguments.length === 2) { - Assert.assertTrue(messageOrCondition, conditionOrUndefined); + Assertions.assertTrue(messageOrCondition, conditionOrUndefined); } else { - Assert.assertTrue(messageOrCondition); + Assertions.assertTrue(messageOrCondition); } } @@ -105,9 +210,9 @@ export function assertFalse(condition: boolean): void export function assertFalse(message: string, condition: boolean): void export function assertFalse(messageOrCondition?: string | boolean, conditionOrUndefined?: boolean): void { if (arguments.length === 2) { - Assert.assertFalse(messageOrCondition, conditionOrUndefined); + Assertions.assertFalse(messageOrCondition, conditionOrUndefined); } else { - Assert.assertFalse(messageOrCondition); + Assertions.assertFalse(messageOrCondition); } } @@ -120,8 +225,53 @@ export function fail(): void export function fail(message: string): void export function fail(message?: string): void { if (message) { - Assert.fail(message); + Assertions.fail(message); } else { - Assert.fail(); + Assertions.fail(); } +} + +/** + * Retrieves all test results collected during this session. + * + * @returns An array of test results. + */ +export function getResults(): Array<{ + name: string; + status: 'passed' | 'failed' | 'skipped'; + duration: number; + error?: string; + stackTrace?: string; + timestamp: number; +}> { + return resultCollector.getResults(); +} + +/** + * Retrieves a summary of test execution. + * + * @returns An object containing counts and total duration. + */ +export function getResultsSummary(): { + total: number; + passed: number; + failed: number; + skipped: number; + duration: number; +} { + return resultCollector.getSummary(); +} + +/** + * Clears all collected test results. + */ +export function clearResults(): void { + resultCollector.clearResults(); +} + +/** + * Clears all collected test results. + */ +export function storeResults(): void { + resultCollector.storeResults(); } \ No newline at end of file diff --git a/components/data/data-export/src/main/java/org/eclipse/dirigible/components/data/export/service/DataAsyncExportService.java b/components/data/data-export/src/main/java/org/eclipse/dirigible/components/data/export/service/DataAsyncExportService.java index 2c1ed45d2a9..aaff62c1c1e 100644 --- a/components/data/data-export/src/main/java/org/eclipse/dirigible/components/data/export/service/DataAsyncExportService.java +++ b/components/data/data-export/src/main/java/org/eclipse/dirigible/components/data/export/service/DataAsyncExportService.java @@ -1,3 +1,12 @@ +/* + * Copyright (c) 2010-2026 Eclipse Dirigible contributors + * + * All rights reserved. This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0 + */ package org.eclipse.dirigible.components.data.export.service; import static java.text.MessageFormat.format; diff --git a/components/group/group-ide/pom.xml b/components/group/group-ide/pom.xml index bb43e2f30d2..cf2c2c7f259 100644 --- a/components/group/group-ide/pom.xml +++ b/components/group/group-ide/pom.xml @@ -24,6 +24,10 @@ org.eclipse.dirigible dirigible-components-ide-git + + org.eclipse.dirigible + dirigible-components-ide-junit-results + org.eclipse.dirigible dirigible-components-ide-template diff --git a/components/ide/ide-junit-results/pom.xml b/components/ide/ide-junit-results/pom.xml new file mode 100644 index 00000000000..701e0ed5638 --- /dev/null +++ b/components/ide/ide-junit-results/pom.xml @@ -0,0 +1,39 @@ + + + 4.0.0 + + + org.eclipse.dirigible + dirigible-components-parent + 13.0.0-SNAPSHOT + ../../pom.xml + + + Components - IDE - JUnit Results + dirigible-components-ide-junit-results + jar + + + + + + org.eclipse.dirigible + dirigible-components-core-base + + + + + org.eclipse.dirigible + dirigible-components-api-security + + + + + + ../../../licensing-header.txt + ../../../ + + + diff --git a/components/ide/ide-junit-results/src/main/java/org/eclipse/dirigible/components/ide/junit/domain/TestResult.java b/components/ide/ide-junit-results/src/main/java/org/eclipse/dirigible/components/ide/junit/domain/TestResult.java new file mode 100644 index 00000000000..ccf9f0bf863 --- /dev/null +++ b/components/ide/ide-junit-results/src/main/java/org/eclipse/dirigible/components/ide/junit/domain/TestResult.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2010-2026 Eclipse Dirigible contributors + * + * All rights reserved. This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.dirigible.components.ide.junit.domain; + +/** + * Represents a single test result. + */ +public class TestResult { + + private String name; + private String status; // passed, failed, skipped + private long duration; + private String error; + private String stackTrace; + private long timestamp; + + /** + * Default constructor. + */ + public TestResult() {} + + /** + * Constructor with all fields. + */ + public TestResult(String name, String status, long duration, String error, String stackTrace, long timestamp) { + this.name = name; + this.status = status; + this.duration = duration; + this.error = error; + this.stackTrace = stackTrace; + this.timestamp = timestamp; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public long getDuration() { + return duration; + } + + public void setDuration(long duration) { + this.duration = duration; + } + + public String getError() { + return error; + } + + public void setError(String error) { + this.error = error; + } + + public String getStackTrace() { + return stackTrace; + } + + public void setStackTrace(String stackTrace) { + this.stackTrace = stackTrace; + } + + public long getTimestamp() { + return timestamp; + } + + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + } + + @Override + public String toString() { + return "TestResult{" + "name='" + name + '\'' + ", status='" + status + '\'' + ", duration=" + duration + ", error='" + error + '\'' + + ", stackTrace='" + stackTrace + '\'' + ", timestamp=" + timestamp + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + TestResult that = (TestResult) o; + + if (duration != that.duration) + return false; + if (timestamp != that.timestamp) + return false; + if (name != null ? !name.equals(that.name) : that.name != null) + return false; + if (status != null ? !status.equals(that.status) : that.status != null) + return false; + if (error != null ? !error.equals(that.error) : that.error != null) + return false; + return stackTrace != null ? stackTrace.equals(that.stackTrace) : that.stackTrace == null; + } + + @Override + public int hashCode() { + int result = name != null ? name.hashCode() : 0; + result = 31 * result + (status != null ? status.hashCode() : 0); + result = 31 * result + (int) (duration ^ (duration >>> 32)); + result = 31 * result + (error != null ? error.hashCode() : 0); + result = 31 * result + (stackTrace != null ? stackTrace.hashCode() : 0); + result = 31 * result + (int) (timestamp ^ (timestamp >>> 32)); + return result; + } + +} diff --git a/components/ide/ide-junit-results/src/main/java/org/eclipse/dirigible/components/ide/junit/domain/TestResultsSummary.java b/components/ide/ide-junit-results/src/main/java/org/eclipse/dirigible/components/ide/junit/domain/TestResultsSummary.java new file mode 100644 index 00000000000..b308bc042ca --- /dev/null +++ b/components/ide/ide-junit-results/src/main/java/org/eclipse/dirigible/components/ide/junit/domain/TestResultsSummary.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2010-2026 Eclipse Dirigible contributors + * + * All rights reserved. This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.dirigible.components.ide.junit.domain; + +/** + * Represents a summary of test execution results. + */ +public class TestResultsSummary { + + private int total; + private int passed; + private int failed; + private int skipped; + private long duration; + + /** + * Default constructor. + */ + public TestResultsSummary() {} + + /** + * Constructor with all fields. + */ + public TestResultsSummary(int total, int passed, int failed, int skipped, long duration) { + this.total = total; + this.passed = passed; + this.failed = failed; + this.skipped = skipped; + this.duration = duration; + } + + public int getTotal() { + return total; + } + + public void setTotal(int total) { + this.total = total; + } + + public int getPassed() { + return passed; + } + + public void setPassed(int passed) { + this.passed = passed; + } + + public int getFailed() { + return failed; + } + + public void setFailed(int failed) { + this.failed = failed; + } + + public int getSkipped() { + return skipped; + } + + public void setSkipped(int skipped) { + this.skipped = skipped; + } + + public long getDuration() { + return duration; + } + + public void setDuration(long duration) { + this.duration = duration; + } + + @Override + public String toString() { + return "TestResultsSummary{" + "total=" + total + ", passed=" + passed + ", failed=" + failed + ", skipped=" + skipped + + ", duration=" + duration + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + TestResultsSummary that = (TestResultsSummary) o; + + if (total != that.total) + return false; + if (passed != that.passed) + return false; + if (failed != that.failed) + return false; + if (skipped != that.skipped) + return false; + return duration == that.duration; + } + + @Override + public int hashCode() { + int result = total; + result = 31 * result + passed; + result = 31 * result + failed; + result = 31 * result + skipped; + result = 31 * result + (int) (duration ^ (duration >>> 32)); + return result; + } + +} diff --git a/components/ide/ide-junit-results/src/main/java/org/eclipse/dirigible/components/ide/junit/endpoint/TestResultsEndpoint.java b/components/ide/ide-junit-results/src/main/java/org/eclipse/dirigible/components/ide/junit/endpoint/TestResultsEndpoint.java new file mode 100644 index 00000000000..adc3e55f9fe --- /dev/null +++ b/components/ide/ide-junit-results/src/main/java/org/eclipse/dirigible/components/ide/junit/endpoint/TestResultsEndpoint.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2010-2026 Eclipse Dirigible contributors + * + * All rights reserved. This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.dirigible.components.ide.junit.endpoint; + +import java.util.List; + +import org.eclipse.dirigible.components.base.endpoint.BaseEndpoint; +import org.eclipse.dirigible.components.ide.junit.domain.TestResult; +import org.eclipse.dirigible.components.ide.junit.domain.TestResultsSummary; +import org.eclipse.dirigible.components.ide.junit.service.TestResultsService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import jakarta.annotation.security.RolesAllowed; + +/** + * REST endpoint for managing test results. + */ +@RestController +@RequestMapping(BaseEndpoint.PREFIX_ENDPOINT_IDE + "junit-results") +@RolesAllowed({"ADMINISTRATOR", "DEVELOPER"}) +public class TestResultsEndpoint { + + @Autowired + private TestResultsService testResultsService; + + /** + * Get all test results. + * + * @return list of test results + */ + @GetMapping + public ResponseEntity> getResults() { + return ResponseEntity.ok(testResultsService.getResults()); + } + + /** + * Get test results summary. + * + * @return test results summary + */ + @GetMapping("/summary") + public ResponseEntity getSummary() { + return ResponseEntity.ok(testResultsService.getSummary()); + } + + /** + * Get test results filtered by status. + * + * @param status the status to filter by (passed, failed, skipped) + * @return list of test results with the specified status + */ + @GetMapping("/by-status") + public ResponseEntity> getResultsByStatus(@RequestParam String status) { + return ResponseEntity.ok(testResultsService.getResultsByStatus(status)); + } + + /** + * Store new test results. + * + * @param results the test results to store + * @return response entity with status + */ + @PostMapping + public ResponseEntity storeResults(@RequestBody List results) { + testResultsService.storeResults(results); + return ResponseEntity.status(HttpStatus.CREATED) + .body(testResultsService.getSummary()); + } + + /** + * Clear all test results. + * + * @return response entity with status + */ + @DeleteMapping + public ResponseEntity clearResults() { + testResultsService.clearResults(); + return ResponseEntity.noContent() + .build(); + } + +} diff --git a/components/ide/ide-junit-results/src/main/java/org/eclipse/dirigible/components/ide/junit/service/TestResultsFacade.java b/components/ide/ide-junit-results/src/main/java/org/eclipse/dirigible/components/ide/junit/service/TestResultsFacade.java new file mode 100644 index 00000000000..0aad4523c1f --- /dev/null +++ b/components/ide/ide-junit-results/src/main/java/org/eclipse/dirigible/components/ide/junit/service/TestResultsFacade.java @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2010-2026 Eclipse Dirigible contributors + * + * All rights reserved. This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.dirigible.components.ide.junit.service; + +import java.util.List; + +import org.eclipse.dirigible.components.ide.junit.domain.TestResult; +import org.eclipse.dirigible.components.ide.junit.domain.TestResultsSummary; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Global service facade for accessing test results. This can be used to programmatically retrieve + * test results from Java code. + */ +@Component +public class TestResultsFacade { + + @Autowired + private TestResultsService testResultsService; + + /** + * Get all test results collected in the current session. + * + * @return list of test results + */ + public List getAllResults() { + return testResultsService.getResults(); + } + + /** + * Get a summary of test execution statistics. + * + * @return test results summary with counts and duration + */ + public TestResultsSummary getResultsSummary() { + return testResultsService.getSummary(); + } + + /** + * Get test results filtered by status. + * + * @param status the status to filter by (e.g., "passed", "failed", "skipped") + * @return filtered list of test results + */ + public List getResultsByStatus(String status) { + return testResultsService.getResultsByStatus(status); + } + + /** + * Store test results from a test execution. + * + * @param results the test results to store + */ + public void storeTestResults(List results) { + testResultsService.storeResults(results); + } + + /** + * Add a single test result. + * + * @param result the test result to add + */ + public void addTestResult(TestResult result) { + testResultsService.addResult(result); + } + + /** + * Clear all stored test results. + */ + public void clearAllResults() { + testResultsService.clearResults(); + } + + /** + * Check if there are any test failures. + * + * @return true if there are failed tests, false otherwise + */ + public boolean hasFailures() { + TestResultsSummary summary = getResultsSummary(); + return summary.getFailed() > 0; + } + + /** + * Check if all tests passed. + * + * @return true if all tests passed (or no tests), false if any failed + */ + public boolean allTestsPassed() { + return !hasFailures(); + } + + /** + * Get the count of passed tests. + * + * @return number of passed tests + */ + public int getPassedCount() { + return getResultsSummary().getPassed(); + } + + /** + * Get the count of failed tests. + * + * @return number of failed tests + */ + public int getFailedCount() { + return getResultsSummary().getFailed(); + } + + /** + * Get the count of skipped tests. + * + * @return number of skipped tests + */ + public int getSkippedCount() { + return getResultsSummary().getSkipped(); + } + + /** + * Get the total count of tests. + * + * @return total number of tests + */ + public int getTotalTestCount() { + return getResultsSummary().getTotal(); + } + + /** + * Get the total execution duration in milliseconds. + * + * @return total execution duration + */ + public long getTotalDuration() { + return getResultsSummary().getDuration(); + } + +} diff --git a/components/ide/ide-junit-results/src/main/java/org/eclipse/dirigible/components/ide/junit/service/TestResultsService.java b/components/ide/ide-junit-results/src/main/java/org/eclipse/dirigible/components/ide/junit/service/TestResultsService.java new file mode 100644 index 00000000000..e4de63b6f1b --- /dev/null +++ b/components/ide/ide-junit-results/src/main/java/org/eclipse/dirigible/components/ide/junit/service/TestResultsService.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2010-2026 Eclipse Dirigible contributors + * + * All rights reserved. This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.dirigible.components.ide.junit.service; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.dirigible.components.ide.junit.domain.TestResult; +import org.eclipse.dirigible.components.ide.junit.domain.TestResultsSummary; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.stereotype.Service; + +/** + * Service for managing test results. Note: This is an in-memory implementation. Consider adding + * database persistence for production use. + */ +@Service +public class TestResultsService implements InitializingBean { + + private List currentResults = new ArrayList<>(); + + /** The test results service instance. */ + private static TestResultsService INSTANCE; + + /** + * After properties set. + */ + @Override + public void afterPropertiesSet() { + INSTANCE = this; + } + + /** + * Gets the instance. + * + * @return the test results service instance + */ + public static TestResultsService get() { + return INSTANCE; + } + + /** + * Store test results from a test run. + * + * @param results the list of test results to store + */ + public void storeResults(List results) { + this.currentResults = new ArrayList<>(results); + } + + /** + * Add a single test result. + * + * @param result the test result to add + */ + public void addResult(TestResult result) { + this.currentResults.add(result); + } + + /** + * Get all stored test results. + * + * @return list of test results + */ + public List getResults() { + return new ArrayList<>(currentResults); + } + + /** + * Get a summary of test results. + * + * @return test results summary + */ + public TestResultsSummary getSummary() { + TestResultsSummary summary = new TestResultsSummary(); + + for (TestResult result : currentResults) { + summary.setTotal(summary.getTotal() + 1); + summary.setDuration(summary.getDuration() + result.getDuration()); + + if ("passed".equalsIgnoreCase(result.getStatus())) { + summary.setPassed(summary.getPassed() + 1); + } else if ("failed".equalsIgnoreCase(result.getStatus())) { + summary.setFailed(summary.getFailed() + 1); + } else if ("skipped".equalsIgnoreCase(result.getStatus())) { + summary.setSkipped(summary.getSkipped() + 1); + } + } + + return summary; + } + + /** + * Clear all stored test results. + */ + public void clearResults() { + this.currentResults.clear(); + } + + /** + * Get test results filtered by status. + * + * @param status the status to filter by + * @return list of test results with the specified status + */ + public List getResultsByStatus(String status) { + List filtered = new ArrayList<>(); + for (TestResult result : currentResults) { + if (status.equalsIgnoreCase(result.getStatus())) { + filtered.add(result); + } + } + return filtered; + } + +} diff --git a/components/ide/ide-junit-results/src/main/resources/META-INF/dirigible/ide-junit-results/junit-results.css b/components/ide/ide-junit-results/src/main/resources/META-INF/dirigible/ide-junit-results/junit-results.css new file mode 100644 index 00000000000..4f7cbcc958d --- /dev/null +++ b/components/ide/ide-junit-results/src/main/resources/META-INF/dirigible/ide-junit-results/junit-results.css @@ -0,0 +1,404 @@ +/* + * Copyright (c) 2010-2026 Eclipse Dirigible contributors + * + * All rights reserved. This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0 + */ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + font-size: 14px; + color: #333; + background-color: #f5f5f5; +} + +.junit-results-container { + width: 100%; + height: 100vh; + display: flex; + flex-direction: column; + background-color: #fff; + overflow: hidden; +} + +/* Header */ +.results-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 20px; + background-color: #f8f9fa; + border-bottom: 1px solid #dee2e6; +} + +.results-header h1 { + font-size: 24px; + font-weight: 600; + color: #212529; +} + +.controls { + display: flex; + gap: 8px; +} + +/* Buttons */ +.btn { + padding: 8px 16px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + font-weight: 500; + display: inline-flex; + align-items: center; + gap: 6px; + transition: all 0.2s ease; +} + +.btn-primary { + background-color: #0d6efd; + color: white; +} + +.btn-primary:hover { + background-color: #0b5ed7; +} + +.btn-secondary { + background-color: #6c757d; + color: white; +} + +.btn-secondary:hover { + background-color: #5c636a; +} + +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.icon { + font-size: 16px; +} + +/* Summary Panel */ +.summary-panel { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 12px; + padding: 16px 20px; + background-color: #f8f9fa; + border-bottom: 1px solid #dee2e6; +} + +.summary-item { + padding: 12px; + background-color: white; + border-radius: 6px; + border-left: 4px solid #dee2e6; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); +} + +.summary-item.passed { + border-left-color: #198754; + background-color: #f0f8f5; +} + +.summary-item.failed { + border-left-color: #dc3545; + background-color: #fdf8f8; +} + +.summary-item.skipped { + border-left-color: #ffc107; + background-color: #fffaf0; +} + +.summary-label { + font-size: 12px; + font-weight: 600; + color: #6c757d; + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 4px; +} + +.summary-value { + font-size: 24px; + font-weight: 700; + color: #212529; +} + +.summary-item.passed .summary-value { + color: #198754; +} + +.summary-item.failed .summary-value { + color: #dc3545; +} + +.summary-item.skipped .summary-value { + color: #ffc107; +} + +/* Status Bar */ +.status-bar { + height: 4px; + background-color: #f5f5f5; +} + +.status-bar.all-passed { + background: linear-gradient(90deg, #198754 0%, #198754 100%); +} + +.status-bar.has-failed { + background: linear-gradient(90deg, #198754 0%, #dc3545 50%, #198754 100%); +} + +/* Filter Tabs */ +.filter-tabs { + display: flex; + gap: 0; + padding: 0 20px; + background-color: #f8f9fa; + border-bottom: 1px solid #dee2e6; + overflow-x: auto; +} + +.filter-tab { + padding: 12px 16px; + background: none; + border: none; + border-bottom: 2px solid transparent; + cursor: pointer; + font-weight: 500; + color: #6c757d; + white-space: nowrap; + transition: all 0.2s ease; +} + +.filter-tab:hover { + color: #212529; +} + +.filter-tab.active { + color: #0d6efd; + border-bottom-color: #0d6efd; +} + +/* Results List */ +.results-list { + flex: 1; + overflow-y: auto; + padding: 12px 20px; +} + +.test-result-item { + background-color: white; + border: 1px solid #dee2e6; + border-radius: 6px; + padding: 12px; + margin-bottom: 8px; + display: flex; + align-items: flex-start; + gap: 12px; + cursor: pointer; + transition: all 0.2s ease; +} + +.test-result-item:hover { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + transform: translateY(-1px); +} + +.test-result-icon { + font-size: 20px; + min-width: 24px; + text-align: center; + line-height: 24px; +} + +.test-result-content { + flex: 1; + min-width: 0; +} + +.test-result-name { + font-weight: 500; + color: #212529; + word-break: break-word; + margin-bottom: 4px; +} + +.test-result-details { + display: flex; + gap: 16px; + font-size: 12px; + color: #6c757d; +} + +.test-result-status { + font-weight: 500; + padding: 2px 8px; + border-radius: 3px; +} + +.status-passed { + background-color: #d4edda; + color: #155724; +} + +.status-failed { + background-color: #f8d7da; + color: #721c24; +} + +.status-skipped { + background-color: #fff3cd; + color: #856404; +} + +.test-result-error { + margin-top: 8px; + padding: 8px; + background-color: #fdf8f8; + border-left: 3px solid #dc3545; + border-radius: 3px; + font-family: 'Courier New', monospace; + font-size: 12px; + color: #721c24; + white-space: pre-wrap; + word-break: break-word; + max-height: 200px; + overflow-y: auto; +} + +.test-result-item.expanded .test-result-error { + display: block; +} + +.test-result-item:not(.expanded) .test-result-error { + display: none; +} + +/* Loading Indicator */ +.loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + gap: 16px; +} + +.loading p { + color: #6c757d; + font-weight: 500; +} + +.spinner { + width: 40px; + height: 40px; + border: 4px solid #f3f3f3; + border-top: 4px solid #0d6efd; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +/* Empty State */ +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + gap: 12px; + color: #6c757d; +} + +.empty-state p { + margin: 0; +} + +.empty-state p:first-child { + font-weight: 600; + font-size: 16px; + color: #212529; +} + +/* Scrollbar Styling */ +.results-list::-webkit-scrollbar { + width: 8px; +} + +.results-list::-webkit-scrollbar-track { + background: #f1f1f1; +} + +.results-list::-webkit-scrollbar-thumb { + background: #888; + border-radius: 4px; +} + +.results-list::-webkit-scrollbar-thumb:hover { + background: #555; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .summary-panel { + grid-template-columns: repeat(2, 1fr); + } + + .results-header { + flex-direction: column; + align-items: flex-start; + gap: 12px; + } + + .test-result-item { + flex-direction: column; + } + + .test-result-details { + width: 100%; + } +} + +@media (max-width: 480px) { + .summary-panel { + grid-template-columns: 1fr; + } + + .filter-tabs { + overflow-x: auto; + } + + .results-header { + padding: 12px 12px; + } + + .summary-panel { + padding: 12px; + } + + .results-list { + padding: 8px 12px; + } +} diff --git a/components/ide/ide-junit-results/src/main/resources/META-INF/dirigible/ide-junit-results/junit-results.html b/components/ide/ide-junit-results/src/main/resources/META-INF/dirigible/ide-junit-results/junit-results.html new file mode 100644 index 00000000000..0bf8184d36c --- /dev/null +++ b/components/ide/ide-junit-results/src/main/resources/META-INF/dirigible/ide-junit-results/junit-results.html @@ -0,0 +1,95 @@ + + + + + + + + JUnit Test Results + + + +
+ +
+

JUnit Test Results

+
+ + +
+
+ + +
+
+
Total Tests
+
0
+
+
+
Passed
+
0
+
+
+
Failed
+
0
+
+
+
Skipped
+
0
+
+
+
Duration
+
0 ms
+
+
+ + +
+ + +
+ + + + +
+ + +
+ + + + + + +
+ + + + diff --git a/components/ide/ide-junit-results/src/main/resources/META-INF/dirigible/ide-junit-results/junit-results.js b/components/ide/ide-junit-results/src/main/resources/META-INF/dirigible/ide-junit-results/junit-results.js new file mode 100644 index 00000000000..0adcba7b8ef --- /dev/null +++ b/components/ide/ide-junit-results/src/main/resources/META-INF/dirigible/ide-junit-results/junit-results.js @@ -0,0 +1,277 @@ +/* + * Copyright (c) 2010-2026 Eclipse Dirigible contributors + * + * All rights reserved. This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0 + */ + +const API_BASE_URL = '/services/ide/junit-results'; +let currentFilter = 'all'; +let allResults = []; +let currentSummary = null; + +/** + * Initialize the UI on page load + */ +document.addEventListener('DOMContentLoaded', function() { + setupEventListeners(); + loadResults(); + // Auto-refresh every 5 seconds + setInterval(loadResults, 5000); +}); + +/** + * Setup event listeners for buttons and controls + */ +function setupEventListeners() { + const refreshBtn = document.getElementById('refreshBtn'); + const clearBtn = document.getElementById('clearBtn'); + + if (refreshBtn) { + refreshBtn.addEventListener('click', loadResults); + } + + if (clearBtn) { + clearBtn.addEventListener('click', clearResults); + } +} + +/** + * Load test results from the API + */ +async function loadResults() { + const loadingIndicator = document.getElementById('loadingIndicator'); + const resultsList = document.getElementById('resultsList'); + const emptyState = document.getElementById('emptyState'); + + try { + // Show loading state + loadingIndicator.style.display = 'flex'; + resultsList.innerHTML = ''; + emptyState.style.display = 'none'; + + // Fetch results and summary + const [resultsResponse, summaryResponse] = await Promise.all([ + fetch(`${API_BASE_URL}`), + fetch(`${API_BASE_URL}/summary`) + ]); + + if (!resultsResponse.ok || !summaryResponse.ok) { + throw new Error('Failed to load test results'); + } + + allResults = await resultsResponse.json(); + currentSummary = await summaryResponse.json(); + + // Update UI + updateSummary(); + updateStatusBar(); + filterResults(currentFilter); + + // Hide loading indicator + loadingIndicator.style.display = 'none'; + + // Show empty state if no results + if (allResults.length === 0) { + emptyState.style.display = 'flex'; + } + + } catch (error) { + console.error('Error loading test results:', error); + loadingIndicator.style.display = 'none'; + resultsList.innerHTML = `
Failed to load test results: ${error.message}
`; + } +} + +/** + * Update the summary panel with test counts + */ +function updateSummary() { + if (!currentSummary) return; + + document.getElementById('totalTests').textContent = currentSummary.total; + document.getElementById('passedTests').textContent = currentSummary.passed; + document.getElementById('failedTests').textContent = currentSummary.failed; + document.getElementById('skippedTests').textContent = currentSummary.skipped; + + // Format duration + const durationMs = currentSummary.duration; + const durationText = durationMs < 1000 + ? `${durationMs} ms` + : `${(durationMs / 1000).toFixed(2)} s`; + document.getElementById('totalDuration').textContent = durationText; +} + +/** + * Update the status bar based on test results + */ +function updateStatusBar() { + const statusBar = document.getElementById('statusBar'); + + if (!currentSummary) { + statusBar.className = 'status-bar'; + return; + } + + if (currentSummary.total === 0) { + statusBar.className = 'status-bar'; + } else if (currentSummary.failed === 0) { + statusBar.className = 'status-bar all-passed'; + } else { + statusBar.className = 'status-bar has-failed'; + } +} + +/** + * Filter results by status + */ +function filterResults(filter) { + currentFilter = filter; + + // Update active tab + document.querySelectorAll('.filter-tab').forEach(tab => { + tab.classList.remove('active'); + if (tab.dataset.filter === filter) { + tab.classList.add('active'); + } + }); + + // Filter and display results + let filteredResults = allResults; + if (filter !== 'all') { + filteredResults = allResults.filter(result => result.status.toLowerCase() === filter.toLowerCase()); + } + + displayResults(filteredResults); +} + +/** + * Display test results in the list + */ +function displayResults(results) { + const resultsList = document.getElementById('resultsList'); + const emptyState = document.getElementById('emptyState'); + + if (results.length === 0) { + resultsList.innerHTML = ''; + emptyState.style.display = 'flex'; + return; + } + + emptyState.style.display = 'none'; + resultsList.innerHTML = ''; + + results.forEach((result, index) => { + const item = createResultItem(result, index); + resultsList.appendChild(item); + }); +} + +/** + * Create a single result item element + */ +function createResultItem(result, index) { + const item = document.createElement('div'); + item.className = 'test-result-item'; + item.dataset.testIndex = index; + + // Determine icon and status color + const { icon, statusClass } = getStatusInfo(result.status); + + // Build HTML + const hasError = result.error && result.stackTrace; + const errorHtml = hasError + ? `
Error: ${result.error}\n\nStack Trace:\n${escapeHtml(result.stackTrace)}
` + : ''; + + const durationText = result.duration < 1000 + ? `${result.duration} ms` + : `${(result.duration / 1000).toFixed(2)} s`; + + item.innerHTML = ` +
${icon}
+
+
${escapeHtml(result.name)}
+
+ ${result.status.toUpperCase()} + Duration: ${durationText} + at ${new Date(result.timestamp).toLocaleTimeString()} +
+ ${errorHtml} +
+ `; + + // Add click handler to expand/collapse error details + if (hasError) { + item.addEventListener('click', function(e) { + if (!e.target.closest('.test-result-error a')) { + this.classList.toggle('expanded'); + } + }); + } + + return item; +} + +/** + * Get status icon and class based on test status + */ +function getStatusInfo(status) { + const statusLower = status.toLowerCase(); + + const statusMap = { + passed: { icon: '✓', statusClass: 'status-passed' }, + failed: { icon: '✗', statusClass: 'status-failed' }, + skipped: { icon: '—', statusClass: 'status-skipped' } + }; + + return statusMap[statusLower] || { icon: '?', statusClass: 'status-unknown' }; +} + +/** + * Clear all test results + */ +async function clearResults() { + if (!confirm('Are you sure you want to clear all test results?')) { + return; + } + + try { + const response = await fetch(API_BASE_URL, { + method: 'DELETE' + }); + + if (!response.ok) { + throw new Error('Failed to clear test results'); + } + + allResults = []; + currentSummary = null; + loadResults(); + + } catch (error) { + console.error('Error clearing test results:', error); + alert('Failed to clear test results: ' + error.message); + } +} + +/** + * Escape HTML special characters + */ +function escapeHtml(text) { + if (!text) return ''; + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} + +/** + * Format duration in milliseconds to a readable string + */ +function formatDuration(ms) { + if (ms < 1000) return `${ms} ms`; + return `${(ms / 1000).toFixed(2)} s`; +} diff --git a/components/pom.xml b/components/pom.xml index ee175298d92..f6a2e74655a 100644 --- a/components/pom.xml +++ b/components/pom.xml @@ -83,6 +83,7 @@ ide/ide-workspace + ide/ide-junit-results ide/ide-git ide/ide-template ide/ide-terminal @@ -737,6 +738,11 @@ dirigible-components-ide-logs ${project.version}
+ + org.eclipse.dirigible + dirigible-components-ide-junit-results + ${project.version} + org.eclipse.dirigible