diff --git a/README.md b/README.md index ad8547326a..0eb42c83d0 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,28 @@ npm start content-ce -- --configuration=adf Changing the ADF code results in the recompilation and hot-reloading of the ACA application. +## Unit Tests + +Use standard Angular CLI commands to test the projects: + +```sh +ng test +``` + +### Code Coverage + +The projects are already configured to produce code coverage reports in console and HTML output. + +You can view HTML reports in the `./coverage/` folder. + +When working with unit testing and code coverage improvement, you can run unit tests in the "live reload" mode: + +```sh +ng test --watch +``` + +Upon changing unit tests code, you can track the coverage results either in the console output, or by reloading the HTML report in the browser. + ## Triggering the build to use specific branch of ADF with CI flags You can create commits with the intention of running the build pipeline using a specific branch of ADF. To achieve this, you need to add a specific CI flag in your commit message: diff --git a/angular.json b/angular.json index 44f32915fd..e0b8d170c7 100644 --- a/angular.json +++ b/angular.json @@ -367,6 +367,7 @@ "test": { "builder": "@angular-devkit/build-angular:karma", "options": { + "codeCoverage": true, "main": "projects/adf-office-services-ext/src/test.ts", "tsConfig": "projects/adf-office-services-ext/tsconfig.spec.json", "karmaConfig": "projects/adf-office-services-ext/karma.conf.js" @@ -413,6 +414,7 @@ "test": { "builder": "@angular-devkit/build-angular:karma", "options": { + "codeCoverage": true, "main": "projects/aca-shared/test.ts", "tsConfig": "projects/aca-shared/tsconfig.spec.json", "karmaConfig": "projects/aca-shared/karma.conf.js" @@ -465,6 +467,7 @@ "test": { "builder": "@angular-devkit/build-angular:karma", "options": { + "codeCoverage": true, "main": "projects/aca-about/src/test.ts", "tsConfig": "projects/aca-about/tsconfig.spec.json", "karmaConfig": "projects/aca-about/karma.conf.js" @@ -506,6 +509,7 @@ "test": { "builder": "@angular-devkit/build-angular:karma", "options": { + "codeCoverage": true, "main": "projects/aca-folder-rules/src/test.ts", "tsConfig": "projects/aca-folder-rules/tsconfig.spec.json", "karmaConfig": "projects/aca-folder-rules/karma.conf.js" @@ -554,6 +558,7 @@ "test": { "builder": "@angular-devkit/build-angular:karma", "options": { + "codeCoverage": true, "main": "projects/aca-content/src/test.ts", "tsConfig": "projects/aca-content/tsconfig.spec.json", "karmaConfig": "projects/aca-content/karma.conf.js", @@ -605,6 +610,7 @@ "test": { "builder": "@angular-devkit/build-angular:karma", "options": { + "codeCoverage": true, "main": "projects/aca-viewer/src/test.ts", "tsConfig": "projects/aca-viewer/tsconfig.spec.json", "karmaConfig": "projects/aca-viewer/karma.conf.js" @@ -648,6 +654,7 @@ "test": { "builder": "@angular-devkit/build-angular:karma", "options": { + "codeCoverage": true, "main": "projects/aca-preview/src/test.ts", "tsConfig": "projects/aca-preview/tsconfig.spec.json", "karmaConfig": "projects/aca-preview/karma.conf.js" diff --git a/karma.conf.js b/karma.conf.js index 9be6a95d5b..916090daa8 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -1,25 +1,22 @@ // Karma configuration file, see link for more information // https://karma-runner.github.io/1.0/config/configuration-file.html -// process.env.CHROME_BIN = require('puppeteer').executablePath(); -module.exports = function(config) { - config.set({ +const { join } = require('path'); +const { constants } = require('karma'); + +module.exports = () => { + return { basePath: '', frameworks: ['jasmine', '@angular-devkit/build-angular'], plugins: [ require('karma-jasmine'), require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), - require('karma-coverage-istanbul-reporter'), + require('karma-coverage'), require('karma-mocha-reporter'), require('@angular-devkit/build-angular/plugins/karma') ], files: [ - { - pattern: - './node_modules/@angular/material/prebuilt-themes/indigo-pink.css', - watched: false - }, { pattern: './node_modules/@alfresco/adf-core/bundles/assets/adf-core/i18n/en.json', @@ -46,18 +43,36 @@ module.exports = function(config) { '/base/node_modules/@alfresco/adf-content-services/bundles/assets/adf-content-services/i18n/en.json' }, client: { - clearContext: false // leave Jasmine Spec Runner output visible in browser + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false, // leave Jasmine Spec Runner output visible in browser }, - coverageIstanbulReporter: { - dir: require('path').join(__dirname, 'coverage'), - reports: ['html', 'lcovonly'], - fixWebpackSourcePaths: true + jasmineHtmlReporter: { + suppressAll: true, // removes the duplicated traces + }, + + coverageReporter: { + dir: join(__dirname, './coverage'), + subdir: '.', + reporters: [{ type: 'html' }, { type: 'text-summary' }], + check: { + global: { + statements: 75, + branches: 67, + functions: 73, + lines: 75 + } + } }, reporters: ['mocha', 'kjhtml'], port: 9876, colors: true, - logLevel: config.LOG_INFO, + logLevel: constants.LOG_INFO, autoWatch: true, browsers: ['ChromeHeadless'], customLaunchers: { @@ -65,19 +80,15 @@ module.exports = function(config) { base: 'Chrome', flags: [ '--no-sandbox', - // '--headless', + '--headless', '--disable-gpu', '--remote-debugging-port=9222' ] } }, singleRun: true, - captureTimeout: 180000, - browserDisconnectTimeout: 180000, - browserDisconnectTolerance: 3, - browserNoActivityTimeout: 300000, - + restartOnFileChange: true, // workaround for alfresco-js-api builds webpack: { node: { fs: 'empty' } } - }); + }; }; diff --git a/package-lock.json b/package-lock.json index 236726ed14..7813fa3b77 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12916,6 +12916,64 @@ "which": "^1.2.1" } }, + "karma-coverage": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-2.2.0.tgz", + "integrity": "sha512-gPVdoZBNDZ08UCzdMHHhEImKrw1+PAOQOIiffv1YsvxFhBjqvo/SVXNk4tqn1SYqX0BJZT6S/59zgxiBe+9OuA==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.1", + "istanbul-reports": "^3.0.5", + "minimatch": "^3.0.4" + }, + "dependencies": { + "istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "dev": true + }, + "istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + } + }, + "istanbul-reports": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", + "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, "karma-coverage-istanbul-reporter": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-3.0.3.tgz", diff --git a/package.json b/package.json index 670dc6ea06..b68472f7c4 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,6 @@ "build.release": "npm run build -- --configuration=production,release", "build-libs": "ng build aca-shared && ng build adf-office-services-ext && ng build aca-about && ng build aca-viewer && ng build aca-preview && ng build aca-folder-rules && ng build aca-content", "test": "ng test", - "test:ci": "ng test adf-office-services-ext && ng test content-ce --code-coverage", "lint": "NODE_OPTIONS=--max_old_space_size=4096 ng lint", "update-webdriver": "./scripts/update-webdriver.sh", "e2e": "npm run update-webdriver && protractor $SUITE", @@ -102,6 +101,7 @@ "jasmine-spec-reporter": "~5.0.0", "karma": "^6.4.1", "karma-chrome-launcher": "~3.1.1", + "karma-coverage": "^2.2.0", "karma-coverage-istanbul-reporter": "^3.0.3", "karma-jasmine": "~5.1.0", "karma-jasmine-html-reporter": "^2.0.0", diff --git a/projects/aca-about/karma.conf.js b/projects/aca-about/karma.conf.js index 3ec29023d7..ad0765065f 100644 --- a/projects/aca-about/karma.conf.js +++ b/projects/aca-about/karma.conf.js @@ -1,32 +1,15 @@ // Karma configuration file, see link for more information // https://karma-runner.github.io/1.0/config/configuration-file.html +const { join } = require('path'); +const getBaseKarmaConfig = require('../../karma.conf'); module.exports = function (config) { + const baseConfig = getBaseKarmaConfig(); config.set({ - basePath: '', - frameworks: ['jasmine', '@angular-devkit/build-angular'], - plugins: [ - require('karma-jasmine'), - require('karma-chrome-launcher'), - require('karma-jasmine-html-reporter'), - require('karma-coverage-istanbul-reporter'), - require('@angular-devkit/build-angular/plugins/karma') - ], - client: { - clearContext: false // leave Jasmine Spec Runner output visible in browser + ...baseConfig, + coverageReporter: { + ...baseConfig.coverageReporter, + dir: join(__dirname, '../../coverage/aca-about'), }, - coverageIstanbulReporter: { - dir: require('path').join(__dirname, '../../coverage/aca-about'), - reports: ['html', 'lcovonly', 'text-summary'], - fixWebpackSourcePaths: true - }, - reporters: ['progress', 'kjhtml'], - port: 9876, - colors: true, - logLevel: config.LOG_INFO, - autoWatch: true, - browsers: ['Chrome'], - singleRun: true, - restartOnFileChange: true }); }; diff --git a/projects/aca-content/karma.conf.js b/projects/aca-content/karma.conf.js index 993ad4a1f0..546edf5503 100644 --- a/projects/aca-content/karma.conf.js +++ b/projects/aca-content/karma.conf.js @@ -1,32 +1,15 @@ // Karma configuration file, see link for more information // https://karma-runner.github.io/1.0/config/configuration-file.html +const { join } = require('path'); +const getBaseKarmaConfig = require('../../karma.conf'); module.exports = function (config) { + const baseConfig = getBaseKarmaConfig(); config.set({ - basePath: '', - frameworks: ['jasmine', '@angular-devkit/build-angular'], - plugins: [ - require('karma-jasmine'), - require('karma-chrome-launcher'), - require('karma-jasmine-html-reporter'), - require('karma-coverage-istanbul-reporter'), - require('@angular-devkit/build-angular/plugins/karma') - ], - client: { - clearContext: false // leave Jasmine Spec Runner output visible in browser + ...baseConfig, + coverageReporter: { + ...baseConfig.coverageReporter, + dir: join(__dirname, '../../coverage/aca-content'), }, - coverageIstanbulReporter: { - dir: require('path').join(__dirname, '../../coverage/aca-content'), - reports: ['html', 'lcovonly', 'text-summary'], - fixWebpackSourcePaths: true - }, - reporters: ['progress', 'kjhtml'], - port: 9876, - colors: true, - logLevel: config.LOG_INFO, - autoWatch: true, - browsers: ['Chrome'], - singleRun: true, - restartOnFileChange: true }); }; diff --git a/projects/aca-content/src/lib/testing/app-testing.module.ts b/projects/aca-content/src/lib/testing/app-testing.module.ts index 91b83e098f..58d38ef1fd 100644 --- a/projects/aca-content/src/lib/testing/app-testing.module.ts +++ b/projects/aca-content/src/lib/testing/app-testing.module.ts @@ -24,7 +24,7 @@ */ import { NgModule } from '@angular/core'; -import { TranslateService, TranslatePipe } from '@ngx-translate/core'; +import { TranslatePipe, TranslateModule } from '@ngx-translate/core'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { TranslationService, @@ -44,7 +44,6 @@ import { EffectsModule } from '@ngrx/effects'; import { MaterialModule } from '../material.module'; import { INITIAL_STATE } from '../store/initial-state'; import { TranslatePipeMock } from './translate-pipe.directive'; -import { TranslateServiceMock } from '@alfresco/aca-shared'; import { BehaviorSubject, Observable, of } from 'rxjs'; @NgModule({ @@ -53,6 +52,7 @@ import { BehaviorSubject, Observable, of } from 'rxjs'; HttpClientModule, RouterTestingModule, MaterialModule, + TranslateModule.forRoot(), StoreModule.forRoot( { app: appReducer }, { @@ -71,7 +71,6 @@ import { BehaviorSubject, Observable, of } from 'rxjs'; providers: [ { provide: AlfrescoApiService, useClass: AlfrescoApiServiceMock }, { provide: TranslationService, useClass: TranslationMock }, - { provide: TranslateService, useClass: TranslateServiceMock }, { provide: TranslatePipe, useClass: TranslatePipeMock }, { provide: DiscoveryApiService, diff --git a/projects/aca-folder-rules/karma.conf.js b/projects/aca-folder-rules/karma.conf.js index ed84269d4b..6c900ccaff 100644 --- a/projects/aca-folder-rules/karma.conf.js +++ b/projects/aca-folder-rules/karma.conf.js @@ -1,32 +1,15 @@ // Karma configuration file, see link for more information // https://karma-runner.github.io/1.0/config/configuration-file.html +const { join } = require('path'); +const getBaseKarmaConfig = require('../../karma.conf'); module.exports = function (config) { + const baseConfig = getBaseKarmaConfig(); config.set({ - basePath: '', - frameworks: ['jasmine', '@angular-devkit/build-angular'], - plugins: [ - require('karma-jasmine'), - require('karma-chrome-launcher'), - require('karma-jasmine-html-reporter'), - require('karma-coverage-istanbul-reporter'), - require('@angular-devkit/build-angular/plugins/karma') - ], - client: { - clearContext: false // leave Jasmine Spec Runner output visible in browser + ...baseConfig, + coverageReporter: { + ...baseConfig.coverageReporter, + dir: join(__dirname, '../../coverage/aca-folder-rules'), }, - coverageIstanbulReporter: { - dir: require('path').join(__dirname, '../../coverage/aca-folder-rules'), - reports: ['html', 'lcovonly', 'text-summary'], - fixWebpackSourcePaths: true - }, - reporters: ['progress', 'kjhtml'], - port: 9876, - colors: true, - logLevel: config.LOG_INFO, - autoWatch: true, - browsers: ['Chrome'], - singleRun: true, - restartOnFileChange: true }); }; diff --git a/projects/aca-preview/karma.conf.js b/projects/aca-preview/karma.conf.js index 8133041d8c..10f4d7f1f3 100644 --- a/projects/aca-preview/karma.conf.js +++ b/projects/aca-preview/karma.conf.js @@ -1,32 +1,15 @@ // Karma configuration file, see link for more information // https://karma-runner.github.io/1.0/config/configuration-file.html +const { join } = require('path'); +const getBaseKarmaConfig = require('../../karma.conf'); module.exports = function (config) { + const baseConfig = getBaseKarmaConfig(); config.set({ - basePath: '', - frameworks: ['jasmine', '@angular-devkit/build-angular'], - plugins: [ - require('karma-jasmine'), - require('karma-chrome-launcher'), - require('karma-jasmine-html-reporter'), - require('karma-coverage-istanbul-reporter'), - require('@angular-devkit/build-angular/plugins/karma') - ], - client: { - clearContext: false // leave Jasmine Spec Runner output visible in browser + ...baseConfig, + coverageReporter: { + ...baseConfig.coverageReporter, + dir: join(__dirname, '../../coverage/aca-preview'), }, - coverageIstanbulReporter: { - dir: require('path').join(__dirname, '../../coverage/aca-preview'), - reports: ['html', 'lcovonly', 'text-summary'], - fixWebpackSourcePaths: true - }, - reporters: ['progress', 'kjhtml'], - port: 9876, - colors: true, - logLevel: config.LOG_INFO, - autoWatch: true, - browsers: ['Chrome'], - singleRun: true, - restartOnFileChange: true }); }; diff --git a/projects/aca-preview/src/lib/components/preview.component.spec.ts b/projects/aca-preview/src/lib/components/preview.component.spec.ts index 74b3903960..7e206a456a 100644 --- a/projects/aca-preview/src/lib/components/preview.component.spec.ts +++ b/projects/aca-preview/src/lib/components/preview.component.spec.ts @@ -38,11 +38,11 @@ import { UploadService, NodesApiService, DiscoveryApiService } from '@alfresco/a import { AppState, ClosePreviewAction } from '@alfresco/aca-shared/store'; import { PreviewComponent } from './preview.component'; import { BehaviorSubject, Observable, of, throwError } from 'rxjs'; -import { ContentApiService, AppHookService, TranslateServiceMock, DocumentBasePageService } from '@alfresco/aca-shared'; +import { ContentApiService, AppHookService, DocumentBasePageService } from '@alfresco/aca-shared'; import { Store, StoreModule } from '@ngrx/store'; import { Node, NodePaging, FavoritePaging, SharedLinkPaging, PersonEntry, ResultSetPaging, RepositoryInfo, NodeEntry } from '@alfresco/js-api'; import { PreviewModule } from '../preview.module'; -import { TranslateService } from '@ngx-translate/core'; +import { TranslateModule } from '@ngx-translate/core'; import { RouterTestingModule } from '@angular/router/testing'; import { HttpClientModule } from '@angular/common/http'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; @@ -115,6 +115,7 @@ describe('PreviewComponent', () => { NoopAnimationsModule, HttpClientModule, RouterTestingModule, + TranslateModule.forRoot(), StoreModule.forRoot( { app: (state) => state }, { @@ -133,7 +134,6 @@ describe('PreviewComponent', () => { providers: [ { provide: AlfrescoApiService, useClass: AlfrescoApiServiceMock }, { provide: TranslationService, useClass: TranslationMock }, - { provide: TranslateService, useClass: TranslateServiceMock }, { provide: DocumentBasePageService, useVale: new DocumentBasePageServiceMock() }, { provide: DiscoveryApiService, diff --git a/projects/aca-shared/karma.conf.js b/projects/aca-shared/karma.conf.js index cdd1f0599f..c032b41fe9 100644 --- a/projects/aca-shared/karma.conf.js +++ b/projects/aca-shared/karma.conf.js @@ -1,33 +1,15 @@ // Karma configuration file, see link for more information // https://karma-runner.github.io/1.0/config/configuration-file.html +const { join } = require('path'); +const getBaseKarmaConfig = require('../../karma.conf'); module.exports = function (config) { + const baseConfig = getBaseKarmaConfig(); config.set({ - basePath: '', - frameworks: ['jasmine', '@angular-devkit/build-angular'], - plugins: [ - require('karma-jasmine'), - require('karma-chrome-launcher'), - require('karma-jasmine-html-reporter'), - require('karma-coverage-istanbul-reporter'), - require('karma-mocha-reporter'), - require('@angular-devkit/build-angular/plugins/karma') - ], - client: { - clearContext: false // leave Jasmine Spec Runner output visible in browser + ...baseConfig, + coverageReporter: { + ...baseConfig.coverageReporter, + dir: join(__dirname, '../../coverage/aca-shared'), }, - coverageIstanbulReporter: { - dir: require('path').join(__dirname, '../../coverage/aca-shared'), - reports: ['html', 'lcovonly'], - fixWebpackSourcePaths: true - }, - reporters: ['mocha', 'kjhtml'], - port: 9876, - colors: true, - logLevel: config.LOG_INFO, - autoWatch: true, - browsers: ['Chrome'], - singleRun: true, - restartOnFileChange: true }); }; diff --git a/projects/aca-shared/src/lib/components/document-base-page/document-base-page.component.ts b/projects/aca-shared/src/lib/components/document-base-page/document-base-page.component.ts index becd13924c..88163dc109 100644 --- a/projects/aca-shared/src/lib/components/document-base-page/document-base-page.component.ts +++ b/projects/aca-shared/src/lib/components/document-base-page/document-base-page.component.ts @@ -143,12 +143,14 @@ export abstract class PageComponent implements OnInit, OnDestroy, OnChanges { } imageResolver(row: ShareDataRow): string | null { - if (isLocked(row.node)) { - return 'assets/images/baseline-lock-24px.svg'; - } + if (row) { + if (isLocked(row.node)) { + return 'assets/images/baseline-lock-24px.svg'; + } - if (isLibrary(row.node)) { - return 'assets/images/baseline-library_books-24px.svg'; + if (isLibrary(row.node)) { + return 'assets/images/baseline-library_books-24px.svg'; + } } return null; @@ -181,7 +183,7 @@ export abstract class PageComponent implements OnInit, OnDestroy, OnChanges { return location.href.includes('viewer:view'); } - onSortingChanged(event) { + onSortingChanged(event: any) { this.filterSorting = event.detail.key + '-' + event.detail.direction; } diff --git a/projects/aca-shared/src/lib/components/document-base-page/document-base-page.spec.ts b/projects/aca-shared/src/lib/components/document-base-page/document-base-page.spec.ts index fd5190536a..aa1d149add 100644 --- a/projects/aca-shared/src/lib/components/document-base-page/document-base-page.spec.ts +++ b/projects/aca-shared/src/lib/components/document-base-page/document-base-page.spec.ts @@ -38,6 +38,7 @@ import { HttpClientModule } from '@angular/common/http'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { RouterTestingModule } from '@angular/router/testing'; import { EffectsModule } from '@ngrx/effects'; +import { Subscription } from 'rxjs'; export const INITIAL_APP_STATE: AppState = { appName: 'Alfresco Content Application', @@ -98,6 +99,14 @@ class TestComponent extends PageComponent { constructor(store: Store, extensions: AppExtensionService, content: DocumentBasePageService) { super(store, extensions, content); } + + addSubscription(entry: Subscription) { + this.subscriptions.push(entry); + } + + getSubscriptions(): Subscription[] { + return this.subscriptions; + } } describe('PageComponent', () => { @@ -341,4 +350,66 @@ describe('Info Drawer state', () => { } }); }); + + it('should not resolve custom image', () => { + expect(component.imageResolver(null)).toBe(null); + }); + + it('should resolve custom image for locked node', () => { + const row: any = { + node: { + entry: { + isLocked: true + } + } + }; + + expect(component.imageResolver(row)).toBe('assets/images/baseline-lock-24px.svg'); + }); + + it('should resolve custom image for a library', () => { + const row: any = { + node: { + entry: { + nodeType: 'st:site' + } + } + }; + + expect(component.imageResolver(row)).toBe('assets/images/baseline-library_books-24px.svg'); + }); + + it('should track elements by action id ', () => { + const action: any = { id: 'action1' }; + expect(component.trackByActionId(0, action)).toBe('action1'); + }); + + it('should track elements by id ', () => { + const action: any = { id: 'action1' }; + expect(component.trackById(0, action)).toBe('action1'); + }); + + it('should track elements by column id ', () => { + const action: any = { id: 'action1' }; + expect(component.trackByColumnId(0, action)).toBe('action1'); + }); + + it('should cleanup subscriptions on destroy', () => { + const sub = jasmine.createSpyObj('sub', ['unsubscribe']); + + expect(component.getSubscriptions().length).toBe(0); + + component.addSubscription(sub); + expect(component.getSubscriptions().length).toBe(1); + + component.ngOnDestroy(); + expect(sub.unsubscribe).toHaveBeenCalled(); + expect(component.getSubscriptions().length).toBe(0); + }); + + it('should update filter sorting', () => { + const event = new CustomEvent('sorting-changed', { detail: { key: 'name', direction: 'asc' } }); + component.onSortingChanged(event); + expect(component.filterSorting).toBe('name-asc'); + }); }); diff --git a/projects/aca-shared/src/lib/testing/translation.service.ts b/projects/aca-shared/src/lib/components/locked-by/locked-by.component.spec.ts similarity index 70% rename from projects/aca-shared/src/lib/testing/translation.service.ts rename to projects/aca-shared/src/lib/components/locked-by/locked-by.component.spec.ts index 35e227117f..250c973575 100644 --- a/projects/aca-shared/src/lib/testing/translation.service.ts +++ b/projects/aca-shared/src/lib/components/locked-by/locked-by.component.spec.ts @@ -23,21 +23,21 @@ * along with Alfresco. If not, see . */ -import { Injectable } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; -import { Observable, of } from 'rxjs'; +import { LockedByComponent } from './locked-by.component'; -@Injectable() -export class TranslateServiceMock extends TranslateService { - constructor() { - super(null, null, null, null, null, null, true, null, null); - } +describe('LockedByComponent', () => { + it('should evaluate label text', () => { + const component = new LockedByComponent(); + component.node = { + entry: { + properties: { + 'cm:lockOwner': { + displayName: 'owner-name' + } + } + } as any + }; - get(key: string | Array): Observable { - return of(key); - } - - instant(key: string | Array): string | any { - return key; - } -} + expect(component.text).toBe('owner-name'); + }); +}); diff --git a/projects/aca-shared/src/lib/components/locked-by/locked-by.component.ts b/projects/aca-shared/src/lib/components/locked-by/locked-by.component.ts index 6e7c6bd933..0de656c6c1 100644 --- a/projects/aca-shared/src/lib/components/locked-by/locked-by.component.ts +++ b/projects/aca-shared/src/lib/components/locked-by/locked-by.component.ts @@ -45,8 +45,6 @@ export class LockedByComponent { node: NodeEntry; get text(): string { - return ( - this.node && this.node.entry.properties && this.node.entry.properties['cm:lockOwner'] && this.node.entry.properties['cm:lockOwner'].displayName - ); + return this.node?.entry?.properties?.['cm:lockOwner']?.displayName; } } diff --git a/projects/aca-shared/src/lib/components/open-in-app/open-in-app.component.spec.ts b/projects/aca-shared/src/lib/components/open-in-app/open-in-app.component.spec.ts index 35b7807f7c..cc22ac5d40 100644 --- a/projects/aca-shared/src/lib/components/open-in-app/open-in-app.component.spec.ts +++ b/projects/aca-shared/src/lib/components/open-in-app/open-in-app.component.spec.ts @@ -5,6 +5,9 @@ import { OpenInAppComponent } from './open-in-app.component'; import { initialState, LibTestingModule } from '../../testing/lib-testing-module'; import { provideMockStore } from '@ngrx/store/testing'; import { TranslateModule } from '@ngx-translate/core'; +import { MatIconTestingModule } from '@angular/material/icon/testing'; +import { MatIconModule } from '@angular/material/icon'; +import { SharedModule } from '@alfresco/aca-shared'; describe('OpenInAppComponent', () => { let fixture: ComponentFixture; @@ -15,16 +18,15 @@ describe('OpenInAppComponent', () => { open: jasmine.createSpy('open') }; - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [OpenInAppComponent], - imports: [LibTestingModule, TranslateModule], + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [LibTestingModule, TranslateModule, SharedModule.forRoot(), MatIconModule, MatIconTestingModule], providers: [ provideMockStore({ initialState }), { provide: MAT_DIALOG_DATA, useValue: { redirectUrl: 'mockRedirectUrl' } }, { provide: MatDialogRef, useValue: mockDialogRef } ] - }).compileComponents(); + }); fixture = TestBed.createComponent(OpenInAppComponent); component = fixture.componentInstance; diff --git a/projects/aca-shared/src/lib/components/tool-bar/toolbar-action/toolbar-action.component.spec.ts b/projects/aca-shared/src/lib/components/tool-bar/toolbar-action/toolbar-action.component.spec.ts index c9c6c58540..263824d196 100644 --- a/projects/aca-shared/src/lib/components/tool-bar/toolbar-action/toolbar-action.component.spec.ts +++ b/projects/aca-shared/src/lib/components/tool-bar/toolbar-action/toolbar-action.component.spec.ts @@ -23,10 +23,51 @@ * along with Alfresco. If not, see . */ +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ToolbarActionComponent } from './toolbar-action.component'; +import { CommonModule } from '@angular/common'; +import { HttpClientModule } from '@angular/common/http'; +import { TranslateModule } from '@ngx-translate/core'; +import { ToolbarButtonType } from '../toolbar-button/toolbar-button.component'; +import { ChangeDetectorRef } from '@angular/core'; +import { ContentActionType } from '@alfresco/adf-extensions'; +import { IconModule } from '@alfresco/adf-core'; describe('ToolbarActionComponent', () => { - it('should be defined', () => { - expect(ToolbarActionComponent).toBeDefined(); + let fixture: ComponentFixture; + let component: ToolbarActionComponent; + let changeDetectorRef: ChangeDetectorRef; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [CommonModule, HttpClientModule, TranslateModule.forRoot(), IconModule], + providers: [{ provide: ChangeDetectorRef, useValue: { markForCheck() {} } }], + declarations: [ToolbarActionComponent] + }); + + fixture = TestBed.createComponent(ToolbarActionComponent); + component = fixture.componentInstance; + + changeDetectorRef = TestBed.inject(ChangeDetectorRef); + }); + + it('should be icon button by default', () => { + expect(component.type).toBe(ToolbarButtonType.ICON_BUTTON); + }); + + it('should force update UI on check for the viewer', () => { + component = new ToolbarActionComponent(changeDetectorRef); + const markForCheck = spyOn(changeDetectorRef, 'markForCheck'); + + component.actionRef = { + id: '-app.viewer', + type: ContentActionType.button, + actions: { + click: 'ON_CLICK' + } + }; + + component.ngDoCheck(); + expect(markForCheck).toHaveBeenCalled(); }); }); diff --git a/projects/aca-shared/src/lib/components/tool-bar/toolbar-button/toolbar-button.component.spec.ts b/projects/aca-shared/src/lib/components/tool-bar/toolbar-button/toolbar-button.component.spec.ts index c4cd118958..2a4f8ab588 100644 --- a/projects/aca-shared/src/lib/components/tool-bar/toolbar-button/toolbar-button.component.spec.ts +++ b/projects/aca-shared/src/lib/components/tool-bar/toolbar-button/toolbar-button.component.spec.ts @@ -23,10 +23,69 @@ * along with Alfresco. If not, see . */ -import { ToolbarButtonComponent } from './toolbar-button.component'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ToolbarButtonComponent, ToolbarButtonType } from './toolbar-button.component'; +import { CommonModule } from '@angular/common'; +import { HttpClientModule } from '@angular/common/http'; +import { TranslateModule } from '@ngx-translate/core'; +import { IconModule, TranslationMock, TranslationService } from '@alfresco/adf-core'; +import { Store } from '@ngrx/store'; +import { of } from 'rxjs'; +import { AppExtensionService } from '../../../services/app.extension.service'; +import { ContentActionType } from '@alfresco/adf-extensions'; +import { MatButtonModule } from '@angular/material/button'; describe('ToolbarButtonComponent', () => { - it('should be defined', () => { - expect(ToolbarButtonComponent).toBeDefined(); + let fixture: ComponentFixture; + let component: ToolbarButtonComponent; + let appExtensionService: AppExtensionService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [CommonModule, HttpClientModule, TranslateModule.forRoot(), IconModule, MatButtonModule], + declarations: [ToolbarButtonComponent], + providers: [ + { provide: TranslationService, useClass: TranslationMock }, + { provide: AppExtensionService, useValue: { runActionById() {} } }, + { + provide: Store, + useValue: { + dispatch: () => {}, + select: () => of({ count: 1 }) + } + } + ] + }); + + fixture = TestBed.createComponent(ToolbarButtonComponent); + component = fixture.componentInstance; + appExtensionService = TestBed.inject(AppExtensionService); + }); + + it('should be icon button by default', () => { + expect(component.type).toBe(ToolbarButtonType.ICON_BUTTON); + }); + + it('should run action on click', async () => { + const runActionById = spyOn(appExtensionService, 'runActionById'); + + component.actionRef = { + id: 'button1', + type: ContentActionType.button, + actions: { + click: 'ON_CLICK' + } + }; + + fixture.detectChanges(); + await fixture.whenStable(); + + const button: HTMLButtonElement = fixture.nativeElement.querySelector('[id="button1"]'); + button.click(); + + fixture.detectChanges(); + await fixture.whenStable(); + + expect(runActionById).toHaveBeenCalled(); }); }); diff --git a/projects/aca-shared/src/lib/components/tool-bar/toolbar-menu-item/toolbar-menu-item.component.spec.ts b/projects/aca-shared/src/lib/components/tool-bar/toolbar-menu-item/toolbar-menu-item.component.spec.ts index a981849a17..fed9b18698 100644 --- a/projects/aca-shared/src/lib/components/tool-bar/toolbar-menu-item/toolbar-menu-item.component.spec.ts +++ b/projects/aca-shared/src/lib/components/tool-bar/toolbar-menu-item/toolbar-menu-item.component.spec.ts @@ -23,10 +23,98 @@ * along with Alfresco. If not, see . */ +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ToolbarMenuItemComponent } from './toolbar-menu-item.component'; +import { AppExtensionService } from '../../../services/app.extension.service'; +import { CommonModule } from '@angular/common'; +import { HttpClientModule } from '@angular/common/http'; +import { TranslateModule } from '@ngx-translate/core'; +import { Store } from '@ngrx/store'; +import { IconModule, TranslationMock, TranslationService } from '@alfresco/adf-core'; +import { MatButtonModule } from '@angular/material/button'; +import { of } from 'rxjs'; +import { ContentActionRef, ContentActionType } from '@alfresco/adf-extensions'; describe('ToolbarMenuItemComponent', () => { - it('should be defined', () => { - expect(ToolbarMenuItemComponent).toBeDefined(); + let fixture: ComponentFixture; + let component: ToolbarMenuItemComponent; + let appExtensionService: AppExtensionService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [CommonModule, HttpClientModule, TranslateModule.forRoot(), IconModule, MatButtonModule], + declarations: [ToolbarMenuItemComponent], + providers: [ + { provide: TranslationService, useClass: TranslationMock }, + { provide: AppExtensionService, useValue: { runActionById() {} } }, + { + provide: Store, + useValue: { + dispatch: () => {}, + select: () => of({ count: 1 }) + } + } + ] + }); + + fixture = TestBed.createComponent(ToolbarMenuItemComponent); + component = fixture.componentInstance; + appExtensionService = TestBed.inject(AppExtensionService); + }); + + it('should run action on click', async () => { + const runActionById = spyOn(appExtensionService, 'runActionById'); + + component.actionRef = { + id: 'button1', + type: ContentActionType.button, + actions: { + click: 'ON_CLICK' + } + }; + + fixture.detectChanges(); + await fixture.whenStable(); + + const button: HTMLButtonElement = fixture.nativeElement.querySelector('[id="button1"]'); + button.click(); + + fixture.detectChanges(); + await fixture.whenStable(); + + expect(runActionById).toHaveBeenCalled(); + }); + + it('should run action with focus selector on click', async () => { + const runActionById = spyOn(appExtensionService, 'runActionById'); + + component.menuId = 'menu1'; + component.actionRef = { + id: 'button1', + type: ContentActionType.button, + actions: { + click: 'ON_CLICK' + } + }; + + fixture.detectChanges(); + await fixture.whenStable(); + + const button: HTMLButtonElement = fixture.nativeElement.querySelector('[id="button1"]'); + button.click(); + + fixture.detectChanges(); + await fixture.whenStable(); + + expect(runActionById).toHaveBeenCalledWith('ON_CLICK', { focusedElementOnCloseSelector: '#menu1' }); + }); + + it('should track elements by content action id', () => { + const contentActionRef: ContentActionRef = { + id: 'action1', + type: ContentActionType.button + }; + + expect(component.trackByActionId(0, contentActionRef)).toBe('action1'); }); }); diff --git a/projects/aca-shared/src/lib/components/tool-bar/toolbar-menu/toolbar-menu.component.spec.ts b/projects/aca-shared/src/lib/components/tool-bar/toolbar-menu/toolbar-menu.component.spec.ts index 853ff5f20e..6c3aa454ad 100644 --- a/projects/aca-shared/src/lib/components/tool-bar/toolbar-menu/toolbar-menu.component.spec.ts +++ b/projects/aca-shared/src/lib/components/tool-bar/toolbar-menu/toolbar-menu.component.spec.ts @@ -28,7 +28,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MaterialModule } from '@alfresco/adf-core'; import { OverlayModule } from '@angular/cdk/overlay'; import { TranslateModule, TranslateLoader, TranslateFakeLoader } from '@ngx-translate/core'; -import { ContentActionRef } from '@alfresco/adf-extensions'; +import { ContentActionRef, ContentActionType } from '@alfresco/adf-extensions'; +import { QueryList } from '@angular/core'; describe('ToolbarMenuComponent', () => { let fixture: ComponentFixture; @@ -50,6 +51,7 @@ describe('ToolbarMenuComponent', () => { component = fixture.componentInstance; component.matTrigger = jasmine.createSpyObj('MatMenuTrigger', ['closeMenu']); component.actionRef = actions; + fixture.detectChanges(); }); @@ -59,4 +61,23 @@ describe('ToolbarMenuComponent', () => { fixture.detectChanges(); expect(component.matTrigger.closeMenu).toHaveBeenCalled(); }); + + it('should populate underlying menu with toolbar items', () => { + component.toolbarMenuItems = new QueryList(); + component.toolbarMenuItems.reset([{ menuItem: {} } as any]); + expect(component.toolbarMenuItems.length).toBe(1); + + expect(component.menu._allItems.length).toBe(0); + component.ngAfterViewInit(); + expect(component.menu._allItems.length).toBe(1); + }); + + it('should track elements by content action id', () => { + const contentActionRef: ContentActionRef = { + id: 'action1', + type: ContentActionType.button + }; + + expect(component.trackByActionId(0, contentActionRef)).toBe('action1'); + }); }); diff --git a/projects/aca-shared/src/lib/routing/shared.guard.spec.ts b/projects/aca-shared/src/lib/routing/shared.guard.spec.ts new file mode 100644 index 0000000000..24e44827ab --- /dev/null +++ b/projects/aca-shared/src/lib/routing/shared.guard.spec.ts @@ -0,0 +1,51 @@ +/*! + * @license + * Alfresco Example Content Application + * + * Copyright (C) 2005 - 2020 Alfresco Software Limited + * + * This file is part of the Alfresco Example Content Application. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * The Alfresco Example Content Application is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Alfresco Example Content Application is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ + +import { of } from 'rxjs'; +import { AppSharedRuleGuard } from './shared.guard'; + +describe('AppSharedRuleGuard', () => { + it('should allow activation if quick share is enabled', () => { + const store: any = { + select: () => of(true) + }; + const guard = new AppSharedRuleGuard(store); + const emittedSpy = jasmine.createSpy('emitted'); + + guard.canActivate({} as any).subscribe(emittedSpy); + expect(emittedSpy).toHaveBeenCalledWith(true); + }); + + it('should allow child activation if quick share is enabled', () => { + const store: any = { + select: () => of(true) + }; + const guard = new AppSharedRuleGuard(store); + const emittedSpy = jasmine.createSpy('emitted'); + + guard.canActivateChild({} as any).subscribe(emittedSpy); + expect(emittedSpy).toHaveBeenCalledWith(true); + }); +}); diff --git a/projects/aca-shared/src/lib/routing/shared.guard.ts b/projects/aca-shared/src/lib/routing/shared.guard.ts index f5be306c14..242db643c8 100644 --- a/projects/aca-shared/src/lib/routing/shared.guard.ts +++ b/projects/aca-shared/src/lib/routing/shared.guard.ts @@ -39,11 +39,11 @@ export class AppSharedRuleGuard implements CanActivate { this.isQuickShareEnabled$ = store.select(isQuickShareEnabled); } - canActivate(_: ActivatedRouteSnapshot): Observable | Promise | boolean { + canActivate(_: ActivatedRouteSnapshot): Observable { return this.isQuickShareEnabled$; } - canActivateChild(route: ActivatedRouteSnapshot): Observable | Promise | boolean { + canActivateChild(route: ActivatedRouteSnapshot): Observable { return this.canActivate(route); } } diff --git a/projects/aca-shared/src/lib/services/alfresco-office-extension.service.spec.ts b/projects/aca-shared/src/lib/services/alfresco-office-extension.service.spec.ts index e50dcffba7..bba98cfaeb 100644 --- a/projects/aca-shared/src/lib/services/alfresco-office-extension.service.spec.ts +++ b/projects/aca-shared/src/lib/services/alfresco-office-extension.service.spec.ts @@ -28,10 +28,13 @@ import { AppConfigService } from '@alfresco/adf-core'; import { AlfrescoOfficeExtensionService } from './alfresco-office-extension.service'; import { provideMockStore } from '@ngrx/store/testing'; import { initialState, LibTestingModule } from '../testing/lib-testing-module'; +import { Subject } from 'rxjs'; describe('AlfrescoOfficeExtensionService', () => { let appConfig: AppConfigService; let service: AlfrescoOfficeExtensionService; + let onLoad$: Subject; + const mock = () => { let storage: { [key: string]: any } = {}; return { @@ -48,16 +51,44 @@ describe('AlfrescoOfficeExtensionService', () => { providers: [provideMockStore({ initialState })] }); - service = TestBed.inject(AlfrescoOfficeExtensionService); + onLoad$ = new Subject(); appConfig = TestBed.inject(AppConfigService); - appConfig.config = Object.assign(appConfig.config, { - aosPlugin: true - }); + appConfig.onLoad = onLoad$; + appConfig.config.aosPlugin = true; + + service = TestBed.inject(AlfrescoOfficeExtensionService); Object.defineProperty(window, 'localStorage', { value: mock() }); }); + it('should enable plugin on load', () => { + spyOn(localStorage, 'getItem').and.returnValue(null); + spyOn(localStorage, 'setItem'); + + onLoad$.next({ + plugins: { + aosPlugin: true + } + }); + + TestBed.inject(AlfrescoOfficeExtensionService); + expect(localStorage.setItem).toHaveBeenCalledWith('aosPlugin', 'true'); + }); + + it('should disable plugin on load', () => { + spyOn(localStorage, 'removeItem'); + + onLoad$.next({ + plugins: { + aosPlugin: false + } + }); + + TestBed.inject(AlfrescoOfficeExtensionService); + expect(localStorage.removeItem).toHaveBeenCalledWith('aosPlugin'); + }); + it('Should initialize the localStorage with the item aosPlugin true if not present', () => { expect(localStorage.getItem('aosPlugin')).toBeNull('The localStorage aosPlugin is not null'); service.enablePlugin(); diff --git a/projects/aca-shared/src/lib/services/app.extension.service.spec.ts b/projects/aca-shared/src/lib/services/app.extension.service.spec.ts index 2cd3f811e8..4e79732e15 100644 --- a/projects/aca-shared/src/lib/services/app.extension.service.spec.ts +++ b/projects/aca-shared/src/lib/services/app.extension.service.spec.ts @@ -39,15 +39,20 @@ import { ExtensionConfig, NavBarGroupRef } from '@alfresco/adf-extensions'; -import { AppConfigService } from '@alfresco/adf-core'; +import { AppConfigService, LogService } from '@alfresco/adf-core'; import { provideMockStore } from '@ngrx/store/testing'; import { hasQuickShareEnabled } from '@alfresco/aca-shared/rules'; +import { MatIconRegistry } from '@angular/material/icon'; +import { DomSanitizer } from '@angular/platform-browser'; describe('AppExtensionService', () => { let service: AppExtensionService; let store: Store; let extensions: ExtensionService; let appConfigService: AppConfigService; + let logService: LogService; + let iconRegistry: MatIconRegistry; + let sanitizer: DomSanitizer; beforeEach(() => { TestBed.configureTestingModule({ @@ -55,6 +60,8 @@ describe('AppExtensionService', () => { providers: [provideMockStore({ initialState })] }); + iconRegistry = TestBed.inject(MatIconRegistry); + sanitizer = TestBed.inject(DomSanitizer); appConfigService = TestBed.inject(AppConfigService); store = TestBed.inject(Store); @@ -62,6 +69,7 @@ describe('AppExtensionService', () => { service.repository.status.isQuickShareEnabled = true; extensions = TestBed.inject(ExtensionService); + logService = TestBed.inject(LogService); }); const applyConfig = (config: ExtensionConfig, selection?: boolean) => { @@ -78,6 +86,40 @@ describe('AppExtensionService', () => { }; describe('configs', () => { + it('should log an error during setup', async () => { + spyOn(extensions, 'load').and.returnValue(Promise.resolve(null)); + spyOn(logService, 'error').and.stub(); + + await service.load(); + expect(service.config).toBeNull(); + expect(logService.error).toHaveBeenCalledWith('Extension configuration not found'); + }); + + it('should load content metadata presets', () => { + applyConfig({ + $id: 'test', + $name: 'test', + $version: '1.0.0', + $license: 'MIT', + $vendor: 'Good company', + $runtime: '1.5.0', + features: { + 'content-metadata-presets': [ + { + id: 'app.content.metadata.kitten-images', + 'kitten-images': { + id: 'app.content.metadata.kittenAspect', + 'custom:aspect': '*', + 'exif:exif': ['exif:pixelXDimension', 'exif:pixelYDimension'] + } + } + ] + } + }); + + expect(service.contentMetadata).toBeDefined(); + }); + it('should merge two arrays based on [id] keys', () => { const left = [ { @@ -1074,6 +1116,107 @@ describe('AppExtensionService', () => { }); }); + describe('rules', () => { + it('should evaluate rule', () => { + extensions.setEvaluators({ + rule1: () => true + }); + + expect(service.evaluateRule('rule1')).toBeTrue(); + }); + + it('should not evaluate missing rule and return [false] by default', () => { + expect(service.evaluateRule('missing')).toBeFalse(); + }); + + it('should confirm the rule is defined', () => { + extensions.setEvaluators({ + rule1: () => true + }); + + expect(service.isRuleDefined('rule1')).toBeTrue(); + }); + + it('should not confirm the rule is defined', () => { + expect(service.isRuleDefined(null)).toBeFalse(); + expect(service.isRuleDefined('')).toBeFalse(); + expect(service.isRuleDefined('missing')).toBeFalse(); + }); + + it('should allow node preview', () => { + extensions.setEvaluators({ + 'app.canPreview': () => true + }); + + service.viewerRules.canPreview = 'app.canPreview'; + expect(service.canPreviewNode(null)).toBeTrue(); + }); + + it('should allow node preview with no rules', () => { + service.viewerRules = {}; + expect(service.canPreviewNode(null)).toBeTrue(); + }); + + it('should not allow node preview', () => { + extensions.setEvaluators({ + 'app.canPreview': () => false + }); + + service.viewerRules.canPreview = 'app.canPreview'; + expect(service.canPreviewNode(null)).toBeFalse(); + }); + + it('should allow viewer navigation', () => { + extensions.setEvaluators({ + 'app.allowNavigation': () => true + }); + + service.viewerRules.showNavigation = 'app.allowNavigation'; + expect(service.canShowViewerNavigation(null)).toBeTrue(); + }); + + it('should allow viewer navigation with no rules', () => { + service.viewerRules.showNavigation = null; + expect(service.canShowViewerNavigation(null)).toBeTrue(); + }); + + it('should not allow viewer navigation', () => { + extensions.setEvaluators({ + 'app.allowNavigation': () => false + }); + + service.viewerRules.showNavigation = 'app.allowNavigation'; + expect(service.canShowViewerNavigation(null)).toBeFalse(); + }); + + it('should confirm the viewer extension is disabled explicitly', () => { + const extension = { + disabled: true + }; + + expect(service.isViewerExtensionDisabled(extension)).toBeTrue(); + }); + + it('should confirm the viewer extension is disabled via rules', () => { + extensions.setEvaluators({ + 'viewer.disabled': () => true + }); + + const extension = { + disabled: false, + rules: { + disabled: 'viewer.disabled' + } + }; + + expect(service.isViewerExtensionDisabled(extension)).toBeTrue(); + }); + + it('should confirm viewer extension is not disabled by default', () => { + expect(service.isViewerExtensionDisabled({})).toBeFalse(); + }); + }); + describe('rule disable', () => { beforeEach(() => { extensions.setEvaluators({ @@ -1425,4 +1568,107 @@ describe('AppExtensionService', () => { }); }); }); + + describe('custom icons', () => { + it('should register custom icons', () => { + spyOn(iconRegistry, 'addSvgIconInNamespace').and.stub(); + + const rawUrl = './assets/images/ft_ic_ms_excel.svg'; + + applyConfig({ + $id: 'test', + $name: 'test', + $version: '1.0.0', + $license: 'MIT', + $vendor: 'Good company', + $runtime: '1.5.0', + features: { + icons: [ + { + id: 'adf:excel_thumbnail', + value: rawUrl + } + ] + } + }); + + const url = sanitizer.bypassSecurityTrustResourceUrl(rawUrl); + expect(iconRegistry.addSvgIconInNamespace).toHaveBeenCalledWith('adf', 'excel_thumbnail', url); + }); + + it('should warn if icon has no url path', () => { + const warn = spyOn(logService, 'warn').and.stub(); + + applyConfig({ + $id: 'test', + $name: 'test', + $version: '1.0.0', + $license: 'MIT', + $vendor: 'Good company', + $runtime: '1.5.0', + features: { + icons: [ + { + id: 'adf:excel_thumbnail' + } + ] + } + }); + + expect(warn).toHaveBeenCalledWith('Missing icon value for "adf:excel_thumbnail".'); + }); + + it('should warn if icon has incorrect format', () => { + const warn = spyOn(logService, 'warn').and.stub(); + + applyConfig({ + $id: 'test', + $name: 'test', + $version: '1.0.0', + $license: 'MIT', + $vendor: 'Good company', + $runtime: '1.5.0', + features: { + icons: [ + { + id: 'incorrect.format', + value: './assets/images/ft_ic_ms_excel.svg' + } + ] + } + }); + + expect(warn).toHaveBeenCalledWith(`Incorrect icon id format.`); + }); + }); + + it('should resolve main action', (done) => { + extensions.setEvaluators({ + 'action.enabled': () => true + }); + + applyConfig({ + $id: 'test', + $name: 'test', + $version: '1.0.0', + $license: 'MIT', + $vendor: 'Good company', + $runtime: '1.5.0', + features: { + mainAction: { + id: 'action-id', + title: 'action-title', + type: 'button', + rules: { + visible: 'action.enabled' + } + } + } + }); + + service.getMainAction().subscribe((action) => { + expect(action.id).toEqual('action-id'); + done(); + }); + }); }); diff --git a/projects/aca-shared/src/lib/services/app.extension.service.ts b/projects/aca-shared/src/lib/services/app.extension.service.ts index 9ac402f269..2f810a2492 100644 --- a/projects/aca-shared/src/lib/services/app.extension.service.ts +++ b/projects/aca-shared/src/lib/services/app.extension.service.ts @@ -196,9 +196,9 @@ export class AppExtensionService implements RuleContext { const value = icon.value; if (!value) { - console.warn(`Missing icon value for "${icon.id}".`); + this.logger.warn(`Missing icon value for "${icon.id}".`); } else if (!ns || !id) { - console.warn(`Incorrect icon id format: "${icon.id}".`); + this.logger.warn(`Incorrect icon id format.`); } else { this.matIconRegistry.addSvgIconInNamespace(ns, id, this.sanitizer.bypassSecurityTrustResourceUrl(value)); } @@ -290,12 +290,10 @@ export class AppExtensionService implements RuleContext { let presets = {}; presets = this.filterDisabled(mergeObjects(presets, ...elements)); - try { - this.appConfig.config['content-metadata'].presets = presets; - } catch (error) { - this.logger.error(error, '- could not change content-metadata presets from app.config -'); - } + const metadata = this.appConfig.config['content-metadata'] || {}; + metadata.presets = presets; + this.appConfig.config['content-metadata'] = metadata; return { presets }; } @@ -310,11 +308,7 @@ export class AppExtensionService implements RuleContext { .filter((entry) => this.filterVisible(entry)) .sort(sortByOrder); - try { - this.appConfig.config['search'] = search; - } catch (error) { - this.logger.error(error, '- could not change search from app.config -'); - } + this.appConfig.config['search'] = search; return search; } diff --git a/projects/aca-shared/src/lib/services/app.service.spec.ts b/projects/aca-shared/src/lib/services/app.service.spec.ts index c4727c5701..a06d422710 100644 --- a/projects/aca-shared/src/lib/services/app.service.spec.ts +++ b/projects/aca-shared/src/lib/services/app.service.spec.ts @@ -30,60 +30,49 @@ import { AppConfigService, AlfrescoApiService, PageTitleService, - UserPreferencesService, AlfrescoApiServiceMock, TranslationMock, TranslationService } from '@alfresco/adf-core'; import { BehaviorSubject, Observable, of, Subject } from 'rxjs'; import { HttpClientModule } from '@angular/common/http'; -import { SharedLinksApiService, GroupService, SearchQueryBuilderService, UploadService, DiscoveryApiService } from '@alfresco/adf-content-services'; -import { ActivatedRoute, Router } from '@angular/router'; -import { ContentApiService } from './content-api.service'; -import { RouterExtensionService } from './router.extension.service'; -import { OverlayContainer } from '@angular/cdk/overlay'; -import { AppStore, STORE_INITIAL_APP_DATA } from '../../../store/src/states/app.state'; -import { MockStore, provideMockStore } from '@ngrx/store/testing'; +import { + DiscoveryApiService, + FileUploadErrorEvent, + GroupService, + SearchQueryBuilderService, + SharedLinksApiService, + UploadService +} from '@alfresco/adf-content-services'; +import { ActivatedRoute } from '@angular/router'; +import { STORE_INITIAL_APP_DATA } from '../../../store/src/states/app.state'; +import { provideMockStore } from '@ngrx/store/testing'; import { CommonModule } from '@angular/common'; -import { TranslateService } from '@ngx-translate/core'; -import { TranslateServiceMock } from '../testing/translation.service'; import { RouterTestingModule } from '@angular/router/testing'; import { RepositoryInfo } from '@alfresco/js-api'; -import { AcaMobileAppSwitcherService } from './aca-mobile-app-switcher.service'; import { MatDialogModule } from '@angular/material/dialog'; +import { TranslateModule } from '@ngx-translate/core'; +import { Store } from '@ngrx/store'; +import { SnackbarErrorAction } from '../../../store/src/actions/snackbar.actions'; +import { ContentApiService } from './content-api.service'; +import { SetRepositoryInfoAction, SetUserProfileAction } from '../../../store/src/actions/app.actions'; describe('AppService', () => { let service: AppService; let auth: AuthenticationService; let appConfig: AppConfigService; let searchQueryBuilderService: SearchQueryBuilderService; - let userPreferencesService: UserPreferencesService; - let router: Router; - let activatedRoute: ActivatedRoute; - let routerExtensionService: RouterExtensionService; - let pageTitleService: PageTitleService; let uploadService: UploadService; - let contentApiService: ContentApiService; + let store: Store; let sharedLinksApiService: SharedLinksApiService; - let overlayContainer: OverlayContainer; - let alfrescoApiService: AlfrescoApiService; + let contentApi: ContentApiService; let groupService: GroupService; - let storeInitialAppData: any; - let store: MockStore; - let acaMobileAppSwitcherService: AcaMobileAppSwitcherService; beforeEach(() => { TestBed.configureTestingModule({ - imports: [HttpClientModule, RouterTestingModule.withRoutes([]), MatDialogModule], + imports: [CommonModule, HttpClientModule, TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), MatDialogModule], providers: [ - CommonModule, SearchQueryBuilderService, - UserPreferencesService, - RouterExtensionService, - UploadService, - ContentApiService, - SharedLinksApiService, - OverlayContainer, provideMockStore({}), { provide: PageTitleService, @@ -118,48 +107,19 @@ describe('AppService', () => { isLoggedIn: () => false } }, - { provide: TranslationService, useClass: TranslationMock }, - { provide: TranslateService, useClass: TranslateServiceMock } + { provide: TranslationService, useClass: TranslationMock } ] }); appConfig = TestBed.inject(AppConfigService); + auth = TestBed.inject(AuthenticationService); searchQueryBuilderService = TestBed.inject(SearchQueryBuilderService); - userPreferencesService = TestBed.inject(UserPreferencesService); - router = TestBed.inject(Router); - activatedRoute = TestBed.inject(ActivatedRoute); - routerExtensionService = TestBed.inject(RouterExtensionService); - pageTitleService = TestBed.inject(PageTitleService); uploadService = TestBed.inject(UploadService); - contentApiService = TestBed.inject(ContentApiService); + store = TestBed.inject(Store); sharedLinksApiService = TestBed.inject(SharedLinksApiService); - overlayContainer = TestBed.inject(OverlayContainer); - alfrescoApiService = TestBed.inject(AlfrescoApiService); + contentApi = TestBed.inject(ContentApiService); groupService = TestBed.inject(GroupService); - storeInitialAppData = TestBed.inject(STORE_INITIAL_APP_DATA); - store = TestBed.inject(MockStore); - auth = TestBed.inject(AuthenticationService); - acaMobileAppSwitcherService = TestBed.inject(AcaMobileAppSwitcherService); - - service = new AppService( - userPreferencesService, - auth, - store, - router, - activatedRoute, - appConfig, - pageTitleService, - alfrescoApiService, - uploadService, - routerExtensionService, - contentApiService, - sharedLinksApiService, - groupService, - overlayContainer, - storeInitialAppData, - searchQueryBuilderService, - acaMobileAppSwitcherService - ); + service = TestBed.inject(AppService); }); it('should be ready if [withCredentials] mode is used', (done) => { @@ -169,26 +129,7 @@ describe('AppService', () => { } }; - const instance = new AppService( - userPreferencesService, - auth, - store, - router, - activatedRoute, - appConfig, - pageTitleService, - alfrescoApiService, - uploadService, - routerExtensionService, - contentApiService, - sharedLinksApiService, - groupService, - overlayContainer, - storeInitialAppData, - searchQueryBuilderService, - acaMobileAppSwitcherService - ); - + const instance = TestBed.inject(AppService); expect(instance.withCredentials).toBeTruthy(); instance.ready$.subscribe(() => { @@ -204,4 +145,98 @@ describe('AppService', () => { auth.onLogin.next(); await expect(isReady).toEqual(true); }); + + it('should reset search to defaults upon logout', async () => { + const resetToDefaults = spyOn(searchQueryBuilderService, 'resetToDefaults'); + auth.onLogout.next(true); + + await expect(resetToDefaults).toHaveBeenCalled(); + }); + + it('should rase notification on share link error', () => { + spyOn(store, 'select').and.returnValue(of('')); + service.init(); + const dispatch = spyOn(store, 'dispatch'); + + sharedLinksApiService.error.next({ message: 'Error Message', statusCode: 1 }); + expect(dispatch).toHaveBeenCalledWith(new SnackbarErrorAction('Error Message')); + }); + + it('should raise notification on upload error', async () => { + spyOn(store, 'select').and.returnValue(of('')); + service.init(); + const dispatch = spyOn(store, 'dispatch'); + + uploadService.fileUploadError.next(new FileUploadErrorEvent(null, { status: 403 })); + expect(dispatch).toHaveBeenCalledWith(new SnackbarErrorAction('APP.MESSAGES.UPLOAD.ERROR.403')); + dispatch.calls.reset(); + + uploadService.fileUploadError.next(new FileUploadErrorEvent(null, { status: 404 })); + expect(dispatch).toHaveBeenCalledWith(new SnackbarErrorAction('APP.MESSAGES.UPLOAD.ERROR.404')); + dispatch.calls.reset(); + + uploadService.fileUploadError.next(new FileUploadErrorEvent(null, { status: 409 })); + expect(dispatch).toHaveBeenCalledWith(new SnackbarErrorAction('APP.MESSAGES.UPLOAD.ERROR.CONFLICT')); + dispatch.calls.reset(); + + uploadService.fileUploadError.next(new FileUploadErrorEvent(null, { status: 500 })); + expect(dispatch).toHaveBeenCalledWith(new SnackbarErrorAction('APP.MESSAGES.UPLOAD.ERROR.500')); + dispatch.calls.reset(); + + uploadService.fileUploadError.next(new FileUploadErrorEvent(null, { status: 504 })); + expect(dispatch).toHaveBeenCalledWith(new SnackbarErrorAction('APP.MESSAGES.UPLOAD.ERROR.504')); + dispatch.calls.reset(); + + uploadService.fileUploadError.next(new FileUploadErrorEvent(null, { status: 403 })); + expect(dispatch).toHaveBeenCalledWith(new SnackbarErrorAction('APP.MESSAGES.UPLOAD.ERROR.403')); + dispatch.calls.reset(); + + uploadService.fileUploadError.next(new FileUploadErrorEvent(null, {})); + expect(dispatch).toHaveBeenCalledWith(new SnackbarErrorAction('APP.MESSAGES.UPLOAD.ERROR.GENERIC')); + }); + + it('should load custom css', () => { + const appendChild = spyOn(document.head, 'appendChild'); + spyOn(store, 'select').and.returnValue(of('/custom.css')); + service.init(); + + const cssLinkElement = document.createElement('link'); + cssLinkElement.setAttribute('rel', 'stylesheet'); + cssLinkElement.setAttribute('type', 'text/css'); + cssLinkElement.setAttribute('href', '/custom.css'); + + expect(appendChild).toHaveBeenCalledWith(cssLinkElement); + }); + + it('should load repository status on login', () => { + const repository: any = {}; + spyOn(contentApi, 'getRepositoryInformation').and.returnValue(of({ entry: { repository } })); + spyOn(store, 'select').and.returnValue(of('')); + service.init(); + + const dispatch = spyOn(store, 'dispatch'); + auth.onLogin.next(true); + + expect(dispatch).toHaveBeenCalledWith(new SetRepositoryInfoAction(repository)); + }); + + it('should load user profile on login', async () => { + const person: any = { id: 'person' }; + + const group: any = { entry: {} }; + const groups: any[] = [group]; + + spyOn(contentApi, 'getRepositoryInformation').and.returnValue(of({} as any)); + spyOn(groupService, 'listAllGroupMembershipsForPerson').and.returnValue(Promise.resolve(groups)); + spyOn(contentApi, 'getPerson').and.returnValue(of({ entry: person })); + + spyOn(store, 'select').and.returnValue(of('')); + service.init(); + + const dispatch = spyOn(store, 'dispatch'); + auth.onLogin.next(true); + + await expect(groupService.listAllGroupMembershipsForPerson).toHaveBeenCalled(); + await expect(dispatch).toHaveBeenCalledWith(new SetUserProfileAction({ person, groups: [group.entry] })); + }); }); diff --git a/projects/aca-shared/src/lib/services/app.service.ts b/projects/aca-shared/src/lib/services/app.service.ts index 5f4f6bb0b3..0060036956 100644 --- a/projects/aca-shared/src/lib/services/app.service.ts +++ b/projects/aca-shared/src/lib/services/app.service.ts @@ -23,13 +23,13 @@ * along with Alfresco. If not, see . */ -import { Inject, Injectable, OnDestroy } from '@angular/core'; +import { Inject, Injectable } from '@angular/core'; import { AuthenticationService, AppConfigService, AlfrescoApiService, PageTitleService, UserPreferencesService } from '@alfresco/adf-core'; -import { Observable, BehaviorSubject, Subject } from 'rxjs'; +import { Observable, BehaviorSubject } from 'rxjs'; import { GroupService, SearchQueryBuilderService, SharedLinksApiService, UploadService, FileUploadErrorEvent } from '@alfresco/adf-content-services'; import { OverlayContainer } from '@angular/cdk/overlay'; import { ActivatedRoute, ActivationEnd, NavigationStart, Router } from '@angular/router'; -import { filter, map, takeUntil, tap } from 'rxjs/operators'; +import { filter, map, tap } from 'rxjs/operators'; import { AppState, AppStore, @@ -54,7 +54,7 @@ import { AcaMobileAppSwitcherService } from './aca-mobile-app-switcher.service'; providedIn: 'root' }) // After moving shell to ADF to core, AppService will implement ShellAppService -export class AppService implements OnDestroy { +export class AppService { private ready: BehaviorSubject; ready$: Observable; @@ -63,8 +63,6 @@ export class AppService implements OnDestroy { hideSidenavConditions = ['/preview/']; minimizeSidenavConditions = ['search']; - onDestroy$ = new Subject(); - /** * Whether `withCredentials` mode is enabled. * Usually means that `Kerberos` mode is used. @@ -110,11 +108,6 @@ export class AppService implements OnDestroy { ); } - ngOnDestroy(): void { - this.onDestroy$.next(true); - this.onDestroy$.complete(); - } - init(): void { this.alfrescoApiService.getInstance().on('error', (error: { status: number; response: any }) => { if (error.status === 401 && !this.alfrescoApiService.isExcludedErrorListener(error?.response?.req?.url)) { @@ -146,24 +139,21 @@ export class AppService implements OnDestroy { this.store.dispatch(new SetCurrentUrlAction(router.url)); }); - this.router.events - .pipe( - filter((event) => event instanceof NavigationStart), - takeUntil(this.onDestroy$) - ) - .subscribe(() => { - this.store.dispatch(new ResetSelectionAction()); - }); + this.router.events.pipe(filter((event) => event instanceof NavigationStart)).subscribe(() => { + this.store.dispatch(new ResetSelectionAction()); + }); this.routerExtensionService.mapExtensionRoutes(); this.uploadService.fileUploadError.subscribe((error) => this.onFileUploadedError(error)); - this.sharedLinksApiService.error.pipe(takeUntil(this.onDestroy$)).subscribe((err: { message: string }) => { - this.store.dispatch(new SnackbarErrorAction(err.message)); + this.sharedLinksApiService.error.subscribe((err: { message: string }) => { + if (err?.message) { + this.store.dispatch(new SnackbarErrorAction(err.message)); + } }); - this.ready$.pipe(takeUntil(this.onDestroy$)).subscribe((isReady) => { + this.ready$.subscribe((isReady) => { if (isReady) { this.loadRepositoryStatus(); this.loadUserProfile(); @@ -182,7 +172,9 @@ export class AppService implements OnDestroy { private loadRepositoryStatus() { this.contentApi.getRepositoryInformation().subscribe((response: DiscoveryEntry) => { - this.store.dispatch(new SetRepositoryInfoAction(response.entry.repository)); + if (response?.entry?.repository) { + this.store.dispatch(new SetRepositoryInfoAction(response.entry.repository)); + } }); } @@ -201,7 +193,7 @@ export class AppService implements OnDestroy { } loadAppSettings() { - let baseShareUrl = this.config.get('baseShareUrl'); + let baseShareUrl = this.config.get('baseShareUrl', ''); if (!baseShareUrl.endsWith('/')) { baseShareUrl += '/'; } @@ -224,23 +216,23 @@ export class AppService implements OnDestroy { onFileUploadedError(error: FileUploadErrorEvent) { let message = 'APP.MESSAGES.UPLOAD.ERROR.GENERIC'; - if (error.error.status === 403) { + if (error?.error?.status === 403) { message = 'APP.MESSAGES.UPLOAD.ERROR.403'; } - if (error.error.status === 404) { + if (error?.error?.status === 404) { message = 'APP.MESSAGES.UPLOAD.ERROR.404'; } - if (error.error.status === 409) { + if (error?.error?.status === 409) { message = 'APP.MESSAGES.UPLOAD.ERROR.CONFLICT'; } - if (error.error.status === 500) { + if (error?.error?.status === 500) { message = 'APP.MESSAGES.UPLOAD.ERROR.500'; } - if (error.error.status === 504) { + if (error?.error?.status === 504) { message = 'APP.MESSAGES.UPLOAD.ERROR.504'; } diff --git a/projects/aca-shared/src/lib/services/content-api.service.ts b/projects/aca-shared/src/lib/services/content-api.service.ts index 687a821772..24cf9fdb61 100644 --- a/projects/aca-shared/src/lib/services/content-api.service.ts +++ b/projects/aca-shared/src/lib/services/content-api.service.ts @@ -61,61 +61,61 @@ import { map } from 'rxjs/operators'; providedIn: 'root' }) export class ContentApiService { - _nodesApi: NodesApi; + private _nodesApi: NodesApi; get nodesApi(): NodesApi { this._nodesApi = this._nodesApi ?? new NodesApi(this.api.getInstance()); return this._nodesApi; } - _trashcanApi: TrashcanApi; + private _trashcanApi: TrashcanApi; get trashcanApi(): TrashcanApi { this._trashcanApi = this._trashcanApi ?? new TrashcanApi(this.api.getInstance()); return this._trashcanApi; } - _sharedLinksApi: SharedlinksApi; + private _sharedLinksApi: SharedlinksApi; get sharedLinksApi(): SharedlinksApi { this._sharedLinksApi = this._sharedLinksApi ?? new SharedlinksApi(this.api.getInstance()); return this._sharedLinksApi; } - _discoveryApi: DiscoveryApi; + private _discoveryApi: DiscoveryApi; get discoveryApi(): DiscoveryApi { this._discoveryApi = this._discoveryApi ?? new DiscoveryApi(this.api.getInstance()); return this._discoveryApi; } - _favoritesApi: FavoritesApi; + private _favoritesApi: FavoritesApi; get favoritesApi(): FavoritesApi { this._favoritesApi = this._favoritesApi ?? new FavoritesApi(this.api.getInstance()); return this._favoritesApi; } - _contentApi: ContentApi; + private _contentApi: ContentApi; get contentApi(): ContentApi { this._contentApi = this._contentApi ?? new ContentApi(this.api.getInstance()); return this._contentApi; } - _sitesApi: SitesApi; + private _sitesApi: SitesApi; get sitesApi(): SitesApi { this._sitesApi = this._sitesApi ?? new SitesApi(this.api.getInstance()); return this._sitesApi; } - _searchApi: SearchApi; + private _searchApi: SearchApi; get searchApi(): SearchApi { this._searchApi = this._searchApi ?? new SearchApi(this.api.getInstance()); return this._searchApi; } - _peopleApi: PeopleApi; + private _peopleApi: PeopleApi; get peopleApi(): PeopleApi { this._peopleApi = this._peopleApi ?? new PeopleApi(this.api.getInstance()); return this._peopleApi; } - _versionsApi: VersionsApi; + private _versionsApi: VersionsApi; get versionsApi(): VersionsApi { this._versionsApi = this._versionsApi ?? new VersionsApi(this.api.getInstance()); return this._versionsApi; diff --git a/projects/aca-shared/src/lib/utils/node.utils.ts b/projects/aca-shared/src/lib/utils/node.utils.ts index 41ce3cd0aa..0b5c7b1d94 100644 --- a/projects/aca-shared/src/lib/utils/node.utils.ts +++ b/projects/aca-shared/src/lib/utils/node.utils.ts @@ -26,16 +26,21 @@ import { Node } from '@alfresco/js-api'; export function isLocked(node: { entry: Node }): boolean { - const { entry } = node; + if (node?.entry) { + const { entry } = node; - return ( - (entry && entry.isLocked) || - (entry.properties && (entry.properties['cm:lockType'] === 'READ_ONLY_LOCK' || entry.properties['cm:lockType'] === 'WRITE_LOCK')) - ); + return entry.isLocked || entry.properties?.['cm:lockType'] === 'READ_ONLY_LOCK' || entry.properties?.['cm:lockType'] === 'WRITE_LOCK'; + } else { + return false; + } } export function isLibrary(node: { entry: Node | any }): boolean { - const { entry } = node; + if (node?.entry) { + const { entry } = node; - return (entry.guid && entry.id && entry.preset && entry.title && entry.visibility) || entry.nodeType === 'st:site'; + return !!(entry.guid && entry.id && entry.preset && entry.title && entry.visibility) || entry.nodeType === 'st:site'; + } else { + return false; + } } diff --git a/projects/aca-shared/src/lib/utils/note.utils.spec.ts b/projects/aca-shared/src/lib/utils/note.utils.spec.ts new file mode 100644 index 0000000000..2b73357832 --- /dev/null +++ b/projects/aca-shared/src/lib/utils/note.utils.spec.ts @@ -0,0 +1,115 @@ +/*! + * @license + * Alfresco Example Content Application + * + * Copyright (C) 2005 - 2020 Alfresco Software Limited + * + * This file is part of the Alfresco Example Content Application. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * The Alfresco Example Content Application is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Alfresco Example Content Application is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ + +import { isLibrary, isLocked } from './node.utils'; + +describe('NodeUtils', () => { + describe('isLocked', () => { + it('should return [false] if entry is not defined', () => { + expect(isLocked(null)).toBeFalse(); + expect(isLocked({ entry: null })).toBeFalse(); + }); + + it('should return [true] if entry is locked', () => { + expect( + isLocked({ + entry: { + isLocked: true + } as any + }) + ).toBeTrue(); + }); + + it('should return [true] for [READ_ONLY_LOCK] type', () => { + expect( + isLocked({ + entry: { + isLocked: false, + properties: { + 'cm:lockType': 'READ_ONLY_LOCK' + } + } as any + }) + ).toBeTrue(); + }); + + it('should return [true] for [WRITE_LOCK] type', () => { + expect( + isLocked({ + entry: { + isLocked: false, + properties: { + 'cm:lockType': 'WRITE_LOCK' + } + } as any + }) + ).toBeTrue(); + }); + + it('should return [false] for unknown lock type', () => { + expect( + isLocked({ + entry: { + isLocked: false, + properties: { + 'cm:lockType': 'UNKNOWN' + } + } as any + }) + ).toBeFalse(); + }); + }); + + describe('isLibrary', () => { + it('should return [false] if entry is not defined', () => { + expect(isLibrary(null)).toBeFalse(); + expect(isLibrary({ entry: null })).toBeFalse(); + }); + + it('should detect library by [st:site] node type', () => { + expect( + isLibrary({ + entry: { + nodeType: 'st:site' + } + }) + ).toBeTrue(); + }); + + it('should detect library by common properties', () => { + expect( + isLibrary({ + entry: { + guid: '', + id: '', + preset: '', + title: '', + visibility: '<visibility>' + } + }) + ).toBeTrue(); + }); + }); +}); diff --git a/projects/aca-shared/src/public-api.ts b/projects/aca-shared/src/public-api.ts index 528363dda3..fb505ab3a9 100644 --- a/projects/aca-shared/src/public-api.ts +++ b/projects/aca-shared/src/public-api.ts @@ -65,4 +65,3 @@ export * from './lib/services/alfresco-office-extension.service'; export * from './lib/utils/node.utils'; export * from './lib/shared.module'; export * from './lib/testing/lib-testing-module'; -export * from './lib/testing/translation.service'; diff --git a/projects/aca-viewer/karma.conf.js b/projects/aca-viewer/karma.conf.js index 06a2430394..d24b4c381b 100644 --- a/projects/aca-viewer/karma.conf.js +++ b/projects/aca-viewer/karma.conf.js @@ -1,32 +1,15 @@ // Karma configuration file, see link for more information // https://karma-runner.github.io/1.0/config/configuration-file.html +const { join } = require('path'); +const getBaseKarmaConfig = require('../../karma.conf'); module.exports = function (config) { + const baseConfig = getBaseKarmaConfig(); config.set({ - basePath: '', - frameworks: ['jasmine', '@angular-devkit/build-angular'], - plugins: [ - require('karma-jasmine'), - require('karma-chrome-launcher'), - require('karma-jasmine-html-reporter'), - require('karma-coverage-istanbul-reporter'), - require('@angular-devkit/build-angular/plugins/karma') - ], - client: { - clearContext: false // leave Jasmine Spec Runner output visible in browser + ...baseConfig, + coverageReporter: { + ...baseConfig.coverageReporter, + dir: join(__dirname, '../../coverage/aca-viewer'), }, - coverageIstanbulReporter: { - dir: require('path').join(__dirname, '../../coverage/aca-viewer'), - reports: ['html', 'lcovonly', 'text-summary'], - fixWebpackSourcePaths: true - }, - reporters: ['progress', 'kjhtml'], - port: 9876, - colors: true, - logLevel: config.LOG_INFO, - autoWatch: true, - browsers: ['Chrome'], - singleRun: true, - restartOnFileChange: true }); }; diff --git a/projects/adf-office-services-ext/assets/i18n/en.json b/projects/adf-office-services-ext/assets/i18n/en.json index 9d146cb187..32a664082d 100644 --- a/projects/adf-office-services-ext/assets/i18n/en.json +++ b/projects/adf-office-services-ext/assets/i18n/en.json @@ -1,5 +1,10 @@ { "AOS": { - "ACTION_TITLE": "Edit in Microsoft Office™" + "ACTION_TITLE": "Edit in Microsoft Office™", + "ERRORS": { + "UNSUPPORTED_PLATFORM": "Only supported for Windows and MacOS platforms", + "ALREADY_LOCKED": "Document '{{ nodeId }}' is locked by '{{ lockOwner }}'", + "MISSING_PROTOCOL_HANDLER": "No protocol handler found for '{{ nodeName }}'" + } } } diff --git a/projects/adf-office-services-ext/karma.conf.js b/projects/adf-office-services-ext/karma.conf.js index 6ad0b69917..cc275b2515 100644 --- a/projects/adf-office-services-ext/karma.conf.js +++ b/projects/adf-office-services-ext/karma.conf.js @@ -1,35 +1,15 @@ // Karma configuration file, see link for more information // https://karma-runner.github.io/1.0/config/configuration-file.html +const { join } = require('path'); +const getBaseKarmaConfig = require('../../karma.conf'); -module.exports = function(config) { +module.exports = function (config) { + const baseConfig = getBaseKarmaConfig(); config.set({ - basePath: '', - frameworks: ['jasmine', '@angular-devkit/build-angular'], - plugins: [ - require('karma-jasmine'), - require('karma-chrome-launcher'), - require('karma-jasmine-html-reporter'), - require('karma-coverage-istanbul-reporter'), - require('karma-mocha-reporter'), - require('@angular-devkit/build-angular/plugins/karma') - ], - client: { - clearContext: false // leave Jasmine Spec Runner output visible in browser + ...baseConfig, + coverageReporter: { + ...baseConfig.coverageReporter, + dir: join(__dirname, '../../coverage/aca-office-services-ext'), }, - coverageIstanbulReporter: { - dir: require('path').join( - __dirname, - '../../coverage/adf-office-services-ext' - ), - reports: ['html', 'lcovonly'], - fixWebpackSourcePaths: true - }, - reporters: ['mocha', 'kjhtml'], - port: 9876, - colors: true, - logLevel: config.LOG_INFO, - autoWatch: true, - browsers: ['Chrome'], - singleRun: true }); }; diff --git a/projects/adf-office-services-ext/src/lib/aos-extension.service.spec.ts b/projects/adf-office-services-ext/src/lib/aos-extension.service.spec.ts new file mode 100644 index 0000000000..b35ff5e972 --- /dev/null +++ b/projects/adf-office-services-ext/src/lib/aos-extension.service.spec.ts @@ -0,0 +1,156 @@ +/*! + * @license + * Alfresco Example Content Application + * + * Copyright (C) 2005 - 2020 Alfresco Software Limited + * + * This file is part of the Alfresco Example Content Application. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * The Alfresco Example Content Application is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Alfresco Example Content Application is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see <http://www.gnu.org/licenses/>. + */ + +import { TestBed } from '@angular/core/testing'; +import { AosEditOnlineService } from './aos-extension.service'; +import { AppConfigService, AuthenticationService, CoreModule, LogService, NotificationService } from '@alfresco/adf-core'; +import { TranslateModule } from '@ngx-translate/core'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; + +describe('AosEditOnlineService', () => { + let aosEditOnlineService: AosEditOnlineService; + let notificationService: NotificationService; + let authenticationService: AuthenticationService; + let appConfigService: AppConfigService; + let userAgent: jasmine.Spy; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [NoopAnimationsModule, TranslateModule.forRoot(), CoreModule.forRoot()], + providers: [{ provide: LogService, useValue: { error() {} } }] + }); + + aosEditOnlineService = TestBed.inject(AosEditOnlineService); + notificationService = TestBed.inject(NotificationService); + authenticationService = TestBed.inject(AuthenticationService); + appConfigService = TestBed.inject(AppConfigService); + + spyOn(authenticationService, 'getEcmUsername').and.returnValue('user1'); + spyOn(appConfigService, 'get').and.returnValue('http://localhost:3000'); + userAgent = spyOnProperty(navigator, 'userAgent').and.returnValue('mac'); + }); + + it('should raise error if file is already locked by another user', () => { + const showError = spyOn(notificationService, 'showError').and.stub(); + const node: any = { + id: 'node1', + isFile: true, + isLocked: true, + properties: { + 'cm:lockType': 'WRITE_LOCK', + 'cm:lockOwner': { id: 'user2' } + } + }; + + aosEditOnlineService.onActionEditOnlineAos(node); + expect(showError).toHaveBeenCalledWith(`AOS.ERRORS.ALREADY_LOCKED`, null, { nodeId: 'node1', lockOwner: 'user2' }); + }); + + it('should open document if locked by the owner', () => { + const openByUrl = spyOn(aosEditOnlineService, 'openByUrl').and.stub(); + + const node: any = { + id: 'node1', + name: 'file.docx', + isFile: true, + isLocked: true, + properties: { + 'cm:lockType': 'READ_ONLY_LOCK', + 'cm:lockOwner': { id: 'user1' } + } + }; + + aosEditOnlineService.onActionEditOnlineAos(node); + // eslint-disable-next-line @cspell/spellchecker + expect(openByUrl).toHaveBeenCalledWith('ms-word', 'http://localhost:3000/Company Home/_aos_nodeid/node1/file.docx'); + }); + + it('should open document for node with 1 path segment', () => { + const openByUrl = spyOn(aosEditOnlineService, 'openByUrl').and.stub(); + + const node: any = { + id: 'node1', + name: 'file.docx', + isFile: true, + isLocked: false, + path: { + elements: [{ name: 'folder1' }] + }, + properties: {} + }; + + aosEditOnlineService.onActionEditOnlineAos(node); + expect(openByUrl).toHaveBeenCalledWith('ms-word', 'http://localhost:3000/file.docx'); + }); + + it('should open document for node with multiple path segments', () => { + const openByUrl = spyOn(aosEditOnlineService, 'openByUrl').and.stub(); + + const node: any = { + id: 'node1', + name: 'file.docx', + isFile: true, + isLocked: false, + path: { + elements: [{ name: 'parent' }, { name: 'child' }] + }, + properties: {} + }; + + aosEditOnlineService.onActionEditOnlineAos(node); + expect(openByUrl).toHaveBeenCalledWith('ms-word', 'http://localhost:3000/child/_aos_nodeid/node1/file.docx'); + }); + + it('should raise error when protocol handler is not supported', () => { + const showError = spyOn(notificationService, 'showError').and.stub(); + + const node: any = { + id: 'node1', + name: 'file.txt', + isFile: true, + isLocked: false, + properties: {} + }; + + aosEditOnlineService.onActionEditOnlineAos(node); + expect(showError).toHaveBeenCalledWith('AOS.ERRORS.MISSING_PROTOCOL_HANDLER', null, { nodeName: 'file.txt' }); + }); + + it('should raise error for unsupported platform', () => { + const showError = spyOn(notificationService, 'showError').and.stub(); + userAgent.and.returnValue('unknown'); + + const node: any = { + id: 'node1', + name: 'file.docx', + isFile: true, + isLocked: false, + properties: {} + }; + + aosEditOnlineService.onActionEditOnlineAos(node); + expect(showError).toHaveBeenCalledWith('AOS.ERRORS.UNSUPPORTED_PLATFORM'); + }); +}); diff --git a/projects/adf-office-services-ext/src/lib/aos-extension.service.ts b/projects/adf-office-services-ext/src/lib/aos-extension.service.ts index b617fc2cd8..f02aa3aab3 100644 --- a/projects/adf-office-services-ext/src/lib/aos-extension.service.ts +++ b/projects/adf-office-services-ext/src/lib/aos-extension.service.ts @@ -24,7 +24,7 @@ */ /* cspell:disable */ -import { AppConfigService, AuthenticationService, NotificationService } from '@alfresco/adf-core'; +import { AppConfigService, AuthenticationService, LogService, NotificationService } from '@alfresco/adf-core'; import { Injectable } from '@angular/core'; import { MinimalNodeEntryEntity } from '@alfresco/js-api'; import { getFileExtension, supportedExtensions } from '@alfresco/aca-shared/rules'; @@ -38,9 +38,10 @@ export interface IAosEditOnlineService { }) export class AosEditOnlineService implements IAosEditOnlineService { constructor( - private alfrescoAuthenticationService: AuthenticationService, + private authenticationService: AuthenticationService, private appConfigService: AppConfigService, - private notificationService: NotificationService + private notificationService: NotificationService, + private logService: LogService ) {} onActionEditOnlineAos(node: MinimalNodeEntryEntity): void { @@ -51,10 +52,10 @@ export class AosEditOnlineService implements IAosEditOnlineService { // ); const checkedOut = node.properties['cm:lockType'] === 'WRITE_LOCK' || node.properties['cm:lockType'] === 'READ_ONLY_LOCK'; const lockOwner = node.properties['cm:lockOwner']; - const differentLockOwner = lockOwner.id !== this.alfrescoAuthenticationService.getEcmUsername(); + const differentLockOwner = lockOwner.id !== this.authenticationService.getEcmUsername(); if (checkedOut && differentLockOwner) { - this.onAlreadyLockedNotification(node.id, lockOwner); + this.onAlreadyLockedNotification(node.id, lockOwner.id); } else { this.triggerEditOnlineAos(node); } @@ -77,17 +78,21 @@ export class AosEditOnlineService implements IAosEditOnlineService { } private onAlreadyLockedNotification(nodeId: string, lockOwner: string) { - this.notificationService.openSnackMessage(`Document ${nodeId} locked by ${lockOwner}`, 3000); + this.logService.error('Document already locked by another user'); + this.notificationService.showError(`AOS.ERRORS.ALREADY_LOCKED`, null, { + nodeId, + lockOwner + }); } private getProtocolForFileExtension(fileExtension: string) { - return supportedExtensions[fileExtension]; + return fileExtension && supportedExtensions[fileExtension]; } private triggerEditOnlineAos(node: MinimalNodeEntryEntity): void { const aosHost = this.appConfigService.get('aosHost'); let url: string; - const pathElements = (node.path.elements || []).map((segment) => segment.name); + const pathElements = (node.path?.elements || []).map((segment) => segment.name); if (!pathElements.length) { url = `${aosHost}/Company Home/_aos_nodeid/${this.getNodeId(node)}/${encodeURIComponent(node.name)}`; @@ -106,23 +111,25 @@ export class AosEditOnlineService implements IAosEditOnlineService { const protocolHandler = this.getProtocolForFileExtension(fileExtension); if (protocolHandler === undefined) { - this.notificationService.openSnackMessage(`No protocol handler found for {fileExtension}`, 3000); + this.logService.error('Protocol handler missing'); + this.notificationService.showError(`AOS.ERRORS.MISSING_PROTOCOL_HANDLER`, null, { nodeName: node.name }); return; } if (!this.isWindows() && !this.isMacOs()) { - this.notificationService.openSnackMessage('Only supported for Windows and Mac', 3000); + this.logService.error('Unsupported platform'); + this.notificationService.showError('AOS.ERRORS.UNSUPPORTED_PLATFORM'); } else { - this.aosTryToLaunchOfficeByMsProtocolHandler(protocolHandler, url); + this.openByUrl(protocolHandler, url); } } - private aosTryToLaunchOfficeByMsProtocolHandler(protocolHandler: string, url: string) { - const protocolUrl = protocolHandler + ':ofe%7Cu%7C' + url; + openByUrl(protocolHandler: string, url: string) { + const finalUrl = protocolHandler + ':ofe%7Cu%7C' + url; const iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = protocolUrl; + iframe.src = finalUrl; document.body.appendChild(iframe);