[ACS-4865] setup and enable code coverage for all projects (#3074)

This commit is contained in:
Denys Vuika 2023-03-20 09:06:19 -04:00 committed by GitHub
parent 86c0bbb998
commit 9609393d1e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 1300 additions and 408 deletions

View File

@ -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. 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 <project>
```
### 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/<project>` folder.
When working with unit testing and code coverage improvement, you can run unit tests in the "live reload" mode:
```sh
ng test <project> --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 ## 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: 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:

View File

@ -367,6 +367,7 @@
"test": { "test": {
"builder": "@angular-devkit/build-angular:karma", "builder": "@angular-devkit/build-angular:karma",
"options": { "options": {
"codeCoverage": true,
"main": "projects/adf-office-services-ext/src/test.ts", "main": "projects/adf-office-services-ext/src/test.ts",
"tsConfig": "projects/adf-office-services-ext/tsconfig.spec.json", "tsConfig": "projects/adf-office-services-ext/tsconfig.spec.json",
"karmaConfig": "projects/adf-office-services-ext/karma.conf.js" "karmaConfig": "projects/adf-office-services-ext/karma.conf.js"
@ -413,6 +414,7 @@
"test": { "test": {
"builder": "@angular-devkit/build-angular:karma", "builder": "@angular-devkit/build-angular:karma",
"options": { "options": {
"codeCoverage": true,
"main": "projects/aca-shared/test.ts", "main": "projects/aca-shared/test.ts",
"tsConfig": "projects/aca-shared/tsconfig.spec.json", "tsConfig": "projects/aca-shared/tsconfig.spec.json",
"karmaConfig": "projects/aca-shared/karma.conf.js" "karmaConfig": "projects/aca-shared/karma.conf.js"
@ -465,6 +467,7 @@
"test": { "test": {
"builder": "@angular-devkit/build-angular:karma", "builder": "@angular-devkit/build-angular:karma",
"options": { "options": {
"codeCoverage": true,
"main": "projects/aca-about/src/test.ts", "main": "projects/aca-about/src/test.ts",
"tsConfig": "projects/aca-about/tsconfig.spec.json", "tsConfig": "projects/aca-about/tsconfig.spec.json",
"karmaConfig": "projects/aca-about/karma.conf.js" "karmaConfig": "projects/aca-about/karma.conf.js"
@ -506,6 +509,7 @@
"test": { "test": {
"builder": "@angular-devkit/build-angular:karma", "builder": "@angular-devkit/build-angular:karma",
"options": { "options": {
"codeCoverage": true,
"main": "projects/aca-folder-rules/src/test.ts", "main": "projects/aca-folder-rules/src/test.ts",
"tsConfig": "projects/aca-folder-rules/tsconfig.spec.json", "tsConfig": "projects/aca-folder-rules/tsconfig.spec.json",
"karmaConfig": "projects/aca-folder-rules/karma.conf.js" "karmaConfig": "projects/aca-folder-rules/karma.conf.js"
@ -554,6 +558,7 @@
"test": { "test": {
"builder": "@angular-devkit/build-angular:karma", "builder": "@angular-devkit/build-angular:karma",
"options": { "options": {
"codeCoverage": true,
"main": "projects/aca-content/src/test.ts", "main": "projects/aca-content/src/test.ts",
"tsConfig": "projects/aca-content/tsconfig.spec.json", "tsConfig": "projects/aca-content/tsconfig.spec.json",
"karmaConfig": "projects/aca-content/karma.conf.js", "karmaConfig": "projects/aca-content/karma.conf.js",
@ -605,6 +610,7 @@
"test": { "test": {
"builder": "@angular-devkit/build-angular:karma", "builder": "@angular-devkit/build-angular:karma",
"options": { "options": {
"codeCoverage": true,
"main": "projects/aca-viewer/src/test.ts", "main": "projects/aca-viewer/src/test.ts",
"tsConfig": "projects/aca-viewer/tsconfig.spec.json", "tsConfig": "projects/aca-viewer/tsconfig.spec.json",
"karmaConfig": "projects/aca-viewer/karma.conf.js" "karmaConfig": "projects/aca-viewer/karma.conf.js"
@ -648,6 +654,7 @@
"test": { "test": {
"builder": "@angular-devkit/build-angular:karma", "builder": "@angular-devkit/build-angular:karma",
"options": { "options": {
"codeCoverage": true,
"main": "projects/aca-preview/src/test.ts", "main": "projects/aca-preview/src/test.ts",
"tsConfig": "projects/aca-preview/tsconfig.spec.json", "tsConfig": "projects/aca-preview/tsconfig.spec.json",
"karmaConfig": "projects/aca-preview/karma.conf.js" "karmaConfig": "projects/aca-preview/karma.conf.js"

View File

@ -1,25 +1,22 @@
// Karma configuration file, see link for more information // Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html // https://karma-runner.github.io/1.0/config/configuration-file.html
// process.env.CHROME_BIN = require('puppeteer').executablePath();
module.exports = function(config) { const { join } = require('path');
config.set({ const { constants } = require('karma');
module.exports = () => {
return {
basePath: '', basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'], frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [ plugins: [
require('karma-jasmine'), require('karma-jasmine'),
require('karma-chrome-launcher'), require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'), require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'), require('karma-coverage'),
require('karma-mocha-reporter'), require('karma-mocha-reporter'),
require('@angular-devkit/build-angular/plugins/karma') require('@angular-devkit/build-angular/plugins/karma')
], ],
files: [ files: [
{
pattern:
'./node_modules/@angular/material/prebuilt-themes/indigo-pink.css',
watched: false
},
{ {
pattern: pattern:
'./node_modules/@alfresco/adf-core/bundles/assets/adf-core/i18n/en.json', './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' '/base/node_modules/@alfresco/adf-content-services/bundles/assets/adf-content-services/i18n/en.json'
}, },
client: { 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: { jasmineHtmlReporter: {
dir: require('path').join(__dirname, 'coverage'), suppressAll: true, // removes the duplicated traces
reports: ['html', 'lcovonly'], },
fixWebpackSourcePaths: true
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'], reporters: ['mocha', 'kjhtml'],
port: 9876, port: 9876,
colors: true, colors: true,
logLevel: config.LOG_INFO, logLevel: constants.LOG_INFO,
autoWatch: true, autoWatch: true,
browsers: ['ChromeHeadless'], browsers: ['ChromeHeadless'],
customLaunchers: { customLaunchers: {
@ -65,19 +80,15 @@ module.exports = function(config) {
base: 'Chrome', base: 'Chrome',
flags: [ flags: [
'--no-sandbox', '--no-sandbox',
// '--headless', '--headless',
'--disable-gpu', '--disable-gpu',
'--remote-debugging-port=9222' '--remote-debugging-port=9222'
] ]
} }
}, },
singleRun: true, singleRun: true,
captureTimeout: 180000, restartOnFileChange: true,
browserDisconnectTimeout: 180000,
browserDisconnectTolerance: 3,
browserNoActivityTimeout: 300000,
// workaround for alfresco-js-api builds // workaround for alfresco-js-api builds
webpack: { node: { fs: 'empty' } } webpack: { node: { fs: 'empty' } }
}); };
}; };

58
package-lock.json generated
View File

@ -12916,6 +12916,64 @@
"which": "^1.2.1" "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": { "karma-coverage-istanbul-reporter": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-3.0.3.tgz", "resolved": "https://registry.npmjs.org/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-3.0.3.tgz",

View File

@ -16,7 +16,6 @@
"build.release": "npm run build -- --configuration=production,release", "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", "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": "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", "lint": "NODE_OPTIONS=--max_old_space_size=4096 ng lint",
"update-webdriver": "./scripts/update-webdriver.sh", "update-webdriver": "./scripts/update-webdriver.sh",
"e2e": "npm run update-webdriver && protractor $SUITE", "e2e": "npm run update-webdriver && protractor $SUITE",
@ -102,6 +101,7 @@
"jasmine-spec-reporter": "~5.0.0", "jasmine-spec-reporter": "~5.0.0",
"karma": "^6.4.1", "karma": "^6.4.1",
"karma-chrome-launcher": "~3.1.1", "karma-chrome-launcher": "~3.1.1",
"karma-coverage": "^2.2.0",
"karma-coverage-istanbul-reporter": "^3.0.3", "karma-coverage-istanbul-reporter": "^3.0.3",
"karma-jasmine": "~5.1.0", "karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "^2.0.0", "karma-jasmine-html-reporter": "^2.0.0",

View File

@ -1,32 +1,15 @@
// Karma configuration file, see link for more information // Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html // 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({ config.set({
basePath: '', ...baseConfig,
frameworks: ['jasmine', '@angular-devkit/build-angular'], coverageReporter: {
plugins: [ ...baseConfig.coverageReporter,
require('karma-jasmine'), dir: join(__dirname, '../../coverage/aca-about'),
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
}, },
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
}); });
}; };

View File

@ -1,32 +1,15 @@
// Karma configuration file, see link for more information // Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html // 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({ config.set({
basePath: '', ...baseConfig,
frameworks: ['jasmine', '@angular-devkit/build-angular'], coverageReporter: {
plugins: [ ...baseConfig.coverageReporter,
require('karma-jasmine'), dir: join(__dirname, '../../coverage/aca-content'),
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
}, },
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
}); });
}; };

View File

@ -24,7 +24,7 @@
*/ */
import { NgModule } from '@angular/core'; 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 { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { import {
TranslationService, TranslationService,
@ -44,7 +44,6 @@ import { EffectsModule } from '@ngrx/effects';
import { MaterialModule } from '../material.module'; import { MaterialModule } from '../material.module';
import { INITIAL_STATE } from '../store/initial-state'; import { INITIAL_STATE } from '../store/initial-state';
import { TranslatePipeMock } from './translate-pipe.directive'; import { TranslatePipeMock } from './translate-pipe.directive';
import { TranslateServiceMock } from '@alfresco/aca-shared';
import { BehaviorSubject, Observable, of } from 'rxjs'; import { BehaviorSubject, Observable, of } from 'rxjs';
@NgModule({ @NgModule({
@ -53,6 +52,7 @@ import { BehaviorSubject, Observable, of } from 'rxjs';
HttpClientModule, HttpClientModule,
RouterTestingModule, RouterTestingModule,
MaterialModule, MaterialModule,
TranslateModule.forRoot(),
StoreModule.forRoot( StoreModule.forRoot(
{ app: appReducer }, { app: appReducer },
{ {
@ -71,7 +71,6 @@ import { BehaviorSubject, Observable, of } from 'rxjs';
providers: [ providers: [
{ provide: AlfrescoApiService, useClass: AlfrescoApiServiceMock }, { provide: AlfrescoApiService, useClass: AlfrescoApiServiceMock },
{ provide: TranslationService, useClass: TranslationMock }, { provide: TranslationService, useClass: TranslationMock },
{ provide: TranslateService, useClass: TranslateServiceMock },
{ provide: TranslatePipe, useClass: TranslatePipeMock }, { provide: TranslatePipe, useClass: TranslatePipeMock },
{ {
provide: DiscoveryApiService, provide: DiscoveryApiService,

View File

@ -1,32 +1,15 @@
// Karma configuration file, see link for more information // Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html // 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({ config.set({
basePath: '', ...baseConfig,
frameworks: ['jasmine', '@angular-devkit/build-angular'], coverageReporter: {
plugins: [ ...baseConfig.coverageReporter,
require('karma-jasmine'), dir: join(__dirname, '../../coverage/aca-folder-rules'),
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
}, },
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
}); });
}; };

View File

@ -1,32 +1,15 @@
// Karma configuration file, see link for more information // Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html // 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({ config.set({
basePath: '', ...baseConfig,
frameworks: ['jasmine', '@angular-devkit/build-angular'], coverageReporter: {
plugins: [ ...baseConfig.coverageReporter,
require('karma-jasmine'), dir: join(__dirname, '../../coverage/aca-preview'),
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
}, },
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
}); });
}; };

View File

@ -38,11 +38,11 @@ import { UploadService, NodesApiService, DiscoveryApiService } from '@alfresco/a
import { AppState, ClosePreviewAction } from '@alfresco/aca-shared/store'; import { AppState, ClosePreviewAction } from '@alfresco/aca-shared/store';
import { PreviewComponent } from './preview.component'; import { PreviewComponent } from './preview.component';
import { BehaviorSubject, Observable, of, throwError } from 'rxjs'; 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 { Store, StoreModule } from '@ngrx/store';
import { Node, NodePaging, FavoritePaging, SharedLinkPaging, PersonEntry, ResultSetPaging, RepositoryInfo, NodeEntry } from '@alfresco/js-api'; import { Node, NodePaging, FavoritePaging, SharedLinkPaging, PersonEntry, ResultSetPaging, RepositoryInfo, NodeEntry } from '@alfresco/js-api';
import { PreviewModule } from '../preview.module'; import { PreviewModule } from '../preview.module';
import { TranslateService } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
import { HttpClientModule } from '@angular/common/http'; import { HttpClientModule } from '@angular/common/http';
import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { NoopAnimationsModule } from '@angular/platform-browser/animations';
@ -115,6 +115,7 @@ describe('PreviewComponent', () => {
NoopAnimationsModule, NoopAnimationsModule,
HttpClientModule, HttpClientModule,
RouterTestingModule, RouterTestingModule,
TranslateModule.forRoot(),
StoreModule.forRoot( StoreModule.forRoot(
{ app: (state) => state }, { app: (state) => state },
{ {
@ -133,7 +134,6 @@ describe('PreviewComponent', () => {
providers: [ providers: [
{ provide: AlfrescoApiService, useClass: AlfrescoApiServiceMock }, { provide: AlfrescoApiService, useClass: AlfrescoApiServiceMock },
{ provide: TranslationService, useClass: TranslationMock }, { provide: TranslationService, useClass: TranslationMock },
{ provide: TranslateService, useClass: TranslateServiceMock },
{ provide: DocumentBasePageService, useVale: new DocumentBasePageServiceMock() }, { provide: DocumentBasePageService, useVale: new DocumentBasePageServiceMock() },
{ {
provide: DiscoveryApiService, provide: DiscoveryApiService,

View File

@ -1,33 +1,15 @@
// Karma configuration file, see link for more information // Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html // 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({ config.set({
basePath: '', ...baseConfig,
frameworks: ['jasmine', '@angular-devkit/build-angular'], coverageReporter: {
plugins: [ ...baseConfig.coverageReporter,
require('karma-jasmine'), dir: join(__dirname, '../../coverage/aca-shared'),
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
}, },
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
}); });
}; };

View File

@ -143,12 +143,14 @@ export abstract class PageComponent implements OnInit, OnDestroy, OnChanges {
} }
imageResolver(row: ShareDataRow): string | null { imageResolver(row: ShareDataRow): string | null {
if (isLocked(row.node)) { if (row) {
return 'assets/images/baseline-lock-24px.svg'; if (isLocked(row.node)) {
} return 'assets/images/baseline-lock-24px.svg';
}
if (isLibrary(row.node)) { if (isLibrary(row.node)) {
return 'assets/images/baseline-library_books-24px.svg'; return 'assets/images/baseline-library_books-24px.svg';
}
} }
return null; return null;
@ -181,7 +183,7 @@ export abstract class PageComponent implements OnInit, OnDestroy, OnChanges {
return location.href.includes('viewer:view'); return location.href.includes('viewer:view');
} }
onSortingChanged(event) { onSortingChanged(event: any) {
this.filterSorting = event.detail.key + '-' + event.detail.direction; this.filterSorting = event.detail.key + '-' + event.detail.direction;
} }

View File

@ -38,6 +38,7 @@ import { HttpClientModule } from '@angular/common/http';
import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
import { EffectsModule } from '@ngrx/effects'; import { EffectsModule } from '@ngrx/effects';
import { Subscription } from 'rxjs';
export const INITIAL_APP_STATE: AppState = { export const INITIAL_APP_STATE: AppState = {
appName: 'Alfresco Content Application', appName: 'Alfresco Content Application',
@ -98,6 +99,14 @@ class TestComponent extends PageComponent {
constructor(store: Store<AppStore>, extensions: AppExtensionService, content: DocumentBasePageService) { constructor(store: Store<AppStore>, extensions: AppExtensionService, content: DocumentBasePageService) {
super(store, extensions, content); super(store, extensions, content);
} }
addSubscription(entry: Subscription) {
this.subscriptions.push(entry);
}
getSubscriptions(): Subscription[] {
return this.subscriptions;
}
} }
describe('PageComponent', () => { 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');
});
}); });

View File

@ -23,21 +23,21 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>. * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { Injectable } from '@angular/core'; import { LockedByComponent } from './locked-by.component';
import { TranslateService } from '@ngx-translate/core';
import { Observable, of } from 'rxjs';
@Injectable() describe('LockedByComponent', () => {
export class TranslateServiceMock extends TranslateService { it('should evaluate label text', () => {
constructor() { const component = new LockedByComponent();
super(null, null, null, null, null, null, true, null, null); component.node = {
} entry: {
properties: {
'cm:lockOwner': {
displayName: 'owner-name'
}
}
} as any
};
get(key: string | Array<string>): Observable<string | any> { expect(component.text).toBe('owner-name');
return of(key); });
} });
instant(key: string | Array<string>): string | any {
return key;
}
}

View File

@ -45,8 +45,6 @@ export class LockedByComponent {
node: NodeEntry; node: NodeEntry;
get text(): string { get text(): string {
return ( return this.node?.entry?.properties?.['cm:lockOwner']?.displayName;
this.node && this.node.entry.properties && this.node.entry.properties['cm:lockOwner'] && this.node.entry.properties['cm:lockOwner'].displayName
);
} }
} }

View File

@ -5,6 +5,9 @@ import { OpenInAppComponent } from './open-in-app.component';
import { initialState, LibTestingModule } from '../../testing/lib-testing-module'; import { initialState, LibTestingModule } from '../../testing/lib-testing-module';
import { provideMockStore } from '@ngrx/store/testing'; import { provideMockStore } from '@ngrx/store/testing';
import { TranslateModule } from '@ngx-translate/core'; 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', () => { describe('OpenInAppComponent', () => {
let fixture: ComponentFixture<OpenInAppComponent>; let fixture: ComponentFixture<OpenInAppComponent>;
@ -15,16 +18,15 @@ describe('OpenInAppComponent', () => {
open: jasmine.createSpy('open') open: jasmine.createSpy('open')
}; };
beforeEach(async () => { beforeEach(() => {
await TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [OpenInAppComponent], imports: [LibTestingModule, TranslateModule, SharedModule.forRoot(), MatIconModule, MatIconTestingModule],
imports: [LibTestingModule, TranslateModule],
providers: [ providers: [
provideMockStore({ initialState }), provideMockStore({ initialState }),
{ provide: MAT_DIALOG_DATA, useValue: { redirectUrl: 'mockRedirectUrl' } }, { provide: MAT_DIALOG_DATA, useValue: { redirectUrl: 'mockRedirectUrl' } },
{ provide: MatDialogRef, useValue: mockDialogRef } { provide: MatDialogRef, useValue: mockDialogRef }
] ]
}).compileComponents(); });
fixture = TestBed.createComponent(OpenInAppComponent); fixture = TestBed.createComponent(OpenInAppComponent);
component = fixture.componentInstance; component = fixture.componentInstance;

View File

@ -23,10 +23,51 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>. * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ToolbarActionComponent } from './toolbar-action.component'; 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', () => { describe('ToolbarActionComponent', () => {
it('should be defined', () => { let fixture: ComponentFixture<ToolbarActionComponent>;
expect(ToolbarActionComponent).toBeDefined(); 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();
}); });
}); });

View File

@ -23,10 +23,69 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>. * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/ */
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', () => { describe('ToolbarButtonComponent', () => {
it('should be defined', () => { let fixture: ComponentFixture<ToolbarButtonComponent>;
expect(ToolbarButtonComponent).toBeDefined(); 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();
}); });
}); });

View File

@ -23,10 +23,98 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>. * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ToolbarMenuItemComponent } from './toolbar-menu-item.component'; 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', () => { describe('ToolbarMenuItemComponent', () => {
it('should be defined', () => { let fixture: ComponentFixture<ToolbarMenuItemComponent>;
expect(ToolbarMenuItemComponent).toBeDefined(); 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');
}); });
}); });

View File

@ -28,7 +28,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MaterialModule } from '@alfresco/adf-core'; import { MaterialModule } from '@alfresco/adf-core';
import { OverlayModule } from '@angular/cdk/overlay'; import { OverlayModule } from '@angular/cdk/overlay';
import { TranslateModule, TranslateLoader, TranslateFakeLoader } from '@ngx-translate/core'; 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', () => { describe('ToolbarMenuComponent', () => {
let fixture: ComponentFixture<ToolbarMenuComponent>; let fixture: ComponentFixture<ToolbarMenuComponent>;
@ -50,6 +51,7 @@ describe('ToolbarMenuComponent', () => {
component = fixture.componentInstance; component = fixture.componentInstance;
component.matTrigger = jasmine.createSpyObj('MatMenuTrigger', ['closeMenu']); component.matTrigger = jasmine.createSpyObj('MatMenuTrigger', ['closeMenu']);
component.actionRef = actions; component.actionRef = actions;
fixture.detectChanges(); fixture.detectChanges();
}); });
@ -59,4 +61,23 @@ describe('ToolbarMenuComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
expect(component.matTrigger.closeMenu).toHaveBeenCalled(); 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');
});
}); });

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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);
});
});

View File

@ -39,11 +39,11 @@ export class AppSharedRuleGuard implements CanActivate {
this.isQuickShareEnabled$ = store.select(isQuickShareEnabled); this.isQuickShareEnabled$ = store.select(isQuickShareEnabled);
} }
canActivate(_: ActivatedRouteSnapshot): Observable<boolean> | Promise<boolean> | boolean { canActivate(_: ActivatedRouteSnapshot): Observable<boolean> {
return this.isQuickShareEnabled$; return this.isQuickShareEnabled$;
} }
canActivateChild(route: ActivatedRouteSnapshot): Observable<boolean> | Promise<boolean> | boolean { canActivateChild(route: ActivatedRouteSnapshot): Observable<boolean> {
return this.canActivate(route); return this.canActivate(route);
} }
} }

View File

@ -28,10 +28,13 @@ import { AppConfigService } from '@alfresco/adf-core';
import { AlfrescoOfficeExtensionService } from './alfresco-office-extension.service'; import { AlfrescoOfficeExtensionService } from './alfresco-office-extension.service';
import { provideMockStore } from '@ngrx/store/testing'; import { provideMockStore } from '@ngrx/store/testing';
import { initialState, LibTestingModule } from '../testing/lib-testing-module'; import { initialState, LibTestingModule } from '../testing/lib-testing-module';
import { Subject } from 'rxjs';
describe('AlfrescoOfficeExtensionService', () => { describe('AlfrescoOfficeExtensionService', () => {
let appConfig: AppConfigService; let appConfig: AppConfigService;
let service: AlfrescoOfficeExtensionService; let service: AlfrescoOfficeExtensionService;
let onLoad$: Subject<any>;
const mock = () => { const mock = () => {
let storage: { [key: string]: any } = {}; let storage: { [key: string]: any } = {};
return { return {
@ -48,16 +51,44 @@ describe('AlfrescoOfficeExtensionService', () => {
providers: [provideMockStore({ initialState })] providers: [provideMockStore({ initialState })]
}); });
service = TestBed.inject(AlfrescoOfficeExtensionService); onLoad$ = new Subject();
appConfig = TestBed.inject(AppConfigService); appConfig = TestBed.inject(AppConfigService);
appConfig.config = Object.assign(appConfig.config, { appConfig.onLoad = onLoad$;
aosPlugin: true appConfig.config.aosPlugin = true;
});
service = TestBed.inject(AlfrescoOfficeExtensionService);
Object.defineProperty(window, 'localStorage', { value: mock() }); 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', () => { it('Should initialize the localStorage with the item aosPlugin true if not present', () => {
expect(localStorage.getItem('aosPlugin')).toBeNull('The localStorage aosPlugin is not null'); expect(localStorage.getItem('aosPlugin')).toBeNull('The localStorage aosPlugin is not null');
service.enablePlugin(); service.enablePlugin();

View File

@ -39,15 +39,20 @@ import {
ExtensionConfig, ExtensionConfig,
NavBarGroupRef NavBarGroupRef
} from '@alfresco/adf-extensions'; } from '@alfresco/adf-extensions';
import { AppConfigService } from '@alfresco/adf-core'; import { AppConfigService, LogService } from '@alfresco/adf-core';
import { provideMockStore } from '@ngrx/store/testing'; import { provideMockStore } from '@ngrx/store/testing';
import { hasQuickShareEnabled } from '@alfresco/aca-shared/rules'; import { hasQuickShareEnabled } from '@alfresco/aca-shared/rules';
import { MatIconRegistry } from '@angular/material/icon';
import { DomSanitizer } from '@angular/platform-browser';
describe('AppExtensionService', () => { describe('AppExtensionService', () => {
let service: AppExtensionService; let service: AppExtensionService;
let store: Store<AppStore>; let store: Store<AppStore>;
let extensions: ExtensionService; let extensions: ExtensionService;
let appConfigService: AppConfigService; let appConfigService: AppConfigService;
let logService: LogService;
let iconRegistry: MatIconRegistry;
let sanitizer: DomSanitizer;
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
@ -55,6 +60,8 @@ describe('AppExtensionService', () => {
providers: [provideMockStore({ initialState })] providers: [provideMockStore({ initialState })]
}); });
iconRegistry = TestBed.inject(MatIconRegistry);
sanitizer = TestBed.inject(DomSanitizer);
appConfigService = TestBed.inject(AppConfigService); appConfigService = TestBed.inject(AppConfigService);
store = TestBed.inject(Store); store = TestBed.inject(Store);
@ -62,6 +69,7 @@ describe('AppExtensionService', () => {
service.repository.status.isQuickShareEnabled = true; service.repository.status.isQuickShareEnabled = true;
extensions = TestBed.inject(ExtensionService); extensions = TestBed.inject(ExtensionService);
logService = TestBed.inject(LogService);
}); });
const applyConfig = (config: ExtensionConfig, selection?: boolean) => { const applyConfig = (config: ExtensionConfig, selection?: boolean) => {
@ -78,6 +86,40 @@ describe('AppExtensionService', () => {
}; };
describe('configs', () => { 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', () => { it('should merge two arrays based on [id] keys', () => {
const left = [ 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', () => { describe('rule disable', () => {
beforeEach(() => { beforeEach(() => {
extensions.setEvaluators({ 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();
});
});
}); });

View File

@ -196,9 +196,9 @@ export class AppExtensionService implements RuleContext {
const value = icon.value; const value = icon.value;
if (!value) { if (!value) {
console.warn(`Missing icon value for "${icon.id}".`); this.logger.warn(`Missing icon value for "${icon.id}".`);
} else if (!ns || !id) { } else if (!ns || !id) {
console.warn(`Incorrect icon id format: "${icon.id}".`); this.logger.warn(`Incorrect icon id format.`);
} else { } else {
this.matIconRegistry.addSvgIconInNamespace(ns, id, this.sanitizer.bypassSecurityTrustResourceUrl(value)); this.matIconRegistry.addSvgIconInNamespace(ns, id, this.sanitizer.bypassSecurityTrustResourceUrl(value));
} }
@ -290,12 +290,10 @@ export class AppExtensionService implements RuleContext {
let presets = {}; let presets = {};
presets = this.filterDisabled(mergeObjects(presets, ...elements)); presets = this.filterDisabled(mergeObjects(presets, ...elements));
try { const metadata = this.appConfig.config['content-metadata'] || {};
this.appConfig.config['content-metadata'].presets = presets; metadata.presets = presets;
} catch (error) {
this.logger.error(error, '- could not change content-metadata presets from app.config -');
}
this.appConfig.config['content-metadata'] = metadata;
return { presets }; return { presets };
} }
@ -310,11 +308,7 @@ export class AppExtensionService implements RuleContext {
.filter((entry) => this.filterVisible(entry)) .filter((entry) => this.filterVisible(entry))
.sort(sortByOrder); .sort(sortByOrder);
try { this.appConfig.config['search'] = search;
this.appConfig.config['search'] = search;
} catch (error) {
this.logger.error(error, '- could not change search from app.config -');
}
return search; return search;
} }

View File

@ -30,60 +30,49 @@ import {
AppConfigService, AppConfigService,
AlfrescoApiService, AlfrescoApiService,
PageTitleService, PageTitleService,
UserPreferencesService,
AlfrescoApiServiceMock, AlfrescoApiServiceMock,
TranslationMock, TranslationMock,
TranslationService TranslationService
} from '@alfresco/adf-core'; } from '@alfresco/adf-core';
import { BehaviorSubject, Observable, of, Subject } from 'rxjs'; import { BehaviorSubject, Observable, of, Subject } from 'rxjs';
import { HttpClientModule } from '@angular/common/http'; import { HttpClientModule } from '@angular/common/http';
import { SharedLinksApiService, GroupService, SearchQueryBuilderService, UploadService, DiscoveryApiService } from '@alfresco/adf-content-services'; import {
import { ActivatedRoute, Router } from '@angular/router'; DiscoveryApiService,
import { ContentApiService } from './content-api.service'; FileUploadErrorEvent,
import { RouterExtensionService } from './router.extension.service'; GroupService,
import { OverlayContainer } from '@angular/cdk/overlay'; SearchQueryBuilderService,
import { AppStore, STORE_INITIAL_APP_DATA } from '../../../store/src/states/app.state'; SharedLinksApiService,
import { MockStore, provideMockStore } from '@ngrx/store/testing'; 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 { CommonModule } from '@angular/common';
import { TranslateService } from '@ngx-translate/core';
import { TranslateServiceMock } from '../testing/translation.service';
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
import { RepositoryInfo } from '@alfresco/js-api'; import { RepositoryInfo } from '@alfresco/js-api';
import { AcaMobileAppSwitcherService } from './aca-mobile-app-switcher.service';
import { MatDialogModule } from '@angular/material/dialog'; 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', () => { describe('AppService', () => {
let service: AppService; let service: AppService;
let auth: AuthenticationService; let auth: AuthenticationService;
let appConfig: AppConfigService; let appConfig: AppConfigService;
let searchQueryBuilderService: SearchQueryBuilderService; let searchQueryBuilderService: SearchQueryBuilderService;
let userPreferencesService: UserPreferencesService;
let router: Router;
let activatedRoute: ActivatedRoute;
let routerExtensionService: RouterExtensionService;
let pageTitleService: PageTitleService;
let uploadService: UploadService; let uploadService: UploadService;
let contentApiService: ContentApiService; let store: Store;
let sharedLinksApiService: SharedLinksApiService; let sharedLinksApiService: SharedLinksApiService;
let overlayContainer: OverlayContainer; let contentApi: ContentApiService;
let alfrescoApiService: AlfrescoApiService;
let groupService: GroupService; let groupService: GroupService;
let storeInitialAppData: any;
let store: MockStore<AppStore>;
let acaMobileAppSwitcherService: AcaMobileAppSwitcherService;
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [HttpClientModule, RouterTestingModule.withRoutes([]), MatDialogModule], imports: [CommonModule, HttpClientModule, TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), MatDialogModule],
providers: [ providers: [
CommonModule,
SearchQueryBuilderService, SearchQueryBuilderService,
UserPreferencesService,
RouterExtensionService,
UploadService,
ContentApiService,
SharedLinksApiService,
OverlayContainer,
provideMockStore({}), provideMockStore({}),
{ {
provide: PageTitleService, provide: PageTitleService,
@ -118,48 +107,19 @@ describe('AppService', () => {
isLoggedIn: () => false isLoggedIn: () => false
} }
}, },
{ provide: TranslationService, useClass: TranslationMock }, { provide: TranslationService, useClass: TranslationMock }
{ provide: TranslateService, useClass: TranslateServiceMock }
] ]
}); });
appConfig = TestBed.inject(AppConfigService); appConfig = TestBed.inject(AppConfigService);
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);
sharedLinksApiService = TestBed.inject(SharedLinksApiService);
overlayContainer = TestBed.inject(OverlayContainer);
alfrescoApiService = TestBed.inject(AlfrescoApiService);
groupService = TestBed.inject(GroupService);
storeInitialAppData = TestBed.inject(STORE_INITIAL_APP_DATA);
store = TestBed.inject(MockStore);
auth = TestBed.inject(AuthenticationService); auth = TestBed.inject(AuthenticationService);
acaMobileAppSwitcherService = TestBed.inject(AcaMobileAppSwitcherService); searchQueryBuilderService = TestBed.inject(SearchQueryBuilderService);
uploadService = TestBed.inject(UploadService);
service = new AppService( store = TestBed.inject(Store);
userPreferencesService, sharedLinksApiService = TestBed.inject(SharedLinksApiService);
auth, contentApi = TestBed.inject(ContentApiService);
store, groupService = TestBed.inject(GroupService);
router, service = TestBed.inject(AppService);
activatedRoute,
appConfig,
pageTitleService,
alfrescoApiService,
uploadService,
routerExtensionService,
contentApiService,
sharedLinksApiService,
groupService,
overlayContainer,
storeInitialAppData,
searchQueryBuilderService,
acaMobileAppSwitcherService
);
}); });
it('should be ready if [withCredentials] mode is used', (done) => { it('should be ready if [withCredentials] mode is used', (done) => {
@ -169,26 +129,7 @@ describe('AppService', () => {
} }
}; };
const instance = new AppService( const instance = TestBed.inject(AppService);
userPreferencesService,
auth,
store,
router,
activatedRoute,
appConfig,
pageTitleService,
alfrescoApiService,
uploadService,
routerExtensionService,
contentApiService,
sharedLinksApiService,
groupService,
overlayContainer,
storeInitialAppData,
searchQueryBuilderService,
acaMobileAppSwitcherService
);
expect(instance.withCredentials).toBeTruthy(); expect(instance.withCredentials).toBeTruthy();
instance.ready$.subscribe(() => { instance.ready$.subscribe(() => {
@ -204,4 +145,98 @@ describe('AppService', () => {
auth.onLogin.next(); auth.onLogin.next();
await expect(isReady).toEqual(true); 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] }));
});
}); });

View File

@ -23,13 +23,13 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>. * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { Inject, Injectable, OnDestroy } from '@angular/core'; import { Inject, Injectable } from '@angular/core';
import { AuthenticationService, AppConfigService, AlfrescoApiService, PageTitleService, UserPreferencesService } from '@alfresco/adf-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 { GroupService, SearchQueryBuilderService, SharedLinksApiService, UploadService, FileUploadErrorEvent } from '@alfresco/adf-content-services';
import { OverlayContainer } from '@angular/cdk/overlay'; import { OverlayContainer } from '@angular/cdk/overlay';
import { ActivatedRoute, ActivationEnd, NavigationStart, Router } from '@angular/router'; import { ActivatedRoute, ActivationEnd, NavigationStart, Router } from '@angular/router';
import { filter, map, takeUntil, tap } from 'rxjs/operators'; import { filter, map, tap } from 'rxjs/operators';
import { import {
AppState, AppState,
AppStore, AppStore,
@ -54,7 +54,7 @@ import { AcaMobileAppSwitcherService } from './aca-mobile-app-switcher.service';
providedIn: 'root' providedIn: 'root'
}) })
// After moving shell to ADF to core, AppService will implement ShellAppService // After moving shell to ADF to core, AppService will implement ShellAppService
export class AppService implements OnDestroy { export class AppService {
private ready: BehaviorSubject<boolean>; private ready: BehaviorSubject<boolean>;
ready$: Observable<boolean>; ready$: Observable<boolean>;
@ -63,8 +63,6 @@ export class AppService implements OnDestroy {
hideSidenavConditions = ['/preview/']; hideSidenavConditions = ['/preview/'];
minimizeSidenavConditions = ['search']; minimizeSidenavConditions = ['search'];
onDestroy$ = new Subject<boolean>();
/** /**
* Whether `withCredentials` mode is enabled. * Whether `withCredentials` mode is enabled.
* Usually means that `Kerberos` mode is used. * 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 { init(): void {
this.alfrescoApiService.getInstance().on('error', (error: { status: number; response: any }) => { this.alfrescoApiService.getInstance().on('error', (error: { status: number; response: any }) => {
if (error.status === 401 && !this.alfrescoApiService.isExcludedErrorListener(error?.response?.req?.url)) { 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.store.dispatch(new SetCurrentUrlAction(router.url));
}); });
this.router.events this.router.events.pipe(filter((event) => event instanceof NavigationStart)).subscribe(() => {
.pipe( this.store.dispatch(new ResetSelectionAction());
filter((event) => event instanceof NavigationStart), });
takeUntil(this.onDestroy$)
)
.subscribe(() => {
this.store.dispatch(new ResetSelectionAction());
});
this.routerExtensionService.mapExtensionRoutes(); this.routerExtensionService.mapExtensionRoutes();
this.uploadService.fileUploadError.subscribe((error) => this.onFileUploadedError(error)); this.uploadService.fileUploadError.subscribe((error) => this.onFileUploadedError(error));
this.sharedLinksApiService.error.pipe(takeUntil(this.onDestroy$)).subscribe((err: { message: string }) => { this.sharedLinksApiService.error.subscribe((err: { message: string }) => {
this.store.dispatch(new SnackbarErrorAction(err.message)); if (err?.message) {
this.store.dispatch(new SnackbarErrorAction(err.message));
}
}); });
this.ready$.pipe(takeUntil(this.onDestroy$)).subscribe((isReady) => { this.ready$.subscribe((isReady) => {
if (isReady) { if (isReady) {
this.loadRepositoryStatus(); this.loadRepositoryStatus();
this.loadUserProfile(); this.loadUserProfile();
@ -182,7 +172,9 @@ export class AppService implements OnDestroy {
private loadRepositoryStatus() { private loadRepositoryStatus() {
this.contentApi.getRepositoryInformation().subscribe((response: DiscoveryEntry) => { 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() { loadAppSettings() {
let baseShareUrl = this.config.get<string>('baseShareUrl'); let baseShareUrl = this.config.get<string>('baseShareUrl', '');
if (!baseShareUrl.endsWith('/')) { if (!baseShareUrl.endsWith('/')) {
baseShareUrl += '/'; baseShareUrl += '/';
} }
@ -224,23 +216,23 @@ export class AppService implements OnDestroy {
onFileUploadedError(error: FileUploadErrorEvent) { onFileUploadedError(error: FileUploadErrorEvent) {
let message = 'APP.MESSAGES.UPLOAD.ERROR.GENERIC'; let message = 'APP.MESSAGES.UPLOAD.ERROR.GENERIC';
if (error.error.status === 403) { if (error?.error?.status === 403) {
message = 'APP.MESSAGES.UPLOAD.ERROR.403'; message = 'APP.MESSAGES.UPLOAD.ERROR.403';
} }
if (error.error.status === 404) { if (error?.error?.status === 404) {
message = 'APP.MESSAGES.UPLOAD.ERROR.404'; message = 'APP.MESSAGES.UPLOAD.ERROR.404';
} }
if (error.error.status === 409) { if (error?.error?.status === 409) {
message = 'APP.MESSAGES.UPLOAD.ERROR.CONFLICT'; message = 'APP.MESSAGES.UPLOAD.ERROR.CONFLICT';
} }
if (error.error.status === 500) { if (error?.error?.status === 500) {
message = 'APP.MESSAGES.UPLOAD.ERROR.500'; message = 'APP.MESSAGES.UPLOAD.ERROR.500';
} }
if (error.error.status === 504) { if (error?.error?.status === 504) {
message = 'APP.MESSAGES.UPLOAD.ERROR.504'; message = 'APP.MESSAGES.UPLOAD.ERROR.504';
} }

View File

@ -61,61 +61,61 @@ import { map } from 'rxjs/operators';
providedIn: 'root' providedIn: 'root'
}) })
export class ContentApiService { export class ContentApiService {
_nodesApi: NodesApi; private _nodesApi: NodesApi;
get nodesApi(): NodesApi { get nodesApi(): NodesApi {
this._nodesApi = this._nodesApi ?? new NodesApi(this.api.getInstance()); this._nodesApi = this._nodesApi ?? new NodesApi(this.api.getInstance());
return this._nodesApi; return this._nodesApi;
} }
_trashcanApi: TrashcanApi; private _trashcanApi: TrashcanApi;
get trashcanApi(): TrashcanApi { get trashcanApi(): TrashcanApi {
this._trashcanApi = this._trashcanApi ?? new TrashcanApi(this.api.getInstance()); this._trashcanApi = this._trashcanApi ?? new TrashcanApi(this.api.getInstance());
return this._trashcanApi; return this._trashcanApi;
} }
_sharedLinksApi: SharedlinksApi; private _sharedLinksApi: SharedlinksApi;
get sharedLinksApi(): SharedlinksApi { get sharedLinksApi(): SharedlinksApi {
this._sharedLinksApi = this._sharedLinksApi ?? new SharedlinksApi(this.api.getInstance()); this._sharedLinksApi = this._sharedLinksApi ?? new SharedlinksApi(this.api.getInstance());
return this._sharedLinksApi; return this._sharedLinksApi;
} }
_discoveryApi: DiscoveryApi; private _discoveryApi: DiscoveryApi;
get discoveryApi(): DiscoveryApi { get discoveryApi(): DiscoveryApi {
this._discoveryApi = this._discoveryApi ?? new DiscoveryApi(this.api.getInstance()); this._discoveryApi = this._discoveryApi ?? new DiscoveryApi(this.api.getInstance());
return this._discoveryApi; return this._discoveryApi;
} }
_favoritesApi: FavoritesApi; private _favoritesApi: FavoritesApi;
get favoritesApi(): FavoritesApi { get favoritesApi(): FavoritesApi {
this._favoritesApi = this._favoritesApi ?? new FavoritesApi(this.api.getInstance()); this._favoritesApi = this._favoritesApi ?? new FavoritesApi(this.api.getInstance());
return this._favoritesApi; return this._favoritesApi;
} }
_contentApi: ContentApi; private _contentApi: ContentApi;
get contentApi(): ContentApi { get contentApi(): ContentApi {
this._contentApi = this._contentApi ?? new ContentApi(this.api.getInstance()); this._contentApi = this._contentApi ?? new ContentApi(this.api.getInstance());
return this._contentApi; return this._contentApi;
} }
_sitesApi: SitesApi; private _sitesApi: SitesApi;
get sitesApi(): SitesApi { get sitesApi(): SitesApi {
this._sitesApi = this._sitesApi ?? new SitesApi(this.api.getInstance()); this._sitesApi = this._sitesApi ?? new SitesApi(this.api.getInstance());
return this._sitesApi; return this._sitesApi;
} }
_searchApi: SearchApi; private _searchApi: SearchApi;
get searchApi(): SearchApi { get searchApi(): SearchApi {
this._searchApi = this._searchApi ?? new SearchApi(this.api.getInstance()); this._searchApi = this._searchApi ?? new SearchApi(this.api.getInstance());
return this._searchApi; return this._searchApi;
} }
_peopleApi: PeopleApi; private _peopleApi: PeopleApi;
get peopleApi(): PeopleApi { get peopleApi(): PeopleApi {
this._peopleApi = this._peopleApi ?? new PeopleApi(this.api.getInstance()); this._peopleApi = this._peopleApi ?? new PeopleApi(this.api.getInstance());
return this._peopleApi; return this._peopleApi;
} }
_versionsApi: VersionsApi; private _versionsApi: VersionsApi;
get versionsApi(): VersionsApi { get versionsApi(): VersionsApi {
this._versionsApi = this._versionsApi ?? new VersionsApi(this.api.getInstance()); this._versionsApi = this._versionsApi ?? new VersionsApi(this.api.getInstance());
return this._versionsApi; return this._versionsApi;

View File

@ -26,16 +26,21 @@
import { Node } from '@alfresco/js-api'; import { Node } from '@alfresco/js-api';
export function isLocked(node: { entry: Node }): boolean { export function isLocked(node: { entry: Node }): boolean {
const { entry } = node; if (node?.entry) {
const { entry } = node;
return ( return entry.isLocked || entry.properties?.['cm:lockType'] === 'READ_ONLY_LOCK' || entry.properties?.['cm:lockType'] === 'WRITE_LOCK';
(entry && entry.isLocked) || } else {
(entry.properties && (entry.properties['cm:lockType'] === 'READ_ONLY_LOCK' || entry.properties['cm:lockType'] === 'WRITE_LOCK')) return false;
); }
} }
export function isLibrary(node: { entry: Node | any }): boolean { 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;
}
} }

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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: '<guid>',
id: '<id>',
preset: '<preset>',
title: '<title>',
visibility: '<visibility>'
}
})
).toBeTrue();
});
});
});

View File

@ -65,4 +65,3 @@ export * from './lib/services/alfresco-office-extension.service';
export * from './lib/utils/node.utils'; export * from './lib/utils/node.utils';
export * from './lib/shared.module'; export * from './lib/shared.module';
export * from './lib/testing/lib-testing-module'; export * from './lib/testing/lib-testing-module';
export * from './lib/testing/translation.service';

View File

@ -1,32 +1,15 @@
// Karma configuration file, see link for more information // Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html // 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({ config.set({
basePath: '', ...baseConfig,
frameworks: ['jasmine', '@angular-devkit/build-angular'], coverageReporter: {
plugins: [ ...baseConfig.coverageReporter,
require('karma-jasmine'), dir: join(__dirname, '../../coverage/aca-viewer'),
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
}, },
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
}); });
}; };

View File

@ -1,5 +1,10 @@
{ {
"AOS": { "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 }}'"
}
} }
} }

View File

@ -1,35 +1,15 @@
// Karma configuration file, see link for more information // Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html // 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({ config.set({
basePath: '', ...baseConfig,
frameworks: ['jasmine', '@angular-devkit/build-angular'], coverageReporter: {
plugins: [ ...baseConfig.coverageReporter,
require('karma-jasmine'), dir: join(__dirname, '../../coverage/aca-office-services-ext'),
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
}, },
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
}); });
}; };

View File

@ -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');
});
});

View File

@ -24,7 +24,7 @@
*/ */
/* cspell:disable */ /* 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 { Injectable } from '@angular/core';
import { MinimalNodeEntryEntity } from '@alfresco/js-api'; import { MinimalNodeEntryEntity } from '@alfresco/js-api';
import { getFileExtension, supportedExtensions } from '@alfresco/aca-shared/rules'; import { getFileExtension, supportedExtensions } from '@alfresco/aca-shared/rules';
@ -38,9 +38,10 @@ export interface IAosEditOnlineService {
}) })
export class AosEditOnlineService implements IAosEditOnlineService { export class AosEditOnlineService implements IAosEditOnlineService {
constructor( constructor(
private alfrescoAuthenticationService: AuthenticationService, private authenticationService: AuthenticationService,
private appConfigService: AppConfigService, private appConfigService: AppConfigService,
private notificationService: NotificationService private notificationService: NotificationService,
private logService: LogService
) {} ) {}
onActionEditOnlineAos(node: MinimalNodeEntryEntity): void { 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 checkedOut = node.properties['cm:lockType'] === 'WRITE_LOCK' || node.properties['cm:lockType'] === 'READ_ONLY_LOCK';
const lockOwner = node.properties['cm:lockOwner']; const lockOwner = node.properties['cm:lockOwner'];
const differentLockOwner = lockOwner.id !== this.alfrescoAuthenticationService.getEcmUsername(); const differentLockOwner = lockOwner.id !== this.authenticationService.getEcmUsername();
if (checkedOut && differentLockOwner) { if (checkedOut && differentLockOwner) {
this.onAlreadyLockedNotification(node.id, lockOwner); this.onAlreadyLockedNotification(node.id, lockOwner.id);
} else { } else {
this.triggerEditOnlineAos(node); this.triggerEditOnlineAos(node);
} }
@ -77,17 +78,21 @@ export class AosEditOnlineService implements IAosEditOnlineService {
} }
private onAlreadyLockedNotification(nodeId: string, lockOwner: string) { 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) { private getProtocolForFileExtension(fileExtension: string) {
return supportedExtensions[fileExtension]; return fileExtension && supportedExtensions[fileExtension];
} }
private triggerEditOnlineAos(node: MinimalNodeEntryEntity): void { private triggerEditOnlineAos(node: MinimalNodeEntryEntity): void {
const aosHost = this.appConfigService.get('aosHost'); const aosHost = this.appConfigService.get('aosHost');
let url: string; let url: string;
const pathElements = (node.path.elements || []).map((segment) => segment.name); const pathElements = (node.path?.elements || []).map((segment) => segment.name);
if (!pathElements.length) { if (!pathElements.length) {
url = `${aosHost}/Company Home/_aos_nodeid/${this.getNodeId(node)}/${encodeURIComponent(node.name)}`; 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); const protocolHandler = this.getProtocolForFileExtension(fileExtension);
if (protocolHandler === undefined) { 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; return;
} }
if (!this.isWindows() && !this.isMacOs()) { 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 { } else {
this.aosTryToLaunchOfficeByMsProtocolHandler(protocolHandler, url); this.openByUrl(protocolHandler, url);
} }
} }
private aosTryToLaunchOfficeByMsProtocolHandler(protocolHandler: string, url: string) { openByUrl(protocolHandler: string, url: string) {
const protocolUrl = protocolHandler + ':ofe%7Cu%7C' + url; const finalUrl = protocolHandler + ':ofe%7Cu%7C' + url;
const iframe = document.createElement('iframe'); const iframe = document.createElement('iframe');
iframe.style.display = 'none'; iframe.style.display = 'none';
iframe.src = protocolUrl; iframe.src = finalUrl;
document.body.appendChild(iframe); document.body.appendChild(iframe);