diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 72a9247aab..355d6f3b5d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -130,13 +130,33 @@ jobs: with: node-version: ${{ matrix.node-version }} cache: 'npm' + + # Download Jest coverage from generate-and-backend-tests job - name: Download jest coverage artifact uses: actions/download-artifact@v4 with: name: jest-coverage + path: jest-coverage/ + + # Download Cypress coverage from cypress-zigbee job + - name: Download cypress-zigbee coverage artifact + uses: actions/download-artifact@v4 + with: + name: cypress-zigbee-coverage + path: cypress-coverage/ + continue-on-error: true + - run: sudo ./src-script/install-packages-ubuntu + - run: sudo apt-get install --fix-missing xvfb - run: npm ci - - run: npm run report + - run: npm run version-stamp + - run: npm rebuild canvas --update-binary + - run: npm rebuild libxmljs --update-binary + + # Combine Jest and Cypress coverage and enforce 80% threshold + - name: Combine coverage and check 80% threshold + run: npm run coverage:combine + - name: Codecov uses: codecov/codecov-action@v3.1.1 diff --git a/babel.config.js b/babel.config.js index 73f623cd2c..8504b30fd2 100644 --- a/babel.config.js +++ b/babel.config.js @@ -17,6 +17,12 @@ module.exports = (api) => { presets: [ ['@quasar/babel-preset-app', envOptions], '@babel/preset-typescript' - ] + ], + plugins: [ + // Add Istanbul plugin for code coverage instrumentation when testing + process.env.NODE_ENV === 'test' && 'istanbul', + // Add coverage plugin for Cypress + process.env.CYPRESS_COVERAGE && 'istanbul' + ].filter(Boolean) } } diff --git a/cypress.config.js b/cypress.config.js index 5ba41b5017..3208c0eb36 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -9,7 +9,17 @@ export default defineConfig({ // We've imported your old cypress plugins here. // You may want to clean this up later by importing these. setupNodeEvents(on, config) { - return require('./cypress/plugins/index.js')(on, config) + require('@cypress/code-coverage/task')(on, config) + + // Optional: Add coverage configuration + on('task', { + coverage(coverage) { + // Custom coverage processing if needed + return null + } + }) + + return config }, testIsolation: false, excludeSpecPattern: [ diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js index 0643550a7f..52542fd210 100644 --- a/cypress/support/e2e.js +++ b/cypress/support/e2e.js @@ -19,3 +19,11 @@ import './commands' // Alternatively you can use CommonJS syntax: // require('./commands') import '@cypress/code-coverage/support' + +// Collect coverage from window object +Cypress.on('window:before:load', (win) => { + if (win.__coverage__) { + // Coverage data is available + console.log('Coverage data found on window object') + } +}) diff --git a/jest.config.js b/jest.config.js index c951a52d4a..b75b5dbd4f 100755 --- a/jest.config.js +++ b/jest.config.js @@ -58,5 +58,6 @@ module.exports = { coveragePathIgnorePatterns: [ '/node_modules/', '/src-electron/generator/matter/darwin/Framework/CHIP/templates/helper.js' - ] + ], + testEnvironment: 'jsdom' } diff --git a/package-lock.json b/package-lock.json index bd9e37d185..5d21b19279 100644 --- a/package-lock.json +++ b/package-lock.json @@ -60,6 +60,7 @@ "@vue/test-utils": "^2.4.6", "7zip-bin": "^5.2.0", "autoprefixer": "^10.4.20", + "babel-plugin-istanbul": "^7.0.1", "cross-env": "^7.0.3", "cross-spawn": "^7.0.3", "cypress": "^13.6.6", @@ -3751,15 +3752,6 @@ "node": ">=8" } }, - "node_modules/@jest/reporters/node_modules/istanbul-lib-coverage": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.1.tgz", - "integrity": "sha512-opCrKqbthmq3SKZ10mFMQG9dk3fTa3quaOLD35kJa5ejwZHd9xAr+kLuziiZz2cG32s4lMZxNdmdcEQnTDP4+g==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/@jest/reporters/node_modules/istanbul-lib-instrument": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.1.tgz", @@ -3918,6 +3910,23 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/@jest/transform/node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@jest/transform/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -8602,6 +8611,23 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/babel-jest/node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/babel-jest/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -8684,19 +8710,53 @@ } }, "node_modules/babel-plugin-istanbul": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", - "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", + "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", "dev": true, + "license": "BSD-3-Clause", + "workspaces": [ + "test/babel-8" + ], "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^5.0.4", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", "test-exclude": "^6.0.0" }, "engines": { - "node": ">=8" + "node": ">=12" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/babel-plugin-jest-hoist": { @@ -17915,10 +17975,11 @@ "dev": true }, "node_modules/istanbul-lib-coverage": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", - "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=8" } @@ -17951,15 +18012,6 @@ "node": ">=8" } }, - "node_modules/istanbul-lib-instrument/node_modules/istanbul-lib-coverage": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.1.tgz", - "integrity": "sha512-opCrKqbthmq3SKZ10mFMQG9dk3fTa3quaOLD35kJa5ejwZHd9xAr+kLuziiZz2cG32s4lMZxNdmdcEQnTDP4+g==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/istanbul-lib-processinfo": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.3.tgz", @@ -17977,15 +18029,6 @@ "node": ">=8" } }, - "node_modules/istanbul-lib-processinfo/node_modules/istanbul-lib-coverage": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.1.tgz", - "integrity": "sha512-opCrKqbthmq3SKZ10mFMQG9dk3fTa3quaOLD35kJa5ejwZHd9xAr+kLuziiZz2cG32s4lMZxNdmdcEQnTDP4+g==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/istanbul-lib-processinfo/node_modules/p-map": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", diff --git a/package.json b/package.json index e7850a3100..6124ae0043 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,10 @@ "test:unit:watch": "jest --watch", "test:unit:watchAll": "jest --watchAll", "test:e2e": "node src-script/zap-uitest.js open zigbee", - "test:e2e-ci": "node src-script/zap-uitest.js run zigbee", + "test:e2e-ci": "cross-env NODE_ENV=test CYPRESS_COVERAGE=true node src-script/zap-uitest.js run zigbee", + "test:e2e-coverage": "npm run test:e2e-ci && npm run coverage:combine", + "coverage:combine": "node src-script/zap-combine-reports.js", + "test:coverage": "npm run test:unit:coverage && npm run test:e2e-ci && npm run coverage:combine", "test:e2e-matter": "node src-script/zap-uitest.js open matter", "test:e2e-matter-ci": "node src-script/zap-uitest.js run matter", "report": "node src-script/zap-combine-reports.js", @@ -173,6 +176,7 @@ "@vue/test-utils": "^2.4.6", "7zip-bin": "^5.2.0", "autoprefixer": "^10.4.20", + "babel-plugin-istanbul": "^7.0.1", "cross-env": "^7.0.3", "cross-spawn": "^7.0.3", "cypress": "^13.6.6", @@ -206,10 +210,10 @@ "pkg": "^5.8.1", "prettier": "^3.3.3", "pretty-quick": "^4.0.0", + "sonar-scanner": "^3.1.0", "test-utils": "^1.1.1", "typescript": "4.6", - "workbox-webpack-plugin": "^7.1.0", - "sonar-scanner": "^3.1.0" + "workbox-webpack-plugin": "^7.1.0" }, "engines": { "node": ">= 8.9.0", @@ -314,13 +318,30 @@ }, "nyc": { "report-dir": "cypress-coverage", + "reporter": [ + "json", + "lcov", + "text" + ], "include": [ "src/**/*.js", - "src/**/*.ts", "src-electron/**/*.js", - "src-electron/**/*.ts", - "src-shared/**/*.js", - "src-shared/**/*.ts" + "src-shared/**/*.js" + ], + "exclude": [ + "**/*.test.js", + "**/*.spec.js", + "**/node_modules/**", + "cypress/**/*", + "dist/**/*" + ], + "all": true, + "cache": false, + "instrument": true, + "sourceMap": false, + "extension": [ + ".js", + ".vue" ] }, "pkg": { diff --git a/quasar.conf.js b/quasar.conf.js index 5937b31551..5c54b5de65 100644 --- a/quasar.conf.js +++ b/quasar.conf.js @@ -2,6 +2,7 @@ // https://quasar.dev/quasar-cli/quasar-conf-js const ESLintPlugin = require('eslint-webpack-plugin') const { configure } = require('quasar/wrappers') +const path = require('path') module.exports = configure(function (ctx) { return { @@ -84,6 +85,39 @@ module.exports = configure(function (ctx) { loader: 'file-loader', exclude: /node_modules/ }) + }, + chainWebpack(chain, { isServer, isClient }) { + // Add Istanbul loader for coverage when testing + if (process.env.CYPRESS_COVERAGE) { + // Handle JS files + chain.module + .rule('istanbul-js') + .test(/\.js$/) + .enforce('post') + .include.add(path.resolve(__dirname, 'src')) + .add(path.resolve(__dirname, 'src-electron')) + .add(path.resolve(__dirname, 'src-shared')) + .end() + .exclude.add(/node_modules/) + .add(/\.spec\.js$/) + .add(/cypress/) + .end() + .use('istanbul-instrumenter-loader') + .loader('istanbul-instrumenter-loader') + .options({ + esModules: true + }) + + // Handle Vue files - target the compiled JavaScript from vue-loader + chain.module + .rule('vue') + .use('istanbul-instrumenter-loader') + .loader('istanbul-instrumenter-loader') + .options({ + esModules: true + }) + .before('vue-loader') + } } }, diff --git a/sonar-project.properties b/sonar-project.properties index 40125d8669..9239c47559 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -13,7 +13,7 @@ sonar.organization=project-chip #sonar.projectKey=watt_zap sonar.projectName=Zcl Advanced Platform sonar.sources=src,src-electron,src-shared -sonar.javascript.lcov.reportPaths=jest-coverage/lcov.info,cypress-coverage/lcov.info +sonar.javascript.lcov.reportPaths=coverage/lcov.info sonar.host.url=https://sonarqube.silabs.net sonar.token=put your token here which can be created from the above url # sonar.plsql.jdbc.url=test/.zap/test-server.sqlite diff --git a/src-electron/db/query-command.js b/src-electron/db/query-command.js index ac289daf59..fe72d25498 100644 --- a/src-electron/db/query-command.js +++ b/src-electron/db/query-command.js @@ -1923,7 +1923,11 @@ async function selectCommandDetailsFromAllEndpointTypesAndClusters( } // Ordering of the results: - query = query + ' ORDER BY MANUFACTURER_CODE, CODE, NAME' + query = + query + + (doGroupBy + ? ' ORDER BY MANUFACTURER_CODE, CODE, NAME' + : ' ORDER BY C.MANUFACTURER_CODE, C.CODE, C.NAME') return dbApi.dbAll(db, query).then((rows) => rows.map(commandMapFunction)) } diff --git a/src-electron/generator/helper-zcl.js b/src-electron/generator/helper-zcl.js index 80245d3557..20d916d265 100644 --- a/src-electron/generator/helper-zcl.js +++ b/src-electron/generator/helper-zcl.js @@ -1776,6 +1776,7 @@ function isEnabled(enable) { * @returns boolean */ function isCommandAvailable(clusterSide, incoming, outgoing, source, name) { + if (!clusterSide || !source) return false if (0 == clusterSide.localeCompare(source)) { return false } diff --git a/src-electron/generator/matter/app/zap-templates/common/attributes/Accessors.js b/src-electron/generator/matter/app/zap-templates/common/attributes/Accessors.js index a9136e6e23..536b201030 100644 --- a/src-electron/generator/matter/app/zap-templates/common/attributes/Accessors.js +++ b/src-electron/generator/matter/app/zap-templates/common/attributes/Accessors.js @@ -128,3 +128,6 @@ exports.meta = { category: dbEnum.helperCategory.matter, alias: ['common/attributes/Accessors.js', 'matter-accessors'] }; + +// Exports for unit testing +exports.isUnsupportedType = isUnsupportedType; diff --git a/src-electron/main-process/startup.js b/src-electron/main-process/startup.js index 809a46db5f..0df715bd31 100644 --- a/src-electron/main-process/startup.js +++ b/src-electron/main-process/startup.js @@ -1206,3 +1206,7 @@ exports.shutdown = shutdown exports.quit = quit exports.generateSingleFile = generateSingleFile exports.upgradeZapFile = upgradeZapFile +exports.gatherFiles = gatherFiles +exports.noopConvert = noopConvert +exports.findZapFiles = findZapFiles +exports.outputFile = outputFile diff --git a/src-electron/rest/user-data.js b/src-electron/rest/user-data.js index eda1dba13a..ea140ccbcb 100644 --- a/src-electron/rest/user-data.js +++ b/src-electron/rest/user-data.js @@ -1359,3 +1359,6 @@ exports.patch = [ callback: httpPatchUpdateBitOfFeatureMapAttribute } ] + +//Exports added for testing +exports.httpPostDuplicateEndpointType = httpPostDuplicateEndpointType diff --git a/src-script/zap-combine-reports.js b/src-script/zap-combine-reports.js index c0102e3e2e..cdc8d04cb6 100755 --- a/src-script/zap-combine-reports.js +++ b/src-script/zap-combine-reports.js @@ -22,50 +22,177 @@ const fsExtra = require('fs-extra') //workaround: executeCmd()/spawn() fails silently without complaining about missing path to electron process.env.PATH = process.env.PATH + ':/usr/local/bin/' +/** + * Normalize coverage data structure + * @param {*} coverageData + * @returns normalized coverage data + */ +function normalizeCoverageData(coverageData) { + // If it has a 'data' wrapper (Jest format), extract it + if (coverageData.data && typeof coverageData.data === 'object') { + return coverageData.data + } + // Otherwise return as-is (Cypress format) + return coverageData +} + +/** + * Check if coverage data is valid (has actual coverage info) + * @param {*} coverageData + * @returns boolean + */ +function isValidCoverageData(coverageData) { + if (!coverageData || typeof coverageData !== 'object') { + return false + } + + // Check if it has at least one file with coverage data + const keys = Object.keys(coverageData) + return ( + keys.length > 0 && + keys.some( + (key) => + coverageData[key] && + typeof coverageData[key] === 'object' && + 'path' in coverageData[key] + ) + ) +} + +/** + * Check coverage thresholds and fail if below 80% + * @param {string} coverageSummaryPath + */ +function checkCoverageThresholds(coverageSummaryPath) { + if (!fsExtra.existsSync(coverageSummaryPath)) { + console.error('āŒ Coverage summary not found') + process.exit(1) + } + + const coverageSummary = fsExtra.readJsonSync(coverageSummaryPath) + const total = coverageSummary.total + + const thresholds = { + lines: 80 + } + + let failed = false + console.log('\nšŸ“Š Coverage Summary:') + + Object.keys(thresholds).forEach((key) => { + const actual = total[key].pct + const threshold = thresholds[key] + const status = actual >= threshold ? 'āœ…' : 'āŒ' + + console.log(`${status} ${key}: ${actual}% (threshold: ${threshold}%)`) + + if (actual < threshold) { + failed = true + } + }) + + if (failed) { + console.error('\nāŒ Coverage below 80% threshold. Please add more tests.') + process.exit(1) + } else { + console.log('\nāœ… All coverage thresholds passed!') + } +} + /** * Execute the coverage report script. */ async function executeScript() { try { // Create directory if it does not exist - await fsExtra.ensureDir('reports') + await fsExtra.ensureDir('.nyc_output') + + let hasCoverage = false + let combinedCoverage = {} if (fsExtra.existsSync('cypress-coverage/coverage-final.json')) { - await fsExtra.copy( - 'cypress-coverage/coverage-final.json', - 'reports/from-cypress.json' + const cypressData = await fsExtra.readJson( + 'cypress-coverage/coverage-final.json' + ) + const normalizedCypress = normalizeCoverageData(cypressData) + + if (isValidCoverageData(normalizedCypress)) { + // Merge into combined coverage + Object.assign(combinedCoverage, normalizedCypress) + console.log('āœ… Cypress coverage found and processed') + hasCoverage = true + } else { + console.log( + 'āš ļø Cypress coverage file found but contains no valid coverage data' + ) + } + } else { + console.log( + 'āš ļø No Cypress coverage file found at cypress-coverage/coverage-final.json' ) } if (fsExtra.existsSync('jest-coverage/coverage-final.json')) { - await fsExtra.copy( - 'jest-coverage/coverage-final.json', - 'reports/from-jest.json' + const jestData = await fsExtra.readJson( + 'jest-coverage/coverage-final.json' ) + const normalizedJest = normalizeCoverageData(jestData) + + if (isValidCoverageData(normalizedJest)) { + // Merge into combined coverage (Jest data will override Cypress if same files) + Object.assign(combinedCoverage, normalizedJest) + console.log('āœ… Jest coverage found and processed') + hasCoverage = true + } else { + console.log( + 'āš ļø Jest coverage file found but contains no valid coverage data' + ) + } + } else { + console.log( + 'āš ļø No Jest coverage file found at jest-coverage/coverage-final.json' + ) + } + + if (!hasCoverage) { + console.log( + 'āŒ No valid coverage data found to combine. Run tests first.' + ) + return } - scriptUtil.executeCmd({}, 'npx', ['nyc', 'merge', 'reports']) + // Validate that we have coverage data after combining + if (Object.keys(combinedCoverage).length === 0) { + console.log( + 'āŒ No coverage data found after normalization and combination' + ) + return + } - await fsExtra.move('coverage.json', '.nyc_output/out.json', { - overwrite: true + // Write the combined coverage directly to .nyc_output + await fsExtra.writeJson('.nyc_output/out.json', combinedCoverage, { + spaces: 2 }) + console.log('āœ… Coverage files merged successfully') - scriptUtil.executeCmd( + // Generate final report with JSON summary for threshold checking + await scriptUtil.executeCmd( {}, 'npx', - 'nyc report --reporter lcov --reporter text --report-dir coverage'.split( + 'nyc report --reporter lcov --reporter text --reporter json-summary --report-dir coverage'.split( ' ' ) ) console.log( - `āœ… Please find the combined report (Jest & Cypress) at ./coverage/lcov-report/index.html` + `āœ… Combined coverage report generated at ./coverage/lcov-report/index.html` ) + + // Check coverage thresholds - this will exit with code 1 if below 80% + checkCoverageThresholds('coverage/coverage-summary.json') } catch (err) { - console.log( - 'Error in generating reports at zap-combine-reports.js file and executeScript function: ' + - err - ) + console.log('Error in generating reports:', err) + process.exit(1) } } diff --git a/test/actions.test.js b/test/actions.test.js new file mode 100644 index 0000000000..81513857e5 --- /dev/null +++ b/test/actions.test.js @@ -0,0 +1,441 @@ +/** + * + * Copyright (c) 2025 Silicon Labs + * + * 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 * as actions from '../src/store/zap/actions.js' + +jest.mock('../src/boot/axios', () => ({ + axiosRequests: { + $serverGet: jest.fn((url) => { + if (url.endsWith('zclDeviceType/all')) { + // Return an array of device types as expected by updateZclDeviceTypes + return Promise.resolve({ + data: [ + { + id: 1, + code: 100, + profileId: 260, + label: 'Test Device', + caption: 'Test Device Caption', + domain: 'Test Domain', + packageRef: 1 + } + ] + }) + } + // Default mock for other endpoints + return Promise.resolve({ data: {} }) + }), + $serverPost: jest.fn((url, data) => { + if (url === '/attributeUpdate') { + return Promise.resolve({ + data: { + endpointTypeAttributeData: { + attributeRef: 1, + clusterRef: 1, + included: true, + singleton: false, + bounded: false, + includedReportable: false, + defaultValue: 0, + storageOption: 'ram', + minInterval: 0, + maxInterval: 1, + reportableChange: 0 + } + } + }) + } else if (url === '/endpoint') { + return Promise.resolve({ + data: { + id: 1, + endpointId: 1, + parentEndpointIdentifier: null, + endpointType: 1, + networkId: 1, + profileId: 1, + validationIssues: { + endpointId: [], + networkId: [] + } + } + }) + } + return Promise.resolve({ data: {} }) + }), + $serverPatch: jest.fn(() => + Promise.resolve({ + data: { + endpointId: 1, + parentEndpointIdentifier: null, + changes: [{ updatedKey: 'endpointType', value: 'foo' }], + validationIssues: { + endpointId: [], + networkId: [] + } + } + }) + ), + $serverDelete: jest.fn(() => + Promise.resolve({ data: { successful: true, id: 1 } }) + ) + } +})) +jest.mock('../src-electron/util/util.js', () => ({ + cantorPair: jest.fn(() => 42) +})) +jest.mock('../src-shared/rest-api.js', () => ({ + uri: { + featureMapAttribute: '/featureMapAttribute', + attributeUpdate: '/attributeUpdate', + endpointType: '/endpointType', + endpoint: '/endpoint', + duplicateEndpoint: '/duplicateEndpoint', + duplicateEndpointType: '/duplicateEndpointType', + packages: '/packages', + initialState: '/initialState', + getAllPackages: '/getAllPackages', + sessionPackage: '/sessionPackage', + addNewPackage: '/addNewPackage', + getAllSessionKeyValues: '/getAllSessionKeyValues' + }, + uc: { + componentAdd: '/componentAdd', + componentRemove: '/componentRemove' + } +})) + +describe('zap actions', () => { + let context + + beforeEach(() => { + context = { + commit: jest.fn(), + dispatch: jest.fn(), + state: {}, + rootState: {}, + getters: {}, + rootGetters: {} + } + }) + + it('updateClusters should call $serverGet and commit updateClusters', async () => { + await actions.updateClusters(context) + expect(context.commit).toHaveBeenCalledWith( + 'updateClusters', + expect.anything() + ) + }) + + it('updateAtomics should commit updateAtomics', async () => { + require('../src/boot/axios').axiosRequests.$serverGet.mockResolvedValueOnce( + { data: [1, 2, 3] } + ) + await actions.updateAtomics(context, [1, 2, 3]) + expect(context.commit).toHaveBeenCalledWith('updateAtomics', [1, 2, 3]) + }) + + it('updateSelectedCluster should call $serverGet and commit updateSelectedCluster', async () => { + await actions.updateSelectedCluster(context, { id: 1 }) + expect(context.commit).toHaveBeenCalledWith( + 'updateSelectedCluster', + expect.anything() + ) + }) + + it('updateAttributes should commit updateAttributes', () => { + actions.updateAttributes(context, [1, 2]) + expect(context.commit).toHaveBeenCalledWith('updateAttributes', [1, 2]) + }) + + it('updateCommands should commit updateCommands', () => { + actions.updateCommands(context, [1, 2]) + expect(context.commit).toHaveBeenCalledWith('updateCommands', [1, 2]) + }) + + it('updateEvents should commit updateEvents', () => { + actions.updateEvents(context, [1, 2]) + expect(context.commit).toHaveBeenCalledWith('updateEvents', [1, 2]) + }) + + it('updateFeatures should commit updateFeatures', () => { + actions.updateFeatures(context, [1, 2]) + expect(context.commit).toHaveBeenCalledWith('updateFeatures', [1, 2]) + }) + + it('updateFeatureMapAttribute should call $serverGet and commit updateFeatureMapAttribute', async () => { + await actions.updateFeatureMapAttribute(context, { id: 1 }) + expect(context.commit).toHaveBeenCalledWith( + 'updateFeatureMapAttribute', + expect.anything() + ) + }) + + it('updateEndpointConfigs should commit updateEndpointConfigs', () => { + actions.updateEndpointConfigs(context, [1, 2]) + expect(context.commit).toHaveBeenCalledWith('updateEndpointConfigs', [1, 2]) + }) + + it('selectConfiguration should commit selectConfiguration', () => { + actions.selectConfiguration(context, 'config') + expect(context.commit).toHaveBeenCalledWith('selectConfiguration', 'config') + }) + + it('initSelectedAttribute should call $serverPost and setAttributeState', async () => { + const selectionContext = { id: 1, attributeRef: 1, clusterRef: 1 } + await actions.initSelectedAttribute(context, selectionContext) + expect(context.commit).toHaveBeenCalledWith( + 'setEndpointTypeAttribute', + expect.anything() + ) + }) + + it('updateSelectedAttribute should call setAttributeState and commit setEndpointTypeAttribute', async () => { + // Mock the axios response to return the expected structure + require('../src/boot/axios').axiosRequests.$serverPost.mockResolvedValueOnce( + { + data: { + endpointTypeAttributeData: { + attributeRef: 1, + clusterRef: 2, + included: true, + singleton: false, + bounded: false, + includedReportable: false, + defaultValue: 0, + storageOption: 'ram', + minInterval: 0, + maxInterval: 1, + reportableChange: 0 + } + } + } + ) + await actions.updateSelectedAttribute(context, { id: 1 }) + expect(context.commit).toHaveBeenCalledWith( + 'setEndpointTypeAttribute', + expect.anything() + ) + }) + + it('updateSelectedCommands should call $serverPost and commit updateInclusionList', async () => { + require('../src/boot/axios').axiosRequests.$serverPost.mockResolvedValueOnce( + { + data: { + action: 'boolean', + id: 1, + clusterRef: 2, + added: true, + listType: 'selectedIn' + } + } + ) + await actions.updateSelectedCommands(context, { id: 1 }) + expect(context.commit).toHaveBeenCalledWith( + 'updateInclusionList', + expect.anything() + ) + }) + + it('updateSelectedEvents should call $serverPost and commit updateInclusionList', async () => { + require('../src/boot/axios').axiosRequests.$serverPost.mockResolvedValueOnce( + { + data: { + action: 'boolean', + id: 1, + clusterRef: 2, + added: true, + listType: 'selectedEvents' + } + } + ) + await actions.updateSelectedEvents(context, { id: 1 }) + expect(context.commit).toHaveBeenCalledWith( + 'updateInclusionList', + expect.anything() + ) + }) + + it('updateSelectedComponent should call $serverPost with correct arguments', async () => { + const payload = { id: 1, added: true } + await actions.updateSelectedComponent(context, payload) + expect( + require('../src/boot/axios').axiosRequests.$serverPost + ).toHaveBeenCalledWith('/componentAdd', payload) + }) + + it('getProjectPackages should call $serverGet and commit updateProjectPackages', async () => { + await actions.getProjectPackages(context) + expect(context.commit).toHaveBeenCalledWith( + 'updateProjectPackages', + expect.anything() + ) + }) + + it('initializeDefaultEndpoints should commit initializeDefaultEndpoints', () => { + actions.initializeDefaultEndpoints(context, [1, 2]) + expect(context.commit).toHaveBeenCalledWith( + 'initializeDefaultEndpoints', + [1, 2] + ) + }) + + it('initializeDefaultEndpointsTypes should commit initializeDefaultEndpointsTypes', () => { + actions.initializeDefaultEndpointsTypes(context, [1, 2]) + expect(context.commit).toHaveBeenCalledWith( + 'initializeDefaultEndpointsTypes', + [1, 2] + ) + }) + + it('updateSelectedEndpoint should commit updateSelectedEndpoint', () => { + actions.updateSelectedEndpoint(context, { id: 1 }) + expect(context.commit).toHaveBeenCalledWith('updateSelectedEndpoint', { + id: 1 + }) + }) + + it('updateEndpoint should call $serverPatch and commit updateEndpoint', async () => { + await actions.updateEndpoint(context, { id: 1 }) + expect(context.commit).toHaveBeenCalledWith( + 'updateEndpoint', + expect.anything() + ) + }) + + it('addEndpoint should call $serverPost and commit addEndpoint', async () => { + await actions.addEndpoint(context, { id: 1 }) + expect(context.commit).toHaveBeenCalledWith( + 'addEndpoint', + expect.anything() + ) + }) + + it('addEndpointType should call $serverPost and commit addEndpointType', async () => { + await actions.addEndpointType(context, { id: 1 }) + expect(context.commit).toHaveBeenCalledWith( + 'addEndpointType', + expect.anything() + ) + }) + + it('duplicateEndpointType should call $serverPost', async () => { + const res = await actions.duplicateEndpointType(context, { + endpointTypeId: 1 + }) + expect(res).toBeDefined() + }) + + it('deleteEndpoint should call $serverDelete and commit deleteEndpoint', async () => { + await actions.deleteEndpoint(context, 1) + expect(context.commit).toHaveBeenCalledWith('deleteEndpoint', { id: 1 }) + }) + + it('duplicateEndpoint should call $serverPost', async () => { + const res = await actions.duplicateEndpoint(context, { + endpointId: 1, + endpointIdentifier: 2, + endpointTypeId: 3 + }) + expect(res).toBeDefined() + }) + + it('deleteEndpointType should call $serverDelete and commit removeEndpointType', async () => { + await actions.deleteEndpointType(context, 1) + expect(context.commit).toHaveBeenCalledWith('removeEndpointType', { id: 1 }) + }) + + it('setAttributeState should commit setEndpointTypeAttribute', () => { + actions.setAttributeState(context, { + attributeRef: 1, + clusterRef: 2, + included: true, + singleton: false, + bounded: false, + includedReportable: false, + defaultValue: 0, + storageOption: 'ram', + minInterval: 0, + maxInterval: 1, + reportableChange: 0 + }) + expect(context.commit).toHaveBeenCalledWith( + 'setEndpointTypeAttribute', + expect.anything() + ) + }) + + it('setMiniState should commit setMiniState', () => { + actions.setMiniState(context, { foo: 'bar' }) + expect(context.commit).toHaveBeenCalledWith('setMiniState', { foo: 'bar' }) + }) + + it('loadInitialData should call $serverGet and commit initializeEndpoints, initializeEndpointTypes, initializeSessionKeyValues, setAllPackages', async () => { + await actions.loadInitialData(context) + expect(context.commit).toHaveBeenCalled() + }) + + it('updateShowDevTools should commit updateShowDevTools', () => { + actions.updateShowDevTools(context) + expect(context.commit).toHaveBeenCalledWith('updateShowDevTools') + }) + + it('updateExceptions should commit updateExceptions and toggleShowExceptionIcon', () => { + actions.updateExceptions(context, { foo: 'bar' }) + expect(context.commit).toHaveBeenCalledWith('updateExceptions', { + foo: 'bar' + }) + expect(context.commit).toHaveBeenCalledWith('toggleShowExceptionIcon', true) + }) + + it('updateInformationText should commit updateInformationText after axios resolves', async () => { + require('../src/boot/axios').axiosRequests.$serverPost.mockResolvedValueOnce( + { data: {} } + ) + await actions.updateInformationText(context, 'info') + expect(context.commit).toHaveBeenCalledWith('updateInformationText', 'info') + }) + + it('updateZclDeviceTypes should commit updateZclDeviceTypes with deviceTypeObjects', async () => { + require('../src/boot/axios').axiosRequests.$serverGet.mockResolvedValueOnce( + { + data: [ + { + id: 1, + code: 100, + profileId: 260, + label: 'Test', + caption: 'Desc', + domain: 'dom', + packageRef: 2 + } + ] + } + ) + await actions.updateZclDeviceTypes(context) + expect(context.commit).toHaveBeenCalledWith('updateZclDeviceTypes', { + 1: { + code: 100, + profileId: 260, + label: 'Test', + description: 'Desc', + domain: 'dom', + packageRef: 2 + } + }) + }) +}) diff --git a/test/feature.test.js b/test/feature.test.js index 3ed81a92d8..aaa70978af 100644 --- a/test/feature.test.js +++ b/test/feature.test.js @@ -911,3 +911,28 @@ test( }, testUtil.timeout.short() ) + +test('selectAllFeatures returns all features for given packageIds', async () => { + const features = await queryFeature.selectAllFeatures(db, [pkgId]) + expect(Array.isArray(features)).toBe(true) + expect(features.length).toBeGreaterThan(0) + expect(features[0]).toHaveProperty('featureId') + expect(features[0]).toHaveProperty('name') +}) + +test('selectFeaturesByClusterId returns features for a cluster', async () => { + // Find a clusterRef that exists in the FEATURE table + const featureRow = await dbApi.dbGet( + db, + 'SELECT CLUSTER_REF FROM FEATURE LIMIT 1' + ) + expect(featureRow).toBeDefined() + const clusterId = featureRow.CLUSTER_REF + const features = await queryFeature.selectFeaturesByClusterId(db, clusterId) + expect(Array.isArray(features)).toBe(true) +}) + +test('checkIfConformanceDataExist returns a boolean', async () => { + const result = await queryFeature.checkIfConformanceDataExist(db) + expect(typeof result).toBe('boolean') +}) diff --git a/test/gen-template/zigbee/zcl-test.zapt b/test/gen-template/zigbee/zcl-test.zapt index 7d9590c98d..8fd359b01c 100644 --- a/test/gen-template/zigbee/zcl-test.zapt +++ b/test/gen-template/zigbee/zcl-test.zapt @@ -83,3 +83,56 @@ Last item #undef EMBER_AF_SUPPORT_COMMAND_DISCOVERY {{/if_command_discovery_enabled}} /******************************************/ + + +// Manufaturing specific attributes of Level Control server cluster +{{#all_user_cluster_manufacturer_specific_attributes "Level Control" "server"}} +M-S: {{name}}|{{side}}|{{clusterName}} +{{/all_user_cluster_manufacturer_specific_attributes}} +--- +{{#all_user_cluster_non_manufacturer_specific_attributes "Level Control" "server"}} +NMS: {{name}}|{{side}}|{{clusterName}} +{{/all_user_cluster_non_manufacturer_specific_attributes}} +--- +{{#enabled_attributes_for_cluster_and_side "Level Control" "server"}} +EN: {{name}}|{{side}}|{{clusterName}} +{{/enabled_attributes_for_cluster_and_side}} + +/********* INCOMING/OUTGOING/COMBINED COMMANDS *********/ +{{#zcl_commands}} +CMD: {{name}}|{{source}} +{{/zcl_commands}} +--- +{{#zcl_commands_with_arguments}} +CMD_ARG: {{name}}|{{clusterName}}|ARGS:{{#each commandArgs}}{{this.name}},{{/each}} +{{/zcl_commands_with_arguments}} +--- + +/******** USER CLUSTER HAS ENABLED COMMAND TEST ********/ +{{#if (user_cluster_has_enabled_command "Level Control" "server")}} +HAS_ENABLED_COMMAND: Level Control|server +{{else}} +NO_ENABLED_COMMAND: None|None +{{/if}} +{{#if (user_cluster_has_enabled_command "Alarms" "server")}} +HAS_ENABLED_COMMAND: None|None +{{else}} +NO_ENABLED_COMMAND: Alarms|server +{{/if}} +--- +/******** ALL INCOMING COMMANDS FOR CLUSTER COMBINED TEST ********/ +{{#all_incoming_commands_for_cluster_combined "Level Control" true true}} +INCOMING_COMBINED: {{commandName}}|{{clusterName}}|{{clusterSide}} +{{/all_incoming_commands_for_cluster_combined}} + +/******** MFG specific commands ********/ +{{#all_user_cluster_manufacturer_specific_commands "Level Control" "server"}} +MS-CMD: {{name}}|{{side}}|{{clusterName}} +{{/all_user_cluster_manufacturer_specific_commands}} + +/******** Non-MFG specific commands ********/ +{{#all_user_cluster_non_manufacturer_specific_commands "Level Control" "server"}} +NMSCMD: {{name}}|{{side}}|{{clusterName}} +{{/all_user_cluster_non_manufacturer_specific_commands}} + + diff --git a/test/helpers.test.js b/test/helpers.test.js index 31bd776a19..d310a07008 100644 --- a/test/helpers.test.js +++ b/test/helpers.test.js @@ -27,6 +27,8 @@ const zclLoader = require('../src-electron/zcl/zcl-loader') const zclHelper = require('../src-electron/generator/helper-zcl') const dbEnum = require('../src-shared/db-enum') const zapHelper = require('../src-electron/generator/helper-zap') +const accessors = require('../src-electron/generator/matter/app/zap-templates/common/attributes/Accessors.js') +const path = require('path') let db let zclContext @@ -634,3 +636,39 @@ test( }, testUtil.timeout.short() ) + +describe('Accessors.js', () => { + test('isUnsupportedType returns true for unsupported types', () => { + expect(accessors.isUnsupportedType('EUI64')).toBe(true) + expect(accessors.isUnsupportedType('eui64')).toBe(true) + expect(accessors.isUnsupportedType('STRING')).toBe(false) + }) + + test('canHaveSimpleAccessors returns false for arrays and lists', () => { + expect( + accessors.canHaveSimpleAccessors({ isArray: true, type: 'int8u' }) + ).toBe(false) + // This triggers ListHelper.isList, which returns true for 'array' + expect( + accessors.canHaveSimpleAccessors({ isArray: false, type: 'array' }) + ).toBe(false) + }) + + test('canHaveSimpleAccessors returns false for unsupported types', () => { + expect( + accessors.canHaveSimpleAccessors({ isArray: false, type: 'EUI64' }) + ).toBe(false) + }) + + test('canHaveSimpleAccessors returns true for supported types', () => { + expect( + accessors.canHaveSimpleAccessors({ isArray: false, type: 'int8u' }) + ).toBe(true) + }) + + test('typeAsDelimitedMacro returns correct macro', async () => { + await expect( + accessors.typeAsDelimitedMacro.call(ctx, 'int8u') + ).resolves.toBeDefined() + }) +}) diff --git a/test/importexport.test.js b/test/importexport.test.js index f0ebf6e1b0..1b232b6d07 100644 --- a/test/importexport.test.js +++ b/test/importexport.test.js @@ -635,3 +635,78 @@ test('Import a ZAP file with enabled commands missing response commands and veri ) ) }) + +test( + 'Generation with OnOff cluster attribute helpers', + async () => { + // Use a ZAP file that has the OnOff server cluster enabled, e.g. test/resource/test-light.zap + let sid = await querySession.createBlankSession(db) + await util.ensurePackagesAndPopulateSessionOptions( + templateContext.db, + sid, + { + zcl: env.builtinSilabsZclMetafile(), + template: env.builtinTemplateMetafile() + }, + null, + [templatePkgId] + ) + await importJs.importDataFromFile( + db, + path.join(__dirname, 'resource/test-light.zap'), + { + sessionId: sid + } + ) + + // Generate using the template containing the helpers above + const genResult = await generationEngine.generate( + db, + sid, + templatePkgId, + {}, + { + generateOnly: 'zcl-test.out', + disableDeprecationWarnings: true + } + ) + const output = genResult.content['zcl-test.out'] + + // Check that output contains expected attribute lines for Level control cluster + expect(output).toMatch(/NMS: current level|server|Level Control/) + expect(output).toMatch(/EN: current level|server|Level Control/) + // also check for No MS: + expect(output).not.toMatch(/M-S: .*Level Control/) + + // Check that output contains expected command lines + expect(output).toMatch(/CMD: .*AddGroup/) + expect(output).toMatch( + /CMD_ARG: UpdateCommissionState|Identify|ARGS:action,commissionStateMask,/ + ) + + // Check that output contains expected lines for incoming commands combined + expect(output).toMatch( + /INCOMING_COMBINED: MoveToLevel|Level Control|server/ + ) + expect(output).toMatch(/INCOMING_COMBINED: Stop|Level Control|server/) + expect(output).toMatch( + /INCOMING_COMBINED: MoveToLevelWithOnOff|Level Control|server/ + ) + expect(output).toMatch( + /INCOMING_COMBINED: StepWithOnOff|Level Control|server/ + ) + + // Check that output contains expected lines for enabled commands + expect(output).toMatch(/HAS_ENABLED_COMMAND: Level Control|server/) + expect(output).toMatch(/NO_ENABLED_COMMAND: Alarms|server/) + + // Check that output does not contain expected lines for mfg specific commands + expect(output).not.toMatch(/MS-CMD:/) + + // Check that output contains expected lines for mfg specific commands + expect(output).toMatch(/NMSCMD: Move||Level Control/) + expect(output).toMatch(/NMSCMD: MoveToLevel||Level Control/) + expect(output).toMatch(/NMSCMD: Stop||Level Control/) + }, + testUtil.timeout.medium() +) diff --git a/test/query.test.js b/test/query.test.js index 8257248877..999a19294a 100644 --- a/test/query.test.js +++ b/test/query.test.js @@ -24,6 +24,7 @@ const queryZcl = require('../src-electron/db/query-zcl') const queryDeviceType = require('../src-electron/db/query-device-type') const queryAttribute = require('../src-electron/db/query-attribute') const queryCommand = require('../src-electron/db/query-command') +const queryEvent = require('../src-electron/db/query-event') const queryLoader = require('../src-electron/db/query-loader') const queryConfig = require('../src-electron/db/query-config') const queryEndpointType = require('../src-electron/db/query-endpoint-type') @@ -773,6 +774,349 @@ describe('Endpoint Type Config Queries', () => { ) }) +describe('user-data.js REST API handlers', () => { + it( + 'httpPostDuplicateEndpointType, httpGetEndpointIds, etc)', + async () => { + let ctx = await zclLoader.loadZcl(db, env.builtinSilabsZclMetafile()) + pkgId = ctx.packageId + let dts = await queryDeviceType.selectAllDeviceTypes(db, pkgId) + let haOnOffDeviceTypeArray = dts.filter( + (data) => data.label === 'HA-onoff' + ) + let haOnOffDeviceType = haOnOffDeviceTypeArray[0] + let deviceTypeId = haOnOffDeviceType.id + let allSessionPartitions = + await querySession.getAllSessionPartitionInfoForSession(db, sid) + let endpointTypeId = await queryConfig.insertEndpointType( + db, + allSessionPartitions[0], + 'Test endpoint', + deviceTypeId, + haOnOffDeviceType.code, + 0, + true + ) + let endpointTypes = await queryEndpointType.selectAllEndpointTypes( + db, + sid + ) + let endpointTypesLength1 = endpointTypes.length + const req = { body: { endpointTypeId: endpointTypeId } } + const res = { + status: jest.fn().mockReturnThis(), + json: jest.fn() + } + const handler = + require('../src-electron/rest/user-data').httpPostDuplicateEndpointType( + db + ) + await handler(req, res) + expect(res.status).toHaveBeenCalledWith(200) + expect(res.json).toHaveBeenCalledWith( + expect.objectContaining({ id: expect.anything() }) + ) + // check that the new endpoint type exists in the DB + endpointTypes = await queryEndpointType.selectAllEndpointTypes(db, sid) + expect(endpointTypesLength1 + 1).toEqual(endpointTypes.length) + + // --- httpGetEndpointIds logic moved here --- + let endpointId = await queryEndpoint.insertEndpoint( + db, + sid, + 99, + endpointTypeId, + 0, + 0 + ) + const reqEp = { zapSessionId: sid } + const resEp = { status: jest.fn().mockReturnThis(), json: jest.fn() } + const handlerEp = require('../src-electron/rest/user-data') + .get.find((e) => e.uri === restApi.uri.endpointIds) + .callback(db) + await handlerEp(reqEp, resEp) + expect(resEp.status).toHaveBeenCalledWith(200) + expect(resEp.json).toHaveBeenCalledWith(expect.arrayContaining([99])) + + // --- httpPostAttributeUpdate logic --- + // Insert cluster for endpoint type + let clusters = await queryZcl.selectAllClusters(db, pkgId) + let cluster = clusters.find((c) => c.code === 6) // On/Off cluster + await queryConfig.insertOrReplaceClusterState( + db, + endpointTypeId, + cluster.id, + 'server', + true + ) + // Insert attribute for cluster + let attributes = + await queryZcl.selectAttributesByClusterIdIncludingGlobal( + db, + cluster.id, + pkgId + ) + let attribute = attributes[0] + // Prepare request with real IDs + const reqAttr = { + zapSessionId: sid, + body: { + action: 'update', + endpointTypeIdList: [endpointTypeId], + selectedEndpoint: endpointId, + id: attribute.id, + value: 1, + listType: restApi.updateKey.attributeSelected, + clusterRef: cluster.id, + attributeSide: 'server' + } + } + const resAttr = { status: jest.fn().mockReturnThis(), json: jest.fn() } + const handlerAttr = require('../src-electron/rest/user-data') + .post.find((e) => e.uri === restApi.uri.attributeUpdate) + .callback(db) + await handlerAttr(reqAttr, resAttr) + expect(resAttr.status).toHaveBeenCalledWith(200) + expect(resAttr.json).toHaveBeenCalledWith( + expect.objectContaining({ action: 'update', id: attribute.id }) + ) + + // Insert a command for the cluster + let commands = await queryCommand.selectCommandsByClusterId( + db, + cluster.id, + pkgId + ) + let command = commands[0] + + // httpPostCommandUpdate + const reqCmd = { + zapSessionId: sid, + body: { + action: 'update', + endpointTypeIdList: [endpointTypeId], + id: command.id, + value: 1, + listType: 'selectedIn', + clusterRef: cluster.id, + commandSide: 'server' + } + } + const resCmd = { status: jest.fn().mockReturnThis(), json: jest.fn() } + const handlerCmd = require('../src-electron/rest/user-data') + .post.find((e) => e.uri === restApi.uri.commandUpdate) + .callback(db) + await handlerCmd(reqCmd, resCmd) + expect(resCmd.status).toHaveBeenCalledWith(200) + expect(resCmd.json).toHaveBeenCalledWith( + expect.objectContaining({ action: 'update', id: command.id }) + ) + + // Insert an event for the cluster + let events = await queryEvent.selectEventsByClusterId( + db, + cluster.id, + pkgId + ) + let event = events[0] + + // httpPostEventUpdate + const reqEvent = { + zapSessionId: sid, + body: { + action: 'update', + endpointTypeId: endpointTypeId, + id: 1, + value: 1, + listType: 'selected', + clusterRef: cluster.id, + eventSide: 'server' + } + } + const resEvent = { status: jest.fn().mockReturnThis(), json: jest.fn() } + const handlerEvent = require('../src-electron/rest/user-data') + .post.find((e) => e.uri === restApi.uri.eventUpdate) + .callback(db) + await handlerEvent(reqEvent, resEvent) + expect(resEvent.status).toHaveBeenCalledWith(200) + expect(resEvent.json).toHaveBeenCalledWith( + expect.objectContaining({ action: 'update', id: 1 }) + ) + + // httpPostDuplicateEndpoint + const reqDupEp = { + body: { id: endpointId, endpointIdentifier: 102, endpointTypeId } + } + const resDupEp = { status: jest.fn().mockReturnThis(), json: jest.fn() } + const handlerDupEp = require('../src-electron/rest/user-data') + .post.find((e) => e.uri === restApi.uri.duplicateEndpoint) + .callback(db) + await handlerDupEp(reqDupEp, resDupEp) + expect(resDupEp.status).toHaveBeenCalledWith(200) + expect(resDupEp.json).toHaveBeenCalledWith( + expect.objectContaining({ id: expect.anything() }) + ) + }, + testUtil.timeout.long() + ) + + it('httpGetSessionKeyValues returns session key values', async () => { + await querySession.updateSessionKeyValue(db, sid, 'foo', 'bar') + const req = { zapSessionId: sid } + const res = { status: jest.fn().mockReturnThis(), json: jest.fn() } + const handler = require('../src-electron/rest/user-data') + .get.find((e) => e.uri === restApi.uri.getAllSessionKeyValues) + .callback(db) + await handler(req, res) + expect(res.status).toHaveBeenCalledWith(200) + expect(res.json).toHaveBeenCalledWith( + expect.arrayContaining([ + expect.objectContaining({ key: 'foo', value: 'bar' }) + ]) + ) + }) + + it('httpGetDeviceTypeFeatures returns device type features', async () => { + const req = { query: { deviceTypeRefs: [1], endpointTypeRef: 1 } } + const res = { status: jest.fn().mockReturnThis(), json: jest.fn() } + const handler = require('../src-electron/rest/user-data') + .get.find((e) => e.uri === restApi.uri.deviceTypeFeatures) + .callback(db) + await handler(req, res) + expect(res.status).toHaveBeenCalledWith(200) + expect(res.json).toHaveBeenCalled() + }) + + it('httpPostSaveSessionKeyValue saves and returns key/value', async () => { + const req = { zapSessionId: sid, body: { key: 'baz', value: 'qux' } } + const res = { status: jest.fn().mockReturnThis(), json: jest.fn() } + const handler = require('../src-electron/rest/user-data') + .post.find((e) => e.uri === restApi.uri.saveSessionKeyValue) + .callback(db) + await handler(req, res) + expect(res.status).toHaveBeenCalledWith(200) + expect(res.json).toHaveBeenCalledWith( + expect.objectContaining({ key: 'baz', value: 'qux' }) + ) + }) + + it('httpPostCluster inserts or updates cluster state', async () => { + let ctx = await zclLoader.loadZcl(db, env.builtinSilabsZclMetafile()) + pkgId = ctx.packageId + let dts = await queryDeviceType.selectAllDeviceTypes(db, pkgId) + let haOnOffDeviceType = dts.find((data) => data.label === 'HA-onoff') + let allSessionPartitions = + await querySession.getAllSessionPartitionInfoForSession(db, sid) + let endpointTypeId = await queryConfig.insertEndpointType( + db, + allSessionPartitions[0], + 'Test endpoint', + haOnOffDeviceType.id, + haOnOffDeviceType.code, + 0, + true + ) + const req = { + zapSessionId: sid, + body: { id: 6, side: 'server', flag: true, endpointTypeId } + } + const res = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + send: jest.fn() + } + const handler = require('../src-electron/rest/user-data') + .post.find((e) => e.uri === restApi.uri.cluster) + .callback(db) + await handler(req, res) + expect(res.status).toHaveBeenCalledWith(200) + expect(res.json).toHaveBeenCalledWith( + expect.objectContaining({ + endpointTypeId, + id: 6, + side: 'server', + flag: true + }) + ) + }) + + it('httpGetInitialState returns initial state', async () => { + const req = { zapSessionId: sid } + const res = { status: jest.fn().mockReturnThis(), json: jest.fn() } + const handler = require('../src-electron/rest/user-data') + .get.find((e) => e.uri === restApi.uri.initialState) + .callback(db) + await handler(req, res) + expect(res.status).toHaveBeenCalledWith(200) + expect(res.json).toHaveBeenCalledWith( + expect.objectContaining({ + endpointTypes: expect.anything(), + endpoints: expect.anything(), + sessionKeyValues: expect.anything() + }) + ) + }) + + it('httpGetOption returns options', async () => { + const req = { zapSessionId: sid, params: { category: 'generator' } } + const res = { status: jest.fn().mockReturnThis(), json: jest.fn() } + const handler = require('../src-electron/rest/user-data') + .get.find((e) => e.uri === `${restApi.uri.option}/:category`) + .callback(db) + await handler(req, res) + expect(res.status).toHaveBeenCalledWith(200) + expect(res.json).toHaveBeenCalled() + }) + + it('httpGetUiOptions returns UI options', async () => { + const req = { zapSessionId: sid } + const res = { status: jest.fn().mockReturnThis(), json: jest.fn() } + const handler = require('../src-electron/rest/user-data') + .get.find((e) => e.uri === restApi.uri.uiOptions) + .callback(db) + await handler(req, res) + expect(res.status).toHaveBeenCalledWith(200) + expect(res.json).toHaveBeenCalled() + }) + + it('httpGetPackages returns project packages', async () => { + const req = { zapSessionId: sid } + const res = { status: jest.fn().mockReturnThis(), json: jest.fn() } + const handler = require('../src-electron/rest/user-data') + .get.find((e) => e.uri === restApi.uri.packages) + .callback(db) + await handler(req, res) + expect(res.status).toHaveBeenCalledWith(200) + expect(res.json).toHaveBeenCalled() + }) + + it('httpGetAllPackages returns all packages', async () => { + const req = {} + const res = { status: jest.fn().mockReturnThis(), json: jest.fn() } + const handler = require('../src-electron/rest/user-data') + .get.find((e) => e.uri === restApi.uri.getAllPackages) + .callback(db) + await handler(req, res) + expect(res.status).toHaveBeenCalledWith(200) + expect(res.json).toHaveBeenCalledWith( + expect.objectContaining({ packages: expect.anything() }) + ) + }) + + it('httpPatchUpdateBitOfFeatureMapAttribute updates feature map attribute', async () => { + const req = { body: { featureMapAttributeId: 1, newValue: 1 } } + const res = { status: jest.fn().mockReturnThis(), json: jest.fn() } + const handler = require('../src-electron/rest/user-data') + .patch.find((e) => e.uri === restApi.uri.updateBitOfFeatureMapAttribute) + .callback(db) + await handler(req, res) + expect(res.status).toHaveBeenCalledWith(200) + expect(res.json).toHaveBeenCalledWith( + expect.objectContaining({ successful: expect.any(Boolean) }) + ) + }) +}) + test( 'Test Rest Key to DB Column Test', () => { diff --git a/test/ui.test.js b/test/ui.test.js index b03c5e581f..864670671c 100644 --- a/test/ui.test.js +++ b/test/ui.test.js @@ -43,9 +43,61 @@ import About from '../src/pages/preferences/AboutPage.vue' import MainLayout from '../src/layouts/MainLayout.vue' import routes from '../src/router/routes.js' import { createRouter, createWebHistory } from 'vue-router' +import ZapConfig from '../src/pages/ZapConfig.vue' +import FeatureTableHeader from '../src/components/FeatureTableHeader.vue' +import InitialContent from '../src/components/InitialContent.vue' +import MainSidebar from '../src/components/MainSidebar.vue' +import SettingsSidebar from '../src/components/SettingsSidebar.vue' +import SqlQuery from '../src/components/SqlQuery.vue' +import ZCLToolbar from '../src/components/ZCLToolbar.vue' +import ZclClusterFeatureManager from '../src/components/ZclClusterFeatureManager.vue' +import ZclExceptions from '../src/components/ZclExceptions.vue' +import ZclEventManager from '../src/components/ZclEventManager.vue' +import ZclDeviceTypeFeatureManager from '../src/components/ZclDeviceTypeFeatureManager.vue' + +import EndpointManager from '../src/pages/EndpointManager.vue' +import ExtensionsPage from '../src/pages/ExtensionsPage.vue' +import NotificationsPage from '../src/pages/NotificationsPage.vue' +import OptionsPage from '../src/pages/OptionsPage.vue' +import PreferencePage from '../src/pages/PreferencePage.vue' + +import ApiExceptions from '../src/pages/preferences/devtools/ApiExceptions.vue' +import InformationSetup from '../src/pages/preferences/devtools/InformationSetup.vue' +import SqlQueryDev from '../src/pages/preferences/devtools/SqlQuery.vue' + +import CMPTour from '../src/tutorials/CMPTour.vue' +import EndpointTour from '../src/tutorials/EndpointTour.vue' +import ZclTour from '../src/tutorials/ZclTour.vue' +import App from '../src/App.vue' +import CommonMixin from '../src/util/common-mixin' +import uiOptions from '../src/util/ui-options' import { timeout } from './test-util.js' import ZapStore from '../src/store/index.js' +import { createStore } from 'vuex' +import rendApi from '../src-shared/rend-api.js' + +global.window = global.window || {} +window[rendApi.GLOBAL_SYMBOL_EXECUTE] = jest.fn() + +// Minimal zap state for App.vue and common-mixin +const zapState = { + selectedZapConfig: null, + endpointTypeView: { + deviceTypeRef: {} + }, + showExceptionIcon: false, + query: {}, + isMultiConfig: false +} + +const store = createStore({ + state() { + return { + zap: { ...zapState } + } + } +}) const router = createRouter({ history: createWebHistory(), @@ -56,6 +108,52 @@ installQuasarPlugin() const observable = require('../src/util/observable.js') describe('Component mounting test', () => { + test( + 'ZapConfig renders and requests package selection', + async () => { + const wrapper = shallowMount(ZapConfig, { + global: { + plugins: [ZapStore()], + mocks: { + $serverPost: jest.fn(() => + Promise.resolve({ + data: { + zclProperties: [], + zclGenTemplates: [], + filePath: './resource/test-light.zap', + open: false, + zapFilePackages: [ + { + type: 'zcl-properties', + path: '../zcl-builtin/silabs/zcl.json', + id: 1 + }, + { + type: 'gen-templates-json', + path: './gen-template/zigbee/gen-templates.json', + id: 2 + } + ], + zapFileExtensions: [], + sessions: [] + } + }) + ), + $serverGet: jest.fn(() => + Promise.resolve({ data: { warningMap: {}, errorMap: {} } }) + ), + $q: { loading: { show: jest.fn(), hide: jest.fn() } }, + $router: { push: jest.fn() } + } + } + }) + expect(wrapper.html().toLowerCase()).toContain( + 'warning: please select atleast one package each from zcl metadata and templates.' + ) + }, + timeout.short() + ) + test( 'ZclAttributeManager', () => { @@ -308,6 +406,460 @@ describe('Component mounting test', () => { }, timeout.short() ) + + test( + 'ZapConfig submitForm triggers navigation', + async () => { + const pushMock = jest.fn() + const wrapper = shallowMount(ZapConfig, { + global: { + plugins: [ZapStore()], + mocks: { + $router: { push: pushMock }, + $q: { loading: { show: jest.fn(), hide: jest.fn() } }, + $serverPost: jest.fn(() => + Promise.resolve({ + data: { + zclProperties: [], + zclGenTemplates: [], + filePath: '', + open: false, + zapFilePackages: [], + zapFileExtensions: [], + sessions: [] + } + }) + ), + $serverGet: jest.fn(() => + Promise.resolve({ data: { warningMap: {}, errorMap: {} } }) + ) + } + } + }) + // Set up data to simulate selection + await wrapper.setData({ + customConfig: 'select', + selectedZclPropertiesData: [{ id: 1 }], + selectZclGenInfo: [{ id: 2 }], + zapFileExtensions: [] + }) + // Call submitForm + await wrapper.vm.submitForm() + expect(pushMock).toHaveBeenCalledWith({ path: '/' }) + }, + timeout.short() + ) + + test( + 'FeatureTableHeader', + () => { + const wrapper = shallowMount(FeatureTableHeader, { + global: { plugins: [ZapStore()] }, + props: { + props: { cols: [{ name: 'test', label: 'Test Column' }] } + } + }) + expect(wrapper.html().length).toBeGreaterThan(10) + }, + timeout.short() + ) + test( + 'InitialContent', + () => { + const wrapper = shallowMount(InitialContent, { + global: { plugins: [ZapStore()] } + }) + expect(wrapper.html().length).toBeGreaterThan(10) + }, + timeout.short() + ) + test( + 'MainSidebar', + () => { + const wrapper = shallowMount(MainSidebar, { + global: { plugins: [ZapStore()] } + }) + expect(wrapper.html().length).toBeGreaterThan(10) + }, + timeout.short() + ) + test( + 'SettingsSidebar', + () => { + const wrapper = shallowMount(SettingsSidebar, { + global: { + plugins: [ZapStore()], + mocks: { + $route: { fullPath: '/' } + } + } + }) + expect(wrapper.html().length).toBeGreaterThan(10) + }, + timeout.short() + ) + test( + 'SqlQuery', + () => { + const wrapper = shallowMount(SqlQuery, { + global: { plugins: [ZapStore()] } + }) + expect(wrapper.html().length).toBeGreaterThan(10) + }, + timeout.short() + ) + test( + 'ZCLToolbar', + () => { + const wrapper = shallowMount(ZCLToolbar, { + global: { + plugins: [ZapStore()], + stubs: { + 'router-link': { + template: + '' + } + }, + mocks: { + $route: { fullPath: '/' } + } + } + }) + expect(wrapper.html().length).toBeGreaterThan(10) + }, + timeout.short() + ) + test( + 'ZclClusterFeatureManager', + () => { + const wrapper = shallowMount(ZclClusterFeatureManager, { + global: { plugins: [ZapStore()] } + }) + expect(wrapper.html().length).toBeGreaterThan(10) + }, + timeout.short() + ) + test( + 'ZclExceptions', + () => { + const wrapper = shallowMount(ZclExceptions, { + global: { plugins: [ZapStore()] } + }) + expect(wrapper.html().length).toBeGreaterThan(10) + }, + timeout.short() + ) + test( + 'ZclEventManager', + () => { + const wrapper = shallowMount(ZclEventManager, { + global: { plugins: [ZapStore()] } + }) + expect(wrapper.html().length).toBeGreaterThan(10) + }, + timeout.short() + ) + test( + 'ZclDeviceTypeFeatureManager', + () => { + const wrapper = shallowMount(ZclDeviceTypeFeatureManager, { + global: { plugins: [ZapStore()] } + }) + expect(wrapper.html().length).toBeGreaterThan(10) + }, + timeout.short() + ) + + // Pages + test( + 'EndpointManager', + () => { + const wrapper = shallowMount(EndpointManager, { + global: { plugins: [ZapStore()] } + }) + expect(wrapper.html().length).toBeGreaterThan(10) + }, + timeout.short() + ) + test( + 'ExtensionsPage', + () => { + const wrapper = shallowMount(ExtensionsPage, { + global: { plugins: [ZapStore()] } + }) + expect(wrapper.html().length).toBeGreaterThan(10) + }, + timeout.short() + ) + test( + 'NotificationsPage', + () => { + const wrapper = shallowMount(NotificationsPage, { + global: { plugins: [ZapStore()] } + }) + expect(wrapper.html().length).toBeGreaterThan(10) + }, + timeout.short() + ) + test( + 'OptionsPage', + () => { + const wrapper = shallowMount(OptionsPage, { + global: { plugins: [ZapStore()] } + }) + expect(wrapper.html().length).toBeGreaterThan(10) + }, + timeout.short() + ) + test( + 'PreferencePage', + () => { + const wrapper = shallowMount(PreferencePage, { + global: { plugins: [ZapStore()] } + }) + expect(wrapper.html().length).toBeGreaterThan(10) + }, + timeout.short() + ) + test( + 'ZapConfig', + () => { + const wrapper = shallowMount(ZapConfig, { + global: { + plugins: [ZapStore()], + mocks: { + $serverPost: jest.fn(() => + Promise.resolve({ + data: { + zclProperties: [], + zclGenTemplates: [], + filePath: '', + open: false, + zapFilePackages: [], + zapFileExtensions: [], + sessions: [] + } + }) + ), + $serverGet: jest.fn(() => + Promise.resolve({ data: { warningMap: {}, errorMap: {} } }) + ), + $q: { loading: { show: jest.fn(), hide: jest.fn() } }, + $router: { push: jest.fn() } + } + } + }) + expect(wrapper.html().length).toBeGreaterThan(10) + }, + timeout.short() + ) + test( + 'ZclSettings', + () => { + const wrapper = shallowMount(ZclSettings, { + global: { plugins: [ZapStore()] } + }) + expect(wrapper.html().length).toBeGreaterThan(10) + }, + timeout.short() + ) + + // Preferences/devtools + test( + 'ApiExceptions', + () => { + const wrapper = shallowMount(ApiExceptions, { + global: { plugins: [ZapStore()] } + }) + expect(wrapper.html().length).toBeGreaterThan(10) + }, + timeout.short() + ) + test( + 'InformationSetup', + () => { + const wrapper = shallowMount(InformationSetup, { + global: { plugins: [ZapStore()] } + }) + expect(wrapper.html().length).toBeGreaterThan(10) + }, + timeout.short() + ) + test( + 'SqlQueryDev', + () => { + const wrapper = shallowMount(SqlQueryDev, { + global: { plugins: [ZapStore()] } + }) + expect(wrapper.html().length).toBeGreaterThan(10) + }, + timeout.short() + ) + + // Tutorials + test( + 'CMPTour', + () => { + const wrapper = shallowMount(CMPTour, { + global: { + plugins: [ZapStore()], + stubs: { + 'v-tour': { + template: '
', + methods: { resetTour: jest.fn() } + } + } + }, + mocks: { + $refs: { 'zcl-cmp-tour': { resetTour: jest.fn() } } + } + }) + expect(wrapper.html().length).toBeGreaterThan(10) + }, + timeout.short() + ) + + test( + 'EndpointTour', + () => { + const wrapper = shallowMount(EndpointTour, { + global: { + plugins: [ZapStore()], + stubs: { + 'v-tour': { + template: '
', + methods: { resetTour: jest.fn() } + } + } + }, + mocks: { + $refs: { 'zcl-endpoint-tour': { resetTour: jest.fn() } } + } + }) + expect(wrapper.html().length).toBeGreaterThan(10) + }, + timeout.short() + ) + test( + 'ZclTour', + () => { + const wrapper = shallowMount(ZclTour, { + global: { plugins: [ZapStore()] } + }) + expect(wrapper.html().length).toBeGreaterThan(10) + }, + timeout.short() + ) + + it('renders main elements', () => { + const wrapper = shallowMount(App, { + global: { + plugins: [store], + stubs: { + 'router-view': true, + 'q-ajax-bar': true, + 'q-btn': true, + 'q-icon': true, + 'zcl-tour': true + }, + mocks: { + $router: { push: jest.fn() } + } + } + }) + expect(wrapper.findComponent({ name: 'router-view' }).exists()).toBe(true) + expect(wrapper.findComponent({ name: 'zcl-tour' }).exists()).toBe(true) + }) + + it('shows exception icon when showExceptionIcon is true', async () => { + // Set the store state directly + store.state.zap.showExceptionIcon = true + const wrapper = shallowMount(App, { + global: { + plugins: [store], + stubs: ['router-view', 'q-ajax-bar', 'q-btn', 'q-icon', 'zcl-tour'], + mocks: { + $router: { push: jest.fn() } + } + } + }) + await wrapper.vm.$nextTick() + expect(wrapper.find('q-btn-stub').exists()).toBe(true) + }) + + it('calls viewExceptions when exception button is clicked', async () => { + store.state.zap.showExceptionIcon = true + const spy = jest.spyOn(App.methods, 'viewExceptions') + const wrapper = shallowMount(App, { + global: { + plugins: [store], + stubs: ['router-view', 'q-ajax-bar', 'q-btn', 'q-icon', 'zcl-tour'], + mocks: { + $router: { push: jest.fn() } + } + } + }) + await wrapper.vm.$nextTick() + await wrapper.find('q-btn-stub').trigger('click') + expect(spy).toHaveBeenCalled() + }) + + it('calls addClassToBody and setTheme', () => { + const addClassToBody = jest.fn() + const setTheme = jest.fn() + shallowMount(App, { + global: { + plugins: [store], + stubs: ['router-view', 'q-ajax-bar', 'q-btn', 'q-icon', 'zcl-tour'], + mocks: { + $router: { push: jest.fn() } + } + }, + methods: { addClassToBody, setTheme } + }) + // No assertion needed, just ensure mount does not throw + }) + + it('calls parseQueryString and routePage on created', () => { + const parseQueryString = jest + .spyOn(App.methods, 'parseQueryString') + .mockImplementation(() => {}) + const setTheme = jest + .spyOn(App.methods, 'setTheme') + .mockImplementation(() => {}) + const routePage = jest + .spyOn(App.methods, 'routePage') + .mockImplementation(() => {}) + + shallowMount(App, { + global: { + plugins: [store], + stubs: ['router-view', 'q-ajax-bar', 'q-btn', 'q-icon', 'zcl-tour'], + mocks: { + $router: { push: jest.fn() } + } + } + }) + expect(parseQueryString).toHaveBeenCalled() + expect(setTheme).toHaveBeenCalled() + expect(routePage).toHaveBeenCalled() + }) + + it('calls addClassToBody on mounted', () => { + const spy = jest + .spyOn(App.methods, 'addClassToBody') + .mockImplementation(() => {}) + shallowMount(App, { + global: { + plugins: [store], + stubs: ['router-view', 'q-ajax-bar', 'q-btn', 'q-icon', 'zcl-tour'], + mocks: { + $router: { push: jest.fn() } + } + } + }) + expect(spy).toHaveBeenCalled() + }) }) describe('DOM tests', () => {