diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 424b51941b..371fe5e0fa 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -254,6 +254,8 @@ jobs: name: "Unit Tests" needs: [setup] uses: ./.github/workflows/unit-test-workflow.yml + secrets: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} with: base_ref: ${{ github.base_ref || 'develop' }} diff --git a/.github/workflows/sonar-develop.yml b/.github/workflows/sonar-develop.yml new file mode 100644 index 0000000000..bb8d17828f --- /dev/null +++ b/.github/workflows/sonar-develop.yml @@ -0,0 +1,22 @@ +name: "SonarCloud Full Scan (develop)" + +on: + push: + branches: + - develop + workflow_dispatch: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + full-unit-tests-and-sonar-scan: + name: "Full Unit Tests + SonarCloud Scan" + uses: ./.github/workflows/unit-test-workflow.yml + secrets: inherit + with: + full: true diff --git a/.github/workflows/unit-test-workflow.yml b/.github/workflows/unit-test-workflow.yml index 8759f8b863..2b52941aec 100644 --- a/.github/workflows/unit-test-workflow.yml +++ b/.github/workflows/unit-test-workflow.yml @@ -2,12 +2,21 @@ name: "Unit Tests Workflow" on: workflow_call: + secrets: + SONAR_TOKEN: + description: 'Token for SonarCloud analysis' + required: false inputs: base_ref: description: 'Base branch for affected calculation' required: false type: string default: 'develop' + full: + description: 'Run the full (non-affected) test suite for every project instead of only affected ones' + required: false + type: boolean + default: false jobs: generate-affected-matrix: @@ -30,9 +39,15 @@ jobs: id: set-matrix env: BASE_REF: ${{ inputs.base_ref }} + FULL_RUN: ${{ inputs.full }} run: | - echo "Base ref is $BASE_REF" - AFFECTED_UNIT=$(pnpm nx show projects --affected --target=test --base=origin/$BASE_REF --head=HEAD --select=projects --plain --exclude=cli,stories,eslint-angular) + if [ "$FULL_RUN" == "true" ]; then + echo "Running full (non-affected) test suite" + AFFECTED_UNIT=$(pnpm nx show projects --target=test --select=projects --plain --exclude=cli,stories,eslint-angular) + else + echo "Base ref is $BASE_REF" + AFFECTED_UNIT=$(pnpm nx show projects --affected --target=test --base=origin/$BASE_REF --head=HEAD --select=projects --plain --exclude=cli,stories,eslint-angular) + fi echo "Affected projects for UNIT: $AFFECTED_UNIT" if [ -z "$AFFECTED_UNIT" ]; then @@ -74,8 +89,56 @@ jobs: NODE_OPTIONS: "--max-old-space-size=5120" run: | xvfb-run --auto-servernum pnpm nx run ${{ matrix.project }}:test + - name: Upload coverage report + if: ${{ always() }} + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: coverage-${{ matrix.project }} + path: coverage/${{ matrix.project }}/lcov.info + if-no-files-found: ignore + retention-days: 1 - name: Save nx cache if: ${{ success() }} uses: ./.github/actions/save-nx-cache with: cache-suffix: test-${{ matrix.project }} + + sonarcloud: + name: "SonarCloud Scan" + runs-on: ubuntu-latest + needs: [generate-affected-matrix, unit-tests] + if: ${{ needs.generate-affected-matrix.outputs.hasProjects == 'true' && always() && needs.unit-tests.result != 'cancelled' }} + permissions: + contents: read + pull-requests: read + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + - name: Download all coverage artifacts + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + pattern: coverage-* + path: coverage-reports + - name: Merge coverage reports + run: | + mkdir -p coverage + echo "Artifact structure:" + find coverage-reports -type f -name 'lcov.info' 2>/dev/null || true + for dir in coverage-reports/coverage-*/; do + project_name=$(basename "$dir" | sed 's/^coverage-//') + lcov_file=$(find "$dir" -name 'lcov.info' -type f | head -1) + if [ -n "$lcov_file" ]; then + mkdir -p "coverage/${project_name}" + cp "$lcov_file" "coverage/${project_name}/lcov.info" + echo "Copied coverage for ${project_name}" + fi + done + echo "Coverage files found:" + find coverage -name 'lcov.info' -type f + - name: SonarCloud Scan + uses: SonarSource/sonarqube-scan-action@aa494459d7c39c106cc77b166de8b4250a32bb97 # v5.1.0 + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + SONAR_HOST_URL: https://sonarcloud.io diff --git a/lib/content-services/karma.conf.js b/lib/content-services/karma.conf.js index a37f38f5e1..6880782c7c 100644 --- a/lib/content-services/karma.conf.js +++ b/lib/content-services/karma.conf.js @@ -56,7 +56,7 @@ module.exports = function (config) { coverageReporter: { dir: join(__dirname, '../../coverage/content-services'), subdir: '.', - reporters: [{ type: 'html' }, { type: 'text-summary' }, { type: 'text-summary', subdir: '.', file: 'summary.txt' }], + reporters: [{ type: 'html' }, { type: 'lcov' }, { type: 'text-summary' }, { type: 'text-summary', subdir: '.', file: 'summary.txt' }], check: { global: { statements: 75, diff --git a/lib/core/karma.conf.js b/lib/core/karma.conf.js index 118b0f91a5..1e40fd315f 100644 --- a/lib/core/karma.conf.js +++ b/lib/core/karma.conf.js @@ -69,7 +69,7 @@ module.exports = function (config) { coverageReporter: { dir: join(__dirname, '../../coverage/core'), subdir: '.', - reporters: [{ type: 'html' }, { type: 'text-summary' }, { type: 'text-summary', subdir: '.', file: 'summary.txt' }], + reporters: [{ type: 'html' }, { type: 'lcov' }, { type: 'text-summary' }, { type: 'text-summary', subdir: '.', file: 'summary.txt' }], check: { global: { statements: 75, diff --git a/lib/extensions/karma.conf.js b/lib/extensions/karma.conf.js index 4bdd4cd6e4..99b34c1729 100644 --- a/lib/extensions/karma.conf.js +++ b/lib/extensions/karma.conf.js @@ -23,7 +23,7 @@ module.exports = function (config) { coverageReporter: { dir: join(__dirname, '../../coverage/extensions'), subdir: '.', - reporters: [{ type: 'html' }, { type: 'text-summary' }, { type: 'text-summary', subdir: '.', file: 'summary.txt' }], + reporters: [{ type: 'html' }, { type: 'lcov' }, { type: 'text-summary' }, { type: 'text-summary', subdir: '.', file: 'summary.txt' }], check: { global: { statements: 75, diff --git a/lib/insights/karma.conf.js b/lib/insights/karma.conf.js index 059cf7eb8a..8cf50ccf31 100644 --- a/lib/insights/karma.conf.js +++ b/lib/insights/karma.conf.js @@ -44,7 +44,7 @@ module.exports = function (config) { coverageReporter: { dir: join(__dirname, '../../coverage/insights'), subdir: '.', - reporters: [{ type: 'html' }, { type: 'text-summary' }, { type: 'text-summary', subdir: '.', file: 'summary.txt' }], + reporters: [{ type: 'html' }, { type: 'lcov' }, { type: 'text-summary' }, { type: 'text-summary', subdir: '.', file: 'summary.txt' }], check: { global: { statements: 75, diff --git a/lib/insights/src/lib/diagram/models/chart/chart.model.spec.ts b/lib/insights/src/lib/diagram/models/chart/chart.model.spec.ts new file mode 100644 index 0000000000..e56e4854bc --- /dev/null +++ b/lib/insights/src/lib/diagram/models/chart/chart.model.spec.ts @@ -0,0 +1,148 @@ +/*! + * @license + * Copyright © 2005-2026 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Chart } from './chart.model'; + +describe('Chart Model', () => { + describe('constructor', () => { + it('should create with default values when no argument is provided', () => { + const chart = new Chart(); + expect(chart.labels).toEqual([]); + expect(chart.data).toEqual([]); + expect(chart.datasets).toEqual([]); + expect(chart.showDetails).toBe(false); + }); + + it('should populate properties from input object', () => { + const chart = new Chart({ + id: '1', + title: 'Test Chart', + titleKey: 'KEY', + labels: ['a', 'b'], + data: [1, 2], + datasets: [{ data: [1] }], + showDetails: true, + detailsTable: { key: 'value' }, + options: { responsive: true } + }); + + expect(chart.id).toBe('1'); + expect(chart.title).toBe('Test Chart'); + expect(chart.titleKey).toBe('KEY'); + expect(chart.labels).toEqual(['a', 'b']); + expect(chart.data).toEqual([1, 2]); + expect(chart.datasets).toEqual([{ data: [1] }]); + expect(chart.showDetails).toBe(true); + expect(chart.detailsTable).toEqual({ key: 'value' }); + expect(chart.options).toEqual({ responsive: true }); + }); + + it('should convert type and set icon for pieChart', () => { + const chart = new Chart({ type: 'pieChart' }); + expect(chart.type).toBe('pie'); + expect(chart.icon).toBe('pie_chart'); + }); + + it('should convert type and set icon for barChart', () => { + const chart = new Chart({ type: 'barChart' }); + expect(chart.type).toBe('bar'); + expect(chart.icon).toBe('equalizer'); + }); + + it('should convert type and set icon for line', () => { + const chart = new Chart({ type: 'line' }); + expect(chart.type).toBe('line'); + expect(chart.icon).toBe('show_chart'); + }); + + it('should convert type and set icon for table', () => { + const chart = new Chart({ type: 'table' }); + expect(chart.type).toBe('table'); + expect(chart.icon).toBe('web'); + }); + + it('should convert type and set icon for multiBarChart', () => { + const chart = new Chart({ type: 'multiBarChart' }); + expect(chart.type).toBe('multiBar'); + expect(chart.icon).toBe('poll'); + }); + + it('should convert type and set icon for processDefinitionHeatMap', () => { + const chart = new Chart({ type: 'processDefinitionHeatMap' }); + expect(chart.type).toBe('HeatMap'); + expect(chart.icon).toBe('share'); + }); + + it('should convert type and set icon for masterDetailTable', () => { + const chart = new Chart({ type: 'masterDetailTable' }); + expect(chart.type).toBe('masterDetailTable'); + expect(chart.icon).toBe('subtitles'); + }); + + it('should default to table type for unknown types', () => { + const chart = new Chart({ type: 'unknown' }); + expect(chart.type).toBe('table'); + expect(chart.icon).toBe('web'); + }); + }); + + describe('hasData', () => { + it('should return true when data is not empty', () => { + const chart = new Chart({ data: [1, 2, 3] }); + expect(chart.hasData()).toBe(true); + }); + + it('should return false when data is empty', () => { + const chart = new Chart({ data: [] }); + expect(chart.hasData()).toBe(false); + }); + + it('should return false when no data is provided', () => { + const chart = new Chart(); + expect(chart.hasData()).toBe(false); + }); + }); + + describe('hasDatasets', () => { + it('should return true when datasets is not empty', () => { + const chart = new Chart({ datasets: [{ data: [1] }] }); + expect(chart.hasDatasets()).toBe(true); + }); + + it('should return false when datasets is empty', () => { + const chart = new Chart({ datasets: [] }); + expect(chart.hasDatasets()).toBe(false); + }); + }); + + describe('hasZeroValues', () => { + it('should return true when all data values are zero', () => { + const chart = new Chart({ data: [0, 0, 0] }); + expect(chart.hasZeroValues()).toBe(true); + }); + + it('should return false when at least one value is non-zero', () => { + const chart = new Chart({ data: [0, 1, 0] }); + expect(chart.hasZeroValues()).toBe(false); + }); + + it('should return false when data is empty', () => { + const chart = new Chart({ data: [] }); + expect(chart.hasZeroValues()).toBe(false); + }); + }); +}); diff --git a/lib/process-services-cloud/karma.conf.js b/lib/process-services-cloud/karma.conf.js index a7d6664844..154eff3679 100644 --- a/lib/process-services-cloud/karma.conf.js +++ b/lib/process-services-cloud/karma.conf.js @@ -48,7 +48,7 @@ module.exports = function (config) { coverageReporter: { dir: join(__dirname, '../../coverage/process-services-cloud'), subdir: '.', - reporters: [{ type: 'html' }, { type: 'text-summary' }, { type: 'text-summary', subdir: '.', file: 'summary.txt' }], + reporters: [{ type: 'html' }, { type: 'lcov' }, { type: 'text-summary' }, { type: 'text-summary', subdir: '.', file: 'summary.txt' }], check: { global: { statements: 75, diff --git a/lib/process-services/karma.conf.js b/lib/process-services/karma.conf.js index 03f0c6ac0f..27c5bf8de9 100644 --- a/lib/process-services/karma.conf.js +++ b/lib/process-services/karma.conf.js @@ -43,7 +43,7 @@ module.exports = function (config) { coverageReporter: { dir: join(__dirname, '../../coverage/process-services'), subdir: '.', - reporters: [{ type: 'html' }, { type: 'text-summary' }, { type: 'text-summary', subdir: '.', file: 'summary.txt' }], + reporters: [{ type: 'html' }, { type: 'lcov' }, { type: 'text-summary' }, { type: 'text-summary', subdir: '.', file: 'summary.txt' }], check: { global: { statements: 75, diff --git a/nx.json b/nx.json index 45492e45a3..30bdd681b6 100644 --- a/nx.json +++ b/nx.json @@ -10,7 +10,8 @@ "cache": true }, "test": { - "cache": true + "cache": true, + "outputs": ["{workspaceRoot}/coverage/{projectName}"] }, "stylelint": { "cache": true diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000000..03cc6905f7 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,11 @@ +sonar.organization=alfresco +sonar.projectKey=Alfresco_alfresco-ng2-components + +sonar.sources=lib +sonar.tests=lib +sonar.test.inclusions=**/*.spec.ts +sonar.exclusions=**/node_modules/**,**/dist/**,**/*.spec.ts,**/*.mock.ts,**/mock/**,**/mocks/**,**/testing/**,**/stories/** + +sonar.javascript.lcov.reportPaths=coverage/core/lcov.info,coverage/content-services/lcov.info,coverage/extensions/lcov.info,coverage/insights/lcov.info,coverage/process-services/lcov.info,coverage/process-services-cloud/lcov.info + +sonar.sourceEncoding=UTF-8