diff --git a/.github/workflows/manual-test-matrix-workflow.yaml b/.github/workflows/manual-test-matrix-workflow.yaml index 02cdf561a..d4bf84582 100644 --- a/.github/workflows/manual-test-matrix-workflow.yaml +++ b/.github/workflows/manual-test-matrix-workflow.yaml @@ -88,7 +88,7 @@ jobs: with: name: "${{ github.run_id}}-${{ strategy.job-index }}-artifacts" path: | - packages/testsuite/results + results packages/testsuite/cypress/videos process_report: name: "Collect results & deploy GH pages" @@ -104,6 +104,8 @@ jobs: node-version: "22" - name: "Install necessary tools" run: npm install -g copyfiles gh-pages@3.0.0 mochawesome-merge mochawesome-report-generator + - name: "List downloaded artifacts" + run: find . -name "*.json" -path "*/results/*" | head -50 - name: "Generate Mochawesome Report" run: mochawesome-merge "./*-artifacts/results/packages/testsuite/cypress/e2e/*.json" > mochawesome.json - name: "Copy video assets" diff --git a/.github/workflows/scheduled-run-all-tests-workflow.yaml b/.github/workflows/scheduled-run-all-tests-workflow.yaml index 13c1b9e04..44ac25119 100644 --- a/.github/workflows/scheduled-run-all-tests-workflow.yaml +++ b/.github/workflows/scheduled-run-all-tests-workflow.yaml @@ -80,7 +80,7 @@ jobs: with: name: "${{ github.run_id}}-${{ strategy.job-index }}-artifacts" path: | - packages/testsuite/results + results packages/testsuite/cypress/videos process_report: name: "Collect results & deploy GH pages" @@ -96,6 +96,8 @@ jobs: node-version: "22" - name: "Install necessary tools" run: npm install -g copyfiles gh-pages@3.0.0 mochawesome-merge mochawesome-report-generator + - name: "List downloaded artifacts" + run: find . -name "*.json" -path "*/results/*" | head -50 - name: "Generate Mochawesome Report" run: mochawesome-merge "./*-artifacts/results/packages/testsuite/cypress/e2e/*.json" > mochawesome.json - name: "Copy video assets" diff --git a/config/tasks/wildfly-tasks.ts b/config/tasks/wildfly-tasks.ts index 54c9bcb9c..aa0125d82 100644 --- a/config/tasks/wildfly-tasks.ts +++ b/config/tasks/wildfly-tasks.ts @@ -76,27 +76,29 @@ export function createExecuteInContainer( return Promise.reject(new Error(`Container ${containerName} not found`)); } - return containerToExec - .exec([ - "/bin/sh", - "-c", - `${JBOSS_CLI_PATH} --connect --controller=localhost:${managementPort} --commands=${command}`, - ]) - .then((value) => { - if (value.exitCode === 0) { - return value; - } else { - logger.debug(value); - throw new Error(`Command failed with exit code ${value.exitCode}: ${value.output || ""}`); - } - }) - // Only container.exec() errors and plain Errors from the .then() block can reach here. - // AxiosErrorResponse is not possible — Axios is not used in this function. - .catch((err: unknown) => { - if (err instanceof Error) { - throw err; - } - throw new Error(String(err)); - }); + return ( + containerToExec + .exec([ + "/bin/sh", + "-c", + `${JBOSS_CLI_PATH} --connect --controller=localhost:${managementPort} --commands=${command}`, + ]) + .then((value) => { + if (value.exitCode === 0) { + return value; + } else { + logger.debug(value); + throw new Error(`Command failed with exit code ${value.exitCode}: ${value.output || ""}`); + } + }) + // Only container.exec() errors and plain Errors from the .then() block can reach here. + // AxiosErrorResponse is not possible — Axios is not used in this function. + .catch((err: unknown) => { + if (err instanceof Error) { + throw err; + } + throw new Error(String(err)); + }) + ); }; } diff --git a/packages/testsuite/cypress/e2e/micrometer/test-configuration-subsystem-micrometer-registry-otlp.cy.ts b/packages/testsuite/cypress/e2e/micrometer/test-configuration-subsystem-micrometer-registry-otlp.cy.ts new file mode 100644 index 000000000..0c30dad79 --- /dev/null +++ b/packages/testsuite/cypress/e2e/micrometer/test-configuration-subsystem-micrometer-registry-otlp.cy.ts @@ -0,0 +1,151 @@ +describe("TESTS: Configuration => Subsystem => Micrometer => Registry => OTLP", () => { + const subsystemAddress = ["subsystem", "micrometer"]; + const address = ["subsystem", "micrometer", "registry", "otlp"]; + + // HAL generates form IDs with triple-dash separators for child resources, + // but collapses them to single dashes in some sub-elements (editing div, save button). + const formIds = { + configuration: "model-browser-model-browser-root---registry---otlp-form", + configurationNoDash: "model-browser-model-browser-root-registry-otlp-form", + addWizard: "model-browser-root-registry-singleton-add", + childrenTable: "model-browser-children-table", + }; + + const treeNodes = { + registry: "#model-browser-root___registry", + otlp: "#model-browser-root___registry___otlp", + }; + + const endpointAttr = { + name: "endpoint", + customValue: "http://otel-collector:4318/v1/metrics", + expressionProperty: "micrometer.otlp.endpoint", + expressionPropertyValue: "http://expression-endpoint:4318/v1/metrics", + expressionValue: "${micrometer.otlp.endpoint}", + }; + + const stepAttr = { + name: "step", + customValue: 30, + expressionProperty: "micrometer.otlp.step", + expressionPropertyValue: "45", + expressionValue: "${micrometer.otlp.step}", + }; + + // Context value for prometheus registry (required field, avoids conflict with metrics subsystem) + const prometheusContext = "/prometheus"; + + let managementEndpoint: string; + + function navigateToOtlpRegistry() { + cy.navigateToGenericSubsystemPage(managementEndpoint, subsystemAddress); + cy.get(`${treeNodes.registry} > .jstree-ocl`).click(); + cy.get(treeNodes.otlp).should("be.visible").click(); + cy.get('#model-browser-resource-tab-container a[href="#model-browser-resource-data-tab"]').click(); + } + + // Workaround for JBEAP-28819 - child resource form IDs use triple-dash separators + // for the container but single-dash for the editing div. + function editForm() { + const editButton = "#" + formIds.configuration + ' a.clickable[data-operation="edit"]'; + cy.get(`#${formIds.configurationNoDash}-editing`).should("not.be.visible"); + cy.get(editButton).click(); + for (let reClickTry = 0; reClickTry < 5; reClickTry++) { + cy.get(editButton).then(($button) => { + if ($button.is(":visible")) { + cy.get(editButton).click(); + } + }); + } + cy.get(`#${formIds.configurationNoDash}-editing`).should("be.visible"); + } + + before(function () { + cy.startWildflyContainer().then((result) => { + managementEndpoint = result as string; + cy.skipIf(cy.isEAP(managementEndpoint), this); + // The micrometer extension is not part of the base configuration, so it must be added explicitly + cy.addAddress(managementEndpoint, ["extension", "org.wildfly.extension.micrometer"], {}); + // Register the micrometer subsystem as a parent for the registries + cy.addAddress(managementEndpoint, subsystemAddress, {}); + // System properties used as expression resolution targets in the expression tests + cy.addAddress(managementEndpoint, ["system-property", endpointAttr.expressionProperty], { + value: endpointAttr.expressionPropertyValue, + }); + cy.addAddress(managementEndpoint, ["system-property", stepAttr.expressionProperty], { + value: stepAttr.expressionPropertyValue, + }); + }); + }); + + after(() => { + cy.task("stop:containers"); + }); + + it("Add otlp registry", () => { + cy.navigateToGenericSubsystemPage(managementEndpoint, subsystemAddress); + cy.get(treeNodes.registry).should("be.visible").click(); + cy.get(`#${formIds.childrenTable}_wrapper`).should("be.visible"); + cy.addInTable(formIds.childrenTable); + // First registry added: wizard shows radio button step, otlp has no required fields + cy.get("input[type='radio'][value='otlp']").should("exist").check({ force: true }); + cy.get("input[type='radio'][value='otlp']").should("be.checked"); + cy.confirmNextInWizard(); + cy.confirmFinishInWizard(); + cy.verifySuccess(); + }); + + it("Add prometheus registry", () => { + cy.navigateToGenericSubsystemPage(managementEndpoint, subsystemAddress); + cy.get(treeNodes.registry).should("be.visible").click(); + cy.get(`#${formIds.childrenTable}_wrapper`).should("be.visible"); + cy.addInTable(formIds.childrenTable); + // Second registry added: wizard skips radio button step, prometheus requires context + cy.text(formIds.addWizard, "context", prometheusContext); + cy.confirmAddResourceWizard(); + cy.verifySuccess(); + }); + + it("Edit endpoint", () => { + navigateToOtlpRegistry(); + editForm(); + cy.text(formIds.configurationNoDash, endpointAttr.name, endpointAttr.customValue); + cy.saveForm(formIds.configurationNoDash); + cy.verifySuccess(); + cy.verifyAttribute(managementEndpoint, address, endpointAttr.name, endpointAttr.customValue); + }); + + it("Edit step", () => { + navigateToOtlpRegistry(); + editForm(); + cy.text(formIds.configurationNoDash, stepAttr.name, stepAttr.customValue.toString()); + cy.saveForm(formIds.configurationNoDash); + cy.verifySuccess(); + cy.verifyAttribute(managementEndpoint, address, stepAttr.name, stepAttr.customValue); + }); + + it("Edit endpoint with expression", () => { + const selector = `input#${formIds.configurationNoDash}-${endpointAttr.name}-editing.form-control`; + navigateToOtlpRegistry(); + editForm(); + cy.textExpression(formIds.configurationNoDash, endpointAttr.name, endpointAttr.expressionValue, { selector }); + cy.saveForm(formIds.configurationNoDash); + cy.get(".toast-notifications-list-pf .alert").should("be.visible"); + cy.verifyAttributeAsExpression(managementEndpoint, address, endpointAttr.name, endpointAttr.expressionValue); + }); + + it("Edit step with expression", () => { + const selector = `input#${formIds.configurationNoDash}-${stepAttr.name}-editing.form-control`; + navigateToOtlpRegistry(); + editForm(); + cy.textExpression(formIds.configurationNoDash, stepAttr.name, stepAttr.expressionValue, { selector }); + cy.saveForm(formIds.configurationNoDash); + cy.get(".toast-notifications-list-pf .alert").should("be.visible"); + cy.verifyAttributeAsExpression(managementEndpoint, address, stepAttr.name, stepAttr.expressionValue); + }); + + it("Reset configuration", () => { + navigateToOtlpRegistry(); + cy.resetForm(formIds.configuration, managementEndpoint, address); + }); +}); diff --git a/packages/testsuite/cypress/e2e/micrometer/test-configuration-subsystem-micrometer-registry-prometheus.cy.ts b/packages/testsuite/cypress/e2e/micrometer/test-configuration-subsystem-micrometer-registry-prometheus.cy.ts new file mode 100644 index 000000000..74337bc6e --- /dev/null +++ b/packages/testsuite/cypress/e2e/micrometer/test-configuration-subsystem-micrometer-registry-prometheus.cy.ts @@ -0,0 +1,135 @@ +describe("TESTS: Configuration => Subsystem => Micrometer => Registry => Prometheus", () => { + const subsystemAddress = ["subsystem", "micrometer"]; + const address = ["subsystem", "micrometer", "registry", "prometheus"]; + + // HAL generates form IDs with triple-dash separators for child resources, + // but collapses them to single dashes in some sub-elements (editing div, save button). + const formIds = { + configuration: "model-browser-model-browser-root---registry---prometheus-form", + configurationNoDash: "model-browser-model-browser-root-registry-prometheus-form", + addWizard: "model-browser-root-registry-singleton-add", + childrenTable: "model-browser-children-table", + }; + + const treeNodes = { + registry: "#model-browser-root___registry", + prometheus: "#model-browser-root___registry___prometheus", + }; + + const contextAttr = { + name: "context", + // Default context is set to "/prometheus" to avoid a capability conflict with the metrics subsystem. + // Both serve an HTTP management context, and "/metrics" is already taken by the metrics subsystem. + defaultValue: "/prometheus", + customValue: "/prom-custom", + expressionProperty: "micrometer.prometheus.context", + expressionPropertyValue: "/prom-expression", + expressionValue: "${micrometer.prometheus.context}", + }; + + const securityEnabled = "security-enabled"; + + let managementEndpoint: string; + + function navigateToPrometheusRegistry() { + cy.navigateToGenericSubsystemPage(managementEndpoint, subsystemAddress); + cy.get(`${treeNodes.registry} > .jstree-ocl`).click(); + cy.get(treeNodes.prometheus).should("be.visible").click(); + cy.get('#model-browser-resource-tab-container a[href="#model-browser-resource-data-tab"]').click(); + } + + // Workaround for JBEAP-28819 - child resource form IDs use triple-dash separators + // for the container but single-dash for the editing div. + function editForm() { + const editButton = "#" + formIds.configuration + ' a.clickable[data-operation="edit"]'; + cy.get(`#${formIds.configurationNoDash}-editing`).should("not.be.visible"); + cy.get(editButton).click(); + for (let reClickTry = 0; reClickTry < 5; reClickTry++) { + cy.get(editButton).then(($button) => { + if ($button.is(":visible")) { + cy.get(editButton).click(); + } + }); + } + cy.get(`#${formIds.configurationNoDash}-editing`).should("be.visible"); + } + + before(function () { + cy.startWildflyContainer().then((result) => { + managementEndpoint = result as string; + cy.skipIf(cy.isEAP(managementEndpoint), this); + // The micrometer extension is not part of the base configuration, so it must be added explicitly + cy.addAddress(managementEndpoint, ["extension", "org.wildfly.extension.micrometer"], {}); + // Register the micrometer subsystem as a parent for the registries + cy.addAddress(managementEndpoint, subsystemAddress, {}); + // System property used as expression resolution target in the expression test + cy.addAddress(managementEndpoint, ["system-property", contextAttr.expressionProperty], { + value: contextAttr.expressionPropertyValue, + }); + }); + }); + + after(() => { + cy.task("stop:containers"); + }); + + it("Add otlp registry", () => { + cy.navigateToGenericSubsystemPage(managementEndpoint, subsystemAddress); + cy.get(treeNodes.registry).should("be.visible").click(); + cy.get(`#${formIds.childrenTable}_wrapper`).should("be.visible"); + cy.addInTable(formIds.childrenTable); + // First registry added: wizard shows radio button step, otlp has no required fields + cy.get("input[type='radio'][value='otlp']").should("exist").check({ force: true }); + cy.get("input[type='radio'][value='otlp']").should("be.checked"); + cy.confirmNextInWizard(); + cy.confirmFinishInWizard(); + cy.verifySuccess(); + }); + + it("Add prometheus registry", () => { + cy.navigateToGenericSubsystemPage(managementEndpoint, subsystemAddress); + cy.get(treeNodes.registry).should("be.visible").click(); + cy.get(`#${formIds.childrenTable}_wrapper`).should("be.visible"); + cy.addInTable(formIds.childrenTable); + // Second registry added: wizard skips radio button step, prometheus requires context + cy.text(formIds.addWizard, contextAttr.name, contextAttr.defaultValue); + cy.confirmAddResourceWizard(); + cy.verifySuccess(); + cy.verifyAttribute(managementEndpoint, address, contextAttr.name, contextAttr.defaultValue); + }); + + it("Edit context", () => { + navigateToPrometheusRegistry(); + editForm(); + cy.text(formIds.configurationNoDash, contextAttr.name, contextAttr.customValue); + cy.saveForm(formIds.configurationNoDash); + cy.verifySuccess(); + cy.verifyAttribute(managementEndpoint, address, contextAttr.name, contextAttr.customValue); + }); + + it("Toggle security-enabled", () => { + cy.readAttributeAsBoolean(managementEndpoint, address, securityEnabled).then((defaultValue: boolean) => { + navigateToPrometheusRegistry(); + editForm(); + cy.flip(formIds.configurationNoDash, securityEnabled, defaultValue); + cy.saveForm(formIds.configurationNoDash); + cy.verifySuccess(); + cy.verifyAttribute(managementEndpoint, address, securityEnabled, !defaultValue); + }); + }); + + it("Edit context with expression", () => { + const selector = `input#${formIds.configurationNoDash}-${contextAttr.name}-editing.form-control`; + navigateToPrometheusRegistry(); + editForm(); + cy.textExpression(formIds.configurationNoDash, contextAttr.name, contextAttr.expressionValue, { selector }); + cy.saveForm(formIds.configurationNoDash); + cy.get(".toast-notifications-list-pf .alert").should("be.visible"); + cy.verifyAttributeAsExpression(managementEndpoint, address, contextAttr.name, contextAttr.expressionValue); + }); + + it("Reset configuration", () => { + navigateToPrometheusRegistry(); + cy.resetForm(formIds.configuration, managementEndpoint, address); + }); +}); diff --git a/packages/testsuite/cypress/e2e/micrometer/test-configuration-subsystem-micrometer.cy.ts b/packages/testsuite/cypress/e2e/micrometer/test-configuration-subsystem-micrometer.cy.ts new file mode 100644 index 000000000..cf5c8a577 --- /dev/null +++ b/packages/testsuite/cypress/e2e/micrometer/test-configuration-subsystem-micrometer.cy.ts @@ -0,0 +1,92 @@ +describe("TESTS: Configuration => Subsystem => Micrometer", () => { + const address = ["subsystem", "micrometer"]; + const configurationFormId = "model-browser-model-browser-root-form"; + + const endpointAttr = { + name: "endpoint", + customValue: "http://otel-collector:4318/v1/metrics", + expressionProperty: "micrometer.endpoint", + expressionPropertyValue: "http://expression-endpoint:4318/v1/metrics", + expressionValue: "${micrometer.endpoint}", + }; + + const stepAttr = { + name: "step", + customValue: 30, + expressionProperty: "micrometer.step", + expressionPropertyValue: "45", + expressionValue: "${micrometer.step}", + }; + + let managementEndpoint: string; + + function navigateToMicrometerPage() { + cy.navigateToGenericSubsystemPage(managementEndpoint, address); + cy.get('#model-browser-resource-tab-container a[href="#model-browser-resource-data-tab"]').click(); + } + + before(() => { + cy.startWildflyContainer().then((result) => { + managementEndpoint = result as string; + // The micrometer extension is not part of the base configuration, so it must be added explicitly + cy.addAddress(managementEndpoint, ["extension", "org.wildfly.extension.micrometer"], {}); + // Register the micrometer subsystem so its attributes can be configured and tested + cy.addAddress(managementEndpoint, address, {}); + // System properties used as expression resolution targets in the expression tests + cy.addAddress(managementEndpoint, ["system-property", endpointAttr.expressionProperty], { + value: endpointAttr.expressionPropertyValue, + }); + cy.addAddress(managementEndpoint, ["system-property", stepAttr.expressionProperty], { + value: stepAttr.expressionPropertyValue, + }); + }); + }); + + after(() => { + cy.task("stop:containers"); + }); + + it("Edit endpoint", () => { + navigateToMicrometerPage(); + cy.editForm(configurationFormId); + cy.text(configurationFormId, endpointAttr.name, endpointAttr.customValue); + cy.saveForm(configurationFormId); + cy.verifySuccess(); + cy.verifyAttribute(managementEndpoint, address, endpointAttr.name, endpointAttr.customValue); + }); + + it("Edit step", () => { + navigateToMicrometerPage(); + cy.editForm(configurationFormId); + cy.text(configurationFormId, stepAttr.name, stepAttr.customValue.toString()); + cy.saveForm(configurationFormId); + cy.verifySuccess(); + cy.verifyAttribute(managementEndpoint, address, stepAttr.name, stepAttr.customValue); + }); + + it("Edit endpoint with expression", () => { + const selector = `input#${configurationFormId}-${endpointAttr.name}-editing.form-control`; + navigateToMicrometerPage(); + cy.editForm(configurationFormId); + cy.textExpression(configurationFormId, endpointAttr.name, endpointAttr.expressionValue, { selector }); + cy.saveForm(configurationFormId); + cy.get(".toast-notifications-list-pf .alert").should("be.visible"); + cy.verifyAttributeAsExpression(managementEndpoint, address, endpointAttr.name, endpointAttr.expressionValue); + }); + + it("Edit step with expression", () => { + const selector = `input#${configurationFormId}-${stepAttr.name}-editing.form-control`; + navigateToMicrometerPage(); + cy.editForm(configurationFormId); + cy.textExpression(configurationFormId, stepAttr.name, stepAttr.expressionValue, { selector }); + cy.saveForm(configurationFormId); + cy.get(".toast-notifications-list-pf .alert").should("be.visible"); + cy.verifyAttributeAsExpression(managementEndpoint, address, stepAttr.name, stepAttr.expressionValue); + }); + + it("Reset configuration", () => { + navigateToMicrometerPage(); + cy.get('#model-browser-model-browser-root-form-links > [data-toggle="tooltip"]'); + cy.resetForm(configurationFormId, managementEndpoint, address); + }); +}); diff --git a/packages/testsuite/cypress/support/form-editing.ts b/packages/testsuite/cypress/support/form-editing.ts index 24cb15b49..9b48f71da 100644 --- a/packages/testsuite/cypress/support/form-editing.ts +++ b/packages/testsuite/cypress/support/form-editing.ts @@ -68,7 +68,9 @@ Cypress.Commands.add("resetForm", (formId, managementApi, address) => { name: attributeWithDefaultValue.name, }).then((result) => { expect((result as { outcome: string }).outcome).to.equal("success"); - expect((result as { result: string | number | boolean }).result).to.equal( + // Use deep.equal() for proper comparison of arrays (e.g., ['*'] vs ['*']) + // since equal() uses reference equality which fails for different array instances + expect((result as { result: string | number | boolean | string[] }).result).to.deep.equal( attributeWithDefaultValue.defaultValue, ); }); diff --git a/packages/testsuite/cypress/support/navigation.ts b/packages/testsuite/cypress/support/navigation.ts index b91ec29fe..1b0c462c4 100644 --- a/packages/testsuite/cypress/support/navigation.ts +++ b/packages/testsuite/cypress/support/navigation.ts @@ -28,11 +28,11 @@ Cypress.Commands.add("closeWizard", () => { }); Cypress.Commands.add("confirmNextInWizard", () => { - cy.get('div.modal-footer > button.btn.btn-primary:contains("Next")').click({ force: true }); + cy.get('div.modal-footer > button.btn.btn-primary:contains("Next")').should("be.visible").click(); }); Cypress.Commands.add("confirmFinishInWizard", () => { - cy.get('div.modal-footer > button.btn.btn-primary:contains("Finish")').click({ force: true }); + cy.get('div.modal-footer > button.btn.btn-primary:contains("Finish")').should("be.visible").click(); }); Cypress.Commands.add("navigateToUpdateManagerPage", (managementEndpoint, address) => {